Reorganize FrameBorderPrimitive creation (II)

Step5: Move the view-dependent decomposition from
BorderLinePrimitive2D to SdrFrameBorderPrimitive2D.

It is now possible to use discrete sizes before the
line and edge matching is done what will look much
better. When it was done at BorderLinePrimitive2D
and the matching was already done, that match was
'displaced' with the adapted forced scale to discrete
units.

The space and size used when zooming out for a single
discrete unit (pixel) can heavily vary - it just covers
a much larger logical area than the 'real' line/poly
would do. All this needs to be handled (also for bound
ranges) and can only be in a good way using primitives.

Adapted to no longer do view-dependent changes in
BorderLinePrimitive2D. Adapted to do these now at
SdrFrameBorderPrimitive2D. Currently used to force
the existing border partial lines (up to three) to
not get taller than one logical unit.

Adapted to no longer switch off AntiAliased rendering
in VclPixelProcessor2D for processBorderLinePrimitive2D,
this is problematic with various renderers on various
systems (e.g. vcl still falls back to render multiple
one-pixel-lines when taller than 3.5 pixels which looks
horrible combined with other parts like filled polygons)

All this needs fine balancing on
- all systems
- all renderers
- all apps (which all have their own table implementation)
- all render targets (pixel/PDF/print/slideshow/...)

Done as thorough as possible, but may need additional
finetuning. May also be a motivation to move away from
vcl and implement these urgetly needed system-dependent
primitive renderers...

Adapted UnitTest testDoublePixelProcessing with the needed
comments.

Change-Id: Ie88bb76c2474b6ab3764d45a9cd1669264492acd
Reviewed-on: https://gerrit.libreoffice.org/62344
Tested-by: Jenkins
Reviewed-by: Armin Le Grand <Armin.Le.Grand@cib.de>
diff --git a/drawinglayer/qa/unit/border.cxx b/drawinglayer/qa/unit/border.cxx
index 365c90d..30d278a 100644
--- a/drawinglayer/qa/unit/border.cxx
+++ b/drawinglayer/qa/unit/border.cxx
@@ -189,7 +189,7 @@
        {
            auto pMPLAction = static_cast<MetaPolyLineAction*>(pAction);

            if (0 == pMPLAction->GetLineInfo().GetWidth() && LineStyle::Solid == pMPLAction->GetLineInfo().GetStyle())
            if (0 != pMPLAction->GetLineInfo().GetWidth() && LineStyle::Solid == pMPLAction->GetLineInfo().GetStyle())
            {
                nPolyLineActionCount++;
            }
@@ -198,7 +198,16 @@

    // Check if all eight (2x four) simple lines with width == 0 and
    // solid were created
    const sal_uInt32 nExpectedNumPolyLineActions = 8;
    //
    // This has changed: Now, just the needed 'real' lines get created
    // which have a width of 1. This are two lines. The former multiple
    // lines were a combination of view-dependent force to a single-pixel
    // line width (0 == lineWidth -> hairline) and vcl rendering this
    // using a (insane) combination of single non-AAed lines. All the
    // system-dependent part of the BorderLine stuff is now done in
    // SdrFrameBorderPrimitive2D and svx.
    // Adapted this test - still useful, breaking it may be a hint :-)
    const sal_uInt32 nExpectedNumPolyLineActions = 2;

    CPPUNIT_ASSERT_EQUAL(nExpectedNumPolyLineActions, nPolyLineActionCount);
}
diff --git a/drawinglayer/source/primitive2d/borderlineprimitive2d.cxx b/drawinglayer/source/primitive2d/borderlineprimitive2d.cxx
index 4c7bf97..e23a489 100644
--- a/drawinglayer/source/primitive2d/borderlineprimitive2d.cxx
+++ b/drawinglayer/source/primitive2d/borderlineprimitive2d.cxx
@@ -76,18 +76,6 @@
                && isGap() == rBorderLine.isGap();
        }

        double BorderLine::getAdaptedWidth(double fMinWidth) const
        {
            if(isGap())
            {
                return std::max(getLineAttribute().getWidth(), fMinWidth);
            }
            else
            {
                return getLineAttribute().getWidth();
            }
        }

        // helper to add a centered, maybe stroked line primitive to rContainer
        static void addPolygonStrokePrimitive2D(
            Primitive2DContainer& rContainer,
@@ -124,7 +112,7 @@

            for(const auto& candidate : maBorderLines)
            {
                fRetval += candidate.getAdaptedWidth(mfSmallestAllowedDiscreteGapDistance);
                fRetval += candidate.getLineAttribute().getWidth();
            }

            return fRetval;
@@ -143,7 +131,7 @@

                for(const auto& candidate : maBorderLines)
                {
                    const double fWidth(candidate.getAdaptedWidth(mfSmallestAllowedDiscreteGapDistance));
                    const double fWidth(candidate.getLineAttribute().getWidth());

                    if(!candidate.isGap())
                    {
@@ -289,8 +277,7 @@
            maStart(rStart),
            maEnd(rEnd),
            maBorderLines(rBorderLines),
            maStrokeAttribute(rStrokeAttribute),
            mfSmallestAllowedDiscreteGapDistance(0.0)
            maStrokeAttribute(rStrokeAttribute)
        {
        }

@@ -320,71 +307,6 @@
            return false;
        }

        bool BorderLinePrimitive2D::getSmallestGap(double& rfSmallestGap) const
        {
            bool bGapFound(false);

            for(const auto& candidate : maBorderLines)
            {
                if(candidate.isGap())
                {
                    if(bGapFound)
                    {
                        rfSmallestGap = std::min(rfSmallestGap, candidate.getLineAttribute().getWidth());
                    }
                    else
                    {
                        bGapFound = true;
                        rfSmallestGap = candidate.getLineAttribute().getWidth();
                    }
                }
            }

            return bGapFound;
        }

        void BorderLinePrimitive2D::get2DDecomposition(Primitive2DDecompositionVisitor& rVisitor, const geometry::ViewInformation2D& rViewInformation) const
        {
            ::osl::MutexGuard aGuard(m_aMutex);

            if (!getStart().equal(getEnd()) && getBorderLines().size() > 1)
            {
                // Line with potential gap. In this case, we want to be view-dependent.
                // get the smallest gap
                double fSmallestGap(0.0);

                if(getSmallestGap(fSmallestGap))
                {
                    // Get the current DiscreteUnit, look at X and Y and use the maximum
                    const basegfx::B2DVector aDiscreteVector(rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(1.0, 1.0));
                    const double fDiscreteUnit(std::min(fabs(aDiscreteVector.getX()), fabs(aDiscreteVector.getY())));

                    // When discrete unit is bigger than distance (distance is less than one pixel),
                    // force distance to one pixel. Or expressed different, do not let the distance
                    // get smaller than one pixel. This is done for screen rendering and compatibility.
                    // This can also be done using DiscreteMetricDependentPrimitive2D as base class
                    // for this class, but specialization is better here for later buffering (only
                    // do this when 'double line with gap')
                    const double fNewDiscreteDistance(std::max(fDiscreteUnit, fSmallestGap));

                    if (!rtl::math::approxEqual(fNewDiscreteDistance, mfSmallestAllowedDiscreteGapDistance))
                    {
                        if (!getBuffered2DDecomposition().empty())
                        {
                            // conditions of last local decomposition have changed, delete
                            const_cast< BorderLinePrimitive2D* >(this)->setBuffered2DDecomposition(Primitive2DContainer());
                        }

                        // remember value for usage in create2DDecomposition
                        const_cast< BorderLinePrimitive2D* >(this)->mfSmallestAllowedDiscreteGapDistance = fNewDiscreteDistance;
                    }
                }
            }

            // call base implementation
            BufferedDecompositionPrimitive2D::get2DDecomposition(rVisitor, rViewInformation);
        }

        // provide unique ID
        ImplPrimitive2DIDBlock(BorderLinePrimitive2D, PRIMITIVE2D_ID_BORDERLINEPRIMITIVE2D)

diff --git a/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx b/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx
index 4a89116..ad70cec 100644
--- a/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx
+++ b/drawinglayer/source/processor2d/vclpixelprocessor2d.cxx
@@ -879,17 +879,36 @@

        void VclPixelProcessor2D::processBorderLinePrimitive2D(const drawinglayer::primitive2d::BorderLinePrimitive2D& rBorder)
        {
            // process recursively, but switch off AntiAliasing for
            // Process recursively, but switch off AntiAliasing for
            // horizontal/vertical lines (*not* diagonal lines).
            // Checked using AntialiasingFlags::PixelSnapHairline instead,
            // but with AntiAliasing on the display really is too 'ghosty' when
            // using fine stroking. Correct, but 'ghosty'.

            if (rBorder.isHorizontalOrVertical(getViewInformation2D()))
            // It has shown that there are quite some problems here:
            // - vcl OutDev renderer methods stuill use fallbacks to paint
            //   multiple single lines between discrete sizes of < 3.5 what
            //   looks bad and does not matzch
            // - mix of filled Polygons and Lines is bad when AA switched off
            // - Alignment of AA with non-AA may be bad in diverse different
            //   renderers
            //
            // Due to these reasons I change the strategy: Always draw AAed, but
            // allow fallback to test/check and if needed. The normal case
            // where BorderLines will be system-depenently snapped to have at
            // least a single discrete width per partial line (there may be up to
            // three) works well nowadays due to most renderers moving the AA stuff
            // by 0.5 pixels (discrete units) to match well with the non-AAed parts.
            //
            // Env-Switch for steering this, default is off.
            // Enable by setting at all (and to something)
            static const char* pSwitchOffAntiAliasingForHorVerBorderlines(getenv("SAL_SWITCH_OFF_ANTIALIASING_FOR_HOR_VER_BORTDERLINES"));
            static bool bSwitchOffAntiAliasingForHorVerBorderlines(nullptr != pSwitchOffAntiAliasingForHorVerBorderlines);

            if (bSwitchOffAntiAliasingForHorVerBorderlines && rBorder.isHorizontalOrVertical(getViewInformation2D()))
            {
                AntialiasingFlags nAntiAliasing = mpOutputDevice->GetAntialiasing();
                mpOutputDevice->SetAntialiasing(nAntiAliasing & ~AntialiasingFlags::EnableB2dDraw);

                process(rBorder);
                mpOutputDevice->SetAntialiasing(nAntiAliasing);
            }
diff --git a/include/drawinglayer/primitive2d/borderlineprimitive2d.hxx b/include/drawinglayer/primitive2d/borderlineprimitive2d.hxx
index cb57e40..a3f4198 100644
--- a/include/drawinglayer/primitive2d/borderlineprimitive2d.hxx
+++ b/include/drawinglayer/primitive2d/borderlineprimitive2d.hxx
@@ -38,7 +38,9 @@
        /** BorderLine class
        Helper class holding the style definition for a single part of a full BorderLine definition.
        Line extends are for start/end and for Left/Right, seen in vector direction. If
        Left != Right that means the line has a diagonal start/end
        Left != Right that means the line has a diagonal start/end.
        Think about it similar to a trapezoid, but not aligned to X-Axis and using the
        perpendicular vector to the given one in a right-handed coordinate system.
        */
        class DRAWINGLAYER_DLLPUBLIC BorderLine
        {
@@ -76,9 +78,6 @@
            double getEndRight() const { return mfEndRight; }
            bool isGap() const { return mbIsGap; }

            /// helper to get adapted width (maximum)
            double getAdaptedWidth(double fMinWidth) const;

            /// compare operator
            bool operator==(const BorderLine& rBorderLine) const;
        };
@@ -111,18 +110,10 @@
            /// common style definitions
            const drawinglayer::attribute::StrokeAttribute  maStrokeAttribute;

            // for view dependent decomposition in the case with existing gaps,
            // remember the smallest allowed concrete gap distance, see get2DDecomposition
            // implementation
            double                                          mfSmallestAllowedDiscreteGapDistance;

            /// create local decomposition
            virtual void create2DDecomposition(Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const override;

            /// helper to find smallest defined gap in maBorderLines
            bool getSmallestGap(double& rfSmallestGap) const;

            /// helper to get the full width taking mfSmallestAllowedDiscreteGapDistance into account
            /// helper to get the full width from maBorderLines
            double getFullWidth() const;

        public:
@@ -145,9 +136,6 @@
            /// compare operator
            virtual bool operator==(const BasePrimitive2D& rPrimitive) const override;

            /// Override standard getDecomposition to be view-dependent here
            virtual void get2DDecomposition(Primitive2DDecompositionVisitor& rVisitor, const geometry::ViewInformation2D& rViewInformation) const override;

            /// provide unique ID
            DeclPrimitive2DIDBlock()
        };
diff --git a/include/svx/sdr/primitive2d/sdrframeborderprimitive2d.hxx b/include/svx/sdr/primitive2d/sdrframeborderprimitive2d.hxx
index 29afb35..58f3b94 100644
--- a/include/svx/sdr/primitive2d/sdrframeborderprimitive2d.hxx
+++ b/include/svx/sdr/primitive2d/sdrframeborderprimitive2d.hxx
@@ -72,7 +72,11 @@
                const basegfx::B2DVector& rNormalizedPerpendicular,
                bool bStyleMirrored);

            void create2DDecomposition(Primitive2DContainer& rContainer) const;
            void create2DDecomposition(
                Primitive2DContainer& rContainer,
                double fMinDiscreteUnit) const;

            double getMinimalNonZeroBorderWidth() const;
        };

        typedef std::vector<SdrFrameBorderData> SdrFrameBorderDataVector;
@@ -88,7 +92,10 @@
        {
        private:
            std::shared_ptr<SdrFrameBorderDataVector>   maFrameBorders;
            double                                      mfMinimalNonZeroBorderWidth;
            double                                      mfMinimalNonZeroBorderWidthUsedForDecompose;
            bool                                        mbMergeResult;
            bool                                        mbForceToSingleDiscreteUnit;

        protected:
            // local decomposition.
@@ -99,14 +106,21 @@
        public:
            SdrFrameBorderPrimitive2D(
                std::shared_ptr<SdrFrameBorderDataVector>& rFrameBorders,
                bool bMergeResult);
                bool bMergeResult,
                bool bForceToSingleDiscreteUnit);

            // compare operator
            virtual bool operator==(const BasePrimitive2D& rPrimitive) const override;

            // override to get view-dependent
            virtual void get2DDecomposition(
                Primitive2DDecompositionVisitor& rVisitor,
                const geometry::ViewInformation2D& rViewInformation) const override;

            // data access
            const SdrFrameBorderDataVector& getFrameBorders() const { return *maFrameBorders.get(); }
            bool getMergeResult() const { return mbMergeResult; }
            const std::shared_ptr<SdrFrameBorderDataVector>& getFrameBorders() const { return maFrameBorders; }
            bool doMergeResult() const { return mbMergeResult; }
            bool doForceToSingleDiscreteUnit() const { return mbForceToSingleDiscreteUnit; }

            // provide unique ID
            DeclPrimitive2DIDBlock()
diff --git a/svx/source/dialog/framelinkarray.cxx b/svx/source/dialog/framelinkarray.cxx
index d369da0..0281c03 100644
--- a/svx/source/dialog/framelinkarray.cxx
+++ b/svx/source/dialog/framelinkarray.cxx
@@ -1233,7 +1233,8 @@
            drawinglayer::primitive2d::Primitive2DReference(
                new drawinglayer::primitive2d::SdrFrameBorderPrimitive2D(
                    aData,
                    true)));
                    true,       // try to merge results to have less primitivbes
                    true)));    // force visualization to minimal one discrete unit (pixel)
    }

    return aSequence;
diff --git a/svx/source/sdr/primitive2d/sdrframeborderprimitive2d.cxx b/svx/source/sdr/primitive2d/sdrframeborderprimitive2d.cxx
index f67a24a..072fec2 100644
--- a/svx/source/sdr/primitive2d/sdrframeborderprimitive2d.cxx
+++ b/svx/source/sdr/primitive2d/sdrframeborderprimitive2d.cxx
@@ -19,12 +19,25 @@

#include <svx/sdr/primitive2d/sdrframeborderprimitive2d.hxx>
#include <drawinglayer/primitive2d/borderlineprimitive2d.hxx>
#include <drawinglayer/geometry/viewinformation2d.hxx>
#include <svx/sdr/primitive2d/svx_primitivetypes2d.hxx>
#include <basegfx/polygon/b2dpolygontools.hxx>
#include <svtools/borderhelper.hxx>

namespace
{
    double snapToDiscreteUnit(
        double fValue,
        double fMinimalDiscreteUnit)
    {
        if(0.0 != fValue)
        {
            fValue = std::max(fValue, fMinimalDiscreteUnit);
        }

        return fValue;
    }

    class StyleVectorCombination
    {
    private:
@@ -52,7 +65,8 @@
            const basegfx::B2DVector& rB2DVector,
            double fAngle,
            bool bMirrored,
            const Color* pForceColor)
            const Color* pForceColor,
            double fMinimalDiscreteUnit)
        :   mfRefModeOffset(0.0),
            maB2DVector(rB2DVector),
            mfAngle(fAngle),
@@ -63,9 +77,18 @@
                svx::frame::RefMode aRefMode(rStyle.GetRefMode());
                Color aPrim(rStyle.GetColorPrim());
                Color aSecn(rStyle.GetColorSecn());
                double fPrim(rStyle.Prim());
                double fSecn(rStyle.Secn());
                const bool bSecnUsed(0.0 != fSecn);
                const bool bSecnUsed(0.0 != rStyle.Secn());

                // Get the single segment line widths. This is the point where the
                // minimal discrete unit wil be used if given (fMinimalDiscreteUnit). If
                // not given it's 0.0 and thus will have no influence.
                double fPrim(snapToDiscreteUnit(rStyle.Prim(), fMinimalDiscreteUnit));
                const double fDist(snapToDiscreteUnit(rStyle.Dist(), fMinimalDiscreteUnit));
                double fSecn(snapToDiscreteUnit(rStyle.Secn(), fMinimalDiscreteUnit));

                // Of course also do not use svx::frame::Style::GetWidth() for obvious
                // reasons.
                const double fStyleWidth(fPrim + fDist + fSecn);

                if(bMirrored)
                {
@@ -85,7 +108,7 @@

                if (svx::frame::RefMode::Centered != aRefMode)
                {
                    const double fHalfWidth(rStyle.GetWidth() * 0.5);
                    const double fHalfWidth(fStyleWidth * 0.5);

                    if (svx::frame::RefMode::Begin == aRefMode)
                    {
@@ -108,9 +131,9 @@

                    if(!bPrimTransparent || !bDistTransparent || !bSecnTransparent)
                    {
                        const double a(mfRefModeOffset - (rStyle.GetWidth() * 0.5));
                        const double a(mfRefModeOffset - (fStyleWidth * 0.5));
                        const double b(a + fPrim);
                        const double c(b + rStyle.Dist());
                        const double c(b + fDist);
                        const double d(c + fSecn);

                        maOffsets.push_back(
@@ -122,7 +145,7 @@
                        maOffsets.push_back(
                            OffsetAndHalfWidthAndColor(
                                (b + c) * 0.5,
                                rStyle.Dist() * 0.5,
                                fDist * 0.5,
                                rStyle.UseGapColor()
                                    ? (nullptr != pForceColor ? *pForceColor : rStyle.GetColorGap())
                                    : COL_TRANSPARENT));
@@ -181,14 +204,21 @@
            const svx::frame::Style& rStyle,
            const basegfx::B2DVector& rMyVector,
            const basegfx::B2DVector& rOtherVector,
            bool bMirrored)
            bool bMirrored,
            double fMinimalDiscreteUnit)
        {
            if(rStyle.IsUsed() && !basegfx::areParallel(rMyVector, rOtherVector))
            {
                // create angle between both. angle() needs vectors pointing away from the same point,
                // so take the mirrored one. Add F_PI to get from -pi..+pi to [0..F_PI2] for sorting
                const double fAngle(basegfx::B2DVector(-rMyVector.getX(), -rMyVector.getY()).angle(rOtherVector) + F_PI);
                maEntries.emplace_back(rStyle, rOtherVector, fAngle, bMirrored, nullptr);
                maEntries.emplace_back(
                    rStyle,
                    rOtherVector,
                    fAngle,
                    bMirrored,
                    nullptr,
                    fMinimalDiscreteUnit);
            }
        }

@@ -491,10 +521,17 @@
        const svx::frame::Style& rBorder,                           /// Style of borderline
        const StyleVectorTable& rStartStyleVectorTable,             /// Styles and vectors (pointing away) at borderline start, ccw
        const StyleVectorTable& rEndStyleVectorTable,               /// Styles and vectors (pointing away) at borderline end, cw
        const Color* pForceColor)                                   /// If specified, overrides frame border color.
        const Color* pForceColor,                                   /// If specified, overrides frame border color.
        double fMinimalDiscreteUnit)                                /// minimal discrete unit to use for svx::frame::Style width values
    {
        // get offset color pairs for  style, one per visible line
        const StyleVectorCombination aCombination(rBorder, rX, 0.0, false, pForceColor);
        const StyleVectorCombination aCombination(
            rBorder,
            rX,
            0.0,
            false,
            pForceColor,
            fMinimalDiscreteUnit);

        if(aCombination.empty())
            return;
@@ -515,7 +552,13 @@
        if(bHasEndStyles)
        {
            // Create extends for line ends, create inverse point/vector and inverse offsets.
            const StyleVectorCombination aMirroredCombination(rBorder, -rX, 0.0, true, pForceColor);
            const StyleVectorCombination aMirroredCombination(
                rBorder,
                -rX,
                0.0,
                true,
                pForceColor,
                fMinimalDiscreteUnit);

            getExtends(aExtendSetEnd, rOrigin + rX, aMirroredCombination, -aPerpendX, rEndStyleVectorTable.getEntries());

@@ -568,6 +611,35 @@
                    aBorderlines,
                    aStrokeAttribute)));
    }

    double getMinimalNonZeroValue(double fCurrent, double fNew)
    {
        if(0.0 != fNew)
        {
            if(0.0 != fCurrent)
            {
                fCurrent = std::min(fNew, fCurrent);
            }
            else
            {
                fCurrent = fNew;
            }
        }

        return fCurrent;
    }

    double getMinimalNonZeroBorderWidthFromStyle(double fCurrent, const svx::frame::Style& rStyle)
    {
        if(rStyle.IsUsed())
        {
            fCurrent = getMinimalNonZeroValue(fCurrent, rStyle.Prim());
            fCurrent = getMinimalNonZeroValue(fCurrent, rStyle.Dist());
            fCurrent = getMinimalNonZeroValue(fCurrent, rStyle.Secn());
        }

        return fCurrent;
    }
}

namespace drawinglayer
@@ -618,7 +690,9 @@
            }
        }

        void SdrFrameBorderData::create2DDecomposition(Primitive2DContainer& rContainer) const
        void SdrFrameBorderData::create2DDecomposition(
            Primitive2DContainer& rContainer,
            double fMinimalDiscreteUnit) const
        {
            StyleVectorTable aStartVector;
            StyleVectorTable aEndVector;
@@ -630,7 +704,8 @@
                    rStart.getStyle(),
                    maX,
                    rStart.getNormalizedPerpendicular(),
                    rStart.getStyleMirrored());
                    rStart.getStyleMirrored(),
                    fMinimalDiscreteUnit);
            }

            for(const auto& rEnd : maEnd)
@@ -639,7 +714,8 @@
                    rEnd.getStyle(),
                    aAxis,
                    rEnd.getNormalizedPerpendicular(),
                    rEnd.getStyleMirrored());
                    rEnd.getStyleMirrored(),
                    fMinimalDiscreteUnit);
            }

            aStartVector.sort();
@@ -652,7 +728,25 @@
                maStyle,
                aStartVector,
                aEndVector,
                mbForceColor ? &maColor : nullptr);
                mbForceColor ? &maColor : nullptr,
                fMinimalDiscreteUnit);
        }

        double SdrFrameBorderData::getMinimalNonZeroBorderWidth() const
        {
            double fRetval(getMinimalNonZeroBorderWidthFromStyle(0.0, maStyle));

            for(const auto& rStart : maStart)
            {
                fRetval = getMinimalNonZeroBorderWidthFromStyle(fRetval, rStart.getStyle());
            }

            for(const auto& rEnd : maEnd)
            {
                fRetval = getMinimalNonZeroBorderWidthFromStyle(fRetval, rEnd.getStyle());
            }

            return fRetval;
        }
    } // end of namespace primitive2d
} // end of namespace drawinglayer
@@ -661,19 +755,34 @@
{
    namespace primitive2d
    {
        void SdrFrameBorderPrimitive2D::create2DDecomposition(Primitive2DContainer& rContainer, const geometry::ViewInformation2D& /*aViewInformation*/) const
        void SdrFrameBorderPrimitive2D::create2DDecomposition(
            Primitive2DContainer& rContainer,
            const geometry::ViewInformation2D& /*aViewInformation*/) const
        {
            if(!getFrameBorders())
            {
                return;
            }

            Primitive2DContainer aRetval;

            if(getMergeResult())
            // Check and use the minimal non-zero BorderWidth for decompose
            // if that is set and wanted
            const double fMinimalDiscreteUnit(doForceToSingleDiscreteUnit()
                ? mfMinimalNonZeroBorderWidthUsedForDecompose
                : 0.0);

            if(doMergeResult())
            {
                // decompose all buffered SdrFrameBorderData entries and try to merge them
                // to reduce existing number of BorderLinePrimitive2D(s)
                for(const auto& rCandidate : getFrameBorders())
                for(const auto& rCandidate : *getFrameBorders().get())
                {
                    // get decomposition on one SdrFrameBorderData entry
                    Primitive2DContainer aPartial;
                    rCandidate.create2DDecomposition(aPartial);
                    rCandidate.create2DDecomposition(
                        aPartial,
                        fMinimalDiscreteUnit);

                    for(const auto& aCandidatePartial : aPartial)
                    {
@@ -729,9 +838,11 @@
            else
            {
                // just decompose all buffered SdrFrameBorderData entries, do not try to merge
                for(const auto& rCandidate : getFrameBorders())
                for(const auto& rCandidate : *getFrameBorders().get())
                {
                    rCandidate.create2DDecomposition(aRetval);
                    rCandidate.create2DDecomposition(
                        aRetval,
                        fMinimalDiscreteUnit);
                }
            }

@@ -740,11 +851,25 @@

        SdrFrameBorderPrimitive2D::SdrFrameBorderPrimitive2D(
            std::shared_ptr<SdrFrameBorderDataVector>& rFrameBorders,
            bool bMergeResult)
            bool bMergeResult,
            bool bForceToSingleDiscreteUnit)
        :   BufferedDecompositionPrimitive2D(),
            maFrameBorders(std::move(rFrameBorders)),
            mbMergeResult(bMergeResult)
            mfMinimalNonZeroBorderWidth(0.0),
            mfMinimalNonZeroBorderWidthUsedForDecompose(0.0),
            mbMergeResult(bMergeResult),
            mbForceToSingleDiscreteUnit(bForceToSingleDiscreteUnit)
        {
            if(getFrameBorders() && doForceToSingleDiscreteUnit())
            {
                // detect used minimal non-zero partial border width
                for(const auto& rCandidate : *getFrameBorders().get())
                {
                    mfMinimalNonZeroBorderWidth = getMinimalNonZeroValue(
                        mfMinimalNonZeroBorderWidth,
                        rCandidate.getMinimalNonZeroBorderWidth());
                }
            }
        }

        bool SdrFrameBorderPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
@@ -753,12 +878,50 @@
            {
                const SdrFrameBorderPrimitive2D& rCompare = static_cast<const SdrFrameBorderPrimitive2D&>(rPrimitive);

                return maFrameBorders == rCompare.maFrameBorders;
                return getFrameBorders() == rCompare.getFrameBorders()
                    && doMergeResult() == rCompare.doMergeResult()
                    && doForceToSingleDiscreteUnit() == rCompare.doForceToSingleDiscreteUnit();
            }

            return false;
        }

        void SdrFrameBorderPrimitive2D::get2DDecomposition(
            Primitive2DDecompositionVisitor& rVisitor,
            const geometry::ViewInformation2D& rViewInformation) const
        {
            if(doForceToSingleDiscreteUnit())
            {
                // Get the current DiscreteUnit, look at X and Y and use the maximum
                const basegfx::B2DVector aDiscreteVector(rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(1.0, 1.0));
                double fDiscreteUnit(std::min(fabs(aDiscreteVector.getX()), fabs(aDiscreteVector.getY())));

                if(fDiscreteUnit <= mfMinimalNonZeroBorderWidth)
                {
                    // no need to use it, reset
                    fDiscreteUnit = 0.0;
                }

                if(fDiscreteUnit != mfMinimalNonZeroBorderWidthUsedForDecompose)
                {
                    // conditions of last local decomposition have changed, delete
                    // possible content
                    if(!getBuffered2DDecomposition().empty())
                    {
                        const_cast< SdrFrameBorderPrimitive2D* >(this)->setBuffered2DDecomposition(Primitive2DContainer());
                    }

                    // remember new conditions
                    const_cast< SdrFrameBorderPrimitive2D* >(this)->mfMinimalNonZeroBorderWidthUsedForDecompose = fDiscreteUnit;
                }
            }

            // call parent. This will call back ::create2DDecomposition above
            // where mfMinimalNonZeroBorderWidthUsedForDecompose will be used
            // when doForceToSingleDiscreteUnit() is true
            BufferedDecompositionPrimitive2D::get2DDecomposition(rVisitor, rViewInformation);
        }

        // provide unique ID
        ImplPrimitive2DIDBlock(SdrFrameBorderPrimitive2D, PRIMITIVE2D_ID_SDRFRAMEBORDERTPRIMITIVE2D)

diff --git a/sw/source/core/layout/paintfrm.cxx b/sw/source/core/layout/paintfrm.cxx
index 22122b6..cd65f90 100644
--- a/sw/source/core/layout/paintfrm.cxx
+++ b/sw/source/core/layout/paintfrm.cxx
@@ -2567,7 +2567,8 @@
            drawinglayer::primitive2d::Primitive2DReference(
                new drawinglayer::primitive2d::SdrFrameBorderPrimitive2D(
                    aData,
                    true)));
                    true,       // try to merge results to have less primitivbes
                    true)));    // force visualization to minimal one discrete unit (pixel)
        // paint
        mrTabFrame.ProcessPrimitives(aSequence);
    }
@@ -4630,7 +4631,8 @@
                    drawinglayer::primitive2d::Primitive2DReference(
                        new drawinglayer::primitive2d::SdrFrameBorderPrimitive2D(
                            aData,
                            true)));
                            true,       // try to merge results to have less primitivbes
                            true)));    // force visualization to minimal one discrete unit (pixel)
            }
        }