tdf#130150 Improve clipping in PDF export

For more info and discusson please have a look
at the task. It reverts the change from tdf#99680
which did a wrong paradigm change in how clip in
Region(s) is defined and tries to fix the
underlying error in a more correct way.
This includes problems noted in tdf#44388 and
tdf#113449.
This is a decent improvement, but - due to dealing
with numerical problems - not yet the whole healing.
Still thinking about how to solve this for good.
Adapted PdfExportTest::testTdf99680() and
PdfExportTest::testTdf99680_2() as needed, empty
clip regions are allowed again. Added comments, too.
Had to change solvePolygonOperationAnd to work
on ranges if both inputs *are* ranges. The AND-case
is then completely solvable. Also increased geometry
for transformations of clip geometries - may help
later.

Change-Id: I2370447597faa6efb81d58ee31c63654e304262e
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/89874
Tested-by: Jenkins
Reviewed-by: Thorsten Behrens <Thorsten.Behrens@CIB.de>
diff --git a/basegfx/source/polygon/b2dpolygontools.cxx b/basegfx/source/polygon/b2dpolygontools.cxx
index c194a38..1cb6158 100644
--- a/basegfx/source/polygon/b2dpolygontools.cxx
+++ b/basegfx/source/polygon/b2dpolygontools.cxx
@@ -331,15 +331,15 @@ namespace basegfx::utils
                        const B2DPoint aPreviousPoint(aCurrentPoint);
                        aCurrentPoint = aCandidate.getB2DPoint(a);

                        // cross-over in Y?
                        const bool bCompYA(fTools::more(aPreviousPoint.getY(), rPoint.getY()));
                        const bool bCompYB(fTools::more(aCurrentPoint.getY(), rPoint.getY()));
                        // cross-over in Y? tdf#130150 use full precision, no need for epsilon
                        const bool bCompYA(aPreviousPoint.getY() > rPoint.getY());
                        const bool bCompYB(aCurrentPoint.getY() > rPoint.getY());

                        if(bCompYA != bCompYB)
                        {
                            // cross-over in X?
                            const bool bCompXA(fTools::more(aPreviousPoint.getX(), rPoint.getX()));
                            const bool bCompXB(fTools::more(aCurrentPoint.getX(), rPoint.getX()));
                            // cross-over in X? tdf#130150 use full precision, no need for epsilon
                            const bool bCompXA(aPreviousPoint.getX() > rPoint.getX());
                            const bool bCompXB(aCurrentPoint.getX() > rPoint.getX());

                            if(bCompXA == bCompXB)
                            {
@@ -355,7 +355,8 @@ namespace basegfx::utils
                                    (aPreviousPoint.getX() - aCurrentPoint.getX()) /
                                    (aPreviousPoint.getY() - aCurrentPoint.getY()));

                                if(fTools::more(fCompare, rPoint.getX()))
                                // tdf#130150 use full precision, no need for epsilon
                                if(fCompare > rPoint.getX())
                                {
                                    bRetval = !bRetval;
                                }
diff --git a/basegfx/source/polygon/b2dpolypolygoncutter.cxx b/basegfx/source/polygon/b2dpolypolygoncutter.cxx
index ad185f4..6263c78 100644
--- a/basegfx/source/polygon/b2dpolypolygoncutter.cxx
+++ b/basegfx/source/polygon/b2dpolypolygoncutter.cxx
@@ -984,6 +984,42 @@ namespace basegfx::utils
            }
            else
            {
                // tdf#130150 shortcut & precision: If both are simple ranges,
                // solve based on ranges
                if(basegfx::utils::isRectangle(rCandidateA) && basegfx::utils::isRectangle(rCandidateB))
                {
                    // *if* both are ranges, AND always can be solved
                    const basegfx::B2DRange aRangeA(rCandidateA.getB2DRange());
                    const basegfx::B2DRange aRangeB(rCandidateB.getB2DRange());

                    if(aRangeA.isInside(aRangeB))
                    {
                        // 2nd completely inside 1st -> 2nd is result of AND
                        return rCandidateB;
                    }

                    if(aRangeB.isInside(aRangeA))
                    {
                        // 2nd completely inside 1st -> 2nd is result of AND
                        return rCandidateA;
                    }

                    // solve by intersection
                    basegfx::B2DRange aIntersect(aRangeA);
                    aIntersect.intersect(aRangeB);

                    if(aIntersect.isEmpty())
                    {
                        // no overlap -> empty polygon as result of AND
                        return B2DPolyPolygon();
                    }

                    // create polygon result
                    return B2DPolyPolygon(
                        basegfx::utils::createPolygonFromRect(
                            aIntersect));
                }

                // concatenate polygons, solve crossovers and throw away all sub-polygons
                // with a depth of < 1. This means to keep all polygons where at least two
                // polygons do overlap.
diff --git a/vcl/qa/cppunit/pdfexport/pdfexport.cxx b/vcl/qa/cppunit/pdfexport/pdfexport.cxx
index d55e09a..706c4ca 100644
--- a/vcl/qa/cppunit/pdfexport/pdfexport.cxx
+++ b/vcl/qa/cppunit/pdfexport/pdfexport.cxx
@@ -823,15 +823,17 @@ void PdfExportTest::testTdf99680()
    aZCodec.Decompress(rObjectStream, aUncompressed);
    CPPUNIT_ASSERT(aZCodec.EndCompression());

    // Make sure there are no empty clipping regions.
    OString aEmptyRegion("0 0 m h W* n");
    auto pStart = static_cast<const char*>(aUncompressed.GetData());
    const char* pEnd = pStart + aUncompressed.GetSize();
    auto it = std::search(pStart, pEnd, aEmptyRegion.getStr(), aEmptyRegion.getStr() + aEmptyRegion.getLength());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Empty clipping region detected!", it, pEnd);
    // tdf#130150 See infos in task - short: tdf#99680 was not the
    // correct fix, so empty clip regions are valid - allow again in tests
    //      Make sure there are no empty clipping regions.
    //      OString aEmptyRegion("0 0 m h W* n");
    //      auto it = std::search(pStart, pEnd, aEmptyRegion.getStr(), aEmptyRegion.getStr() + aEmptyRegion.getLength());
    //      CPPUNIT_ASSERT_EQUAL_MESSAGE("Empty clipping region detected!", it, pEnd);

    // Count save graphic state (q) and restore (Q) operators
    // and ensure their amount is equal
    auto pStart = static_cast<const char*>(aUncompressed.GetData());
    const char* pEnd = pStart + aUncompressed.GetSize();
    size_t nSaveCount = std::count(pStart, pEnd, 'q');
    size_t nRestoreCount = std::count(pStart, pEnd, 'Q');
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Save/restore graphic state operators count mismatch!", nSaveCount, nRestoreCount);
@@ -862,15 +864,17 @@ void PdfExportTest::testTdf99680_2()
        aZCodec.Decompress(rObjectStream, aUncompressed);
        CPPUNIT_ASSERT(aZCodec.EndCompression());

        // Make sure there are no empty clipping regions.
        OString aEmptyRegion("0 0 m h W* n");
        auto pStart = static_cast<const char*>(aUncompressed.GetData());
        const char* pEnd = pStart + aUncompressed.GetSize();
        auto it = std::search(pStart, pEnd, aEmptyRegion.getStr(), aEmptyRegion.getStr() + aEmptyRegion.getLength());
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Empty clipping region detected!", it, pEnd);
        // tdf#130150 See infos in task - short: tdf#99680 was not the
        // correct fix, so empty clip regions are valid - allow again in tests
        //      Make sure there are no empty clipping regions.
        //      OString aEmptyRegion("0 0 m h W* n");
        //      auto it = std::search(pStart, pEnd, aEmptyRegion.getStr(), aEmptyRegion.getStr() + aEmptyRegion.getLength());
        //      CPPUNIT_ASSERT_EQUAL_MESSAGE("Empty clipping region detected!", it, pEnd);

        // Count save graphic state (q) and restore (Q) operators
        // and ensure their amount is equal
        auto pStart = static_cast<const char*>(aUncompressed.GetData());
        const char* pEnd = pStart + aUncompressed.GetSize();
        size_t nSaveCount = std::count(pStart, pEnd, 'q');
        size_t nRestoreCount = std::count(pStart, pEnd, 'Q');
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Save/restore graphic state operators count mismatch!", nSaveCount, nRestoreCount);
diff --git a/vcl/source/gdi/pdfwriter_impl.cxx b/vcl/source/gdi/pdfwriter_impl.cxx
index 27a299d..f5efd31 100644
--- a/vcl/source/gdi/pdfwriter_impl.cxx
+++ b/vcl/source/gdi/pdfwriter_impl.cxx
@@ -9641,8 +9641,15 @@ void PDFWriterImpl::updateGraphicsState(Mode const mode)
                if ( rNewState.m_aClipRegion.count() )
                {
                    m_aPages.back().appendPolyPolygon( rNewState.m_aClipRegion, aLine );
                    aLine.append( "W* n\n" );
                }
                else
                {
                    // tdf#130150 Need to revert tdf#99680, that breaks the
                    // rule that an set but empty clip-region clips everything
                    // aka draws nothing -> nothing is in an empty clip-region
                    aLine.append( "0 0 m h " ); // NULL clip, i.e. nothing visible
                }
                aLine.append( "W* n\n" );

                rNewState.m_aMapMode = aNewMapMode;
                SetMapMode( rNewState.m_aMapMode );
@@ -9787,8 +9794,12 @@ void PDFWriterImpl::setMapMode( const MapMode& rMapMode )

void PDFWriterImpl::setClipRegion( const basegfx::B2DPolyPolygon& rRegion )
{
    basegfx::B2DPolyPolygon aRegion = LogicToPixel( rRegion, m_aGraphicsStack.front().m_aMapMode );
    aRegion = PixelToLogic( aRegion, m_aMapMode );
    // tdf#130150 improve coordinate manipulations to double precision transformations
    const basegfx::B2DHomMatrix aCurrentTransform(
        GetInverseViewTransformation(m_aMapMode) * GetViewTransformation(m_aGraphicsStack.front().m_aMapMode));
    basegfx::B2DPolyPolygon aRegion(rRegion);

    aRegion.transform(aCurrentTransform);
    m_aGraphicsStack.front().m_aClipRegion = aRegion;
    m_aGraphicsStack.front().m_bClipRegion = true;
    m_aGraphicsStack.front().m_nUpdateFlags |= GraphicsStateUpdateFlags::ClipRegion;
@@ -9798,16 +9809,26 @@ void PDFWriterImpl::moveClipRegion( sal_Int32 nX, sal_Int32 nY )
{
    if( m_aGraphicsStack.front().m_bClipRegion && m_aGraphicsStack.front().m_aClipRegion.count() )
    {
        Point aPoint( lcl_convert( m_aGraphicsStack.front().m_aMapMode,
                                   m_aMapMode,
                                   this,
                                   Point( nX, nY ) ) );
        aPoint -= lcl_convert( m_aGraphicsStack.front().m_aMapMode,
                               m_aMapMode,
                               this,
                               Point() );
        // tdf#130150 improve coordinate manipulations to double precision transformations
        basegfx::B2DHomMatrix aConvertA;

        if(MapUnit::MapPixel == m_aGraphicsStack.front().m_aMapMode.GetMapUnit())
        {
            aConvertA = GetInverseViewTransformation(m_aMapMode);
        }
        else
        {
            aConvertA = LogicToLogic(m_aGraphicsStack.front().m_aMapMode, m_aMapMode);
        }

        basegfx::B2DPoint aB2DPointA(nX, nY);
        basegfx::B2DPoint aB2DPointB(0.0, 0.0);
        aB2DPointA *= aConvertA;
        aB2DPointB *= aConvertA;
        aB2DPointA -= aB2DPointB;
        basegfx::B2DHomMatrix aMat;
        aMat.translate( aPoint.X(), aPoint.Y() );

        aMat.translate(aB2DPointA.getX(), aB2DPointA.getY());
        m_aGraphicsStack.front().m_aClipRegion.transform( aMat );
        m_aGraphicsStack.front().m_nUpdateFlags |= GraphicsStateUpdateFlags::ClipRegion;
    }
@@ -9822,9 +9843,14 @@ void PDFWriterImpl::intersectClipRegion( const tools::Rectangle& rRect )

void PDFWriterImpl::intersectClipRegion( const basegfx::B2DPolyPolygon& rRegion )
{
    basegfx::B2DPolyPolygon aRegion( LogicToPixel( rRegion, m_aGraphicsStack.front().m_aMapMode ) );
    aRegion = PixelToLogic( aRegion, m_aMapMode );
    // tdf#130150 improve coordinate manipulations to double precision transformations
    const basegfx::B2DHomMatrix aCurrentTransform(
        GetInverseViewTransformation(m_aMapMode) * GetViewTransformation(m_aGraphicsStack.front().m_aMapMode));
    basegfx::B2DPolyPolygon aRegion(rRegion);

    aRegion.transform(aCurrentTransform);
    m_aGraphicsStack.front().m_nUpdateFlags |= GraphicsStateUpdateFlags::ClipRegion;

    if( m_aGraphicsStack.front().m_bClipRegion )
    {
        basegfx::B2DPolyPolygon aOld( basegfx::utils::prepareForPolygonOperation( m_aGraphicsStack.front().m_aClipRegion ) );