/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <sal/config.h>

#include <memory>
#include <string_view>

#include <config_fonts.h>
#include <osl/process.h>

#include <com/sun/star/text/XDocumentIndexesSupplier.hpp>
#include <com/sun/star/util/XRefreshable.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/drawing/XShape.hpp>
#include <com/sun/star/text/XTextDocument.hpp>
#include <com/sun/star/document/XFilter.hpp>
#include <com/sun/star/document/XExporter.hpp>
#include <com/sun/star/io/XOutputStream.hpp>

#include <comphelper/scopeguard.hxx>
#include <comphelper/propertysequence.hxx>
#include <test/unoapi_test.hxx>
#include <unotools/mediadescriptor.hxx>
#include <unotools/tempfile.hxx>
#include <vcl/filter/pdfdocument.hxx>
#include <tools/zcodec.hxx>
#include <tools/XmlWalker.hxx>
#include <vcl/graphicfilter.hxx>
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <unotools/streamwrap.hxx>
#include <rtl/math.hxx>
#include <o3tl/string_view.hxx>
#include <IDocumentDeviceAccess.hxx>
#include <printdata.hxx>
#include <unotxdoc.hxx>
#include <doc.hxx>
#include <docsh.hxx>

#include <vcl/filter/PDFiumLibrary.hxx>
#include <vcl/pdfread.hxx>
#include <comphelper/propertyvalue.hxx>
#include <cmath>

using namespace ::com::sun::star;

namespace
{
/// Tests the PDF export filter.
class PdfExportTest2 : public UnoApiTest
{
protected:
    utl::MediaDescriptor aMediaDescriptor;

public:
    PdfExportTest2()
        : UnoApiTest(u"/vcl/qa/cppunit/pdfexport/data/"_ustr)
    {
    }

    void saveAsPDF(std::u16string_view rFile);
    void load(std::u16string_view rFile, vcl::filter::PDFDocument& rDocument);
};

void PdfExportTest2::saveAsPDF(std::u16string_view rFile)
{
    // Import the bugdoc and export as PDF.
    loadFromFile(rFile);
    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());
}

void PdfExportTest2::load(std::u16string_view rFile, vcl::filter::PDFDocument& rDocument)
{
    saveAsPDF(rFile);

    // Parse the export result.
    SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
    CPPUNIT_ASSERT(rDocument.Read(aStream));
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf160705)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"tdf160705.odt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    int nTable(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;

        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "StructElem")
        {
            auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
            if (pS && pS->GetValue() == "Table")
            {
                auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids);

                // In the first table, the caption element is the first child element
                // In the second table, the caption element is the last child element
                // In the third table, the caption element is the first child element
                int nId = 0;
                if (nTable == 1) // second table
                    nId = pKids->GetElements().size() - 1;
                else if (nTable == 2) // third table
                    nId = 0;

                auto pTableKids = pKids->GetElements();
                auto pRefKid = dynamic_cast<vcl::filter::PDFReferenceElement*>(pTableKids[nId]);
                CPPUNIT_ASSERT(pRefKid);
                auto pObj = pRefKid->LookupObject();
                CPPUNIT_ASSERT(pObj);
                auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObj->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1->GetValue());
                auto pS1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObj->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("TableCaption"_ostr, pS1->GetValue());

                auto pKids2 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObj->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids2);

                // The captions of Table1, Table2 and Table3 also have two standard elements
                CPPUNIT_ASSERT_EQUAL(size_t(2), pKids2->GetElements().size());

                ++nTable;
            }
        }
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf159895)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence({
        { "PDFUACompliance", uno::Any(true) },
        { "ExportFormFields", uno::Any(true) },
    }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"tdf159895.odt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObj = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObj)
            continue;

        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObj->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "XObject")
        {
            auto pFilter = dynamic_cast<vcl::filter::PDFNameElement*>(pObj->Lookup("Filter"_ostr));
            CPPUNIT_ASSERT(pFilter);

            vcl::filter::PDFStreamElement* pStream = pObj->GetStream();
            CPPUNIT_ASSERT(pStream);
            SvMemoryStream& rObjectStream = pStream->GetMemory();
            // Uncompress it.
            SvMemoryStream aUncompressed;
            ZCodec aZCodec;
            aZCodec.BeginCompression();
            rObjectStream.Seek(0);
            aZCodec.Decompress(rObjectStream, aUncompressed);
            CPPUNIT_ASSERT(aZCodec.EndCompression());

            auto pStart = static_cast<const char*>(aUncompressed.GetData());
            const char* const pEnd = pStart + aUncompressed.GetSize();

            OStringBuffer sText;
            while (true)
            {
                auto const pLine = ::std::find(pStart, pEnd, '\n');
                if (pLine == pEnd)
                    break;

                std::string_view const line(pStart, pLine - pStart);
                pStart = pLine + 1;
                if (!line.empty() && line[0] != '%')
                {
                    sText.append(line + "\n"_ostr);
                }
            }
            CPPUNIT_ASSERT_EQUAL("/Tx BMC\nEMC\n"_ostr, sText.toString());
        }
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf124272)
{
    // Import the bugdoc and export as PDF.
    vcl::filter::PDFDocument aDocument;
    load(u"tdf124272.odt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    // The page has a stream.
    vcl::filter::PDFObjectElement* pContents = aPages[0]->LookupObject("Contents"_ostr);
    CPPUNIT_ASSERT(pContents);
    vcl::filter::PDFStreamElement* pStream = pContents->GetStream();
    CPPUNIT_ASSERT(pStream);
    SvMemoryStream& rObjectStream = pStream->GetMemory();
    // Uncompress it.
    SvMemoryStream aUncompressed;
    ZCodec aZCodec;
    aZCodec.BeginCompression();
    rObjectStream.Seek(0);
    aZCodec.Decompress(rObjectStream, aUncompressed);
    CPPUNIT_ASSERT(aZCodec.EndCompression());

    OString aBitmap("Q q 299.9 782.19 m\n"
                    "55.2 435.89 l 299.9 435.89 l 299.9 782.19 l h"_ostr);

    auto pStart = static_cast<const char*>(aUncompressed.GetData());
    const char* pEnd = pStart + aUncompressed.GetSize();
    auto it = std::search(pStart, pEnd, aBitmap.getStr(), aBitmap.getStr() + aBitmap.getLength());
    CPPUNIT_ASSERT(it != pEnd);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf121615)
{
    vcl::filter::PDFDocument aDocument;
    load(u"tdf121615.odt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    // Get access to the only image on the only page.
    vcl::filter::PDFObjectElement* pResources = aPages[0]->LookupObject("Resources"_ostr);
    CPPUNIT_ASSERT(pResources);
    auto pXObjects
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pResources->Lookup("XObject"_ostr));
    CPPUNIT_ASSERT(pXObjects);
    CPPUNIT_ASSERT_EQUAL(size_t(1), pXObjects->GetItems().size());
    vcl::filter::PDFObjectElement* pXObject
        = pXObjects->LookupObject(pXObjects->GetItems().begin()->first);
    CPPUNIT_ASSERT(pXObject);
    vcl::filter::PDFStreamElement* pStream = pXObject->GetStream();
    CPPUNIT_ASSERT(pStream);
    SvMemoryStream& rObjectStream = pStream->GetMemory();

    // Load the embedded image.
    rObjectStream.Seek(0);
    GraphicFilter& rFilter = GraphicFilter::GetGraphicFilter();
    Graphic aGraphic;
    sal_uInt16 format;
    ErrCode bResult = rFilter.ImportGraphic(aGraphic, u"import", rObjectStream,
                                            GRFILTER_FORMAT_DONTKNOW, &format);
    CPPUNIT_ASSERT_EQUAL(ERRCODE_NONE, bResult);

    // The image should be grayscale 8bit JPEG.
    sal_uInt16 jpegFormat = rFilter.GetImportFormatNumberForShortName(JPG_SHORTNAME);
    CPPUNIT_ASSERT(jpegFormat != GRFILTER_FORMAT_NOTFOUND);
    CPPUNIT_ASSERT_EQUAL(jpegFormat, format);
    Bitmap aBitmap = aGraphic.GetBitmap();
    CPPUNIT_ASSERT_EQUAL(tools::Long(200), aBitmap.GetSizePixel().Width());
    CPPUNIT_ASSERT_EQUAL(tools::Long(300), aBitmap.GetSizePixel().Height());
    CPPUNIT_ASSERT_EQUAL(vcl::PixelFormat::N8_BPP, aBitmap.getPixelFormat());
    // tdf#121615 was caused by broken handling of data width with 8bit color,
    // so the test image has some black in the bottomright corner, check it's there
    CPPUNIT_ASSERT_EQUAL(COL_WHITE, aBitmap.GetPixelColor(0, 0));
    CPPUNIT_ASSERT_EQUAL(COL_WHITE, aBitmap.GetPixelColor(0, 299));
    CPPUNIT_ASSERT_EQUAL(COL_WHITE, aBitmap.GetPixelColor(199, 0));
    CPPUNIT_ASSERT_EQUAL(COL_BLACK, aBitmap.GetPixelColor(199, 299));
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf141171)
{
    vcl::filter::PDFDocument aDocument;
    load(u"tdf141171.odt", aDocument);

    int nImages(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Subtype"_ostr));
        if (pType && pType->GetValue() == "Image")
        {
            auto pBitsPerComponent = dynamic_cast<vcl::filter::PDFNumberElement*>(
                pObject->Lookup("BitsPerComponent"_ostr));
            CPPUNIT_ASSERT(pBitsPerComponent);
            CPPUNIT_ASSERT_EQUAL(8.0, pBitsPerComponent->GetValue());

            auto pWidth
                = dynamic_cast<vcl::filter::PDFNumberElement*>(pObject->Lookup("Width"_ostr));
            CPPUNIT_ASSERT(pWidth);
            CPPUNIT_ASSERT_EQUAL(878.0, pWidth->GetValue());

            auto pHeight
                = dynamic_cast<vcl::filter::PDFNumberElement*>(pObject->Lookup("Height"_ostr));
            CPPUNIT_ASSERT(pHeight);
            CPPUNIT_ASSERT_EQUAL(127.0, pHeight->GetValue());

            auto pColorSpace
                = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("ColorSpace"_ostr));
            CPPUNIT_ASSERT(pColorSpace);
            CPPUNIT_ASSERT_EQUAL("DeviceGray"_ostr, pColorSpace->GetValue());

            // Check the first image has a SMask
            if (nImages == 0)
                CPPUNIT_ASSERT(pObject->Lookup("SMask"_ostr));
            ++nImages;
        }
    }
    // Without the fix in place, there would have been only one image
    CPPUNIT_ASSERT_EQUAL(2, nImages);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf161346)
{
    // this crashed
    vcl::filter::PDFDocument aDocument;
    load(u"fdo47811-1_Word2013.docx", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(2), aPages.size());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf129085)
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence({
        // if the Quality is too high (like 90) then the image will be a DIB,
        // not a JPEG! 80 works currently but set it lower to be sure...
        { "Quality", uno::Any(sal_Int32(50)) },
    }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"tdf129085.docx", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    // Get access to the only image on the only page.
    vcl::filter::PDFObjectElement* pResources = aPages[0]->LookupObject("Resources"_ostr);
    CPPUNIT_ASSERT(pResources);
    auto pXObjects
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pResources->Lookup("XObject"_ostr));

    // Without the fix in place, this test would have failed here
    CPPUNIT_ASSERT(pXObjects);
    CPPUNIT_ASSERT_EQUAL(size_t(1), pXObjects->GetItems().size());
    vcl::filter::PDFObjectElement* pXObject
        = pXObjects->LookupObject(pXObjects->GetItems().begin()->first);
    CPPUNIT_ASSERT(pXObject);
    vcl::filter::PDFStreamElement* pStream = pXObject->GetStream();
    CPPUNIT_ASSERT(pStream);
    SvMemoryStream& rObjectStream = pStream->GetMemory();

    // Load the embedded image.
    rObjectStream.Seek(0);
    GraphicFilter& rFilter = GraphicFilter::GetGraphicFilter();
    Graphic aGraphic;
    sal_uInt16 format;
    ErrCode bResult = rFilter.ImportGraphic(aGraphic, u"import", rObjectStream,
                                            GRFILTER_FORMAT_DONTKNOW, &format);
    CPPUNIT_ASSERT_EQUAL(ERRCODE_NONE, bResult);

    sal_uInt16 jpegFormat = rFilter.GetImportFormatNumberForShortName(JPG_SHORTNAME);
    CPPUNIT_ASSERT(jpegFormat != GRFILTER_FORMAT_NOTFOUND);
    CPPUNIT_ASSERT_EQUAL(jpegFormat, format);
    Bitmap aBitmap = aGraphic.GetBitmap();
    CPPUNIT_ASSERT_EQUAL(tools::Long(884), aBitmap.GetSizePixel().Width());
    CPPUNIT_ASSERT_EQUAL(tools::Long(925), aBitmap.GetSizePixel().Height());
    CPPUNIT_ASSERT_EQUAL(vcl::PixelFormat::N24_BPP, aBitmap.getPixelFormat());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTocLink)
{
    // Load the Writer document.
    loadFromFile(u"toc-link.fodt");

    // Update the ToC.
    uno::Reference<text::XDocumentIndexesSupplier> xDocumentIndexesSupplier(mxComponent,
                                                                            uno::UNO_QUERY);
    CPPUNIT_ASSERT(xDocumentIndexesSupplier.is());

    uno::Reference<util::XRefreshable> xToc(
        xDocumentIndexesSupplier->getDocumentIndexes()->getByIndex(0), uno::UNO_QUERY);
    CPPUNIT_ASSERT(xToc.is());

    xToc->refresh();

    // Save as PDF.
    save(TestFilter::PDF_WRITER);

    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);

    // Ensure there is a link on the first page (in the ToC).
    // Without the accompanying fix in place, this test would have failed, as the page contained no
    // links.
    CPPUNIT_ASSERT(pPdfPage->hasLinks());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testReduceSmallImage)
{
    // Load the Writer document.
    saveAsPDF(u"reduce-small-image.fodt");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPdfPage->getObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Image, pPageObject->getType());

    // Make sure we don't scale down a tiny bitmap.
    std::unique_ptr<vcl::pdf::PDFiumBitmap> pBitmap = pPageObject->getImageBitmap();
    CPPUNIT_ASSERT(pBitmap);
    int nWidth = pBitmap->getWidth();
    int nHeight = pBitmap->getHeight();
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 16
    // - Actual  : 6
    // i.e. the image was scaled down to 300 DPI, even if it had tiny size.
    CPPUNIT_ASSERT_EQUAL(16, nWidth);
    CPPUNIT_ASSERT_EQUAL(16, nHeight);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf114256)
{
    saveAsPDF(u"tdf114256.ods");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);

    // Without the fix in place, this test would have failed with
    // - Expected: 13
    // - Actual  : 0
    CPPUNIT_ASSERT_EQUAL(13, pPdfPage->getObjectCount());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf150931)
{
    saveAsPDF(u"tdf150931.ods");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    // Without the fix in place, this test would have failed with
    // - Expected: 15
    // - Actual  : 16
    CPPUNIT_ASSERT_EQUAL(16, nPageObjectCount);

    int nYellowPathCount = 0;
    int nBlackPathCount = 0;
    int nGrayPathCount = 0;
    int nRedPathCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        std::unique_ptr<vcl::pdf::PDFiumPageObject> pPdfPageObject = pPdfPage->getObject(i);
        if (pPdfPageObject->getType() != vcl::pdf::PDFPageObjectType::Path)
            continue;

        int nSegments = pPdfPageObject->getPathSegmentCount();
        CPPUNIT_ASSERT_EQUAL(5, nSegments);

        if (pPdfPageObject->getFillColor() == COL_YELLOW)
            ++nYellowPathCount;
        else if (pPdfPageObject->getFillColor() == COL_BLACK)
            ++nBlackPathCount;
        else if (pPdfPageObject->getFillColor() == COL_GRAY)
            ++nGrayPathCount;
        else if (pPdfPageObject->getFillColor() == COL_LIGHTRED)
            ++nRedPathCount;
    }

    CPPUNIT_ASSERT_EQUAL(3, nYellowPathCount);
    CPPUNIT_ASSERT_EQUAL(3, nRedPathCount);
    CPPUNIT_ASSERT_EQUAL(3, nGrayPathCount);
    CPPUNIT_ASSERT_EQUAL(3, nBlackPathCount);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf147027)
{
    // FIXME: the DPI check should be removed when either (1) the test is fixed to work with
    // non-default DPI; or (2) unit tests on Windows are made to use svp VCL plugin.
    if (!IsDefaultDPI())
        return;

    // Load the Calc document.
    saveAsPDF(u"tdf147027.ods");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);

    // Without the fix in place, this test would have failed with
    // - Expected: 778
    // - Actual  : 40
    CPPUNIT_ASSERT_EQUAL(778, pPdfPage->getObjectCount());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf135346)
{
    // Load the Calc document.
    saveAsPDF(u"tdf135346.ods");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);

    // Without the fix in place, this test would have failed with
    // - Expected: 55
    // - Actual  : 0
    CPPUNIT_ASSERT_EQUAL(55, pPdfPage->getObjectCount());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf147164)
{
    saveAsPDF(u"tdf147164.odp");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(2, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/1);
    CPPUNIT_ASSERT(pPdfPage);

    // Without the fix in place, this test would have failed with
    // - Expected: 22
    // - Actual  : 16
    CPPUNIT_ASSERT_EQUAL(22, pPdfPage->getObjectCount());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testReduceImage)
{
    // Load the Writer document.
    loadFromFile(u"reduce-image.fodt");

    // Save as PDF.
    uno::Reference<css::lang::XMultiServiceFactory> xFactory = getMultiServiceFactory();
    uno::Reference<document::XFilter> xFilter(
        xFactory->createInstance(u"com.sun.star.document.PDFFilter"_ustr), uno::UNO_QUERY);
    uno::Reference<document::XExporter> xExporter(xFilter, uno::UNO_QUERY);
    xExporter->setSourceDocument(mxComponent);

    SvFileStream aOutputStream(maTempFile.GetURL(), StreamMode::WRITE);
    uno::Reference<io::XOutputStream> xOutputStream(new utl::OStreamWrapper(aOutputStream));

    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "ReduceImageResolution", uno::Any(false) } }));

    // This is intentionally in an "unlucky" order, output stream comes before filter data.
    uno::Sequence<beans::PropertyValue> aDescriptor(comphelper::InitPropertySequence({
        { "FilterName", uno::Any(u"writer_pdf_Export"_ustr) },
        { "OutputStream", uno::Any(xOutputStream) },
        { "FilterData", uno::Any(aFilterData) },
    }));
    xFilter->filter(aDescriptor);
    aOutputStream.Close();

    // Parse the PDF: get the image.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPdfPage->getObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Image, pPageObject->getType());

    // Make sure we don't scale down a bitmap.
    std::unique_ptr<vcl::pdf::PDFiumBitmap> pBitmap = pPageObject->getImageBitmap();
    CPPUNIT_ASSERT(pBitmap);
    int nWidth = pBitmap->getWidth();
    int nHeight = pBitmap->getHeight();
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 160
    // - Actual  : 6
    // i.e. the image was scaled down even with ReduceImageResolution=false.
    CPPUNIT_ASSERT_EQUAL(160, nWidth);
    CPPUNIT_ASSERT_EQUAL(160, nHeight);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testLinkWrongPage)
{
    // Import the bugdoc and export as PDF.
    saveAsPDF(u"link-wrong-page.odp");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    // The document has 2 pages.
    CPPUNIT_ASSERT_EQUAL(2, pPdfDocument->getPageCount());

    // First page should have 1 link (2nd slide, 1st was hidden).
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);

    // Without the accompanying fix in place, this test would have failed, as the link of the first
    // page went to the second page due to the hidden first slide.
    CPPUNIT_ASSERT(pPdfPage->hasLinks());

    // Second page should have no links (3rd slide).
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage2 = pPdfDocument->openPage(/*nIndex=*/1);
    CPPUNIT_ASSERT(pPdfPage2);
    CPPUNIT_ASSERT(!pPdfPage2->hasLinks());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testLinkWrongPagePartial)
{
    // Given a Draw document with 3 pages, a link on the 2nd page:
    // When exporting that the 2nd and 3rd page to pdf:
    uno::Sequence<beans::PropertyValue> aFilterData = {
        comphelper::makePropertyValue(u"PageRange"_ustr, u"2-3"_ustr),
    };
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    saveAsPDF(u"link-wrong-page-partial.odg");

    // Then make sure the we have a link on the 1st page, but not on the 2nd one:
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(2, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    // Without the accompanying fix in place, this test would have failed, as the link was on the
    // 2nd page instead.
    CPPUNIT_ASSERT(pPdfPage->hasLinks());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage2 = pPdfDocument->openPage(/*nIndex=*/1);
    CPPUNIT_ASSERT(pPdfPage2);
    CPPUNIT_ASSERT(!pPdfPage2->hasLinks());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testPageRange)
{
    // Given a document with 3 pages:
    // When exporting that document to PDF, skipping the first page:
    aMediaDescriptor[u"FilterOptions"_ustr]
        <<= u"{\"PageRange\":{\"type\":\"string\",\"value\":\"2-\"}}"_ustr;
    saveAsPDF(u"link-wrong-page-partial.odg");

    // Then make sure the resulting PDF has 2 pages:
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 2
    // - Actual  : 3
    // i.e. FilterOptions was ignored.
    CPPUNIT_ASSERT_EQUAL(2, pPdfDocument->getPageCount());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testLargePage)
{
    // Import the bugdoc and export as PDF.
    saveAsPDF(u"6m-wide.odg");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    // The document has 1 page.
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    // Check the value (not the unit) of the page size.
    basegfx::B2DSize aSize = pPdfDocument->getPageSize(0);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 8503.94
    // - Actual  : 17007.875
    // i.e. the value for 600 cm was larger than the 14 400 limit set in the spec.
    CPPUNIT_ASSERT_DOUBLES_EQUAL(8503.94, aSize.getWidth(), 0.01);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testPdfImageResourceInlineXObjectRef)
{
    // Create an empty document.
    loadFromURL(u"private:factory/swriter"_ustr);
    uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY);
    uno::Reference<text::XText> xText = xTextDocument->getText();
    uno::Reference<text::XTextCursor> xCursor = xText->createTextCursor();

    // Insert the PDF image.
    uno::Reference<lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xGraphicObject(
        xFactory->createInstance(u"com.sun.star.text.TextGraphicObject"_ustr), uno::UNO_QUERY);
    OUString aURL = createFileURL(u"pdf-image-resource-inline-xobject-ref.pdf");
    xGraphicObject->setPropertyValue(u"GraphicURL"_ustr, uno::Any(aURL));
    uno::Reference<drawing::XShape> xShape(xGraphicObject, uno::UNO_QUERY);
    xShape->setSize(awt::Size(1000, 1000));
    uno::Reference<text::XTextContent> xTextContent(xGraphicObject, uno::UNO_QUERY);
    xText->insertTextContent(xCursor->getStart(), xTextContent, /*bAbsorb=*/false);

    // Save as PDF.
    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

    // Parse the export result.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    // Make sure that the page -> form -> form has a child image.
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPdfPage->getObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Form, pPageObject->getType());
    // 2: white background and the actual object.
    CPPUNIT_ASSERT_EQUAL(2, pPageObject->getFormObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pFormObject = pPageObject->getFormObject(1);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Form, pFormObject->getType());
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1
    // - Actual  : 0
    // i.e. the sub-form was missing its image.
    CPPUNIT_ASSERT_EQUAL(1, pFormObject->getFormObjectCount());

    // Check if the inner form object (original page object in the pdf image) has the correct
    // rotation.
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pInnerFormObject = pFormObject->getFormObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Form, pInnerFormObject->getType());
    CPPUNIT_ASSERT_EQUAL(1, pInnerFormObject->getFormObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pImage = pInnerFormObject->getFormObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Image, pImage->getType());
    basegfx::B2DHomMatrix aMat = pInnerFormObject->getMatrix();
    basegfx::B2DTuple aScale;
    basegfx::B2DTuple aTranslate;
    double fRotate = 0;
    double fShearX = 0;
    aMat.decompose(aScale, aTranslate, fRotate, fShearX);
    int nRotateDeg = basegfx::rad2deg(fRotate);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: -90
    // - Actual  : 0
    // i.e. rotation was lost on pdf export.
    CPPUNIT_ASSERT_EQUAL(-90, nRotateDeg);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testDefaultVersion)
{
    // Create an empty document.
    loadFromURL(u"private:factory/swriter"_ustr);

    // Save as PDF.
    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

    // Parse the export result.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    int nFileVersion = pPdfDocument->getFileVersion();
    CPPUNIT_ASSERT_EQUAL(17, nFileVersion);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testVersion15)
{
    // Create an empty document.
    loadFromURL(u"private:factory/swriter"_ustr);

    // Save as PDF.
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { { "SelectPdfVersion", uno::Any(static_cast<sal_Int32>(15)) } }));
    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

    // Parse the export result.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    int nFileVersion = pPdfDocument->getFileVersion();
    CPPUNIT_ASSERT_EQUAL(15, nFileVersion);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testVersion20)
{
    // Create an empty document.
    mxComponent = loadFromDesktop("private:factory/swriter");

    // Save as PDF.
    uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY);
    uno::Sequence<beans::PropertyValue> aFilterData = comphelper::InitPropertySequence(
        { { "SelectPdfVersion", uno::Any(static_cast<sal_Int32>(20)) } });
    aMediaDescriptor["FilterName"] <<= OUString("writer_pdf_Export");
    aMediaDescriptor["FilterData"] <<= aFilterData;
    xStorable->storeToURL(maTempFile.GetURL(), aMediaDescriptor.getAsConstPropertyValueList());

    // Parse the export result.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    int nFileVersion = pPdfDocument->getFileVersion();
    CPPUNIT_ASSERT_EQUAL(20, nFileVersion);
}

// Check round-trip of importing and exporting the PDF with PDFium filter,
// which imports the PDF document as multiple PDFs as graphic object.
// Each page in the document has one PDF graphic object which content is
// the corresponding page in the PDF. When such a document is exported,
// the PDF graphic gets embedded into the exported PDF document (as a
// Form XObject).
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testMultiPagePDF)
{
// setenv only works on unix based systems
#ifndef _WIN32
    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        setenv("LO_IMPORT_USE_PDFIUM", "1", false);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            unsetenv("LO_IMPORT_USE_PDFIUM");
    });

    // Load the PDF and save as PDF
    vcl::filter::PDFDocument aDocument;
    load(u"SimpleMultiPagePDF.pdf", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(3), aPages.size());

    vcl::filter::PDFObjectElement* pResources = aPages[0]->LookupObject("Resources"_ostr);
    CPPUNIT_ASSERT(pResources);

    auto pXObjects
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pResources->Lookup("XObject"_ostr));
    CPPUNIT_ASSERT(pXObjects);

    CPPUNIT_ASSERT_EQUAL(size_t(3),
                         pXObjects->GetItems().size()); // 3 PDFs as Form XObjects

    std::vector<OString> rIDs;
    for (auto const& rPair : pXObjects->GetItems())
    {
        rIDs.push_back(rPair.first);
    }

    // Let's check the embedded PDF pages - just make sure the size differs,
    // which should indicate we don't have 3 times the same page.

    { // embedded PDF page 1
        vcl::filter::PDFObjectElement* pXObject1 = pXObjects->LookupObject(rIDs[0]);
        CPPUNIT_ASSERT(pXObject1);
        CPPUNIT_ASSERT_EQUAL("Im21"_ostr, rIDs[0]);

        auto pSubtype1
            = dynamic_cast<vcl::filter::PDFNameElement*>(pXObject1->Lookup("Subtype"_ostr));
        CPPUNIT_ASSERT(pSubtype1);
        CPPUNIT_ASSERT_EQUAL("Form"_ostr, pSubtype1->GetValue());

        auto pXObjectResources
            = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pXObject1->Lookup("Resources"_ostr));
        CPPUNIT_ASSERT(pXObjectResources);
        auto pXObjectForms = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
            pXObjectResources->LookupElement("XObject"_ostr));
        CPPUNIT_ASSERT(pXObjectForms);
        vcl::filter::PDFObjectElement* pForm
            = pXObjectForms->LookupObject(pXObjectForms->GetItems().begin()->first);
        CPPUNIT_ASSERT(pForm);

        vcl::filter::PDFStreamElement* pStream = pForm->GetStream();
        CPPUNIT_ASSERT(pStream);
        SvMemoryStream& rObjectStream = pStream->GetMemory();
        rObjectStream.Seek(STREAM_SEEK_TO_BEGIN);

        SvMemoryStream aUncompressed;
        ZCodec aZCodec;
        aZCodec.BeginCompression();
        aZCodec.Decompress(rObjectStream, aUncompressed);
        CPPUNIT_ASSERT(aZCodec.EndCompression());

        // Just check that the size of the page stream is what is expected.
        CPPUNIT_ASSERT_EQUAL(sal_uInt64(1218), aUncompressed.Tell());
    }

    { // embedded PDF page 2
        vcl::filter::PDFObjectElement* pXObject2 = pXObjects->LookupObject(rIDs[1]);
        CPPUNIT_ASSERT(pXObject2);
        CPPUNIT_ASSERT_EQUAL("Im27"_ostr, rIDs[1]);

        auto pSubtype2
            = dynamic_cast<vcl::filter::PDFNameElement*>(pXObject2->Lookup("Subtype"_ostr));
        CPPUNIT_ASSERT(pSubtype2);
        CPPUNIT_ASSERT_EQUAL("Form"_ostr, pSubtype2->GetValue());

        auto pXObjectResources
            = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pXObject2->Lookup("Resources"_ostr));
        CPPUNIT_ASSERT(pXObjectResources);
        auto pXObjectForms = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
            pXObjectResources->LookupElement("XObject"_ostr));
        CPPUNIT_ASSERT(pXObjectForms);
        vcl::filter::PDFObjectElement* pForm
            = pXObjectForms->LookupObject(pXObjectForms->GetItems().begin()->first);
        CPPUNIT_ASSERT(pForm);

        vcl::filter::PDFStreamElement* pStream = pForm->GetStream();
        CPPUNIT_ASSERT(pStream);
        SvMemoryStream& rObjectStream = pStream->GetMemory();
        rObjectStream.Seek(STREAM_SEEK_TO_BEGIN);

        SvMemoryStream aUncompressed;
        ZCodec aZCodec;
        aZCodec.BeginCompression();
        aZCodec.Decompress(rObjectStream, aUncompressed);
        CPPUNIT_ASSERT(aZCodec.EndCompression());

        // Just check that the size of the page stream is what is expected.
        CPPUNIT_ASSERT_EQUAL(sal_uInt64(3893), aUncompressed.Tell());
    }

    { // embedded PDF page 3
        vcl::filter::PDFObjectElement* pXObject3 = pXObjects->LookupObject(rIDs[2]);
        CPPUNIT_ASSERT(pXObject3);
        CPPUNIT_ASSERT_EQUAL("Im5"_ostr, rIDs[2]);

        auto pSubtype3
            = dynamic_cast<vcl::filter::PDFNameElement*>(pXObject3->Lookup("Subtype"_ostr));
        CPPUNIT_ASSERT(pSubtype3);
        CPPUNIT_ASSERT_EQUAL("Form"_ostr, pSubtype3->GetValue());

        auto pXObjectResources
            = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pXObject3->Lookup("Resources"_ostr));
        CPPUNIT_ASSERT(pXObjectResources);
        auto pXObjectForms = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
            pXObjectResources->LookupElement("XObject"_ostr));
        CPPUNIT_ASSERT(pXObjectForms);
        vcl::filter::PDFObjectElement* pForm
            = pXObjectForms->LookupObject(pXObjectForms->GetItems().begin()->first);
        CPPUNIT_ASSERT(pForm);

        vcl::filter::PDFStreamElement* pStream = pForm->GetStream();
        CPPUNIT_ASSERT(pStream);
        SvMemoryStream& rObjectStream = pStream->GetMemory();
        rObjectStream.Seek(STREAM_SEEK_TO_BEGIN);

        SvMemoryStream aUncompressed;
        ZCodec aZCodec;
        aZCodec.BeginCompression();
        aZCodec.Decompress(rObjectStream, aUncompressed);
        CPPUNIT_ASSERT(aZCodec.EndCompression());

        // Just check that the size of the page stream is what is expected.
        CPPUNIT_ASSERT_EQUAL(sal_uInt64(355), aUncompressed.Tell());
    }
#endif
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testFormFontName)
{
    // Import the bugdoc and export as PDF.
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence({
        { "ExportFormFields", uno::Any(true) },
    }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    saveAsPDF(u"form-font-name.odt");

    // Parse the export result with pdfium.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    // The document has one page.
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);

    // The page has one annotation.
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getAnnotationCount());
    std::unique_ptr<vcl::pdf::PDFiumAnnotation> pAnnot = pPdfPage->getAnnotation(0);

    // Examine the default appearance.
    CPPUNIT_ASSERT(pAnnot->hasKey("DA"_ostr));
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFObjectType::String, pAnnot->getValueType("DA"_ostr));
    OUString aDA = pAnnot->getString("DA"_ostr);

    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 0 0 0 rg /TiRo 12 Tf
    // - Actual  : 0 0 0 rg /F2 12 Tf
    // i.e. Liberation Serif was exposed as a form font as-is, without picking the closest built-in
    // font.
    CPPUNIT_ASSERT_EQUAL(u"0 0 0 rg /TiRo 12 Tf"_ustr, aDA);
}

// Check we don't have duplicated objects when we reexport the PDF multiple
// times or the size will exponentially increase over time.
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testReexportPDF)
{
// setenv only works on unix based systems
#ifndef _WIN32
    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        setenv("LO_IMPORT_USE_PDFIUM", "1", false);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            unsetenv("LO_IMPORT_USE_PDFIUM");
    });

    // Load the PDF and save as PDF
    vcl::filter::PDFDocument aDocument;
    load(u"PDFWithImages.pdf", aDocument);

    // Assert that the XObject in the page resources dictionary is a reference XObject.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();

    // The document has 2 pages.
    CPPUNIT_ASSERT_EQUAL(size_t(2), aPages.size());

    // PAGE 1
    {
        vcl::filter::PDFObjectElement* pResources = aPages[0]->LookupObject("Resources"_ostr);
        CPPUNIT_ASSERT(pResources);

        auto pXObjects
            = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pResources->Lookup("XObject"_ostr));
        CPPUNIT_ASSERT(pXObjects);

        std::vector<OString> rIDs;
        for (auto const& rPair : pXObjects->GetItems())
            rIDs.push_back(rPair.first);

        CPPUNIT_ASSERT_EQUAL(size_t(2), rIDs.size());

        std::vector<int> aBitmapRefs1;
        std::vector<int> aBitmapRefs2;

        {
            // FORM object 1
            OString aID = rIDs[0];
            CPPUNIT_ASSERT_EQUAL("Im14"_ostr, aID);
            vcl::filter::PDFObjectElement* pXObject = pXObjects->LookupObject(aID);
            CPPUNIT_ASSERT(pXObject);

            auto pSubtype
                = dynamic_cast<vcl::filter::PDFNameElement*>(pXObject->Lookup("Subtype"_ostr));
            CPPUNIT_ASSERT(pSubtype);
            CPPUNIT_ASSERT_EQUAL("Form"_ostr, pSubtype->GetValue());

            auto pInnerResources = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                pXObject->Lookup("Resources"_ostr));
            CPPUNIT_ASSERT(pInnerResources);
            auto pInnerXObjects = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                pInnerResources->LookupElement("XObject"_ostr));
            CPPUNIT_ASSERT(pInnerXObjects);
            CPPUNIT_ASSERT_EQUAL(size_t(1), pInnerXObjects->GetItems().size());
            OString aInnerObjectID = pInnerXObjects->GetItems().begin()->first;
            CPPUNIT_ASSERT_EQUAL("Im15"_ostr, aInnerObjectID);

            vcl::filter::PDFObjectElement* pInnerXObject
                = pInnerXObjects->LookupObject(aInnerObjectID);
            CPPUNIT_ASSERT(pInnerXObject);

            auto pInnerSubtype
                = dynamic_cast<vcl::filter::PDFNameElement*>(pInnerXObject->Lookup("Subtype"_ostr));
            CPPUNIT_ASSERT(pInnerSubtype);
            CPPUNIT_ASSERT_EQUAL("Form"_ostr, pInnerSubtype->GetValue());

            auto pInnerInnerResources = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                pInnerXObject->Lookup("Resources"_ostr));
            CPPUNIT_ASSERT(pInnerInnerResources);
            auto pInnerInnerXObjects = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                pInnerInnerResources->LookupElement("XObject"_ostr));
            CPPUNIT_ASSERT(pInnerInnerXObjects);
            CPPUNIT_ASSERT_EQUAL(size_t(2), pInnerInnerXObjects->GetItems().size());

            std::vector<OString> aBitmapIDs1;
            for (auto const& rPair : pInnerInnerXObjects->GetItems())
                aBitmapIDs1.push_back(rPair.first);

            {
                CPPUNIT_ASSERT_EQUAL("Im11"_ostr, aBitmapIDs1[0]);
                auto pRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                    pInnerInnerXObjects->LookupElement(aBitmapIDs1[0]));
                CPPUNIT_ASSERT(pRef);
                aBitmapRefs1.push_back(pRef->GetObjectValue());
                CPPUNIT_ASSERT_EQUAL(0, pRef->GetGenerationValue());

                vcl::filter::PDFObjectElement* pBitmap
                    = pInnerInnerXObjects->LookupObject(aBitmapIDs1[0]);
                CPPUNIT_ASSERT(pBitmap);
                auto pBitmapSubtype
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pBitmap->Lookup("Subtype"_ostr));
                CPPUNIT_ASSERT(pBitmapSubtype);
                CPPUNIT_ASSERT_EQUAL("Image"_ostr, pBitmapSubtype->GetValue());
            }
            {
                CPPUNIT_ASSERT_EQUAL("Im5"_ostr, aBitmapIDs1[1]);
                auto pRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                    pInnerInnerXObjects->LookupElement(aBitmapIDs1[1]));
                CPPUNIT_ASSERT(pRef);
                aBitmapRefs1.push_back(pRef->GetObjectValue());
                CPPUNIT_ASSERT_EQUAL(0, pRef->GetGenerationValue());

                vcl::filter::PDFObjectElement* pBitmap
                    = pInnerInnerXObjects->LookupObject(aBitmapIDs1[1]);
                CPPUNIT_ASSERT(pBitmap);
                auto pBitmapSubtype
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pBitmap->Lookup("Subtype"_ostr));
                CPPUNIT_ASSERT(pBitmapSubtype);
                CPPUNIT_ASSERT_EQUAL("Image"_ostr, pBitmapSubtype->GetValue());
            }
        }

        {
            // FORM object 2
            OString aID = rIDs[1];
            CPPUNIT_ASSERT_EQUAL("Im5"_ostr, aID);
            vcl::filter::PDFObjectElement* pXObject = pXObjects->LookupObject(aID);
            CPPUNIT_ASSERT(pXObject);

            auto pSubtype
                = dynamic_cast<vcl::filter::PDFNameElement*>(pXObject->Lookup("Subtype"_ostr));
            CPPUNIT_ASSERT(pSubtype);
            CPPUNIT_ASSERT_EQUAL("Form"_ostr, pSubtype->GetValue());

            auto pInnerResources = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                pXObject->Lookup("Resources"_ostr));
            CPPUNIT_ASSERT(pInnerResources);
            auto pInnerXObjects = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                pInnerResources->LookupElement("XObject"_ostr));
            CPPUNIT_ASSERT(pInnerXObjects);
            CPPUNIT_ASSERT_EQUAL(size_t(1), pInnerXObjects->GetItems().size());
            OString aInnerObjectID = pInnerXObjects->GetItems().begin()->first;
            CPPUNIT_ASSERT_EQUAL("Im6"_ostr, aInnerObjectID);

            vcl::filter::PDFObjectElement* pInnerXObject
                = pInnerXObjects->LookupObject(aInnerObjectID);
            CPPUNIT_ASSERT(pInnerXObject);

            auto pInnerSubtype
                = dynamic_cast<vcl::filter::PDFNameElement*>(pInnerXObject->Lookup("Subtype"_ostr));
            CPPUNIT_ASSERT(pInnerSubtype);
            CPPUNIT_ASSERT_EQUAL("Form"_ostr, pInnerSubtype->GetValue());

            auto pInnerInnerResources = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                pInnerXObject->Lookup("Resources"_ostr));
            CPPUNIT_ASSERT(pInnerInnerResources);
            auto pInnerInnerXObjects = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                pInnerInnerResources->LookupElement("XObject"_ostr));
            CPPUNIT_ASSERT(pInnerInnerXObjects);
            CPPUNIT_ASSERT_EQUAL(size_t(2), pInnerInnerXObjects->GetItems().size());

            std::vector<OString> aBitmapIDs2;
            for (auto const& rPair : pInnerInnerXObjects->GetItems())
                aBitmapIDs2.push_back(rPair.first);

            {
                CPPUNIT_ASSERT_EQUAL("Im11"_ostr, aBitmapIDs2[0]);
                auto pRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                    pInnerInnerXObjects->LookupElement(aBitmapIDs2[0]));
                CPPUNIT_ASSERT(pRef);
                aBitmapRefs2.push_back(pRef->GetObjectValue());
                CPPUNIT_ASSERT_EQUAL(0, pRef->GetGenerationValue());

                vcl::filter::PDFObjectElement* pBitmap
                    = pInnerInnerXObjects->LookupObject(aBitmapIDs2[0]);
                CPPUNIT_ASSERT(pBitmap);
                auto pBitmapSubtype
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pBitmap->Lookup("Subtype"_ostr));
                CPPUNIT_ASSERT(pBitmapSubtype);
                CPPUNIT_ASSERT_EQUAL("Image"_ostr, pBitmapSubtype->GetValue());
            }
            {
                CPPUNIT_ASSERT_EQUAL("Im5"_ostr, aBitmapIDs2[1]);
                auto pRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                    pInnerInnerXObjects->LookupElement(aBitmapIDs2[1]));
                CPPUNIT_ASSERT(pRef);
                aBitmapRefs2.push_back(pRef->GetObjectValue());
                CPPUNIT_ASSERT_EQUAL(0, pRef->GetGenerationValue());

                vcl::filter::PDFObjectElement* pBitmap
                    = pInnerInnerXObjects->LookupObject(aBitmapIDs2[1]);
                CPPUNIT_ASSERT(pBitmap);
                auto pBitmapSubtype
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pBitmap->Lookup("Subtype"_ostr));
                CPPUNIT_ASSERT(pBitmapSubtype);
                CPPUNIT_ASSERT_EQUAL("Image"_ostr, pBitmapSubtype->GetValue());
            }
        }
        // Ref should point to the same bitmap
        CPPUNIT_ASSERT_EQUAL(aBitmapRefs1[0], aBitmapRefs2[0]);
        CPPUNIT_ASSERT_EQUAL(aBitmapRefs1[1], aBitmapRefs2[1]);
    }

#endif
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf160117)
{
    vcl::filter::PDFDocument aDocument;
    load(u"tdf160117.ods", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    int nCount = 0;
    bool bFound1 = false;
    bool bFound2 = false;
    bool bFound3 = false;
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "FontDescriptor")
        {
            auto pFontName
                = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("FontName"_ostr));
            CPPUNIT_ASSERT(pFontName);
            if ("CAAAAA+LiberationSans-Bold"_ostr == pFontName->GetValue())
                bFound1 = true;
            else if ("DAAAAA+LiberationSans-Italic"_ostr == pFontName->GetValue())
                bFound2 = true;
            else if ("BAAAAA+LiberationSans"_ostr == pFontName->GetValue())
                bFound3 = true;
            ++nCount;
        }
    }
    // Without the fix in place, this test would have failed with
    // - Expected: 3
    // - Actual  : 2
    CPPUNIT_ASSERT_EQUAL(3, nCount);
    CPPUNIT_ASSERT(bFound1);
    CPPUNIT_ASSERT(bFound2);
    CPPUNIT_ASSERT(bFound3);
}

// Check we correctly copy more complex resources (Fonts describing
// glyphs in recursive arrays) to the target PDF
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testReexportDocumentWithComplexResources)
{
// setenv only works on unix based systems
#ifndef _WIN32
    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        setenv("LO_IMPORT_USE_PDFIUM", "1", false);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            unsetenv("LO_IMPORT_USE_PDFIUM");
    });

    // Load the PDF and save as PDF
    vcl::filter::PDFDocument aDocument;
    load(u"ComplexContentDictionary.pdf", aDocument);

    // Assert that the XObject in the page resources dictionary is a reference XObject.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    // Go directly to the Font object (24 0) (number could change if we change how PDF export works)
    auto pFont = aDocument.LookupObject(24);
    CPPUNIT_ASSERT(pFont);

    // Check it is the Font object (Type = Font)
    auto pName = dynamic_cast<vcl::filter::PDFNameElement*>(
        pFont->GetDictionary()->LookupElement("Type"_ostr));
    CPPUNIT_ASSERT(pName);
    CPPUNIT_ASSERT_EQUAL("Font"_ostr, pName->GetValue());

    // Check BaseFont is what we expect
    auto pBaseFont = dynamic_cast<vcl::filter::PDFNameElement*>(
        pFont->GetDictionary()->LookupElement("BaseFont"_ostr));
    CPPUNIT_ASSERT(pBaseFont);
    CPPUNIT_ASSERT_EQUAL("HOTOMR+Calibri,Italic"_ostr, pBaseFont->GetValue());

    // Check and get the W array
    auto pWArray = dynamic_cast<vcl::filter::PDFArrayElement*>(
        pFont->GetDictionary()->LookupElement("W"_ostr));
    CPPUNIT_ASSERT(pWArray);
    CPPUNIT_ASSERT_EQUAL(size_t(26), pWArray->GetElements().size());

    // Check the content of W array
    // ObjectCopier didn't copy this array correctly and the document
    // had glyphs at the wrong places
    {
        // first 2 elements
        auto pNumberAtIndex0 = dynamic_cast<vcl::filter::PDFNumberElement*>(pWArray->GetElement(0));
        CPPUNIT_ASSERT(pNumberAtIndex0);
        CPPUNIT_ASSERT_EQUAL(3.0, pNumberAtIndex0->GetValue());

        auto pArrayAtIndex1 = dynamic_cast<vcl::filter::PDFArrayElement*>(pWArray->GetElement(1));
        CPPUNIT_ASSERT(pArrayAtIndex1);
        CPPUNIT_ASSERT_EQUAL(size_t(1), pArrayAtIndex1->GetElements().size());

        {
            auto pNumber
                = dynamic_cast<vcl::filter::PDFNumberElement*>(pArrayAtIndex1->GetElement(0));
            CPPUNIT_ASSERT(pNumber);
            CPPUNIT_ASSERT_EQUAL(226.0, pNumber->GetValue());
        }

        // last 2 elements
        auto pNumberAtIndex24
            = dynamic_cast<vcl::filter::PDFNumberElement*>(pWArray->GetElement(24));
        CPPUNIT_ASSERT(pNumberAtIndex24);
        CPPUNIT_ASSERT_EQUAL(894.0, pNumberAtIndex24->GetValue());

        auto pArrayAtIndex25 = dynamic_cast<vcl::filter::PDFArrayElement*>(pWArray->GetElement(25));
        CPPUNIT_ASSERT(pArrayAtIndex25);
        CPPUNIT_ASSERT_EQUAL(size_t(2), pArrayAtIndex25->GetElements().size());

        {
            auto pNumber1
                = dynamic_cast<vcl::filter::PDFNumberElement*>(pArrayAtIndex25->GetElement(0));
            CPPUNIT_ASSERT(pNumber1);
            CPPUNIT_ASSERT_EQUAL(303.0, pNumber1->GetValue());

            auto pNumber2
                = dynamic_cast<vcl::filter::PDFNumberElement*>(pArrayAtIndex25->GetElement(1));
            CPPUNIT_ASSERT(pNumber2);
            CPPUNIT_ASSERT_EQUAL(303.0, pNumber2->GetValue());
        }
    }
#endif
}

// Tests that at export the PDF has the PDF/UA metadata properly set
// when we enable PDF/UA support.
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testPdfUaMetadata)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"BrownFoxLazyDog.odt", aDocument);

    auto* pCatalog = aDocument.GetCatalog();
    CPPUNIT_ASSERT(pCatalog);
    auto* pCatalogDictionary = pCatalog->GetDictionary();
    CPPUNIT_ASSERT(pCatalogDictionary);
    auto* pMetadataObject = pCatalogDictionary->LookupObject("Metadata"_ostr);
    CPPUNIT_ASSERT(pMetadataObject);
    auto* pMetadataDictionary = pMetadataObject->GetDictionary();
    auto* pType = dynamic_cast<vcl::filter::PDFNameElement*>(
        pMetadataDictionary->LookupElement("Type"_ostr));
    CPPUNIT_ASSERT(pType);
    CPPUNIT_ASSERT_EQUAL("Metadata"_ostr, pType->GetValue());

    auto* pStreamObject = pMetadataObject->GetStream();
    CPPUNIT_ASSERT(pStreamObject);
    auto& rStream = pStreamObject->GetMemory();
    rStream.Seek(0);

    // Search for the PDF/UA marker in the metadata

    tools::XmlWalker aWalker;
    CPPUNIT_ASSERT(aWalker.open(&rStream));
    CPPUNIT_ASSERT_EQUAL(std::string_view("xmpmeta"), aWalker.name());

    bool bPdfUaMarkerFound = false;
    OString aPdfUaPart;

    aWalker.children();
    while (aWalker.isValid())
    {
        if (aWalker.name() == "RDF"
            && aWalker.namespaceHref() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
        {
            aWalker.children();
            while (aWalker.isValid())
            {
                if (aWalker.name() == "Description"
                    && aWalker.namespaceHref() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
                {
                    aWalker.children();
                    while (aWalker.isValid())
                    {
                        if (aWalker.name() == "part"
                            && aWalker.namespaceHref() == "http://www.aiim.org/pdfua/ns/id/")
                        {
                            aPdfUaPart = aWalker.content();
                            bPdfUaMarkerFound = true;
                        }
                        aWalker.next();
                    }
                    aWalker.parent();
                }
                aWalker.next();
            }
            aWalker.parent();
        }
        aWalker.next();
    }
    aWalker.parent();

    CPPUNIT_ASSERT(bPdfUaMarkerFound);
    CPPUNIT_ASSERT_EQUAL("1"_ostr, aPdfUaPart);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf139736)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) },
                                           { "SelectPdfVersion", uno::Any(sal_Int32(17)) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"tdf139736-1.odt", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    vcl::filter::PDFObjectElement* pContents = aPages[0]->LookupObject("Contents"_ostr);
    CPPUNIT_ASSERT(pContents);
    vcl::filter::PDFStreamElement* pStream = pContents->GetStream();
    CPPUNIT_ASSERT(pStream);
    SvMemoryStream& rObjectStream = pStream->GetMemory();
    // Uncompress it.
    SvMemoryStream aUncompressed;
    ZCodec aZCodec;
    aZCodec.BeginCompression();
    rObjectStream.Seek(0);
    aZCodec.Decompress(rObjectStream, aUncompressed);
    CPPUNIT_ASSERT(aZCodec.EndCompression());

    auto pStart = static_cast<const char*>(aUncompressed.GetData());
    const char* const pEnd = pStart + aUncompressed.GetSize();

    enum
    {
        Default,
        Artifact,
        ArtifactProps1,
        ArtifactProps2,
        Tagged
    } state
        = Default;

    int nLine(0);
    int nTagged(0);
    int nArtifacts(0);
    while (true)
    {
        ++nLine;
        auto const pLine = ::std::find(pStart, pEnd, '\n');
        if (pLine == pEnd)
        {
            break;
        }
        std::string_view const line(pStart, pLine - pStart);
        pStart = pLine + 1;
        if (!line.empty() && line[0] != '%')
        {
            ::std::cerr << nLine << ": " << line << "\n";
            if (line == "/Artifact BMC")
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                state = Artifact;
                ++nArtifacts;
            }
            else if (o3tl::starts_with(line, "/Artifact <<"))
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                // check header/footer properties
                CPPUNIT_ASSERT_EQUAL(std::string_view("/Type/Pagination"), line.substr(12));
                state = ArtifactProps1;
                ++nArtifacts;
            }
            else if (state == ArtifactProps1)
            {
                CPPUNIT_ASSERT_EQUAL(std::string_view("/Subtype/Header"), line);
                state = ArtifactProps2;
            }
            else if (state == ArtifactProps2 && line == ">> BDC")
            {
                state = Artifact;
            }
            else if (line == "/Standard<</MCID 0>>BDC")
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                state = Tagged;
                ++nTagged;
            }
            else if (line == "EMC")
            {
                CPPUNIT_ASSERT_MESSAGE("unexpected end", state != Default);
                state = Default;
            }
            else if (nLine > 1) // first line is expected "0.1 w"
            {
                CPPUNIT_ASSERT_MESSAGE("unexpected content outside MCS", state != Default);
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL_MESSAGE("unclosed MCS", Default, state);
    CPPUNIT_ASSERT_EQUAL(1, nTagged); // text in body
    // 1 image and 1 frame and 1 header text; arbitrary number of aux stuff like borders
    CPPUNIT_ASSERT_GREATEREQUAL(3, nArtifacts);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf152231)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) },
                                           { "ExportNotesInMargin", uno::Any(true) },
                                           { "SelectPdfVersion", uno::Any(sal_Int32(17)) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    vcl::filter::PDFDocument aDocument;
    load(u"tdf152231.fodt", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    vcl::filter::PDFObjectElement* pContents = aPages[0]->LookupObject("Contents"_ostr);
    CPPUNIT_ASSERT(pContents);
    vcl::filter::PDFStreamElement* pStream = pContents->GetStream();
    CPPUNIT_ASSERT(pStream);
    SvMemoryStream& rObjectStream = pStream->GetMemory();
    // Uncompress it.
    SvMemoryStream aUncompressed;
    ZCodec aZCodec;
    aZCodec.BeginCompression();
    rObjectStream.Seek(0);
    aZCodec.Decompress(rObjectStream, aUncompressed);
    CPPUNIT_ASSERT(aZCodec.EndCompression());

    auto pStart = static_cast<const char*>(aUncompressed.GetData());
    const char* const pEnd = pStart + aUncompressed.GetSize();

    enum
    {
        Default,
        Artifact,
        Tagged
    } state
        = Default;

    int nLine(0);
    int nTagged(0);
    int nArtifacts(0);
    while (true)
    {
        ++nLine;
        auto const pLine = ::std::find(pStart, pEnd, '\n');
        if (pLine == pEnd)
        {
            break;
        }
        std::string_view const line(pStart, pLine - pStart);
        pStart = pLine + 1;
        if (!line.empty() && line[0] != '%')
        {
            ::std::cerr << nLine << ": " << line << "\n";
            if (line == "/Artifact BMC")
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                state = Artifact;
                ++nArtifacts;
            }
            else if (o3tl::starts_with(line, "/Standard<</MCID "))
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                state = Tagged;
                ++nTagged;
            }
            else if (line == "EMC")
            {
                CPPUNIT_ASSERT_MESSAGE("unexpected end", state != Default);
                state = Default;
            }
            else if (nLine > 1) // first line is expected "0.1 w"
            {
                CPPUNIT_ASSERT_MESSAGE("unexpected content outside MCS", state != Default);
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL_MESSAGE("unclosed MCS", Default, state);
    CPPUNIT_ASSERT_GREATEREQUAL(12, nTagged); // text in body
    // 1 annotation
    CPPUNIT_ASSERT_GREATEREQUAL(1, nArtifacts);

    int nPara(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "StructElem")
        {
            auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
            if (pS && pS->GetValue() == "Standard")
            {
                ++nPara;
                auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids);
                // one problem was that some StructElem were missing kids
                CPPUNIT_ASSERT(!pKids->GetElements().empty());
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL(12, nPara);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf152235)
{
    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { { "PDFUACompliance", uno::Any(true) },
          { "Watermark", uno::Any(u"kendy"_ustr) },
          // need to set a font to avoid assertions about missing "Helvetica"
          { "WatermarkFontName", uno::Any(u"Liberation Sans"_ustr) },
          { "SelectPdfVersion", uno::Any(sal_Int32(17)) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    loadFromURL(u"private:factory/swriter"_ustr);
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

    vcl::filter::PDFDocument aDocument;
    SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
    CPPUNIT_ASSERT(aDocument.Read(aStream));

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    vcl::filter::PDFObjectElement* pContents = aPages[0]->LookupObject("Contents"_ostr);
    CPPUNIT_ASSERT(pContents);
    vcl::filter::PDFStreamElement* pStream = pContents->GetStream();
    CPPUNIT_ASSERT(pStream);
    SvMemoryStream& rObjectStream = pStream->GetMemory();
    // Uncompress it.
    SvMemoryStream aUncompressed;
    ZCodec aZCodec;
    aZCodec.BeginCompression();
    rObjectStream.Seek(0);
    aZCodec.Decompress(rObjectStream, aUncompressed);
    CPPUNIT_ASSERT(aZCodec.EndCompression());

    auto pStart = static_cast<const char*>(aUncompressed.GetData());
    const char* const pEnd = pStart + aUncompressed.GetSize();

    enum
    {
        Default,
        Artifact,
        Tagged
    } state
        = Default;

    int nLine(0);
    int nTagged(0);
    int nArtifacts(0);
    while (true)
    {
        ++nLine;
        auto const pLine = ::std::find(pStart, pEnd, '\n');
        if (pLine == pEnd)
        {
            break;
        }
        std::string_view const line(pStart, pLine - pStart);
        pStart = pLine + 1;
        if (!line.empty() && line[0] != '%')
        {
            ::std::cerr << nLine << ": " << line << "\n";
            if (o3tl::starts_with(line, "/Artifact "))
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                state = Artifact;
                ++nArtifacts;
            }
            else if (o3tl::starts_with(line, "/Standard<</MCID "))
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                state = Tagged;
                ++nTagged;
            }
            else if (line == "EMC")
            {
                CPPUNIT_ASSERT_MESSAGE("unexpected end", state != Default);
                state = Default;
            }
            else if (nLine > 1) // first line is expected "0.1 w"
            {
                CPPUNIT_ASSERT_MESSAGE("unexpected content outside MCS", state != Default);
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL_MESSAGE("unclosed MCS", Default, state);
    CPPUNIT_ASSERT_GREATEREQUAL(0, nTagged); // text in body
    CPPUNIT_ASSERT_GREATEREQUAL(2, nArtifacts); // 1 watermark + 1 other thing
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf149140)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    vcl::filter::PDFDocument aDocument;
    load(u"TableTH_test_LibreOfficeWriter7.3.3_HeaderRow-HeadersInTopRow.fodt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    int nTH(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "StructElem")
        {
            auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
            if (pS && pS->GetValue() == "TH")
            {
                int nTable(0);
                auto pAttrs
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject->Lookup("A"_ostr));
                CPPUNIT_ASSERT(pAttrs != nullptr);
                for (const auto& rAttrRef : pAttrs->GetElements())
                {
                    auto pAttrDict = dynamic_cast<vcl::filter::PDFDictionaryElement*>(rAttrRef);
                    CPPUNIT_ASSERT(pAttrDict != nullptr);
                    auto pOwner = dynamic_cast<vcl::filter::PDFNameElement*>(
                        pAttrDict->LookupElement("O"_ostr));
                    CPPUNIT_ASSERT(pOwner != nullptr);
                    if (pOwner->GetValue() == "Table")
                    {
                        auto pScope = dynamic_cast<vcl::filter::PDFNameElement*>(
                            pAttrDict->LookupElement("Scope"_ostr));
                        CPPUNIT_ASSERT(pScope != nullptr);
                        CPPUNIT_ASSERT_EQUAL("Column"_ostr, pScope->GetValue());
                        ++nTable;
                    }
                }
                CPPUNIT_ASSERT_EQUAL(1, nTable);
                ++nTH;
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL(6, nTH);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testNestedSection)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"nestedsection.fodt", aDocument);

    // the assert needs 2 follows to reproduce => 3 pages
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(3), aPages.size());

    int nDoc(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructElem")
        {
            auto pS1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("S"_ostr));
            if (pS1 && pS1->GetValue() == "Document")
            {
                auto pKids1
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids1);
                // assume there are no MCID ref at this level
                auto pKids1v = pKids1->GetElements();
                auto pRefKid10 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids1v[0]);
                CPPUNIT_ASSERT(pRefKid10);
                auto pObject10 = pRefKid10->LookupObject();
                CPPUNIT_ASSERT(pObject10);
                auto pType10
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType10->GetValue());
                auto pS10 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Sect"_ostr, pS10->GetValue());

                auto pKids10
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject10->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids10);
                // assume there are no MCID ref at this level
                auto pKids10v = pKids10->GetElements();

                auto pRefKid100 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids10v[0]);
                CPPUNIT_ASSERT(pRefKid100);
                auto pObject100 = pRefKid100->LookupObject();
                CPPUNIT_ASSERT(pObject100);
                auto pType100
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject100->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType100->GetValue());
                auto pS100
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject100->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS100->GetValue());

                auto pRefKid101 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids10v[1]);
                CPPUNIT_ASSERT(pRefKid101);
                auto pObject101 = pRefKid101->LookupObject();
                CPPUNIT_ASSERT(pObject101);
                auto pType101
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject101->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType101->GetValue());
                auto pS101
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject101->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS101->GetValue());

                auto pRefKid102 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids10v[2]);
                CPPUNIT_ASSERT(pRefKid102);
                auto pObject102 = pRefKid102->LookupObject();
                CPPUNIT_ASSERT(pObject102);
                auto pType102
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject102->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType102->GetValue());
                auto pS102
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject102->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Sect"_ostr, pS102->GetValue());

                auto pKids102
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject102->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids102);
                // assume there are no MCID ref at this level
                auto pKids102v = pKids102->GetElements();

                auto pRefKid1020 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids102v[0]);
                CPPUNIT_ASSERT(pRefKid1020);
                auto pObject1020 = pRefKid1020->LookupObject();
                CPPUNIT_ASSERT(pObject1020);
                auto pType1020
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1020->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1020->GetValue());
                auto pS1020
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1020->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS1020->GetValue());

                CPPUNIT_ASSERT_EQUAL(size_t(1), pKids102v.size());

                auto pRefKid103 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids10v[3]);
                CPPUNIT_ASSERT(pRefKid103);
                auto pObject103 = pRefKid103->LookupObject();
                CPPUNIT_ASSERT(pObject103);
                auto pType103
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject103->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType103->GetValue());
                auto pS103
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject103->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS103->GetValue());

                auto pRefKid104 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids10v[4]);
                CPPUNIT_ASSERT(pRefKid104);
                auto pObject104 = pRefKid104->LookupObject();
                CPPUNIT_ASSERT(pObject104);
                auto pType104
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject104->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType104->GetValue());
                auto pS104
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject104->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS104->GetValue());

                CPPUNIT_ASSERT_EQUAL(size_t(5), pKids10v.size());

                auto pRefKid11 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids1v[1]);
                CPPUNIT_ASSERT(pRefKid11);
                auto pObject11 = pRefKid11->LookupObject();
                CPPUNIT_ASSERT(pObject11);
                auto pType11
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject11->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType11->GetValue());
                auto pS11 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject11->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS11->GetValue());

                CPPUNIT_ASSERT_EQUAL(size_t(2), pKids1v.size());
                ++nDoc;
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nDoc);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf157817)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"SimpleTOC.fodt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(2), aPages.size());

    vcl::filter::PDFObjectElement* pTOC(nullptr);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructElem")
        {
            auto pS1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("S"_ostr));
            if (pS1 && pS1->GetValue() == "TOC")
            {
                pTOC = pObject1;
            }
        }
    }
    CPPUNIT_ASSERT(pTOC);

    auto pKidsT = dynamic_cast<vcl::filter::PDFArrayElement*>(pTOC->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsT);
    // assume there are no MCID ref at this level
    auto pKidsTv = pKidsT->GetElements();
    auto pRefKidT0 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsTv[0]);
    CPPUNIT_ASSERT(pRefKidT0);
    auto pObjectT0 = pRefKidT0->LookupObject();
    CPPUNIT_ASSERT(pObjectT0);
    auto pTypeT0 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT0->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT0->GetValue());
    auto pST0 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT0->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Caption"_ostr, pST0->GetValue());

    auto pKidsT0 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectT0->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsT0);
    auto pKidsT0v = pKidsT0->GetElements();
    auto pRefKidT00 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsT0v[0]);
    CPPUNIT_ASSERT(pRefKidT00);
    auto pObjectT00 = pRefKidT00->LookupObject();
    CPPUNIT_ASSERT(pObjectT00);
    auto pTypeT00 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT00->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT00->GetValue());
    auto pST00 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT00->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Contents#20Heading"_ostr, pST00->GetValue());

    auto pRefKidT1 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsTv[1]);
    CPPUNIT_ASSERT(pRefKidT1);
    auto pObjectT1 = pRefKidT1->LookupObject();
    CPPUNIT_ASSERT(pObjectT1);
    auto pTypeT1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT1->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT1->GetValue());
    auto pST1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT1->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("TOCI"_ostr, pST1->GetValue());

    auto pKidsT1 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectT1->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsT1);
    auto pKidsT1v = pKidsT1->GetElements();

    auto pRefKidT10 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsT1v[0]);
    CPPUNIT_ASSERT(pRefKidT10);
    auto pObjectT10 = pRefKidT10->LookupObject();
    CPPUNIT_ASSERT(pObjectT10);
    auto pTypeT10 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT10->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT10->GetValue());
    auto pST10 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT10->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Contents#201"_ostr, pST10->GetValue());

    auto pKidsT10 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectT10->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsT10);
    auto pKidsT10v = pKidsT10->GetElements();
    CPPUNIT_ASSERT_EQUAL(size_t(1), pKidsT10v.size());

    // there is one and only one Link
    auto pRefKidT100 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsT10v[0]);
    CPPUNIT_ASSERT(pRefKidT100);
    auto pObjectT100 = pRefKidT100->LookupObject();
    CPPUNIT_ASSERT(pObjectT100);
    auto pTypeT100 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT100->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT100->GetValue());
    auto pST100 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT100->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Link"_ostr, pST100->GetValue());

    auto pRefKidT2 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsTv[1]);
    CPPUNIT_ASSERT(pRefKidT2);
    auto pObjectT2 = pRefKidT2->LookupObject();
    CPPUNIT_ASSERT(pObjectT2);
    auto pTypeT2 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT2->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT2->GetValue());
    auto pST2 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT2->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("TOCI"_ostr, pST2->GetValue());

    auto pKidsT2 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectT2->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsT2);
    auto pKidsT2v = pKidsT2->GetElements();

    auto pRefKidT20 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsT2v[0]);
    CPPUNIT_ASSERT(pRefKidT20);
    auto pObjectT20 = pRefKidT20->LookupObject();
    CPPUNIT_ASSERT(pObjectT20);
    auto pTypeT20 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT20->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT20->GetValue());
    auto pST20 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT20->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Contents#201"_ostr, pST20->GetValue());

    auto pKidsT20 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectT20->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsT20);
    auto pKidsT20v = pKidsT20->GetElements();
    CPPUNIT_ASSERT_EQUAL(size_t(1), pKidsT20v.size());

    // there is one and only one Link
    auto pRefKidT200 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsT20v[0]);
    CPPUNIT_ASSERT(pRefKidT200);
    auto pObjectT200 = pRefKidT200->LookupObject();
    CPPUNIT_ASSERT(pObjectT200);
    auto pTypeT200 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT200->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT200->GetValue());
    auto pST200 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT200->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Link"_ostr, pST200->GetValue());

    auto pRefKidT3 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsTv[1]);
    CPPUNIT_ASSERT(pRefKidT3);
    auto pObjectT3 = pRefKidT3->LookupObject();
    CPPUNIT_ASSERT(pObjectT3);
    auto pTypeT3 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT3->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT3->GetValue());
    auto pST3 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT3->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("TOCI"_ostr, pST3->GetValue());

    auto pKidsT3 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectT3->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsT3);
    auto pKidsT3v = pKidsT3->GetElements();

    auto pRefKidT30 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsT3v[0]);
    CPPUNIT_ASSERT(pRefKidT30);
    auto pObjectT30 = pRefKidT30->LookupObject();
    CPPUNIT_ASSERT(pObjectT30);
    auto pTypeT30 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT30->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT30->GetValue());
    auto pST30 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT30->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Contents#201"_ostr, pST30->GetValue());

    auto pKidsT30 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectT30->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsT30);
    auto pKidsT30v = pKidsT30->GetElements();
    CPPUNIT_ASSERT_EQUAL(size_t(1), pKidsT30v.size());

    // there is one and only one Link
    auto pRefKidT300 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsT30v[0]);
    CPPUNIT_ASSERT(pRefKidT300);
    auto pObjectT300 = pRefKidT300->LookupObject();
    CPPUNIT_ASSERT(pObjectT300);
    auto pTypeT300 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT300->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeT300->GetValue());
    auto pST300 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectT300->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Link"_ostr, pST300->GetValue());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf135638)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"image-shape.fodt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    int nFigure(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "StructElem")
        {
            auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
            if (pS && pS->GetValue() == "Figure")
            {
                auto pAttrDict
                    = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pObject->Lookup("A"_ostr));
                CPPUNIT_ASSERT(pAttrDict != nullptr);
                auto pOwner = dynamic_cast<vcl::filter::PDFNameElement*>(
                    pAttrDict->LookupElement("O"_ostr));
                CPPUNIT_ASSERT(pOwner != nullptr);
                CPPUNIT_ASSERT_EQUAL("Layout"_ostr, pOwner->GetValue());
                auto pBBox = dynamic_cast<vcl::filter::PDFArrayElement*>(
                    pAttrDict->LookupElement("BBox"_ostr));
                CPPUNIT_ASSERT(pBBox != nullptr);
                if (nFigure == 0)
                {
                    CPPUNIT_ASSERT_DOUBLES_EQUAL(
                        139.5,
                        dynamic_cast<vcl::filter::PDFNumberElement*>(pBBox->GetElements()[0])
                            ->GetValue(),
                        0.01);
                    CPPUNIT_ASSERT_DOUBLES_EQUAL(
                        480.3,
                        dynamic_cast<vcl::filter::PDFNumberElement*>(pBBox->GetElements()[1])
                            ->GetValue(),
                        0.01);
                    CPPUNIT_ASSERT_DOUBLES_EQUAL(
                        472.5,
                        dynamic_cast<vcl::filter::PDFNumberElement*>(pBBox->GetElements()[2])
                            ->GetValue(),
                        0.01);
                    CPPUNIT_ASSERT_DOUBLES_EQUAL(
                        735.3,
                        dynamic_cast<vcl::filter::PDFNumberElement*>(pBBox->GetElements()[3])
                            ->GetValue(),
                        0.01);
                }
                else
                {
                    CPPUNIT_ASSERT_DOUBLES_EQUAL(
                        178.45,
                        dynamic_cast<vcl::filter::PDFNumberElement*>(pBBox->GetElements()[0])
                            ->GetValue(),
                        0.01);
                    CPPUNIT_ASSERT_DOUBLES_EQUAL(
                        318.65,
                        dynamic_cast<vcl::filter::PDFNumberElement*>(pBBox->GetElements()[1])
                            ->GetValue(),
                        0.01);
                    CPPUNIT_ASSERT_DOUBLES_EQUAL(
                        326.35,
                        dynamic_cast<vcl::filter::PDFNumberElement*>(pBBox->GetElements()[2])
                            ->GetValue(),
                        0.01);
                    CPPUNIT_ASSERT_DOUBLES_EQUAL(
                        382.55,
                        dynamic_cast<vcl::filter::PDFNumberElement*>(pBBox->GetElements()[3])
                            ->GetValue(),
                        0.01);
                }
                ++nFigure;
            }
        }
    }
    // the first one is a Writer image, 2nd one SdrRectObj
    CPPUNIT_ASSERT_EQUAL(2, nFigure);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf157703)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"LO_Lbl_Lbody_bug_report.fodt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    vcl::filter::PDFObjectElement* pDocument(nullptr);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructElem")
        {
            auto pS1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("S"_ostr));
            if (pS1 && pS1->GetValue() == "Document")
            {
                pDocument = pObject1;
            }
        }
    }
    CPPUNIT_ASSERT(pDocument);

    auto pKidsD = dynamic_cast<vcl::filter::PDFArrayElement*>(pDocument->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsD);
    // assume there are no MCID ref at this level
    auto pKidsDv = pKidsD->GetElements();
    auto pRefKidD0 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsDv[0]);
    CPPUNIT_ASSERT(pRefKidD0);
    auto pObjectD0 = pRefKidD0->LookupObject();
    CPPUNIT_ASSERT(pObjectD0);
    auto pTypeD0 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectD0->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeD0->GetValue());
    auto pSD0 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectD0->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("H1"_ostr, pSD0->GetValue());

    auto pKidsD0 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectD0->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsD0);
    auto pKidsD0v = pKidsD0->GetElements();
    auto pRefKidD00 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsD0v[0]);
    // MCID for label
    CPPUNIT_ASSERT(!pRefKidD00);

    // MCID for text
    auto pRefKidD01 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsD0v[1]);
    CPPUNIT_ASSERT(!pRefKidD01);

    auto pRefKidD1 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsDv[1]);
    CPPUNIT_ASSERT(pRefKidD1);
    auto pObjectD1 = pRefKidD1->LookupObject();
    CPPUNIT_ASSERT(pObjectD1);
    auto pTypeD1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectD1->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pTypeD1->GetValue());
    auto pSD1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObjectD1->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("H2"_ostr, pSD1->GetValue());

    auto pKidsD1 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObjectD1->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKidsD1);
    auto pKidsD1v = pKidsD1->GetElements();

    // MCID for text
    auto pRefKidD11 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKidsD1v[0]);
    CPPUNIT_ASSERT(!pRefKidD11);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testSpans)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"spanlist.fodt", aDocument);

    // The document has two pages.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(2), aPages.size());

    int nDoc(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructElem")
        {
            auto pS1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("S"_ostr));
            if (pS1 && pS1->GetValue() == "Document")
            {
                auto pKids1
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids1);
                // assume there are no MCID ref at this level
                auto vKids1 = pKids1->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(2), vKids1.size());
                auto pRefKid10 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids1[0]);
                CPPUNIT_ASSERT(pRefKid10);
                auto pObject10 = pRefKid10->LookupObject();
                CPPUNIT_ASSERT(pObject10);
                auto pType10
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType10->GetValue());
                auto pS10 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("L"_ostr, pS10->GetValue());

                auto pKids10
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject10->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids10);
                // assume there are no MCID ref at this level
                auto vKids10 = pKids10->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(4), vKids10.size());

                auto pRefKid100 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10[0]);
                CPPUNIT_ASSERT(pRefKid100);
                auto pObject100 = pRefKid100->LookupObject();
                CPPUNIT_ASSERT(pObject100);
                auto pType100
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject100->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType100->GetValue());
                auto pS100
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject100->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("LI"_ostr, pS100->GetValue());

                auto pKids100
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject100->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids100);
                // assume there are no MCID ref at this level
                auto vKids100 = pKids100->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(2), vKids100.size());

                auto pRefKid1000 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids100[0]);
                CPPUNIT_ASSERT(pRefKid1000);
                auto pObject1000 = pRefKid1000->LookupObject();
                CPPUNIT_ASSERT(pObject1000);
                auto pType1000
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1000->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1000->GetValue());
                auto pS1000
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1000->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Lbl"_ostr, pS1000->GetValue());

                auto pRefKid1001 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids100[1]);
                CPPUNIT_ASSERT(pRefKid1001);
                auto pObject1001 = pRefKid1001->LookupObject();
                CPPUNIT_ASSERT(pObject1001);
                auto pType1001
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1001->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1001->GetValue());
                auto pS1001
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1001->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("LBody"_ostr, pS1001->GetValue());
                auto pKids1001
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1001->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids1001);
                // assume there are no MCID ref at this level
                auto vKids1001 = pKids1001->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(1), vKids1001.size());

                auto pRefKid10010 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids1001[0]);
                CPPUNIT_ASSERT(pRefKid10010);
                auto pObject10010 = pRefKid10010->LookupObject();
                CPPUNIT_ASSERT(pObject10010);
                auto pType10010
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10010->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType10010->GetValue());
                auto pS10010
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10010->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS10010->GetValue());
                auto pKids10010
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject10010->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids10010);
                // assume there are no MCID ref at this level
                auto vKids10010 = pKids10010->GetElements();
                // only one span
                CPPUNIT_ASSERT_EQUAL(size_t(1), vKids10010.size());

                auto pRefKid100100 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10010[0]);
                CPPUNIT_ASSERT(pRefKid100100);
                auto pObject100100 = pRefKid100100->LookupObject();
                CPPUNIT_ASSERT(pObject100100);
                auto pType100100 = dynamic_cast<vcl::filter::PDFNameElement*>(
                    pObject100100->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType100100->GetValue());
                auto pS100100
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject100100->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Span"_ostr, pS100100->GetValue());
                // this span exists because of lang
                auto pLang100100 = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(
                    pObject100100->Lookup("Lang"_ostr));
                CPPUNIT_ASSERT_EQUAL("en-GB"_ostr, pLang100100->GetValue());

                auto pRefKid101 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10[1]);
                CPPUNIT_ASSERT(pRefKid101);
                auto pObject101 = pRefKid101->LookupObject();
                CPPUNIT_ASSERT(pObject101);
                auto pType101
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject101->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType101->GetValue());
                auto pS101
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject101->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("LI"_ostr, pS101->GetValue());

                auto pKids101
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject101->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids101);
                // assume there are no MCID ref at this level
                auto vKids101 = pKids101->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(2), vKids101.size());

                auto pRefKid1010 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids101[0]);
                CPPUNIT_ASSERT(pRefKid1010);
                auto pObject1010 = pRefKid1010->LookupObject();
                CPPUNIT_ASSERT(pObject1010);
                auto pType1010
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1010->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1010->GetValue());
                auto pS1010
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1010->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Lbl"_ostr, pS1010->GetValue());

                auto pRefKid1011 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids101[1]);
                CPPUNIT_ASSERT(pRefKid1011);
                auto pObject1011 = pRefKid1011->LookupObject();
                CPPUNIT_ASSERT(pObject1011);
                auto pType1011
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1011->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1011->GetValue());
                auto pS1011
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1011->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("LBody"_ostr, pS1011->GetValue());

                auto pKids1011
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1011->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids1011);
                // assume there are no MCID ref at this level
                auto vKids1011 = pKids1011->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(1), vKids1011.size());

                auto pRefKid10110 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids1011[0]);
                CPPUNIT_ASSERT(pRefKid10110);
                auto pObject10110 = pRefKid10110->LookupObject();
                CPPUNIT_ASSERT(pObject10110);
                auto pType10110
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10110->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType10110->GetValue());
                auto pS10110
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10110->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS10110->GetValue());
                auto pKids10110
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject10110->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids10110);
                auto vKids10110 = pKids10110->GetElements();
                // only MCIDs, no span
                for (size_t i = 0; i < vKids10110.size(); ++i)
                {
                    auto pKid = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10110[i]);
                    CPPUNIT_ASSERT(!pKid);
                }

                auto pRefKid102 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10[2]);
                CPPUNIT_ASSERT(pRefKid102);
                auto pObject102 = pRefKid102->LookupObject();
                CPPUNIT_ASSERT(pObject102);
                auto pType102
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject102->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType102->GetValue());
                auto pS102
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject102->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("LI"_ostr, pS102->GetValue());

                auto pKids102
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject102->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids102);
                // assume there are no MCID ref at this level
                auto vKids102 = pKids102->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(2), vKids102.size());

                auto pRefKid1020 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids102[0]);
                CPPUNIT_ASSERT(pRefKid1020);
                auto pObject1020 = pRefKid1020->LookupObject();
                CPPUNIT_ASSERT(pObject1020);
                auto pType1020
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1020->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1020->GetValue());
                auto pS1020
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1020->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Lbl"_ostr, pS1020->GetValue());

                auto pRefKid1021 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids102[1]);
                CPPUNIT_ASSERT(pRefKid1021);
                auto pObject1021 = pRefKid1021->LookupObject();
                CPPUNIT_ASSERT(pObject1021);
                auto pType1021
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1021->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1021->GetValue());
                auto pS1021
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1021->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("LBody"_ostr, pS1021->GetValue());

                auto pKids1021
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1021->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids1021);
                // assume there are no MCID ref at this level
                auto vKids1021 = pKids1021->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(1), vKids1021.size());

                auto pRefKid10210 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids1021[0]);
                CPPUNIT_ASSERT(pRefKid10210);
                auto pObject10210 = pRefKid10210->LookupObject();
                CPPUNIT_ASSERT(pObject10210);
                auto pType10210
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10210->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType10210->GetValue());
                auto pS10210
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10210->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS10210->GetValue());
                auto pKids10210
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject10210->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids10210);
                // assume there are no MCID ref at this level
                auto vKids10210 = pKids10210->GetElements();
                // 2 span and a hyperlink
                CPPUNIT_ASSERT_EQUAL(size_t(3), vKids10210.size());

                auto pRefKid102100 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10210[0]);
                CPPUNIT_ASSERT(pRefKid102100);
                auto pObject102100 = pRefKid102100->LookupObject();
                CPPUNIT_ASSERT(pObject102100);
                auto pType102100 = dynamic_cast<vcl::filter::PDFNameElement*>(
                    pObject102100->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType102100->GetValue());
                auto pS102100
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject102100->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Span"_ostr, pS102100->GetValue());
                auto pKids102100
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject102100->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids102100);
                auto vKids102100 = pKids102100->GetElements();
                for (size_t i = 0; i < vKids102100.size(); ++i)
                {
                    auto pKid = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids102100[i]);
                    CPPUNIT_ASSERT(!pKid);
                }

                auto pRefKid102101 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10210[1]);
                CPPUNIT_ASSERT(pRefKid102101);
                auto pObject102101 = pRefKid102101->LookupObject();
                CPPUNIT_ASSERT(pObject102101);
                auto pType102101 = dynamic_cast<vcl::filter::PDFNameElement*>(
                    pObject102101->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType102101->GetValue());
                auto pS102101
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject102101->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS102101->GetValue());
                auto pKids102101
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject102101->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids102101);
                auto vKids102101 = pKids102101->GetElements();
                int nRef(0);
                for (size_t i = 0; i < vKids102101.size(); ++i)
                {
                    auto pKid = dynamic_cast<vcl::filter::PDFDictionaryElement*>(vKids102101[i]);
                    if (pKid)
                    {
                        ++nRef; // annotation
                    }
                }
                CPPUNIT_ASSERT_EQUAL(1, nRef);

                auto pRefKid102102 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10210[2]);
                CPPUNIT_ASSERT(pRefKid102102);
                auto pObject102102 = pRefKid102102->LookupObject();
                CPPUNIT_ASSERT(pObject102102);
                auto pType102102 = dynamic_cast<vcl::filter::PDFNameElement*>(
                    pObject102102->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType102102->GetValue());
                auto pS102102
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject102102->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Span"_ostr, pS102102->GetValue());
                auto pKids102102
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject102102->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids102102);
                auto vKids102102 = pKids102102->GetElements();
                // there is a footnote
                int nFtn(0);
                for (size_t i = 0; i < vKids102102.size(); ++i)
                {
                    auto pKid = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids102102[i]);
                    if (pKid)
                    {
                        auto pObject = pKid->LookupObject();
                        CPPUNIT_ASSERT(pObject);
                        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(
                            pObject->Lookup("Type"_ostr));
                        CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType->GetValue());
                        auto pS
                            = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
                        CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS->GetValue());
                        ++nFtn;
                    }
                }
                CPPUNIT_ASSERT_EQUAL(1, nFtn);

                auto pRefKid103 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10[3]);
                CPPUNIT_ASSERT(pRefKid103);
                auto pObject103 = pRefKid103->LookupObject();
                CPPUNIT_ASSERT(pObject103);
                auto pType103
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject103->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType103->GetValue());
                auto pS103
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject103->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("LI"_ostr, pS103->GetValue());

                auto pKids103
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject103->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids103);
                // assume there are no MCID ref at this level
                auto vKids103 = pKids103->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(2), vKids103.size());

                auto pRefKid1030 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids103[0]);
                CPPUNIT_ASSERT(pRefKid1030);
                auto pObject1030 = pRefKid1030->LookupObject();
                CPPUNIT_ASSERT(pObject1030);
                auto pType1030
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1030->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1030->GetValue());
                auto pS1030
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1030->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Lbl"_ostr, pS1030->GetValue());

                auto pRefKid1031 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids103[1]);
                CPPUNIT_ASSERT(pRefKid1031);
                auto pObject1031 = pRefKid1031->LookupObject();
                CPPUNIT_ASSERT(pObject1031);
                auto pType1031
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1031->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1031->GetValue());
                auto pS1031
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1031->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("LBody"_ostr, pS1031->GetValue());

                auto pKids1031
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1031->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids1031);
                // assume there are no MCID ref at this level
                auto vKids1031 = pKids1031->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(1), vKids1031.size());

                auto pRefKid10310 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids1031[0]);
                CPPUNIT_ASSERT(pRefKid10310);
                auto pObject10310 = pRefKid10310->LookupObject();
                CPPUNIT_ASSERT(pObject10310);
                auto pType10310
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10310->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType10310->GetValue());
                auto pS10310
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject10310->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS10310->GetValue());
                auto pKids10310
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject10310->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids10310);
                // assume there are no MCID ref at this level
                auto vKids10310 = pKids10310->GetElements();
                // only one span, following a MCID for some strike-out gap
                CPPUNIT_ASSERT_EQUAL(size_t(2), vKids10310.size());

                auto pRefKid103100 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10310[0]);
                CPPUNIT_ASSERT(!pRefKid103100);

                auto pRefKid103101 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids10310[1]);
                CPPUNIT_ASSERT(pRefKid103101);
                auto pObject103101 = pRefKid103101->LookupObject();
                CPPUNIT_ASSERT(pObject103101);
                auto pType103101 = dynamic_cast<vcl::filter::PDFNameElement*>(
                    pObject103101->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType103101->GetValue());
                auto pS103101
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject103101->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Span"_ostr, pS103101->GetValue());
                auto pDictA103101 = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
                    pObject103101->Lookup("A"_ostr));
                CPPUNIT_ASSERT(pDictA103101 != nullptr);
                CPPUNIT_ASSERT_EQUAL("Layout"_ostr, dynamic_cast<vcl::filter::PDFNameElement*>(
                                                        pDictA103101->LookupElement("O"_ostr))
                                                        ->GetValue());
                CPPUNIT_ASSERT_EQUAL("LineThrough"_ostr,
                                     dynamic_cast<vcl::filter::PDFNameElement*>(
                                         pDictA103101->LookupElement("TextDecorationType"_ostr))
                                         ->GetValue());

                // now the footnote container - following the list
                auto pRefKid11 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids1[1]);
                CPPUNIT_ASSERT(pRefKid11);
                auto pObject11 = pRefKid11->LookupObject();
                CPPUNIT_ASSERT(pObject11);
                auto pType11
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject11->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType11->GetValue());
                auto pS11 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject11->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Div"_ostr, pS11->GetValue());

                auto pKids11
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject11->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids11);
                // assume there are no MCID ref at this level
                auto vKids11 = pKids11->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(1), vKids11.size());

                auto pRefKid110 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids11[0]);
                CPPUNIT_ASSERT(pRefKid110);
                auto pObject110 = pRefKid110->LookupObject();
                CPPUNIT_ASSERT(pObject110);
                auto pType110
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject110->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType110->GetValue());
                auto pS110
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject110->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Note"_ostr, pS110->GetValue());

                auto pKids110
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject110->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids110);
                // assume there are no MCID ref at this level
                auto vKids110 = pKids110->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(2), vKids110.size());

                auto pRefKid1100 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids110[0]);
                CPPUNIT_ASSERT(pRefKid1100);
                auto pObject1100 = pRefKid1100->LookupObject();
                CPPUNIT_ASSERT(pObject1100);
                auto pType1100
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1100->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1100->GetValue());
                auto pS1100
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1100->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Lbl"_ostr, pS1100->GetValue());

                auto pKids1100
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1100->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids1100);
                // assume there are no MCID ref at this level
                auto vKids1100 = pKids1100->GetElements();
                CPPUNIT_ASSERT_EQUAL(size_t(1), vKids1100.size());

                auto pRefKid11000 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids1100[0]);
                CPPUNIT_ASSERT(pRefKid11000);
                auto pObject11000 = pRefKid11000->LookupObject();
                CPPUNIT_ASSERT(pObject11000);
                auto pType11000
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject11000->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType11000->GetValue());
                auto pS11000
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject11000->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS11000->GetValue());

                auto pRefKid1101 = dynamic_cast<vcl::filter::PDFReferenceElement*>(vKids110[1]);
                CPPUNIT_ASSERT(pRefKid1101);
                auto pObject1101 = pRefKid1101->LookupObject();
                CPPUNIT_ASSERT(pObject1101);
                auto pType1101
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1101->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType1101->GetValue());
                auto pS1101
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1101->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Footnote"_ostr, pS1101->GetValue());

                ++nDoc;
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nDoc);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf157182)
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence({
        { "PDFUACompliance", uno::Any(true) },
        // only happens with PDF/A-1
        { "SelectPdfVersion", uno::Any(static_cast<sal_Int32>(1)) },
    }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    saveAsPDF(u"transparentshape.fodp");

    // just check this does not crash or assert
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf57423)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"Description PDF Export test .odt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    int nFigure(0);
    int nFormula(0);
    int nDiv(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "StructElem")
        {
            auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
            if (pS && pS->GetValue() == "Figure")
            {
                switch (nFigure)
                {
                    case 2:
                        CPPUNIT_ASSERT_EQUAL(u"QR Code - Tells how to get to Mosegaard"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                    case 0:
                        CPPUNIT_ASSERT_EQUAL(u"Title: Arrows - Description:  Explains the "
                                             u"different arrow appearances"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                    case 1:
                        CPPUNIT_ASSERT_EQUAL(
                            u"My blue triangle - Does not need further description"_ustr,
                            ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                    pObject->Lookup("Alt"_ostr))));
                        break;
                }
                ++nFigure;
            }
            if (pS && pS->GetValue() == "Formula")
            {
                CPPUNIT_ASSERT_EQUAL(
                    u"Equation 1 - Now we give the full description of eq 1 here"_ustr,
                    ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                        *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                            pObject->Lookup("Alt"_ostr))));
                ++nFormula;
            }
            if (pS && pS->GetValue() == "Div")
            {
                switch (nDiv)
                {
                    case 0:
                        CPPUNIT_ASSERT_EQUAL(u"This frame has a description"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                    case 1:
                        // no properties set on this
                        CPPUNIT_ASSERT(!pObject->Lookup("Alt"_ostr));
                        break;
                    case 2:
                        CPPUNIT_ASSERT_EQUAL(u"My textbox - Has a light background"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                    case 3:
                        CPPUNIT_ASSERT_EQUAL(u"Hey!  There is no alternate text for Frame "
                                             u"// but maybe not needed?"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                }
                ++nDiv;
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL(3, nFigure);
    CPPUNIT_ASSERT_EQUAL(1, nFormula);
    CPPUNIT_ASSERT_EQUAL(4, nDiv);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf154982)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"tdf154982.odt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    int nFigure(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "StructElem")
        {
            auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
            if (pS && pS->GetValue() == "Figure")
            {
                switch (nFigure)
                {
                    case 0:
                        CPPUNIT_ASSERT_EQUAL(u"Here comes the signature - Please sign here"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                    case 1:
                        CPPUNIT_ASSERT_EQUAL(u"Home"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                }

                // the problem was that the figures in the hell layer were not
                // below their anchor paragraphs in the structure tree
                auto pParentRef
                    = dynamic_cast<vcl::filter::PDFReferenceElement*>(pObject->Lookup("P"_ostr));
                CPPUNIT_ASSERT(pParentRef);
                auto pParent(pParentRef->LookupObject());
                CPPUNIT_ASSERT(pParent);
                auto pParentType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pParent->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pParentType->GetValue());
                auto pParentS
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pParent->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pParentS->GetValue());

                auto pPParentRef
                    = dynamic_cast<vcl::filter::PDFReferenceElement*>(pParent->Lookup("P"_ostr));
                CPPUNIT_ASSERT(pPParentRef);
                auto pPParent(pPParentRef->LookupObject());
                CPPUNIT_ASSERT(pPParent);
                auto pPParentType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pPParent->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pPParentType->GetValue());
                auto pPParentS
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pPParent->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Document"_ostr, pPParentS->GetValue());
                ++nFigure;
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL(2, nFigure);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf157397)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence({
        { "PDFUACompliance", uno::Any(true) },
        { "ExportFormFields", uno::Any(true) },
    }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"PDF_export_with_formcontrol.fodt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    vcl::filter::PDFObjectElement* pDocument(nullptr);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructElem")
        {
            auto pS1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("S"_ostr));
            if (pS1 && pS1->GetValue() == "Document")
            {
                pDocument = pObject1;
            }
        }
    }
    CPPUNIT_ASSERT(pDocument);

    auto pKids1 = dynamic_cast<vcl::filter::PDFArrayElement*>(pDocument->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKids1);
    // assume there are no MCID ref at this level
    auto pKids1v = pKids1->GetElements();
    auto pRefKid12 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids1v[2]);
    CPPUNIT_ASSERT(pRefKid12);
    auto pObject12 = pRefKid12->LookupObject();
    CPPUNIT_ASSERT(pObject12);
    auto pType12 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject12->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType12->GetValue());
    auto pS12 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject12->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Text#20body"_ostr, pS12->GetValue());

    auto pKids12 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject12->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKids12);
    // assume there are no MCID ref at this level
    auto pKids12v = pKids12->GetElements();
    auto pRefKid120 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids12v[0]);
    CPPUNIT_ASSERT(pRefKid120);
    auto pObject120 = pRefKid120->LookupObject();
    CPPUNIT_ASSERT(pObject120);
    auto pType120 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject120->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType120->GetValue());
    auto pS120 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject120->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS120->GetValue());

    {
        auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject120->Lookup("K"_ostr));
        int nMCID(0);
        int nRef(0);
        for (size_t i = 0; i < pKids->GetElements().size(); ++i)
        {
            auto pNum = dynamic_cast<vcl::filter::PDFNumberElement*>(pKids->GetElement(i));
            auto pObjR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pKids->GetElement(i));
            if (pNum)
            {
                ++nMCID;
            }
            if (pObjR)
            {
                ++nRef;
                auto pOType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObjR->LookupElement("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue());
                auto pAnnotRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                    pObjR->LookupElement("Obj"_ostr));
                auto pAnnot = pAnnotRef->LookupObject();
                auto pAType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("Annot"_ostr, pAType->GetValue());
                auto pASubtype
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr));
                CPPUNIT_ASSERT_EQUAL("Link"_ostr, pASubtype->GetValue());
                auto pAContents = dynamic_cast<vcl::filter::PDFHexStringElement*>(
                    pAnnot->Lookup("Contents"_ostr));
                CPPUNIT_ASSERT_EQUAL(
                    u"wiki-seite"_ustr,
                    ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pAContents));
                CPPUNIT_ASSERT_EQUAL("Link"_ostr, pASubtype->GetValue());
                auto pAA
                    = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pAnnot->Lookup("A"_ostr));
                CPPUNIT_ASSERT(pAA);
                auto pAAType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAA->LookupElement("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("Action"_ostr, pAAType->GetValue());
                auto pAAS
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAA->LookupElement("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("URI"_ostr, pAAS->GetValue());
                auto pAAURI = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(
                    pAA->LookupElement("URI"_ostr));
                CPPUNIT_ASSERT_EQUAL("https://klexikon.zum.de/wiki/Kl%C3%A4ranlage"_ostr,
                                     pAAURI->GetValue());
            }
        }
        CPPUNIT_ASSERT_EQUAL(1, nMCID);
        CPPUNIT_ASSERT_EQUAL(1, nRef);
    }

    auto pRefKid13 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids1v[3]);
    CPPUNIT_ASSERT(pRefKid13);
    auto pObject13 = pRefKid13->LookupObject();
    CPPUNIT_ASSERT(pObject13);
    auto pType13 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject13->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType13->GetValue());
    auto pS13 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject13->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Text#20body"_ostr, pS13->GetValue());

    auto pKids13 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject13->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKids13);
    // assume there are no MCID ref at this level
    auto pKids13v = pKids13->GetElements();
    auto pRefKid130 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids13v[0]);
    CPPUNIT_ASSERT(pRefKid130);
    auto pObject130 = pRefKid130->LookupObject();
    CPPUNIT_ASSERT(pObject130);
    auto pType130 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject130->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType130->GetValue());
    auto pS130 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject130->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS130->GetValue());

    {
        auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject130->Lookup("K"_ostr));
        int nMCID(0);
        int nRef(0);
        for (size_t i = 0; i < pKids->GetElements().size(); ++i)
        {
            auto pNum = dynamic_cast<vcl::filter::PDFNumberElement*>(pKids->GetElement(i));
            auto pObjR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pKids->GetElement(i));
            if (pNum)
            {
                ++nMCID;
            }
            if (pObjR)
            {
                ++nRef;
                auto pOType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObjR->LookupElement("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue());
                auto pAnnotRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                    pObjR->LookupElement("Obj"_ostr));
                auto pAnnot = pAnnotRef->LookupObject();
                auto pAType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("Annot"_ostr, pAType->GetValue());
                auto pASubtype
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr));
                CPPUNIT_ASSERT_EQUAL("Link"_ostr, pASubtype->GetValue());
                auto pAContents = dynamic_cast<vcl::filter::PDFHexStringElement*>(
                    pAnnot->Lookup("Contents"_ostr));
                CPPUNIT_ASSERT(!pAContents);
                CPPUNIT_ASSERT_EQUAL("Link"_ostr, pASubtype->GetValue());
                auto pAA
                    = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pAnnot->Lookup("A"_ostr));
                CPPUNIT_ASSERT(pAA);
                auto pAAType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAA->LookupElement("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("Action"_ostr, pAAType->GetValue());
                auto pAAS
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAA->LookupElement("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("URI"_ostr, pAAS->GetValue());
                auto pAAURI = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(
                    pAA->LookupElement("URI"_ostr));
                CPPUNIT_ASSERT_EQUAL(
                    "https://de.wikipedia.org/wiki/Kl%C3%A4ranlage#Mechanische_Vorreinigung"_ostr,
                    pAAURI->GetValue());
            }
        }
        CPPUNIT_ASSERT_EQUAL(1, nMCID);
        CPPUNIT_ASSERT_EQUAL(1, nRef);
    }

    auto pRefKid14 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids1v[4]);
    CPPUNIT_ASSERT(pRefKid14);
    auto pObject14 = pRefKid14->LookupObject();
    CPPUNIT_ASSERT(pObject14);
    auto pType14 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject14->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType14->GetValue());
    auto pS14 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject14->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Text#20body"_ostr, pS14->GetValue());

    auto pKids14 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject14->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKids14);
    // assume there are no MCID ref at this level
    auto pKids14v = pKids14->GetElements();
    auto pRefKid140 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids14v[0]);
    CPPUNIT_ASSERT(pRefKid140);
    auto pObject140 = pRefKid140->LookupObject();
    CPPUNIT_ASSERT(pObject140);
    auto pType140 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject140->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType140->GetValue());
    auto pS140 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject140->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS140->GetValue());

    {
        auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject140->Lookup("K"_ostr));
        int nMCID(0);
        int nRef(0);
        for (size_t i = 0; i < pKids->GetElements().size(); ++i)
        {
            auto pNum = dynamic_cast<vcl::filter::PDFNumberElement*>(pKids->GetElement(i));
            auto pObjR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pKids->GetElement(i));
            if (pNum)
            {
                ++nMCID;
            }
            if (pObjR)
            {
                ++nRef;
                auto pOType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObjR->LookupElement("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue());
                auto pAnnotRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                    pObjR->LookupElement("Obj"_ostr));
                auto pAnnot = pAnnotRef->LookupObject();
                auto pAType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("Annot"_ostr, pAType->GetValue());
                auto pASubtype
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr));
                CPPUNIT_ASSERT_EQUAL("Link"_ostr, pASubtype->GetValue());
                auto pAContents = dynamic_cast<vcl::filter::PDFHexStringElement*>(
                    pAnnot->Lookup("Contents"_ostr));
                CPPUNIT_ASSERT_EQUAL(
                    u"tour"_ustr, ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pAContents));
                CPPUNIT_ASSERT_EQUAL("Link"_ostr, pASubtype->GetValue());
                auto pAA
                    = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pAnnot->Lookup("A"_ostr));
                CPPUNIT_ASSERT(pAA);
                auto pAAType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAA->LookupElement("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("Action"_ostr, pAAType->GetValue());
                auto pAAS
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAA->LookupElement("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("URI"_ostr, pAAS->GetValue());
                auto pAAURI = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(
                    pAA->LookupElement("URI"_ostr));
                CPPUNIT_ASSERT_EQUAL(
                    "https://vr-easy.com/tour/usr/220113-virtuellerschulausflug/#pano=24"_ostr,
                    pAAURI->GetValue());
            }
        }
        CPPUNIT_ASSERT_EQUAL(1, nMCID);
        CPPUNIT_ASSERT_EQUAL(1, nRef);
    }

    auto pRefKid16 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids1v[6]);
    CPPUNIT_ASSERT(pRefKid16);
    auto pObject16 = pRefKid16->LookupObject();
    CPPUNIT_ASSERT(pObject16);
    auto pType16 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject16->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType16->GetValue());
    auto pS16 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject16->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Text#20body"_ostr, pS16->GetValue());

    auto pKids16 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject16->Lookup("K"_ostr));
    CPPUNIT_ASSERT(pKids16);
    // assume there are no MCID ref at this level
    auto pKids16v = pKids16->GetElements();
    auto pRefKid160 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKids16v[0]);
    CPPUNIT_ASSERT(pRefKid160);
    auto pObject160 = pRefKid160->LookupObject();
    CPPUNIT_ASSERT(pObject160);
    auto pType160 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject160->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType160->GetValue());
    auto pS160 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject160->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Form"_ostr, pS160->GetValue());
    auto pA160Dict = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pObject160->Lookup("A"_ostr));
    CPPUNIT_ASSERT(pA160Dict);
    auto pA160O = dynamic_cast<vcl::filter::PDFNameElement*>(pA160Dict->LookupElement("O"_ostr));
    CPPUNIT_ASSERT(pA160O);
    CPPUNIT_ASSERT_EQUAL("PrintField"_ostr, pA160O->GetValue());
    auto pA160Role
        = dynamic_cast<vcl::filter::PDFNameElement*>(pA160Dict->LookupElement("Role"_ostr));
    CPPUNIT_ASSERT(pA160Role);
    CPPUNIT_ASSERT_EQUAL("tv"_ostr, pA160Role->GetValue());

    {
        auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject160->Lookup("K"_ostr));
        int nMCID(0);
        int nRef(0);
        for (size_t i = 0; i < pKids->GetElements().size(); ++i)
        {
            auto pNum = dynamic_cast<vcl::filter::PDFNumberElement*>(pKids->GetElement(i));
            auto pObjR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pKids->GetElement(i));
            if (pNum)
            {
                ++nMCID;
            }
            if (pObjR)
            {
                ++nRef;
                auto pOType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pObjR->LookupElement("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue());
                auto pAnnotRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                    pObjR->LookupElement("Obj"_ostr));
                auto pAnnot = pAnnotRef->LookupObject();
                auto pAType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("Annot"_ostr, pAType->GetValue());
                auto pASubtype
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr));
                CPPUNIT_ASSERT_EQUAL("Widget"_ostr, pASubtype->GetValue());
            }
        }
        CPPUNIT_ASSERT_EQUAL(1, nMCID);
        CPPUNIT_ASSERT_EQUAL(1, nRef);
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf135192)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"tdf135192-1.fodp", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    int nTable(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructElem")
        {
            auto pS1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("S"_ostr));
            if (pS1 && pS1->GetValue() == "Table")
            {
                int nTR(0);
                auto pKids1
                    = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1->Lookup("K"_ostr));
                CPPUNIT_ASSERT(pKids1);
                // there can be additional children, such as MCID ref
                for (auto pKid1 : pKids1->GetElements())
                {
                    auto pRefKid1 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKid1);
                    if (pRefKid1)
                    {
                        auto pObject2 = pRefKid1->LookupObject();
                        if (pObject2)
                        {
                            auto pType2 = dynamic_cast<vcl::filter::PDFNameElement*>(
                                pObject2->Lookup("Type"_ostr));
                            if (pType2 && pType2->GetValue() == "StructElem")
                            {
                                auto pS2 = dynamic_cast<vcl::filter::PDFNameElement*>(
                                    pObject2->Lookup("S"_ostr));
                                if (pS2 && pS2->GetValue() == "TR")
                                {
                                    int nTD(0);
                                    auto pKids2 = dynamic_cast<vcl::filter::PDFArrayElement*>(
                                        pObject2->Lookup("K"_ostr));
                                    CPPUNIT_ASSERT(pKids2);
                                    for (auto pKid2 : pKids2->GetElements())
                                    {
                                        auto pRefKid2
                                            = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                                                pKid2);
                                        if (pRefKid2)
                                        {
                                            auto pObject3 = pRefKid2->LookupObject();
                                            if (pObject3)
                                            {
                                                auto pType3
                                                    = dynamic_cast<vcl::filter::PDFNameElement*>(
                                                        pObject3->Lookup("Type"_ostr));
                                                if (pType3 && pType3->GetValue() == "StructElem")
                                                {
                                                    auto pS3 = dynamic_cast<
                                                        vcl::filter::PDFNameElement*>(
                                                        pObject3->Lookup("S"_ostr));
                                                    if (nTR == 0 && pS3 && pS3->GetValue() == "TH")
                                                    {
                                                        int nOTable(0);
                                                        auto pAttrs = dynamic_cast<
                                                            vcl::filter::PDFArrayElement*>(
                                                            pObject3->Lookup("A"_ostr));
                                                        CPPUNIT_ASSERT(pAttrs != nullptr);
                                                        for (const auto& rAttrRef :
                                                             pAttrs->GetElements())
                                                        {
                                                            auto pAttrDict = dynamic_cast<
                                                                vcl::filter::PDFDictionaryElement*>(
                                                                rAttrRef);
                                                            CPPUNIT_ASSERT(pAttrDict != nullptr);
                                                            auto pOwner = dynamic_cast<
                                                                vcl::filter::PDFNameElement*>(
                                                                pAttrDict->LookupElement("O"_ostr));
                                                            CPPUNIT_ASSERT(pOwner != nullptr);
                                                            if (pOwner->GetValue() == "Table")
                                                            {
                                                                auto pScope = dynamic_cast<
                                                                    vcl::filter::PDFNameElement*>(
                                                                    pAttrDict->LookupElement(
                                                                        "Scope"_ostr));
                                                                CPPUNIT_ASSERT(pScope != nullptr);
                                                                CPPUNIT_ASSERT_EQUAL(
                                                                    "Column"_ostr,
                                                                    pScope->GetValue());
                                                                ++nOTable;
                                                            }
                                                        }
                                                        CPPUNIT_ASSERT_EQUAL(1, nOTable);
                                                        ++nTD;
                                                    }
                                                    else if (nTR != 0 && pS3
                                                             && pS3->GetValue() == "TD")
                                                    {
                                                        ++nTD;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    CPPUNIT_ASSERT_EQUAL(3, nTD);
                                    ++nTR;
                                }
                            }
                        }
                    }
                }
                CPPUNIT_ASSERT_EQUAL(2, nTR);
                ++nTable;
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nTable);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf154955)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    vcl::filter::PDFDocument aDocument;
    load(u"grouped-shape.fodt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    vcl::filter::PDFObjectElement* pContents = aPages[0]->LookupObject("Contents"_ostr);
    CPPUNIT_ASSERT(pContents);
    vcl::filter::PDFStreamElement* pStream = pContents->GetStream();
    CPPUNIT_ASSERT(pStream);
    SvMemoryStream& rObjectStream = pStream->GetMemory();
    // Uncompress it.
    SvMemoryStream aUncompressed;
    ZCodec aZCodec;
    aZCodec.BeginCompression();
    rObjectStream.Seek(0);
    aZCodec.Decompress(rObjectStream, aUncompressed);
    CPPUNIT_ASSERT(aZCodec.EndCompression());

    auto pStart = static_cast<const char*>(aUncompressed.GetData());
    const char* const pEnd = pStart + aUncompressed.GetSize();

    enum
    {
        Default,
        Artifact,
        Tagged
    } state
        = Default;

    int nLine(0);
    int nTagged(0);
    int nArtifacts(0);
    while (true)
    {
        ++nLine;
        auto const pLine = ::std::find(pStart, pEnd, '\n');
        if (pLine == pEnd)
        {
            break;
        }
        std::string_view const line(pStart, pLine - pStart);
        pStart = pLine + 1;
        if (!line.empty() && line[0] != '%')
        {
            ::std::cerr << nLine << ": " << line << "\n";
            if (o3tl::starts_with(line, "/Artifact "))
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                state = Artifact;
                ++nArtifacts;
            }
            else if (o3tl::starts_with(line, "/Figure<</MCID "))
            {
                CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state);
                state = Tagged;
                ++nTagged;
            }
            else if (line == "EMC")
            {
                CPPUNIT_ASSERT_MESSAGE("unexpected end", state != Default);
                state = Default;
            }
            else if (nLine > 1) // first line is expected "0.1 w"
            {
                CPPUNIT_ASSERT_MESSAGE("unexpected content outside MCS", state != Default);
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL_MESSAGE("unclosed MCS", Default, state);
    CPPUNIT_ASSERT_EQUAL(2, nTagged);
    CPPUNIT_ASSERT_GREATEREQUAL(1, nArtifacts);

    int nFigure(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "StructElem")
        {
            auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
            if (pS && pS->GetValue() == "Figure")
            {
                switch (nFigure)
                {
                    case 0:
                        CPPUNIT_ASSERT_EQUAL(u"Two rectangles - Grouped"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                    case 1:
                        CPPUNIT_ASSERT_EQUAL(u"these ones are green"_ustr,
                                             ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                 *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                     pObject->Lookup("Alt"_ostr))));
                        break;
                }

                auto pParentRef
                    = dynamic_cast<vcl::filter::PDFReferenceElement*>(pObject->Lookup("P"_ostr));
                CPPUNIT_ASSERT(pParentRef);
                auto pParent(pParentRef->LookupObject());
                CPPUNIT_ASSERT(pParent);
                auto pParentType
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pParent->Lookup("Type"_ostr));
                CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pParentType->GetValue());
                auto pParentS
                    = dynamic_cast<vcl::filter::PDFNameElement*>(pParent->Lookup("S"_ostr));
                CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pParentS->GetValue());

                ++nFigure;
            }
        }
    }
    // the problem was that there were 4 shapes (the sub-shapes of the 2 groups)
    CPPUNIT_ASSERT_EQUAL(2, nFigure);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf155190)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"tdf155190.odt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    int nDiv(0);
    int nFigure(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));

        auto pS1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("S"_ostr));
        // start with the text box
        if (pType1 && pType1->GetValue() == "StructElem" && pS1 && pS1->GetValue() == "Div")
        {
            ++nDiv;
            auto pKids1 = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject1->Lookup("K"_ostr));
            CPPUNIT_ASSERT(pKids1);
            for (auto pKid1 : pKids1->GetElements())
            {
                auto pRefKid1 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKid1);
                if (pRefKid1)
                {
                    auto pObject2 = pRefKid1->LookupObject();
                    CPPUNIT_ASSERT(pObject2);
                    auto pType2
                        = dynamic_cast<vcl::filter::PDFNameElement*>(pObject2->Lookup("Type"_ostr));
                    CPPUNIT_ASSERT(pType2);
                    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType2->GetValue());
                    auto pS2
                        = dynamic_cast<vcl::filter::PDFNameElement*>(pObject2->Lookup("S"_ostr));
                    CPPUNIT_ASSERT_EQUAL("FigureCaption"_ostr, pS2->GetValue());
                    auto pKids2
                        = dynamic_cast<vcl::filter::PDFArrayElement*>(pObject2->Lookup("K"_ostr));
                    CPPUNIT_ASSERT(pKids2);
                    // there are additional children, MCID ref
                    for (auto pKid2 : pKids2->GetElements())
                    {
                        auto pRefKid2 = dynamic_cast<vcl::filter::PDFReferenceElement*>(pKid2);
                        if (pRefKid2)
                        {
                            auto pObject3 = pRefKid2->LookupObject();
                            CPPUNIT_ASSERT(pObject3);
                            auto pType3 = dynamic_cast<vcl::filter::PDFNameElement*>(
                                pObject3->Lookup("Type"_ostr));
                            if (pType3 && pType3->GetValue() == "StructElem")
                            {
                                auto pS3 = dynamic_cast<vcl::filter::PDFNameElement*>(
                                    pObject3->Lookup("S"_ostr));
                                CPPUNIT_ASSERT_EQUAL("Figure"_ostr, pS3->GetValue());
                                auto pAlt = dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                    pObject3->Lookup("Alt"_ostr));
                                CPPUNIT_ASSERT_EQUAL(
                                    u"Picture of apples"_ustr,
                                    ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pAlt));
                                auto pKids3 = dynamic_cast<vcl::filter::PDFArrayElement*>(
                                    pObject3->Lookup("K"_ostr));
                                CPPUNIT_ASSERT(pKids3);
                                // the problem was that this didn't reference an MCID
                                CPPUNIT_ASSERT(!pKids3->GetElements().empty());
                                ++nFigure;
                            }
                        }
                    }
                }
            }
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nDiv);
    CPPUNIT_ASSERT_EQUAL(1, nFigure);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testMediaShapeAnnot)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"vid.odt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    auto pAnnots = dynamic_cast<vcl::filter::PDFArrayElement*>(aPages[0]->Lookup("Annots"_ostr));
    CPPUNIT_ASSERT(pAnnots);

    // There should be one annotation
    CPPUNIT_ASSERT_EQUAL(size_t(1), pAnnots->GetElements().size());
    auto pAnnotReference
        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pAnnots->GetElements()[0]);
    CPPUNIT_ASSERT(pAnnotReference);
    // check /Annot - produced by sw
    vcl::filter::PDFObjectElement* pAnnot = pAnnotReference->LookupObject();
    CPPUNIT_ASSERT(pAnnot);
    CPPUNIT_ASSERT_EQUAL(
        "Annot"_ostr,
        static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr))->GetValue());
    CPPUNIT_ASSERT_EQUAL(
        "Screen"_ostr,
        static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr))->GetValue());

    auto pA = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pAnnot->Lookup("A"_ostr));
    CPPUNIT_ASSERT(pA);
    auto pR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pA->LookupElement("R"_ostr));
    CPPUNIT_ASSERT(pR);
    auto pC = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pR->LookupElement("C"_ostr));
    CPPUNIT_ASSERT(pC);
    auto pCT = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(pC->LookupElement("CT"_ostr));
    CPPUNIT_ASSERT_EQUAL("video/webm"_ostr, pCT->GetValue());
    auto pD = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pC->LookupElement("D"_ostr));
    CPPUNIT_ASSERT(pD);
    auto pDesc = dynamic_cast<vcl::filter::PDFHexStringElement*>(pD->LookupElement("Desc"_ostr));
    CPPUNIT_ASSERT(pDesc);
    CPPUNIT_ASSERT_EQUAL(u"alternativloser text\nand some description"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pDesc));
    auto pAlt = dynamic_cast<vcl::filter::PDFArrayElement*>(pC->LookupElement("Alt"_ostr));
    CPPUNIT_ASSERT(pAlt);
    auto pLang = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(pAlt->GetElement(0));
    CPPUNIT_ASSERT_EQUAL(""_ostr, pLang->GetValue());
    auto pAltText = dynamic_cast<vcl::filter::PDFHexStringElement*>(pAlt->GetElement(1));
    CPPUNIT_ASSERT_EQUAL(u"alternativloser text\nand some description"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pAltText));

    auto pStructParent
        = dynamic_cast<vcl::filter::PDFNumberElement*>(pAnnot->Lookup("StructParent"_ostr));
    CPPUNIT_ASSERT(pStructParent);

    vcl::filter::PDFReferenceElement* pStructElemRef(nullptr);

    // check ParentTree to find StructElem
    int nRoots(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructTreeRoot")
        {
            ++nRoots;
            auto pParentTree = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                pObject1->Lookup("ParentTree"_ostr));
            CPPUNIT_ASSERT(pParentTree);
            auto pNumTree = pParentTree->LookupObject();
            CPPUNIT_ASSERT(pNumTree);
            auto pNums = dynamic_cast<vcl::filter::PDFArrayElement*>(pNumTree->Lookup("Nums"_ostr));
            CPPUNIT_ASSERT(pNums);
            int nFound(0);
            for (size_t i = 0; i < pNums->GetElements().size(); i += 2)
            {
                auto pI = dynamic_cast<vcl::filter::PDFNumberElement*>(pNums->GetElement(i));
                if (pI->GetValue() == pStructParent->GetValue())
                {
                    ++nFound;
                    CPPUNIT_ASSERT(i < pNums->GetElements().size() - 1);
                    pStructElemRef
                        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pNums->GetElement(i + 1));
                    CPPUNIT_ASSERT(pStructElemRef);
                }
            }
            CPPUNIT_ASSERT_EQUAL(1, nFound);
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nRoots);

    // check /StructElem - produced by drawinglayer
    CPPUNIT_ASSERT(pStructElemRef);
    auto pStructElem(pStructElemRef->LookupObject());
    CPPUNIT_ASSERT(pStructElem);

    auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pStructElem->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType->GetValue());
    auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pStructElem->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Annot"_ostr, pS->GetValue());
    auto pSEAlt = dynamic_cast<vcl::filter::PDFHexStringElement*>(pStructElem->Lookup("Alt"_ostr));
    CPPUNIT_ASSERT_EQUAL(u"alternativloser text - and some description"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pSEAlt));
    auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pStructElem->Lookup("K"_ostr));
    int nMCID(0);
    int nRef(0);
    for (size_t i = 0; i < pKids->GetElements().size(); ++i)
    {
        auto pNum = dynamic_cast<vcl::filter::PDFNumberElement*>(pKids->GetElement(i));
        auto pObjR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pKids->GetElement(i));
        if (pNum)
        {
            ++nMCID;
        }
        if (pObjR)
        {
            ++nRef;
            auto pOType
                = dynamic_cast<vcl::filter::PDFNameElement*>(pObjR->LookupElement("Type"_ostr));
            CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue());
            auto pAnnotRef
                = dynamic_cast<vcl::filter::PDFReferenceElement*>(pObjR->LookupElement("Obj"_ostr));
            CPPUNIT_ASSERT_EQUAL(pAnnot, pAnnotRef->LookupObject());
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nMCID);
    CPPUNIT_ASSERT_EQUAL(1, nRef);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testFlyFrameHyperlinkAnnot)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "PDFUACompliance", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"image-hyperlink-alttext.fodt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    auto pAnnots = dynamic_cast<vcl::filter::PDFArrayElement*>(aPages[0]->Lookup("Annots"_ostr));
    CPPUNIT_ASSERT(pAnnots);

    // There should be one annotation
    CPPUNIT_ASSERT_EQUAL(size_t(1), pAnnots->GetElements().size());
    auto pAnnotReference
        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pAnnots->GetElements()[0]);
    CPPUNIT_ASSERT(pAnnotReference);
    // check /Annot - produced by sw
    vcl::filter::PDFObjectElement* pAnnot = pAnnotReference->LookupObject();
    CPPUNIT_ASSERT(pAnnot);
    CPPUNIT_ASSERT_EQUAL(
        "Annot"_ostr,
        static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr))->GetValue());
    CPPUNIT_ASSERT_EQUAL(
        "Link"_ostr,
        static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr))->GetValue());

    auto pContents
        = dynamic_cast<vcl::filter::PDFHexStringElement*>(pAnnot->Lookup("Contents"_ostr));
    CPPUNIT_ASSERT_EQUAL(u"Ship to Bugzilla"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pContents));

    auto pStructParent
        = dynamic_cast<vcl::filter::PDFNumberElement*>(pAnnot->Lookup("StructParent"_ostr));
    CPPUNIT_ASSERT(pStructParent);

    vcl::filter::PDFReferenceElement* pStructElemRef(nullptr);

    // check ParentTree to find StructElem
    int nRoots(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructTreeRoot")
        {
            ++nRoots;
            auto pParentTree = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                pObject1->Lookup("ParentTree"_ostr));
            CPPUNIT_ASSERT(pParentTree);
            auto pNumTree = pParentTree->LookupObject();
            CPPUNIT_ASSERT(pNumTree);
            auto pNums = dynamic_cast<vcl::filter::PDFArrayElement*>(pNumTree->Lookup("Nums"_ostr));
            CPPUNIT_ASSERT(pNums);
            int nFound(0);
            for (size_t i = 0; i < pNums->GetElements().size(); i += 2)
            {
                auto pI = dynamic_cast<vcl::filter::PDFNumberElement*>(pNums->GetElement(i));
                if (pI->GetValue() == pStructParent->GetValue())
                {
                    ++nFound;
                    CPPUNIT_ASSERT(i < pNums->GetElements().size() - 1);
                    pStructElemRef
                        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pNums->GetElement(i + 1));
                    CPPUNIT_ASSERT(pStructElemRef);
                }
            }
            CPPUNIT_ASSERT_EQUAL(1, nFound);
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nRoots);

    // check /StructElem - produced by sw painting code
    CPPUNIT_ASSERT(pStructElemRef);
    auto pStructElem(pStructElemRef->LookupObject());
    CPPUNIT_ASSERT(pStructElem);

    auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pStructElem->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType->GetValue());
    auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pStructElem->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS->GetValue());
    auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pStructElem->Lookup("K"_ostr));
    int nMCID(0);
    int nRef(0);
    for (size_t i = 0; i < pKids->GetElements().size(); ++i)
    {
        auto pNum = dynamic_cast<vcl::filter::PDFNumberElement*>(pKids->GetElement(i));
        auto pObjR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pKids->GetElement(i));
        if (pNum)
        {
            ++nMCID;
        }
        if (pObjR)
        {
            ++nRef;
            auto pOType
                = dynamic_cast<vcl::filter::PDFNameElement*>(pObjR->LookupElement("Type"_ostr));
            CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue());
            auto pAnnotRef
                = dynamic_cast<vcl::filter::PDFReferenceElement*>(pObjR->LookupElement("Obj"_ostr));
            CPPUNIT_ASSERT_EQUAL(pAnnot, pAnnotRef->LookupObject());
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nMCID);
    CPPUNIT_ASSERT_EQUAL(1, nRef);

    // the Link is inside a Figure
    auto pParentRef
        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pStructElem->Lookup("P"_ostr));
    CPPUNIT_ASSERT(pParentRef);
    auto pParent(pParentRef->LookupObject());
    CPPUNIT_ASSERT(pParent);
    auto pParentType = dynamic_cast<vcl::filter::PDFNameElement*>(pParent->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pParentType->GetValue());
    auto pParentS = dynamic_cast<vcl::filter::PDFNameElement*>(pParent->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Figure"_ostr, pParentS->GetValue());
    auto pAlt = dynamic_cast<vcl::filter::PDFHexStringElement*>(pParent->Lookup("Alt"_ostr));
    CPPUNIT_ASSERT_EQUAL(u"Ship drawing - Very cute"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pAlt));
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testFormControlAnnot)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence({
        { "PDFUACompliance", uno::Any(true) },
        { "ExportFormFields", uno::Any(true) },
    }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    vcl::filter::PDFDocument aDocument;
    load(u"formcontrol.fodt", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    auto pAnnots = dynamic_cast<vcl::filter::PDFArrayElement*>(aPages[0]->Lookup("Annots"_ostr));
    CPPUNIT_ASSERT(pAnnots);

    // There should be one annotation
    CPPUNIT_ASSERT_EQUAL(size_t(1), pAnnots->GetElements().size());
    auto pAnnotReference
        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pAnnots->GetElements()[0]);
    CPPUNIT_ASSERT(pAnnotReference);
    // check /Annot
    vcl::filter::PDFObjectElement* pAnnot = pAnnotReference->LookupObject();
    CPPUNIT_ASSERT(pAnnot);
    CPPUNIT_ASSERT_EQUAL(
        "Annot"_ostr,
        static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr))->GetValue());
    CPPUNIT_ASSERT_EQUAL(
        "Widget"_ostr,
        static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr))->GetValue());
    auto pT = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(pAnnot->Lookup("T"_ostr));
    CPPUNIT_ASSERT(pT);
    CPPUNIT_ASSERT_EQUAL("Check Box 1"_ostr, pT->GetValue());
    auto pTU = dynamic_cast<vcl::filter::PDFHexStringElement*>(pAnnot->Lookup("TU"_ostr));
    CPPUNIT_ASSERT(pTU);
    CPPUNIT_ASSERT_EQUAL(u"helpful text"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pTU));

    auto pStructParent
        = dynamic_cast<vcl::filter::PDFNumberElement*>(pAnnot->Lookup("StructParent"_ostr));
    CPPUNIT_ASSERT(pStructParent);

    vcl::filter::PDFReferenceElement* pStructElemRef(nullptr);

    // check ParentTree to find StructElem
    int nRoots(0);
    for (const auto& rDocElement : aDocument.GetElements())
    {
        auto pObject1 = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
        if (!pObject1)
            continue;
        auto pType1 = dynamic_cast<vcl::filter::PDFNameElement*>(pObject1->Lookup("Type"_ostr));
        if (pType1 && pType1->GetValue() == "StructTreeRoot")
        {
            ++nRoots;
            auto pParentTree = dynamic_cast<vcl::filter::PDFReferenceElement*>(
                pObject1->Lookup("ParentTree"_ostr));
            CPPUNIT_ASSERT(pParentTree);
            auto pNumTree = pParentTree->LookupObject();
            CPPUNIT_ASSERT(pNumTree);
            auto pNums = dynamic_cast<vcl::filter::PDFArrayElement*>(pNumTree->Lookup("Nums"_ostr));
            CPPUNIT_ASSERT(pNums);
            int nFound(0);
            for (size_t i = 0; i < pNums->GetElements().size(); i += 2)
            {
                auto pI = dynamic_cast<vcl::filter::PDFNumberElement*>(pNums->GetElement(i));
                if (pI->GetValue() == pStructParent->GetValue())
                {
                    ++nFound;
                    CPPUNIT_ASSERT(i < pNums->GetElements().size() - 1);
                    pStructElemRef
                        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pNums->GetElement(i + 1));
                    CPPUNIT_ASSERT(pStructElemRef);
                }
            }
            CPPUNIT_ASSERT_EQUAL(1, nFound);
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nRoots);

    // check /StructElem
    CPPUNIT_ASSERT(pStructElemRef);
    auto pStructElem(pStructElemRef->LookupObject());
    CPPUNIT_ASSERT(pStructElem);

    auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pStructElem->Lookup("Type"_ostr));
    CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType->GetValue());
    auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pStructElem->Lookup("S"_ostr));
    CPPUNIT_ASSERT_EQUAL("Form"_ostr, pS->GetValue());
    auto pAlt = dynamic_cast<vcl::filter::PDFHexStringElement*>(pStructElem->Lookup("Alt"_ostr));
    CPPUNIT_ASSERT_EQUAL(u"textuelle alternative - a box to check"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(*pAlt));
    auto pA = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pStructElem->Lookup("A"_ostr));
    CPPUNIT_ASSERT(pA);
    auto pO = dynamic_cast<vcl::filter::PDFNameElement*>(pA->LookupElement("O"_ostr));
    CPPUNIT_ASSERT(pO);
    CPPUNIT_ASSERT_EQUAL("PrintField"_ostr, pO->GetValue());
    auto pRole = dynamic_cast<vcl::filter::PDFNameElement*>(pA->LookupElement("Role"_ostr));
    CPPUNIT_ASSERT(pRole);
    CPPUNIT_ASSERT_EQUAL("cb"_ostr, pRole->GetValue());
    auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pStructElem->Lookup("K"_ostr));
    int nMCID(0);
    int nRef(0);
    for (size_t i = 0; i < pKids->GetElements().size(); ++i)
    {
        auto pNum = dynamic_cast<vcl::filter::PDFNumberElement*>(pKids->GetElement(i));
        auto pObjR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pKids->GetElement(i));
        if (pNum)
        {
            ++nMCID;
        }
        if (pObjR)
        {
            ++nRef;
            auto pOType
                = dynamic_cast<vcl::filter::PDFNameElement*>(pObjR->LookupElement("Type"_ostr));
            CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue());
            auto pAnnotRef
                = dynamic_cast<vcl::filter::PDFReferenceElement*>(pObjR->LookupElement("Obj"_ostr));
            CPPUNIT_ASSERT_EQUAL(pAnnot, pAnnotRef->LookupObject());
        }
    }
    CPPUNIT_ASSERT_EQUAL(1, nMCID);
    CPPUNIT_ASSERT_EQUAL(1, nRef);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf142129)
{
    loadFromFile(u"master.odm");

    // update linked section
    dispatchCommand(mxComponent, u".uno:UpdateAllLinks"_ustr, {});

    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;

    // Enable Outlines export
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "ExportBookmarks", uno::Any(true) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

    // Parse the export result.
    vcl::filter::PDFDocument aDocument;
    SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
    CPPUNIT_ASSERT(aDocument.Read(aStream));

    auto* pCatalog = aDocument.GetCatalog();
    CPPUNIT_ASSERT(pCatalog);
    auto* pCatalogDictionary = pCatalog->GetDictionary();
    CPPUNIT_ASSERT(pCatalogDictionary);
    auto* pOutlinesObject = pCatalogDictionary->LookupObject("Outlines"_ostr);
    CPPUNIT_ASSERT(pOutlinesObject);
    auto* pOutlinesDictionary = pOutlinesObject->GetDictionary();
#if 0
    // Type isn't actually written currently
    auto* pType
        = dynamic_cast<vcl::filter::PDFNameElement*>(pOutlinesDictionary->LookupElement("Type"));
    CPPUNIT_ASSERT(pType);
    CPPUNIT_ASSERT_EQUAL(OString("Outlines"), pType->GetValue());
#endif

    auto* pFirst = dynamic_cast<vcl::filter::PDFReferenceElement*>(
        pOutlinesDictionary->LookupElement("First"_ostr));
    CPPUNIT_ASSERT(pFirst);
    auto* pFirstD = pFirst->LookupObject()->GetDictionary();
    CPPUNIT_ASSERT(pFirstD);
    //CPPUNIT_ASSERT_EQUAL(OString("Outlines"), dynamic_cast<vcl::filter::PDFNameElement*>(pFirstD->LookupElement("Type"))->GetValue());
    CPPUNIT_ASSERT_EQUAL(u"Preface"_ustr, ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                              *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                  pFirstD->LookupElement("Title"_ostr))));

    auto* pFirst1
        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pFirstD->LookupElement("First"_ostr));
    CPPUNIT_ASSERT(pFirst1);
    auto* pFirst1D = pFirst1->LookupObject()->GetDictionary();
    CPPUNIT_ASSERT(pFirst1D);
    // here is a hidden section with headings "Copyright" etc.; check that
    // there are no outline entries for it
    //CPPUNIT_ASSERT_EQUAL(OString("Outlines"), dynamic_cast<vcl::filter::PDFNameElement*>(pFirst1D->LookupElement("Type"))->GetValue());
    CPPUNIT_ASSERT_EQUAL(u"Who is this book for?"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                             *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                 pFirst1D->LookupElement("Title"_ostr))));

    auto* pFirst2
        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pFirst1D->LookupElement("Next"_ostr));
    auto* pFirst2D = pFirst2->LookupObject()->GetDictionary();
    CPPUNIT_ASSERT(pFirst2D);
    //CPPUNIT_ASSERT_EQUAL(OString("Outlines"), dynamic_cast<vcl::filter::PDFNameElement*>(pFirst2D->LookupElement("Type"))->GetValue());
    CPPUNIT_ASSERT_EQUAL(u"What\u2019s in this book?"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                             *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                 pFirst2D->LookupElement("Title"_ostr))));

    auto* pFirst3
        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pFirst2D->LookupElement("Next"_ostr));
    auto* pFirst3D = pFirst3->LookupObject()->GetDictionary();
    CPPUNIT_ASSERT(pFirst3D);
    //CPPUNIT_ASSERT_EQUAL(OString("Outlines"), dynamic_cast<vcl::filter::PDFNameElement*>(pFirst3D->LookupElement("Type"))->GetValue());
    CPPUNIT_ASSERT_EQUAL(u"Minimum requirements for using LibreOffice"_ustr,
                         ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                             *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                 pFirst3D->LookupElement("Title"_ostr))));

    CPPUNIT_ASSERT_EQUAL(static_cast<vcl::filter::PDFElement*>(nullptr),
                         pFirst3D->LookupElement("Next"_ostr));
    CPPUNIT_ASSERT_EQUAL(static_cast<vcl::filter::PDFElement*>(nullptr),
                         pFirstD->LookupElement("Next"_ostr));
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testPdfImageRotate180)
{
    // Create an empty document.
    loadFromURL(u"private:factory/swriter"_ustr);
    uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY);
    uno::Reference<text::XText> xText = xTextDocument->getText();
    uno::Reference<text::XTextCursor> xCursor = xText->createTextCursor();

    // Insert the PDF image.
    uno::Reference<lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xGraphicObject(
        xFactory->createInstance(u"com.sun.star.text.TextGraphicObject"_ustr), uno::UNO_QUERY);
    OUString aURL = createFileURL(u"pdf-image-rotate-180.pdf");
    xGraphicObject->setPropertyValue(u"GraphicURL"_ustr, uno::Any(aURL));
    uno::Reference<drawing::XShape> xShape(xGraphicObject, uno::UNO_QUERY);
    xShape->setSize(awt::Size(1000, 1000));
    uno::Reference<text::XTextContent> xTextContent(xGraphicObject, uno::UNO_QUERY);
    xText->insertTextContent(xCursor->getStart(), xTextContent, /*bAbsorb=*/false);

    // Save as PDF.
    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

    // Parse the export result.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    // Make sure that the page -> form -> form has a child image.
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPdfPage->getObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Form, pPageObject->getType());
    // 2: white background and the actual object.
    CPPUNIT_ASSERT_EQUAL(2, pPageObject->getFormObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pFormObject = pPageObject->getFormObject(1);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Form, pFormObject->getType());
    CPPUNIT_ASSERT_EQUAL(1, pFormObject->getFormObjectCount());

    // Check if the inner form object (original page object in the pdf image) has the correct
    // rotation.
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pInnerFormObject = pFormObject->getFormObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Form, pInnerFormObject->getType());
    CPPUNIT_ASSERT_EQUAL(1, pInnerFormObject->getFormObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pImage = pInnerFormObject->getFormObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Image, pImage->getType());
    basegfx::B2DHomMatrix aMat = pInnerFormObject->getMatrix();
    basegfx::B2DTuple aScale;
    basegfx::B2DTuple aTranslate;
    double fRotate = 0;
    double fShearX = 0;
    aMat.decompose(aScale, aTranslate, fRotate, fShearX);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: -1
    // - Actual  : 1
    // i.e. the 180 degrees rotation didn't happen (via a combination of horizontal + vertical
    // flip).
    CPPUNIT_ASSERT_DOUBLES_EQUAL(-1.0, aScale.getX(), 0.01);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf144222)
{
// Assume Windows has the font for U+4E2D
#ifdef _WIN32
    saveAsPDF(u"tdf144222.ods");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    // The document has one page.
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    std::unique_ptr<vcl::pdf::PDFiumTextPage> pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    const OUString sChar = u"\u4E2D"_ustr;
    basegfx::B2DRectangle aRect1, aRect2;
    int nCount = 0;

    for (int i = 0; i < nPageObjectCount; ++i)
    {
        std::unique_ptr<vcl::pdf::PDFiumPageObject> pPdfPageObject = pPdfPage->getObject(i);
        if (pPdfPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            ++nCount;
            OUString sText = pPdfPageObject->getText(pTextPage);
            if (sText == sChar)
                aRect1 = pPdfPageObject->getBounds();
            else
                aRect2 = pPdfPageObject->getBounds();
        }
    }

    CPPUNIT_ASSERT_EQUAL(2, nCount);
    CPPUNIT_ASSERT(!aRect1.isEmpty());
    CPPUNIT_ASSERT(!aRect2.isEmpty());
    CPPUNIT_ASSERT(!aRect1.overlaps(aRect2));
#endif
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf145873)
{
    // Import the bugdoc and export as PDF.
    saveAsPDF(u"tdf145873.pptx");

    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    // The document has one page.
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    int nPageObjectCount = pPdfPage->getObjectCount();

    // tdf#145873: Without the fix #1 in place, this test would have failed with
    // - Expected: 66
    // - Actual  : 3
    CPPUNIT_ASSERT_EQUAL(66, nPageObjectCount);

    auto pObject = pPdfPage->getObject(4);
    CPPUNIT_ASSERT_MESSAGE("no object", pObject != nullptr);

    // tdf#145873: Without the fix #2 in place, this test would have failed with
    // - Expected: 13.40
    // - Actual  : 3.57...
    // - Delta   : 0.1
    CPPUNIT_ASSERT_DOUBLES_EQUAL(13.40, pObject->getBounds().getWidth(), 0.1);
    // - Expected: 13.79
    // - Actual  : 3.74...
    // - Delta   : 0.1
    CPPUNIT_ASSERT_DOUBLES_EQUAL(13.79, pObject->getBounds().getHeight(), 0.1);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testPdfImageHyperlink)
{
    // Given a Draw file, containing a PDF image, which has a hyperlink in it:
    // When saving to PDF:
    saveAsPDF(u"pdf-image-hyperlink.odg");

    // Then make sure that link is preserved:
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    // Without the accompanying fix in place, this test would have failed, the hyperlink of the PDF
    // image was lost.
    CPPUNIT_ASSERT(pPdfPage->hasLinks());

    // Also test the precision of the form XObject.
    // Given a full-page form XObject, page height is 27.94 cm (792 points):
    // When writing the reciprocal of the object height to PDF:
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pFormObject;
    for (int i = 0; i < pPdfPage->getObjectCount(); ++i)
    {
        std::unique_ptr<vcl::pdf::PDFiumPageObject> pObject = pPdfPage->getObject(i);
        if (pObject->getType() == vcl::pdf::PDFPageObjectType::Form)
        {
            pFormObject = std::move(pObject);
            break;
        }
    }
    CPPUNIT_ASSERT(pFormObject);
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pInnerFormObject;
    for (int i = 0; i < pFormObject->getFormObjectCount(); ++i)
    {
        std::unique_ptr<vcl::pdf::PDFiumPageObject> pObject = pFormObject->getFormObject(i);
        if (pObject->getType() == vcl::pdf::PDFPageObjectType::Form)
        {
            pInnerFormObject = std::move(pObject);
            break;
        }
    }
    CPPUNIT_ASSERT(pInnerFormObject);
    // Then make sure that enough digits are used, so the point size is unchanged:
    basegfx::B2DHomMatrix aMatrix = pInnerFormObject->getMatrix();
    basegfx::B2DTuple aScale;
    basegfx::B2DTuple aTranslate;
    double fRotate{};
    double fShearX{};
    aMatrix.decompose(aScale, aTranslate, fRotate, fShearX);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 0.0012626264
    // - Actual  : 0.00126
    // i.e. the rounded reciprocal was 794 points, not the original 792.
    CPPUNIT_ASSERT_EQUAL(0.001262626, rtl::math::round(aScale.getY(), 9));
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testURIs)
{
    struct
    {
        OUString in;
        OString out;
        bool relativeFsys;
    } URIs[] = { {
                     u"http://example.com/"_ustr,
                     "http://example.com/"_ostr,
                     true,
                 },
                 {
                     u"file://localfile.odt/"_ustr,
                     "file://localfile.odt/"_ostr,
                     true,
                 },
                 {
                     // tdf 143216
                     u"http://username:password@example.com"_ustr,
                     "http://username:password@example.com"_ostr,
                     true,
                 },
                 {
                     u"git://git.example.org/project/example"_ustr,
                     "git://git.example.org/project/example"_ostr,
                     true,
                 },
                 {
                     // The odt/pdf gets substituted due to 'ConvertOOoTargetToPDFTarget'
                     u"filebypath.odt"_ustr,
                     "filebypath.pdf"_ostr,
                     true,
                 },
                 {
                     // The odt/pdf gets substituted due to 'ConvertOOoTargetToPDFTarget'
                     // but this time with ExportLinksRelativeFsys off the path is added
                     u"filebypath.odt"_ustr,
                     OUStringToOString(utl::GetTempNameBaseDirectory(), RTL_TEXTENCODING_UTF8)
                         + "filebypath.pdf",
                     false,
                 },
                 {
                     // This also gets made relative due to 'ExportLinksRelativeFsys'
                     utl::GetTempNameBaseDirectory() + "fileintempdir.odt",
                     "fileintempdir.pdf"_ostr,
                     true,
                 } };

    // Create an empty document.
    // Note: The test harness gets very upset if we try and create multiple
    // documents, or recreate it; so reuse one instance for all the links
    loadFromURL(u"private:factory/swriter"_ustr);
    uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY);
    uno::Reference<text::XText> xText = xTextDocument->getText();
    uno::Reference<text::XTextCursor> xCursor = xText->createTextCursor();
    xText->insertString(xCursor, u"Test pdf"_ustr, /*bAbsorb=*/false);

    // Set the name so it can do relative name replacement
    uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY);
    xModel->attachResource(maTempFile.GetURL(), xModel->getArgs());

    for (const auto& uri : URIs)
    {
        // Test the filename rewriting
        uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence({
            { "ExportLinksRelativeFsys", uno::Any(uri.relativeFsys) },
            { "ConvertOOoTargetToPDFTarget", uno::Any(true) },
        }));
        aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

        // Add a link (based on testNestedHyperlink in rtfexport3)
        xCursor->gotoStart(/*bExpand=*/false);
        xCursor->gotoEnd(/*bExpand=*/true);
        uno::Reference<beans::XPropertySet> xCursorProps(xCursor, uno::UNO_QUERY);
        xCursorProps->setPropertyValue(u"HyperLinkURL"_ustr, uno::Any(uri.in));
        xCursorProps->setPropertyValue(u"HyperLinkName"_ustr, uno::Any(u"Testname"_ustr));

        // Save as PDF.
        aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
        saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

        // Use the filter rather than the pdfium route, as per the tdf105093 test, it's
        // easier to parse the annotations
        vcl::filter::PDFDocument aDocument;

        // Parse the export result.
        SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
        CPPUNIT_ASSERT(aDocument.Read(aStream));

        // The document has one page.
        std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
        CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());
        auto pAnnots
            = dynamic_cast<vcl::filter::PDFArrayElement*>(aPages[0]->Lookup("Annots"_ostr));
        CPPUNIT_ASSERT(pAnnots);

        // There should be one annotation
        CPPUNIT_ASSERT_EQUAL(size_t(1), pAnnots->GetElements().size());
        auto pAnnotReference
            = dynamic_cast<vcl::filter::PDFReferenceElement*>(pAnnots->GetElements()[0]);
        CPPUNIT_ASSERT(pAnnotReference);
        vcl::filter::PDFObjectElement* pAnnot = pAnnotReference->LookupObject();
        CPPUNIT_ASSERT(pAnnot);
        // We're expecting something like /Type /Annot /A << /Type /Action /S /URI /URI (path)
        CPPUNIT_ASSERT_EQUAL(
            "Annot"_ostr,
            static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr))->GetValue());
        CPPUNIT_ASSERT_EQUAL(
            "Link"_ostr,
            static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr))->GetValue());
        auto pAction = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pAnnot->Lookup("A"_ostr));
        CPPUNIT_ASSERT(pAction);
        auto pURIElem = dynamic_cast<vcl::filter::PDFLiteralStringElement*>(
            pAction->LookupElement("URI"_ostr));
        CPPUNIT_ASSERT(pURIElem);
        // Check it matches
        CPPUNIT_ASSERT_EQUAL(uri.out, pURIElem->GetValue());
        // tdf#148934 check a11y
        CPPUNIT_ASSERT_EQUAL(u"Testname"_ustr, ::vcl::filter::PDFDocument::DecodeHexStringUTF16BE(
                                                   *dynamic_cast<vcl::filter::PDFHexStringElement*>(
                                                       pAnnot->Lookup("Contents"_ostr))));
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testPdfImageAnnots)
{
    // Given a document with a PDF image that has 2 comments (popup, text) and a hyperlink:
    // When saving to PDF:
    saveAsPDF(u"pdf-image-annots.odg");

    // Then make sure only the hyperlink is kept, since Draw itself has its own comments:
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1
    // - Actual  : 3
    // i.e. not only the hyperlink but also the 2 comments were exported, leading to duplication.
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getAnnotationCount());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testPdfImageEncryption)
{
    // Given an empty document, with an inserted PDF image:
    loadFromURL(u"private:factory/swriter"_ustr);
    uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY);
    uno::Reference<text::XText> xText = xTextDocument->getText();
    uno::Reference<text::XTextCursor> xCursor = xText->createTextCursor();
    uno::Reference<lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xGraphicObject(
        xFactory->createInstance(u"com.sun.star.text.TextGraphicObject"_ustr), uno::UNO_QUERY);
    OUString aURL = createFileURL(u"rectangles.pdf");
    xGraphicObject->setPropertyValue(u"GraphicURL"_ustr, uno::Any(aURL));
    uno::Reference<drawing::XShape> xShape(xGraphicObject, uno::UNO_QUERY);
    xShape->setSize(awt::Size(1000, 1000));
    uno::Reference<text::XTextContent> xTextContent(xGraphicObject, uno::UNO_QUERY);
    xText->insertTextContent(xCursor->getStart(), xTextContent, /*bAbsorb=*/false);

    // When saving as encrypted PDF:
    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
    uno::Sequence<beans::PropertyValue> aFilterData = {
        comphelper::makePropertyValue(u"EncryptFile"_ustr, true),
        comphelper::makePropertyValue(u"DocumentOpenPassword"_ustr, u"secret"_ustr),
    };
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

    // Then make sure that the image is not lost:
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport("secret"_ostr);
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getObjectCount());
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPdfPage->getObject(0);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Form, pPageObject->getType());
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 2
    // - Actual  : 0
    // i.e. instead of the white background and the actual form child, the image was lost due to
    // missing encryption.
    CPPUNIT_ASSERT_EQUAL(2, pPageObject->getFormObjectCount());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testBitmapScaledown)
{
    // FIXME: the DPI check should be removed when either (1) the test is fixed to work with
    // non-default DPI; or (2) unit tests on Windows are made to use svp VCL plugin.
    if (!IsDefaultDPI())
        return;

    // Given a document with an upscaled and rotated barcode bitmap in it:
    // When saving as PDF:
    saveAsPDF(u"bitmap-scaledown.odt");

    // Then verify that the bitmap is not downscaled:
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    int nPageObjectCount = pPdfPage->getObjectCount();
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPdfPage->getObject(i);
        if (pPageObject->getType() != vcl::pdf::PDFPageObjectType::Image)
            continue;

        std::unique_ptr<vcl::pdf::PDFiumBitmap> pBitmap = pPageObject->getImageBitmap();
        CPPUNIT_ASSERT(pBitmap);
        // In-file sizes: good is 2631x380, bad is 1565x14.
        int nWidth = pBitmap->getWidth();
        // Without the accompanying fix in place, this test would have failed with:
        // - Expected: 2616
        // - Actual  : 1565
        // i.e. the bitmap in the pdf result was small enough to be blurry.
        CPPUNIT_ASSERT_EQUAL(2616, nWidth);
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf139627)
{
#if HAVE_MORE_FONTS
    saveAsPDF(u"justified-arabic-kashida.odt");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    // The document has one page.
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);

    // 7 objects, 3 text, others are path
    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(7, nPageObjectCount);

    // 3 text objects
    OUString sText[3];

    /* With "Noto Sans Arabic" font, these are the X ranges on Linux:
        0: ( 61.75 - 415.94)
        1: (479.70 - 422.40)
        2: (209.40 - 453.2)
    */
    basegfx::B2DRectangle aRect[3];

    std::unique_ptr<vcl::pdf::PDFiumTextPage> pTextPage = pPdfPage->getTextPage();
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            sText[nTextObjectCount] = pPageObject->getText(pTextPage);
            aRect[nTextObjectCount] = pPageObject->getBounds();
            ++nTextObjectCount;
        }
    }
    CPPUNIT_ASSERT_EQUAL(3, nTextObjectCount);

    // Text: جِـرم (which means "mass" in Persian)
    // Rendered as (left to right): "reh + mim" - "kasreh" - "jeh + tatweel"
    int rehmim = 0, kasreh = 1, jehtatweel = 2;

    CPPUNIT_ASSERT_EQUAL(u"رم"_ustr, sText[rehmim].trim());
    CPPUNIT_ASSERT_EQUAL(OUString::fromUtf8("\xD9\x90\xD8\xAC"), sText[kasreh].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, sText[jehtatweel].trim());

    // "Kasreh" should be within "jeh" character
    CPPUNIT_ASSERT_GREATER(aRect[jehtatweel].getMinX(), aRect[kasreh].getMinX());
    CPPUNIT_ASSERT_LESS(aRect[jehtatweel].getMaxX(), aRect[kasreh].getMaxX());

    // "Tatweel" should cover "jeh" and "reh"+"mim" to avoid gap
    // Checking right gap
    //CPPUNIT_ASSERT_GREATER(aRect[jehtatweel].getMinX(), aRect[tatweel].getMaxX());
    // Checking left gap
    // Kashida fails to reach to rehmim before the series of patches starting
    // with 3901e029bd39575f700e69a73818565d62226a23. The visible symptom is
    // a gap in the left of Kashida.
    CPPUNIT_ASSERT_LESS(aRect[rehmim].getMaxX(), aRect[jehtatweel].getMinX());

    // Overlappings of Kashida and surrounding characters is ~9% of the width
    // of the "jeh" character, while using "Noto Arabic Sans" font in this
    // specific example.
    // We set the hard limit of 10% here.
    CPPUNIT_ASSERT_LESS(0.1, fabs(aRect[rehmim].getMaxX() - aRect[jehtatweel].getMinX())
                                 / aRect[jehtatweel].getWidth());
#endif
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testRexportRefToKids)
{
    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        osl_setEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData, u"1"_ustr.pData);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            osl_clearEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData);
    });

    // Load the PDF and save as PDF
    vcl::filter::PDFDocument aDocument;
    load(u"ref-to-kids.pdf", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(5), aPages.size());

    vcl::filter::PDFObjectElement* pResources = aPages[0]->LookupObject("Resources"_ostr);
    CPPUNIT_ASSERT(pResources);

    auto pXObjects
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pResources->Lookup("XObject"_ostr));
    CPPUNIT_ASSERT(pXObjects);

    // Without the fix LookupObject for all /Im's will fail.
    for (auto const& rPair : pXObjects->GetItems())
    {
        if (rPair.first.startsWith("Im"))
            CPPUNIT_ASSERT(pXObjects->LookupObject(rPair.first));
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testRexportFilterSingletonArray)
{
    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        osl_setEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData, u"1"_ustr.pData);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            osl_clearEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData);
    });

    // the test fails with tagged PDF enabled
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "UseTaggedPDF", uno::Any(false) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;

    // Load the PDF and save as PDF
    vcl::filter::PDFDocument aDocument;
    load(u"ref-to-kids.pdf", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(5), aPages.size());

    // Directly go to the inner XObject Im5 that contains the rectangle drawings.
    auto pInnerIm = aDocument.LookupObject(5);
    CPPUNIT_ASSERT(pInnerIm);

    auto pFilter = dynamic_cast<vcl::filter::PDFNameElement*>(pInnerIm->Lookup("Filter"_ostr));
    CPPUNIT_ASSERT(pFilter);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Filter must be FlateDecode", "FlateDecode"_ostr,
                                 pFilter->GetValue());

    vcl::filter::PDFStreamElement* pStream = pInnerIm->GetStream();
    CPPUNIT_ASSERT(pStream);
    SvMemoryStream& rObjectStream = pStream->GetMemory();
    // Uncompress it.
    SvMemoryStream aUncompressed;
    ZCodec aZCodec;
    aZCodec.BeginCompression();
    rObjectStream.Seek(0);
    aZCodec.Decompress(rObjectStream, aUncompressed);
    CPPUNIT_ASSERT(aZCodec.EndCompression());

    // Without the fix, the stream is doubly compressed,
    // hence one decompression will not yield the "re" expressions.
    auto pStart = static_cast<const char*>(aUncompressed.GetData());
    const char* pEnd = pStart + aUncompressed.GetSize();
    OString aImage = "100 0 30 50 re B*\n70 67 50 30 re B*\n"_ostr;
    auto it = std::search(pStart, pEnd, aImage.getStr(), aImage.getStr() + aImage.getLength());
    CPPUNIT_ASSERT(it != pEnd);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testRexportMediaBoxOrigin)
{
    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        osl_setEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData, u"1"_ustr.pData);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            osl_clearEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData);
    });

    // Load the PDF and save as PDF
    vcl::filter::PDFDocument aDocument;
    load(u"ref-to-kids.pdf", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(5), aPages.size());

    // Directly go to the inner XObject Im12 that contains the rectangle drawings in page 2.
    auto pInnerIm = aDocument.LookupObject(12);
    CPPUNIT_ASSERT(pInnerIm);

    static constexpr sal_Int32 aOrigin[2] = { -800, -600 };
    sal_Int32 aSize[2] = { 0, 0 };

    auto pBBox = dynamic_cast<vcl::filter::PDFArrayElement*>(pInnerIm->Lookup("BBox"_ostr));
    CPPUNIT_ASSERT(pBBox);
    const auto& rElements2 = pBBox->GetElements();
    CPPUNIT_ASSERT_EQUAL(size_t(4), rElements2.size());
    for (sal_Int32 nIdx = 0; nIdx < 4; ++nIdx)
    {
        const auto* pNumElement = dynamic_cast<vcl::filter::PDFNumberElement*>(rElements2[nIdx]);
        CPPUNIT_ASSERT(pNumElement);
        if (nIdx < 2)
            CPPUNIT_ASSERT_EQUAL(aOrigin[nIdx], static_cast<sal_Int32>(pNumElement->GetValue()));
        else
            aSize[nIdx - 2] = static_cast<sal_Int32>(pNumElement->GetValue()) - aOrigin[nIdx - 2];
    }

    auto pMatrix = dynamic_cast<vcl::filter::PDFArrayElement*>(pInnerIm->Lookup("Matrix"_ostr));
    CPPUNIT_ASSERT(pMatrix);
    const auto& rElements = pMatrix->GetElements();
    CPPUNIT_ASSERT_EQUAL(size_t(6), rElements.size());
    sal_Int32 aMatTranslate[6]
        = { // Rotation by $\theta$ $cos(\theta), sin(\theta), -sin(\theta), cos(\theta)$
            0, -1, 1, 0,
            // Translate x,y
            -aOrigin[1] - aSize[1] / vcl::PDF_INSERT_MAGIC_SCALE_FACTOR / 2
                + aSize[0] / vcl::PDF_INSERT_MAGIC_SCALE_FACTOR / 2,
            aOrigin[0] + aSize[0] / vcl::PDF_INSERT_MAGIC_SCALE_FACTOR / 2
                + aSize[1] / vcl::PDF_INSERT_MAGIC_SCALE_FACTOR / 2
          };

    for (sal_Int32 nIdx = 0; nIdx < 6; ++nIdx)
    {
        const auto* pNumElement = dynamic_cast<vcl::filter::PDFNumberElement*>(rElements[nIdx]);
        CPPUNIT_ASSERT(pNumElement);
        CPPUNIT_ASSERT_EQUAL(aMatTranslate[nIdx], static_cast<sal_Int32>(pNumElement->GetValue()));
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testRexportResourceItemReference)
{
    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        osl_setEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData, u"1"_ustr.pData);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            osl_clearEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData);
    });

    // Load the PDF and save as PDF
    vcl::filter::PDFDocument aDocument;
    load(u"ref-to-kids.pdf", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(5), aPages.size());

    // Directly go to the inner XObject Im12 that has reference to Font in page 2.
    auto pInnerIm = aDocument.LookupObject(12);
    CPPUNIT_ASSERT(pInnerIm);

    auto pResources
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pInnerIm->Lookup("Resources"_ostr));
    CPPUNIT_ASSERT(pResources);
    auto pFontsReference
        = dynamic_cast<vcl::filter::PDFReferenceElement*>(pResources->LookupElement("Font"_ostr));
    CPPUNIT_ASSERT(pFontsReference);

    auto pFontsObject = pFontsReference->LookupObject();
    CPPUNIT_ASSERT(pFontsObject);

    auto pFontDict
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pFontsObject->Lookup("FF132"_ostr));
    CPPUNIT_ASSERT(pFontDict);

    auto pFontDescriptor = pFontDict->LookupObject("FontDescriptor"_ostr);
    CPPUNIT_ASSERT(pFontDescriptor);

    auto pFontWidths = pFontDict->LookupObject("Widths"_ostr);
    CPPUNIT_ASSERT(pFontWidths);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf152246)
{
    // Import the bugdoc and export as PDF.
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence({
        { "ExportFormFields", uno::Any(true) },
    }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    vcl::filter::PDFDocument aDocument;
    load(u"content-control-rtl.docx", aDocument);

    // The document has one page.
    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    // Position array
    static constexpr double aPos[5][4] = { { 55.699, 706.701, 132.401, 722.499 },
                                           { 197.499, 706.701, 274.201, 722.499 },
                                           { 302.349, 679.101, 379.051, 694.899 },
                                           { 479.599, 679.101, 556.301, 694.899 },
                                           { 55.699, 651.501, 132.401, 667.299 } };

    // Get page annotations.
    auto pAnnots = dynamic_cast<vcl::filter::PDFArrayElement*>(aPages[0]->Lookup("Annots"_ostr));
    CPPUNIT_ASSERT(pAnnots);
    CPPUNIT_ASSERT_EQUAL(size_t(5), pAnnots->GetElements().size());
    for (sal_Int32 i = 0; i < 5; ++i)
    {
        auto pAnnotReference
            = dynamic_cast<vcl::filter::PDFReferenceElement*>(pAnnots->GetElements()[i]);
        CPPUNIT_ASSERT(pAnnotReference);
        vcl::filter::PDFObjectElement* pAnnot = pAnnotReference->LookupObject();
        CPPUNIT_ASSERT(pAnnot);
        CPPUNIT_ASSERT_EQUAL(
            "Annot"_ostr,
            static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Type"_ostr))->GetValue());
        CPPUNIT_ASSERT_EQUAL(
            "Widget"_ostr,
            static_cast<vcl::filter::PDFNameElement*>(pAnnot->Lookup("Subtype"_ostr))->GetValue());

        auto pRect = dynamic_cast<vcl::filter::PDFArrayElement*>(pAnnot->Lookup("Rect"_ostr));
        CPPUNIT_ASSERT(pRect);
        const auto& rElements = pRect->GetElements();
        CPPUNIT_ASSERT_EQUAL(size_t(4), rElements.size());
        for (sal_Int32 nIdx = 0; nIdx < 4; ++nIdx)
        {
            const auto* pNumElement = dynamic_cast<vcl::filter::PDFNumberElement*>(rElements[nIdx]);
            CPPUNIT_ASSERT(pNumElement);
            CPPUNIT_ASSERT_DOUBLES_EQUAL(aPos[i][nIdx], pNumElement->GetValue(), 1e-6);
        }
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf155161)
{
// TODO: We seem to get a fallback font on Windows
#ifndef _WIN32
    vcl::filter::PDFDocument aDocument;
    load(u"tdf155161.odt", aDocument);

    // Check that all fonts in the document are Type 3 fonts
    int nFonts = 0;
    for (const auto& aElement : aDocument.GetElements())
    {
        auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(aElement.get());
        if (!pObject)
            continue;
        auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
        if (pType && pType->GetValue() == "Font")
        {
            auto pSubtype
                = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Subtype"_ostr));
            CPPUNIT_ASSERT(pSubtype);
            CPPUNIT_ASSERT_EQUAL("Type3"_ostr, pSubtype->GetValue());

            auto pName = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Name"_ostr));
            CPPUNIT_ASSERT(pName);
            CPPUNIT_ASSERT_EQUAL("Cantarell-Regular"_ostr, pName->GetValue());

            nFonts++;
        }
    }

#ifdef MACOSX
    // There must be two fonts
    CPPUNIT_ASSERT_EQUAL(2, nFonts);
#else
    // But it seems that embedded variable fonts don’t register all supported
    // styles on Linux, so the bold and regular text use the same regular font.
    CPPUNIT_ASSERT(nFonts);
#endif
#endif
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf48707_1)
{
    // Import the bugdoc and export as PDF.
    saveAsPDF(u"tdf48707-1.fodt");

    // Parse the export result with pdfium.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPage = pPdfDocument->openPage(0);
    CPPUNIT_ASSERT(pPage);

    int nPageObjectCount = pPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(6, nPageObjectCount);

    auto pTextPage = pPage->getTextPage();

    for (int i = 0; i < nPageObjectCount; ++i)
    {
        std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPage->getObject(i);
        // The text and path objects (underline and overline) should all be red.
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
            CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, pPageObject->getFillColor());
        else
            CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, pPageObject->getStrokeColor());
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf48707_2)
{
    // Import the bugdoc and export as PDF.
    saveAsPDF(u"tdf48707-2.fodt");

    // Parse the export result with pdfium.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPage = pPdfDocument->openPage(0);
    CPPUNIT_ASSERT(pPage);

    int nPageObjectCount = pPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(13, nPageObjectCount);

    auto pTextPage = pPage->getTextPage();

    for (int i = 0; i < nPageObjectCount; ++i)
    {
        std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPage->getObject(i);
        if (pPageObject->getType() != vcl::pdf::PDFPageObjectType::Path)
            continue;

        // The table-like paths should be red, underline and overline should be black.
        if (i >= 8)
            CPPUNIT_ASSERT_EQUAL(COL_BLACK, pPageObject->getStrokeColor());
        else
            CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, pPageObject->getStrokeColor());
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf156528)
{
    saveAsPDF(u"wide_page1.fodt");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    // The document has two pages
    CPPUNIT_ASSERT_EQUAL(2, pPdfDocument->getPageCount());

    // 1st page (5100 mm width x 210 mm high, UserUnit = 2)
    auto pPdfPage = pPdfDocument->openPage(0);
    CPPUNIT_ASSERT(pPdfPage);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(o3tl::convert(5100.0 / 2, o3tl::Length::mm, o3tl::Length::pt),
                                 pPdfPage->getWidth(), 1);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(o3tl::convert(210.0 / 2, o3tl::Length::mm, o3tl::Length::pt),
                                 pPdfPage->getHeight(), 1);

    // 1 object (rectangle 5060 mm width x 170 mm high, UserUnit = 2)
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getObjectCount());
    auto pRect = pPdfPage->getObject(0);
    CPPUNIT_ASSERT(pRect);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Path, pRect->getType());
    auto bounds = pRect->getBounds();
    CPPUNIT_ASSERT_DOUBLES_EQUAL(o3tl::convert(5060.0 / 2, o3tl::Length::mm, o3tl::Length::pt),
                                 bounds.getWidth(), 1);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(o3tl::convert(170.0 / 2, o3tl::Length::mm, o3tl::Length::pt),
                                 bounds.getHeight(), 1);

    // 2nd page (210 mm width x 297 mm high, UserUnit = 1)
    pPdfPage = pPdfDocument->openPage(1);
    CPPUNIT_ASSERT(pPdfPage);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(o3tl::convert(210.0, o3tl::Length::mm, o3tl::Length::pt),
                                 pPdfPage->getWidth(), 1);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(o3tl::convert(297.0, o3tl::Length::mm, o3tl::Length::pt),
                                 pPdfPage->getHeight(), 1);

    // 1 object (rectangle 170 mm width x 257 mm high, UserUnit = 1)
    CPPUNIT_ASSERT_EQUAL(1, pPdfPage->getObjectCount());
    pRect = pPdfPage->getObject(0);
    CPPUNIT_ASSERT(pRect);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Path, pRect->getType());
    bounds = pRect->getBounds();
    // Without the fix, this would fail with
    // - Expected: 481.889763779528
    // - Actual  : 241.925001144409
    // - Delta   : 1
    CPPUNIT_ASSERT_DOUBLES_EQUAL(o3tl::convert(170.0, o3tl::Length::mm, o3tl::Length::pt),
                                 bounds.getWidth(), 1);
    //
    // - Expected: 728.503937007874
    // - Actual  : 365.25
    // - Delta   : 1
    CPPUNIT_ASSERT_DOUBLES_EQUAL(o3tl::convert(257.0, o3tl::Length::mm, o3tl::Length::pt),
                                 bounds.getHeight(), 1);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf113866)
{
    loadFromFile(u"tdf113866.odt");

    // Set -- Printer Settings->Options->Print text in Black -- to true
    SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxComponent.get());
    SwDoc* pDoc = pTextDoc->GetDocShell()->GetDoc();
    IDocumentDeviceAccess& rDocAccess = pDoc->getIDocumentDeviceAccess();
    SwPrintData aDocPrintData = rDocAccess.getPrintData();
    aDocPrintData.SetPrintBlackFont(true);
    rDocAccess.setPrintData(aDocPrintData);

    // Export to pdf
    aMediaDescriptor[u"FilterName"_ustr] <<= u"writer_pdf_Export"_ustr;
    saveWithParams(aMediaDescriptor.getAsConstPropertyValueList());

    // Parse the export result with pdfium.
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    // Non-NULL pPdfDocument means pdfium is available.
    if (pPdfDocument != nullptr)
    {
        std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(0);
        CPPUNIT_ASSERT(pPdfPage);

        int nPageObjectCount = pPdfPage->getObjectCount();
        for (int i = 0; i < nPageObjectCount; ++i)
        {
            std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPdfPage->getObject(i);

            if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
                // Without the bug fix in place the test will fail with
                // - Expected: rgba[008000ff]
                // - Actual  : rgba[000000ff]
                // With the bug fixed, the green text in the test doc will stay green,
                // when exported to pdf, while Print Text in Black is true
                CPPUNIT_ASSERT_EQUAL(COL_GREEN, pPageObject->getFillColor());
        }
    }
}

// Form controls coordinates scrambled when exporting to pdf with unchecked form creation in Writer
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf159817)
{
    // Enable PDF/UA
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "ExportFormFields", uno::Any(false) } }));
    aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData;
    saveAsPDF(u"tdf159817.fodt");

    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0);
    CPPUNIT_ASSERT(pPdfPage);
    std::unique_ptr<vcl::pdf::PDFiumTextPage> pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    // So I extracted these values by using SAL_WARN(... << ...getMinimum()), but it appears
    // that the C++ stream operators do not output double values with sufficient resolution for me
    // to recreate those values in code, sigh, so resort to rounding things.
    auto roundPoint = [&pPdfPage](int i) {
        auto p = pPdfPage->getObject(i)->getBounds().getMinimum();
        return basegfx::B2DPoint(std::floor(p.getX() * 10) / 10.0,
                                 std::floor(p.getY() * 10) / 10.0);
    };
    // before the fix these co-ordinates would have been way further down the page
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(8.6, 677.3), roundPoint(13));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(9.3, 677.9), roundPoint(14));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(9.8, 678.5), roundPoint(15));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(85.0, 677.3), roundPoint(16));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(85.6, 677.9), roundPoint(17));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(170.1, 677.3), roundPoint(18));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(170.6, 677.9), roundPoint(19));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(0.0, 654.0), roundPoint(20));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(0.6, 654.6), roundPoint(21));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.3, 655.5), roundPoint(22));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.3, 655.5), roundPoint(23));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.2, 655.5), roundPoint(24));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.2, 655.5), roundPoint(25));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.4, 655.5), roundPoint(26));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.4, 655.5), roundPoint(27));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.1, 655.5), roundPoint(28));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.1, 655.5), roundPoint(29));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.5, 655.5), roundPoint(30));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.5, 655.5), roundPoint(31));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.0, 655.5), roundPoint(32));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(1.0, 655.5), roundPoint(33));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(28.3, 641.4), roundPoint(34));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(28.3, 623.7), roundPoint(35));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(28.3, 623.8), roundPoint(36));
    CPPUNIT_ASSERT_EQUAL(basegfx::B2DPoint(138.6, 623.7), roundPoint(37));
}

// Tests that kerning is correctly applied across color changes
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf61444)
{
    saveAsPDF(u"tdf61444.odt");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    // Get the first page
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    std::unique_ptr<vcl::pdf::PDFiumTextPage> pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    // 4 text objects should be present
    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(4, nPageObjectCount);

    OUString sText[4];
    basegfx::B2DRectangle aRect[4];

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            sText[nTextObjectCount] = pPageObject->getText(pTextPage);
            aRect[nTextObjectCount] = pPageObject->getBounds();
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(4, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"Wait"_ustr, sText[0].trim());
    CPPUNIT_ASSERT_EQUAL(u"W"_ustr, sText[1].trim());
    CPPUNIT_ASSERT_EQUAL(u"ai"_ustr, sText[2].trim());
    CPPUNIT_ASSERT_EQUAL(u"t"_ustr, sText[3].trim());

    // Both lines should have the same kerning, so should end at approximately the same X coordinate
    auto solid_extent = aRect[0].getMaxX();
    auto color_extent = aRect[3].getMaxX();

    CPPUNIT_ASSERT_DOUBLES_EQUAL(solid_extent, color_extent, /*delta*/ 0.15);
}

// tdf#124116 - Tests that track-changes inside a grapheme cluster does not break positioning
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf124116TrackUntrack)
{
    saveAsPDF(u"tdf124116-hebrew-track-untrack.odt");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    // Get the first page
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    std::unique_ptr<vcl::pdf::PDFiumTextPage> pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(15, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    // The underlying document has 4 lines:
    // - שמחַ plain
    // - שמחַ tracked
    // - שמחַ with patah tracked
    // - שמחַ with everything except patah tracked
    // ---
    // However, due to the way text items are inserted for Hebrew, there will be 10:
    // - het with an improperly spaced patah, then שמ for the first 2 lines
    // - as above, followed by a blank for the next 2 representing the actual diacritic
    // ---
    // This test will likely need to be rewritten if tdf#158329 is fixed.
    CPPUNIT_ASSERT_EQUAL(10, nTextObjectCount);

    // All that matters for this test is that the patah is positioned well under the het
    auto het_x0 = aRect.at(4).getMinX();
    auto patah_x0 = aRect.at(6).getMinX();
    CPPUNIT_ASSERT_GREATER(10.0, patah_x0 - het_x0);

    auto het_x1 = aRect.at(7).getMinX();
    auto patah_x1 = aRect.at(9).getMinX();
    CPPUNIT_ASSERT_GREATER(10.0, patah_x1 - het_x1);
}

// tdf#134226 - Tests that shaping is not broken by invisible spans
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf134226)
{
    saveAsPDF(u"tdf134226-shadda-in-hidden-span.fodt");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    // Get the first page
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    std::unique_ptr<vcl::pdf::PDFiumTextPage> pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(8, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(8, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"ة"_ustr, aText[0].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText[1].trim());
    CPPUNIT_ASSERT_EQUAL(u"\u0651ق"_ustr, aText[2].trim());
    CPPUNIT_ASSERT_EQUAL(u"ش"_ustr, aText[3].trim());
    CPPUNIT_ASSERT_EQUAL(u"\u0651ق"_ustr, aText[4].trim());
    CPPUNIT_ASSERT_EQUAL(u"ش"_ustr, aText[5].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText[6].trim());
    CPPUNIT_ASSERT_EQUAL(u"ة"_ustr, aText[7].trim());

    // Verify that the corresponding text segments are positioned roughly equally
    auto fnEqualPos
        = [](const basegfx::B2DRectangle& stExpected, const basegfx::B2DRectangle& stFound) {
              CPPUNIT_ASSERT_DOUBLES_EQUAL(stExpected.getMinX(), stFound.getMinX(), /*delta*/ 0.15);
              CPPUNIT_ASSERT_DOUBLES_EQUAL(stExpected.getMaxX(), stFound.getMaxX(), /*delta*/ 0.15);
          };

    fnEqualPos(aRect[0], aRect[7]);
    fnEqualPos(aRect[1], aRect[6]);
    fnEqualPos(aRect[2], aRect[4]);
    fnEqualPos(aRect[3], aRect[5]);
}

// tdf#71956 - Tests that glyphs can be individually styled
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf71956)
{
    saveAsPDF(u"tdf71956-styled-diacritics.fodt");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    // Get the first page
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    std::unique_ptr<vcl::pdf::PDFiumTextPage> pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(12, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(12, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"ه"_ustr, aText[0].trim());
    CPPUNIT_ASSERT_EQUAL(u"\u064e\u0651\u0670ل"_ustr, aText[1].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText[2].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText[3].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText[4].trim());
    CPPUNIT_ASSERT_EQUAL(u"ل"_ustr, aText[5].trim());
    CPPUNIT_ASSERT_EQUAL(u"ل"_ustr, aText[6].trim());
    CPPUNIT_ASSERT_EQUAL(u"\u064e\u0651\u0670ل"_ustr, aText[7].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText[8].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText[9].trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText[10].trim());
    CPPUNIT_ASSERT_EQUAL(u"ه"_ustr, aText[11].trim());

    // Verify that the corresponding text segments are positioned roughly equally
    auto fnEqualPos
        = [](const basegfx::B2DRectangle& stExpected, const basegfx::B2DRectangle& stFound) {
              CPPUNIT_ASSERT_DOUBLES_EQUAL(stExpected.getMinX(), stFound.getMinX(), /*delta*/ 0.15);
              CPPUNIT_ASSERT_DOUBLES_EQUAL(stExpected.getMaxX(), stFound.getMaxX(), /*delta*/ 0.15);
          };

    fnEqualPos(aRect[0], aRect[11]);
    fnEqualPos(aRect[1], aRect[10]);
    fnEqualPos(aRect[2], aRect[8]);
    fnEqualPos(aRect[3], aRect[9]);
    fnEqualPos(aRect[4], aRect[7]);
    fnEqualPos(aRect[5], aRect[6]);
}

// tdf#101686 - Verifies that drawinglayer clears RTL flags while drawing Writer text boxes
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf101686)
{
    saveAsPDF(u"tdf101686.fodt");
    std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();

    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    // Get the first pace
    std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    std::unique_ptr<vcl::pdf::PDFiumTextPage> pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(3, nPageObjectCount);

    std::vector<OUString> aText;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(3, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"Frame"_ustr, aText[0].trim());

    // Without the fix, one of these two will be "xobtxeT"
    CPPUNIT_ASSERT_EQUAL(u"Textbox"_ustr, aText[1].trim());
    CPPUNIT_ASSERT_EQUAL(u"Textbox"_ustr, aText[2].trim());
}

// tdf#162161 reexport appears to have blank image
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testRexportXnViewColorspace)
{
    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        osl_setEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData, u"1"_ustr.pData);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            osl_clearEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData);
    });

    // Load the PDF and save as PDF
    vcl::filter::PDFDocument aDocument;
    load(u"xnview-colorspace.pdf", aDocument);

    std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());

    // Get access to the only image on the only page.
    vcl::filter::PDFObjectElement* pResources = aPages[0]->LookupObject("Resources"_ostr);
    CPPUNIT_ASSERT(pResources);

    auto pXObjects
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pResources->Lookup("XObject"_ostr));
    CPPUNIT_ASSERT(pXObjects);
    CPPUNIT_ASSERT_EQUAL(size_t(1), pXObjects->GetItems().size());
    vcl::filter::PDFObjectElement* pXObject
        = pXObjects->LookupObject(pXObjects->GetItems().begin()->first);
    CPPUNIT_ASSERT(pXObject);

    auto pSubResources
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pXObject->Lookup("Resources"_ostr));
    CPPUNIT_ASSERT(pSubResources);
    pXObjects = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
        pSubResources->LookupElement("XObject"_ostr));
    CPPUNIT_ASSERT(pXObjects);
    CPPUNIT_ASSERT_EQUAL(size_t(1), pXObjects->GetItems().size());
    pXObject = pXObjects->LookupObject(pXObjects->GetItems().begin()->first);
    CPPUNIT_ASSERT(pXObject);

    pSubResources
        = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pXObject->Lookup("Resources"_ostr));
    CPPUNIT_ASSERT(pSubResources);
    pXObjects = dynamic_cast<vcl::filter::PDFDictionaryElement*>(
        pSubResources->LookupElement("XObject"_ostr));
    CPPUNIT_ASSERT(pXObjects);
    CPPUNIT_ASSERT_EQUAL(size_t(1), pXObjects->GetItems().size());
    pXObject = pXObjects->LookupObject(pXObjects->GetItems().begin()->first);
    CPPUNIT_ASSERT(pXObject);

    // Dig all the way down to this element which is originally
    // 8 0 obj
    // /DeviceRGB
    // endobj
    // and appeared blank when we lost the /DeviceRGB line
    auto pColorspace = pXObject->LookupObject("ColorSpace"_ostr);
    CPPUNIT_ASSERT(pColorspace);
    auto pColorSpaceElement = pColorspace->GetNameElement();
    CPPUNIT_ASSERT(pColorSpaceElement);
    CPPUNIT_ASSERT_EQUAL("DeviceRGB"_ostr, pColorSpaceElement->GetValue());
}

// tdf#157390 - Verifies metrics are correct for PDF export mixing horizontal and vertical CJK
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf157390)
{
    saveAsPDF(u"tdf157390-overlapping-kanji.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);

    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    // This bug manifests as aberrant character advances in the middle horizontal text.

    // Locate the text on the page
    auto aStr = u"無い有る有る"_ustr;

    int nBaseIndex = 0;
    for (int i = 0; i < pTextPage->countChars(); ++i)
    {
        if (pTextPage->getUnicode(i) == static_cast<unsigned int>(aStr[0]))
        {
            nBaseIndex = i;
            break;
        }
    }

    CPPUNIT_ASSERT(nBaseIndex + 6 <= pTextPage->countChars());

    // Extract the character rects
    std::vector<basegfx::B2DRectangle> aRects;
    for (int i = 0; i < 6; ++i)
    {
        auto nPageIndex = nBaseIndex + i;
        CPPUNIT_ASSERT_EQUAL(static_cast<unsigned int>(aStr[i]), pTextPage->getUnicode(nPageIndex));
        aRects.push_back(pTextPage->getCharBox(nPageIndex, /*fPageHeight*/ 1000.0));
    }

    // Verify glyph advances don't exceed some pessimistic range
    double nGuess = aRects.at(0).getMinX();
    for (const auto& stRect : aRects)
    {
        std::cout << stRect << std::endl;

        CPPUNIT_ASSERT_GREATER(nGuess - 0.1 * stRect.getWidth(), stRect.getMinX());
        CPPUNIT_ASSERT_LESS(nGuess + 0.5 * stRect.getWidth(), stRect.getMinX());

        nGuess = stRect.getMaxX();
    }
}

// tdf#162205 - Verifies bidi portions on vertical left-to-right pages are rendered correctly
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf162205Ltr)
{
    saveAsPDF(u"tdf162205-ltr.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(10, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(10, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"T"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_EQUAL(u"h"_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_EQUAL(u"e"_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_EQUAL(u"\u0644"_ustr, aText.at(4).trim()); // lam
    CPPUNIT_ASSERT_EQUAL(u"\u0627"_ustr, aText.at(5).trim()); // alef
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(6).trim());
    CPPUNIT_ASSERT_EQUAL(u"T"_ustr, aText.at(7).trim());
    CPPUNIT_ASSERT_EQUAL(u"h"_ustr, aText.at(8).trim());
    CPPUNIT_ASSERT_EQUAL(u"e"_ustr, aText.at(9).trim());

    // When the bug occurs, the Arabic portion is rendered far to the left of the English portions.
    // Verify that Arabic characters are within range.
    auto fnWithinRange = [](const auto& stExpected, const auto& stFound) {
        CPPUNIT_ASSERT_LESS(20.0, std::abs(stExpected.getMinX() - stFound.getMinX()));
    };

    fnWithinRange(aRect.at(0), aRect.at(4));
    fnWithinRange(aRect.at(7), aRect.at(5));
}

// tdf#162205 - Verifies bidi portions on vertical left-to-right pages are rendered correctly
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf162205Rtl)
{
    saveAsPDF(u"tdf162205-rtl.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(10, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(10, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"T"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_EQUAL(u"h"_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_EQUAL(u"e"_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_EQUAL(u"\u0644"_ustr, aText.at(4).trim()); // lam
    CPPUNIT_ASSERT_EQUAL(u"\u0627"_ustr, aText.at(5).trim()); // alef
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(6).trim());
    CPPUNIT_ASSERT_EQUAL(u"T"_ustr, aText.at(7).trim());
    CPPUNIT_ASSERT_EQUAL(u"h"_ustr, aText.at(8).trim());
    CPPUNIT_ASSERT_EQUAL(u"e"_ustr, aText.at(9).trim());

    // When the bug occurs, the Arabic portion is rendered far to the left of the English portions.
    // Verify that Arabic characters are within range.
    auto fnWithinRange = [](const auto& stExpected, const auto& stFound) {
        CPPUNIT_ASSERT_LESS(20.0, std::abs(stExpected.getMinX() - stFound.getMinX()));
    };

    fnWithinRange(aRect.at(0), aRect.at(4));
    fnWithinRange(aRect.at(7), aRect.at(5));
}

// tdf#162194 - Verifies soft hyphens inside ligatures are rendered correctly.
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf162194SoftHyphen)
{
    saveAsPDF(u"tdf162194-soft-hyphen.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(4, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(4, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"Waffle"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_EQUAL(u"AAA Waf"_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_EQUAL(u"-"_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_EQUAL(u"fle"_ustr, aText.at(3).trim());
}

// tdf#160786 - Tests that Calc format code with repeat char is measured correctly
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf160786)
{
    saveAsPDF(u"tdf160786.fods");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(5, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(5, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"A"_ustr, aText.at(3).trim());

    // The currency line is padded with an unknown number of 'f' characters. It doesn't matter how
    // many are used, as long as the cell is padded to the expected width. Just verify that this
    // text object is the expected one.
    CPPUNIT_ASSERT(o3tl::trim(aText.at(4)).starts_with(u"$"));

    // The currency cell must not overlap the adjacent cell
    CPPUNIT_ASSERT_GREATEREQUAL(aRect.at(3).getMaxX(), aRect.at(4).getMinX());

    // The currency cell must be padded to occupy its space reasonably well.
    // As a heuristic, ensure the free space is no more than the width of "A"
    CPPUNIT_ASSERT_LESS(aRect.at(3).getMaxX() + aRect.at(3).getWidth(), aRect.at(4).getMinX());
}

// tdf#151748 - Textboxes should validate kashida positions
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf151748KashidaSpace)
{
    saveAsPDF(u"tdf151748.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(21, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(17, nTextObjectCount);

    // Box 1: Not enough room for kashida
    CPPUNIT_ASSERT_EQUAL(u"خط تخوردگی"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_EQUAL(u"وتوسط"_ustr, aText.at(1).trim());

    // Box 2: One kashida toward end
    CPPUNIT_ASSERT_EQUAL(u"وردگی"_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim()); // Kashida
    CPPUNIT_ASSERT_EQUAL(u"خط تخ"_ustr, aText.at(4).trim());
    CPPUNIT_ASSERT_EQUAL(u"وتوسط"_ustr, aText.at(5).trim());

    // Box 3: Two kashida
    CPPUNIT_ASSERT_EQUAL(u"وردگی"_ustr, aText.at(6).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(7).trim()); // Kashida
    CPPUNIT_ASSERT_EQUAL(u"ط تخ"_ustr, aText.at(8).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(9).trim()); // Kashida
    CPPUNIT_ASSERT_EQUAL(u"خ"_ustr, aText.at(10).trim());
    CPPUNIT_ASSERT_EQUAL(u"وتوسط"_ustr, aText.at(11).trim());

    // Box 4: One kashida (text size change)
    CPPUNIT_ASSERT_EQUAL(u"خط"_ustr, aText.at(12).trim());
    CPPUNIT_ASSERT_EQUAL(u"وردگی"_ustr, aText.at(13).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(14).trim()); // Kashida
    CPPUNIT_ASSERT_EQUAL(u"تخ"_ustr, aText.at(15).trim());
    CPPUNIT_ASSERT_EQUAL(u"وتوسط"_ustr, aText.at(16).trim());
}

// tdf#163105 - Writer kashida justification should expand spaces
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf163105SwKashidaSpaceExpansion)
{
    saveAsPDF(u"tdf163105-kashida-spaces.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(5, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(5, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"یده"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_EQUAL(u"ه کش"_ustr, aText.at(2).trim()); // This span is whitespace justified
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_EQUAL(u"نویس"_ustr, aText.at(4).trim());

    // Without the fix, this will be less than 25
    CPPUNIT_ASSERT_GREATER(150.0, aRect.at(2).getWidth());
}

// tdf#163105 - Writer should use font information when choosing kashida positions
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf163105Writer)
{
    saveAsPDF(u"tdf163105-writer.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    // The fix allows kashida justification in this document.
    // Without the fix, this will be 1.
    CPPUNIT_ASSERT_EQUAL(5, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(5, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"ارسی"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_EQUAL(u"تن ف"_ustr, aText.at(2).trim()); // This span is whitespace justified
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_EQUAL(u"م"_ustr, aText.at(4).trim());

    // Without the fix, this will be greater than X
    CPPUNIT_ASSERT_LESS(170.0, aRect.at(2).getWidth());
}

// tdf#163105 - Edit Engine should use font information when choosing kashida positions
CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf163105Editeng)
{
    saveAsPDF(u"tdf163105-editeng.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    // The fix allows kashida justification in this document.
    // Without the fix, this will be 1.
    CPPUNIT_ASSERT_EQUAL(5, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(5, nTextObjectCount);

    CPPUNIT_ASSERT_EQUAL(u"ارسی"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_EQUAL(u"تن ف"_ustr, aText.at(2).trim()); // This span is whitespace justified
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_EQUAL(u"م"_ustr, aText.at(4).trim());

    CPPUNIT_ASSERT_LESS(170.0, aRect.at(2).getWidth());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf140767SyriacJustification)
{
    saveAsPDF(u"tdf140767.odt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(11, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    int nTextObjectCount = 0;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
            ++nTextObjectCount;
        }
    }

    CPPUNIT_ASSERT_EQUAL(11, nTextObjectCount);

    std::cout << "Strings" << std::endl;
    for (auto const& em : aText)
    {
        std::cout << em << std::endl;
        for (sal_Int32 i = 0; i < em.getLength(); ++i)
        {
            std::cout << std::hex << static_cast<uint32_t>(em[i]) << " ";
        }
        std::cout << std::endl;
    }

    CPPUNIT_ASSERT_EQUAL(u"ܝ"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_EQUAL(u"ܺܛ"_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_EQUAL(u"ܰܚ"_ustr, aText.at(4).trim());
    CPPUNIT_ASSERT_EQUAL(u"ܕ"_ustr, aText.at(5).trim()); // This span is whitespace justified
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(6).trim());
    CPPUNIT_ASSERT_EQUAL(u"ܰܓ"_ustr, aText.at(7).trim());
    CPPUNIT_ASSERT_EQUAL(u"ܒ"_ustr, aText.at(8).trim());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(9).trim());
    CPPUNIT_ASSERT_EQUAL(u"ܰܐ"_ustr, aText.at(10).trim());

    // Without kashida justification, this space will be 224.328
    CPPUNIT_ASSERT_LESS(90.0, aRect.at(5).getWidth());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf36709FirstLineIndentEm)
{
    saveAsPDF(u"tdf36709.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(16, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
        }
    }

    CPPUNIT_ASSERT_EQUAL(size_t(16), aText.size());

    // Lines from the Writer portion
    CPPUNIT_ASSERT_EQUAL(u"0 em constant size"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(57.256, aRect.at(0).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"1 em constant size"_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(69.856, aRect.at(1).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"2 em constant size"_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(81.328, aRect.at(2).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"3 em constant size"_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(93.376, aRect.at(3).getMinX(), /*delta*/ 2.0);

    CPPUNIT_ASSERT_EQUAL(u"2 em variable size"_ustr, aText.at(4).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(81.328, aRect.at(4).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"2 em variable size"_ustr, aText.at(5).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(89.504, aRect.at(5).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"2 em variable size"_ustr, aText.at(6).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(97.680, aRect.at(6).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"2 em variable size"_ustr, aText.at(7).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(105.856, aRect.at(7).getMinX(), /*delta*/ 2.0);

    // Lines from the Edit Engine portion
    CPPUNIT_ASSERT_EQUAL(u"0 em constant size"_ustr, aText.at(8).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(62.106, aRect.at(8).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"1 em constant size"_ustr, aText.at(9).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(76.010, aRect.at(9).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"2 em constant size"_ustr, aText.at(10).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(88.778, aRect.at(10).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"3 em constant size"_ustr, aText.at(11).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(102.126, aRect.at(11).getMinX(), /*delta*/ 2.0);

    CPPUNIT_ASSERT_EQUAL(u"2 em variable size"_ustr, aText.at(12).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(88.778, aRect.at(12).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"2 em variable size"_ustr, aText.at(13).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(97.754, aRect.at(13).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"2 em variable size"_ustr, aText.at(14).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(106.830, aRect.at(14).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"2 em variable size"_ustr, aText.at(15).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(115.906, aRect.at(15).getMinX(), /*delta*/ 2.0);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf163913LeftRightMarginEm)
{
    saveAsPDF(u"tdf163913.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(9, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
        }
    }

    CPPUNIT_ASSERT_EQUAL(size_t(9), aText.size());

    // Lines from the Writer portion
    CPPUNIT_ASSERT_EQUAL(u"AAAAAAAAAAA"_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(56.800, aRect.at(0).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"AAAA"_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(56.800, aRect.at(1).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"AAAAAAAA"_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(117.400, aRect.at(2).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"AAAAAAA"_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(117.400, aRect.at(3).getMinX(), /*delta*/ 2.0);

    CPPUNIT_ASSERT_EQUAL(u""_ustr, aText.at(4).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(56.800, aRect.at(4).getMinX(), /*delta*/ 2.0);

    // Lines from the Edit Engine portion
    CPPUNIT_ASSERT_EQUAL(u"AAAAAAAAAAA"_ustr, aText.at(5).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(56.800, aRect.at(5).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"AAAA"_ustr, aText.at(6).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(56.800, aRect.at(6).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"AAAAAAAA"_ustr, aText.at(7).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(123.750, aRect.at(7).getMinX(), /*delta*/ 2.0);
    CPPUNIT_ASSERT_EQUAL(u"AAAAAAA"_ustr, aText.at(8).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(123.750, aRect.at(8).getMinX(), /*delta*/ 2.0);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testFormRoundtrip)
{
    // Loads and saves a PDF with filled forms. This checks the forms survive the round-trip.

    // We need to enable PDFium import (and make sure to disable after the test)
    bool bResetEnvVar = false;
    if (getenv("LO_IMPORT_USE_PDFIUM") == nullptr)
    {
        bResetEnvVar = true;
        osl_setEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData, u"1"_ustr.pData);
    }
    comphelper::ScopeGuard aPDFiumEnvVarGuard([&]() {
        if (bResetEnvVar)
            osl_clearEnvironment(u"LO_IMPORT_USE_PDFIUM"_ustr.pData);
    });

    // Need to properly set the PDF export options
    aMediaDescriptor["FilterName"] <<= OUString("draw_pdf_Export");
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "UseTaggedPDF", uno::Any(true) } }));
    aMediaDescriptor["FilterData"] <<= aFilterData;

    saveAsPDF(u"FilledUpForm.pdf");

    // Parse the round-tripped document with PDFium
    auto pPdfDocument = parsePDFExport();
    // Should be 1 page
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());
    std::unique_ptr<vcl::pdf::PDFiumPage> pPage = pPdfDocument->openPage(0);
    std::unique_ptr<vcl::pdf::PDFiumPageObject> pPageObject = pPage->getObject(1);

    // 5 annotations means 5 form fields
    CPPUNIT_ASSERT_EQUAL(5, pPage->getAnnotationCount());

    // Check each form
    {
        std::unique_ptr<vcl::pdf::PDFiumAnnotation> pAnnotation = pPage->getAnnotation(0);
        CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFFormFieldType::CheckBox,
                             pAnnotation->getFormFieldType(pPdfDocument.get()));
    }

    {
        std::unique_ptr<vcl::pdf::PDFiumAnnotation> pAnnotation = pPage->getAnnotation(1);
        CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFFormFieldType::ComboBox,
                             pAnnotation->getFormFieldType(pPdfDocument.get()));
    }

    {
        std::unique_ptr<vcl::pdf::PDFiumAnnotation> pAnnotation = pPage->getAnnotation(2);
        CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFFormFieldType::TextField,
                             pAnnotation->getFormFieldType(pPdfDocument.get()));
    }

    {
        std::unique_ptr<vcl::pdf::PDFiumAnnotation> pAnnotation = pPage->getAnnotation(3);
        CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFFormFieldType::TextField,
                             pAnnotation->getFormFieldType(pPdfDocument.get()));
    }

    {
        std::unique_ptr<vcl::pdf::PDFiumAnnotation> pAnnotation = pPage->getAnnotation(4);
        CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFFormFieldType::TextField,
                             pAnnotation->getFormFieldType(pPdfDocument.get()));
    }
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf162750SmallCapsLigature)
{
    saveAsPDF(u"tdf162750.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(3, nPageObjectCount);

    std::vector<OUString> aText;
    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
        }
    }

    CPPUNIT_ASSERT_EQUAL(size_t(3), aText.size());
    CPPUNIT_ASSERT_EQUAL(u"ffi"_ustr, aText.at(0).trim());

    // Without the fix, this will be "ffi"
    CPPUNIT_ASSERT_EQUAL(u"f"_ustr, aText.at(1).trim());

    CPPUNIT_ASSERT_EQUAL(u"FI"_ustr, aText.at(2).trim());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf164106SplitReorderedClusters)
{
    saveAsPDF(u"tdf164106.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(14, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
        }
    }

    CPPUNIT_ASSERT_EQUAL(size_t(14), aText.size());

    auto fnCompareIndices = [&](size_t nSplit, size_t nCombined) {
        CPPUNIT_ASSERT_EQUAL(aText.at(nSplit).trim(), aText.at(nCombined).trim());
        CPPUNIT_ASSERT_DOUBLES_EQUAL(aRect.at(nSplit).getMinX(), aRect.at(nCombined).getMinX(),
                                     /*delta*/ 0.2);
        CPPUNIT_ASSERT_DOUBLES_EQUAL(aRect.at(nSplit).getMaxX(), aRect.at(nCombined).getMaxX(),
                                     /*delta*/ 0.2);
    };

    fnCompareIndices(0, 7);
    fnCompareIndices(1, 8);
    fnCompareIndices(2, 9);
    fnCompareIndices(3, 10);
    fnCompareIndices(4, 11);
    fnCompareIndices(5, 12);
    fnCompareIndices(6, 13);
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testPDFAttachmentsWithEncryptedFile)
{
    // Encrypt the document and use the hybrid mode.
    // The original ODF document will be saved to the PDF as an attachment.

    aMediaDescriptor["FilterName"] <<= OUString("writer_pdf_Export");
    uno::Sequence<beans::PropertyValue> aFilterData
        = { comphelper::makePropertyValue("IsAddStream", true),
            comphelper::makePropertyValue("EncryptFile", true),
            comphelper::makePropertyValue("DocumentOpenPassword", OUString("secret")) };
    aMediaDescriptor["FilterData"] <<= aFilterData;

    saveAsPDF(u"SimpleTestDocument.fodt");

    // Parse the round-tripped document with PDFium
    auto pPdfDocument = parsePDFExport("secret"_ostr);

    // Should be 1 page
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    // Should have 1 attachment
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getAttachmentCount());

    // Get the attachment
    auto pAttachment = pPdfDocument->getAttachment(0);
    CPPUNIT_ASSERT(pAttachment);

    // Check the filename of the attachment
    CPPUNIT_ASSERT_EQUAL(u"Original.odt"_ustr, pAttachment->getName());

    // Write the attachment to the buffer
    std::vector<sal_uInt8> aBuffer;
    CPPUNIT_ASSERT(pAttachment->getFile(aBuffer));
    CPPUNIT_ASSERT_GREATER(size_t(0), aBuffer.size());

    // Create a temp file and store the content of the attachment
    utl::TempFileNamed aTempFile;
    aTempFile.EnableKillingFile();
    {
        SvFileStream aOutputStream(aTempFile.GetURL(), StreamMode::WRITE | StreamMode::TRUNC);
        aOutputStream.WriteBytes(aBuffer.data(), aBuffer.size());
    }

    // Load the attached document from the temp file
    UnoApiTest::loadFromURL(aTempFile.GetURL());

    // Check the content - first paragraph
    uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY);
    CPPUNIT_ASSERT(xTextDocument.is());
    uno::Reference<container::XEnumerationAccess> xParagraphEnumAccess(xTextDocument->getText(),
                                                                       uno::UNO_QUERY);
    CPPUNIT_ASSERT(xParagraphEnumAccess.is());
    uno::Reference<container::XEnumeration> xParagraphEnum
        = xParagraphEnumAccess->createEnumeration();
    uno::Reference<text::XTextContent> const xElement(xParagraphEnum->nextElement(),
                                                      uno::UNO_QUERY);
    CPPUNIT_ASSERT(xElement.is());
    uno::Reference<text::XTextRange> const xParagraph(xElement, uno::UNO_QUERY);
    CPPUNIT_ASSERT(xParagraph.is());

    CPPUNIT_ASSERT_EQUAL(u"This is a test document."_ustr, xParagraph->getString());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTextBoxRuby)
{
    // This test exercises a work-in-progress Edit Engine ruby feature.
    // It is expected that this test will fail and need to be updated
    // as the feature is refined.

    saveAsPDF(u"textbox-ruby.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(17, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
        }
    }

    CPPUNIT_ASSERT_EQUAL(size_t(17), aText.size());

    // Lines from the Writer portion
    CPPUNIT_ASSERT_EQUAL(u"Prototype test for ruby characters in Edit Engine"_ustr,
                         aText.at(0).trim());

    // Lines from the Edit Engine portion
    CPPUNIT_ASSERT_EQUAL(u"Left-aligned:"_ustr, aText.at(1).trim());

    CPPUNIT_ASSERT_EQUAL(u"top1"_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(171.0, aRect.at(2).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(734.0, aRect.at(2).getMaxY(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_EQUAL(u"BASE"_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(171.0, aRect.at(3).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(719.0, aRect.at(3).getMinY(), /*delta*/ 5.0);

    CPPUNIT_ASSERT_EQUAL(u"Centered:"_ustr, aText.at(4).trim());

    CPPUNIT_ASSERT_EQUAL(u"top2"_ustr, aText.at(5).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(165.0, aRect.at(5).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(715.0, aRect.at(5).getMaxY(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_EQUAL(u"BASE"_ustr, aText.at(6).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(156.0, aRect.at(6).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(701.0, aRect.at(6).getMinY(), /*delta*/ 5.0);

    CPPUNIT_ASSERT_EQUAL(u"Right-aligned:"_ustr, aText.at(7).trim());

    CPPUNIT_ASSERT_EQUAL(u"top3"_ustr, aText.at(8).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(198.0, aRect.at(8).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(697.0, aRect.at(8).getMaxY(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_EQUAL(u"BASE"_ustr, aText.at(9).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(178.0, aRect.at(9).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(682.0, aRect.at(9).getMinY(), /*delta*/ 5.0);

    CPPUNIT_ASSERT_EQUAL(u"Below:"_ustr, aText.at(10).trim());

    CPPUNIT_ASSERT_EQUAL(u"top4"_ustr, aText.at(11).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(153.0, aRect.at(11).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(667.0, aRect.at(11).getMaxY(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_EQUAL(u"BASE"_ustr, aText.at(12).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(144.0, aRect.at(12).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(668.0, aRect.at(12).getMinY(), /*delta*/ 5.0);

    CPPUNIT_ASSERT_EQUAL(u"Line wrapped: other"_ustr, aText.at(13).trim());

    CPPUNIT_ASSERT_EQUAL(u"top5"_ustr, aText.at(14).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(133.0, aRect.at(14).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(650.0, aRect.at(14).getMaxY(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_EQUAL(u"BASE BASE"_ustr, aText.at(15).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(106.0, aRect.at(15).getMinX(), /*delta*/ 5.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(636.0, aRect.at(15).getMinY(), /*delta*/ 5.0);

    CPPUNIT_ASSERT_EQUAL(u"other"_ustr, aText.at(16).trim());
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf166044ContFootnoteOnlyOnePgNum)
{
    saveAsPDF(u"tdf166044-cont-footnote-one-pgnum.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(2, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();
    CPPUNIT_ASSERT_EQUAL(32, nPageObjectCount);

    auto pContNoticeObject = pPdfPage->getObject(29);
    CPPUNIT_ASSERT(pContNoticeObject);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Text, pContNoticeObject->getType());

    // Without the fix, this assert would fail with:
    // - Expected: ມະນດ
    // - Actual  : ມະນດ2
    CPPUNIT_ASSERT_EQUAL(u"ມະນດ"_ustr, pContNoticeObject->getText(pTextPage));

    auto pPgNumObject = pPdfPage->getObject(30);
    CPPUNIT_ASSERT(pPgNumObject);
    CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Text, pPgNumObject->getType());
    CPPUNIT_ASSERT_EQUAL(u"2"_ustr, pPgNumObject->getText(pTextPage));
}

CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf118350StartEndParaAlign)
{
    saveAsPDF(u"tdf118350-start-end-para-align.fodt");

    auto pPdfDocument = parsePDFExport();
    CPPUNIT_ASSERT_EQUAL(1, pPdfDocument->getPageCount());

    auto pPdfPage = pPdfDocument->openPage(/*nIndex*/ 0);
    CPPUNIT_ASSERT(pPdfPage);
    auto pTextPage = pPdfPage->getTextPage();
    CPPUNIT_ASSERT(pTextPage);

    int nPageObjectCount = pPdfPage->getObjectCount();

    CPPUNIT_ASSERT_EQUAL(24, nPageObjectCount);

    std::vector<OUString> aText;
    std::vector<basegfx::B2DRectangle> aRect;

    for (int i = 0; i < nPageObjectCount; ++i)
    {
        auto pPageObject = pPdfPage->getObject(i);
        CPPUNIT_ASSERT_MESSAGE("no object", pPageObject != nullptr);
        if (pPageObject->getType() == vcl::pdf::PDFPageObjectType::Text)
        {
            aText.push_back(pPageObject->getText(pTextPage));
            aRect.push_back(pPageObject->getBounds());
        }
    }

    CPPUNIT_ASSERT_EQUAL(size_t(24), aText.size());

    // Lines from the Writer portion
    CPPUNIT_ASSERT_EQUAL(u"This paragraph is LTR aligned left."_ustr, aText.at(0).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(57.0, aRect.at(0).getMinX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is LTR aligned start."_ustr, aText.at(1).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(57.0, aRect.at(1).getMinX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is LTR aligned right."_ustr, aText.at(2).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(550.0, aRect.at(2).getMaxX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is LTR aligned end."_ustr, aText.at(3).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(550.0, aRect.at(3).getMaxX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is RTL aligned left"_ustr, aText.at(4).trim());
    CPPUNIT_ASSERT_EQUAL(u"."_ustr, aText.at(5).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(57.0, aRect.at(5).getMinX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is RTL aligned start"_ustr, aText.at(6).trim());
    CPPUNIT_ASSERT_EQUAL(u"."_ustr, aText.at(7).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(550.0, aRect.at(6).getMaxX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is RTL aligned right"_ustr, aText.at(8).trim());
    CPPUNIT_ASSERT_EQUAL(u"."_ustr, aText.at(9).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(550.0, aRect.at(8).getMaxX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is RTL aligned end"_ustr, aText.at(10).trim());
    CPPUNIT_ASSERT_EQUAL(u"."_ustr, aText.at(11).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(57.0, aRect.at(11).getMinX(), /*delta*/ 10.0);

    // Lines from the Edit Engine portion
    CPPUNIT_ASSERT_EQUAL(u"This paragraph is LTR aligned left."_ustr, aText.at(12).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(99.0, aRect.at(12).getMinX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is LTR aligned start."_ustr, aText.at(13).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(99.0, aRect.at(13).getMinX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is LTR aligned right."_ustr, aText.at(14).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(537.0, aRect.at(14).getMaxX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is LTR aligned end."_ustr, aText.at(15).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(537.0, aRect.at(15).getMaxX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is RTL aligned left"_ustr, aText.at(16).trim());
    CPPUNIT_ASSERT_EQUAL(u"."_ustr, aText.at(17).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(99.0, aRect.at(17).getMinX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is RTL aligned start"_ustr, aText.at(18).trim());
    CPPUNIT_ASSERT_EQUAL(u"."_ustr, aText.at(19).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(537.0, aRect.at(18).getMaxX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is RTL aligned right"_ustr, aText.at(20).trim());
    CPPUNIT_ASSERT_EQUAL(u"."_ustr, aText.at(21).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(537.0, aRect.at(20).getMaxX(), /*delta*/ 10.0);

    CPPUNIT_ASSERT_EQUAL(u"This paragraph is RTL aligned end"_ustr, aText.at(22).trim());
    CPPUNIT_ASSERT_EQUAL(u"."_ustr, aText.at(23).trim());
    CPPUNIT_ASSERT_DOUBLES_EQUAL(99.0, aRect.at(23).getMinX(), /*delta*/ 10.0);
}

} // end anonymous namespace

CPPUNIT_PLUGIN_IMPLEMENT();

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
