tdf#143875 tdf#55058 EMF+ Add support for individual line endings

EMF+ is allowing different caps and arrows on both ends
It is not possible to implement that with css::drawing::LineCap,
as it is set line endings on both line start and line end.
Additionally when the Dash Pattern is used, the css::drawing::LineCap
is also applied there.

To resolve that limitation, the Cap needs to be implemented
independetly by using PolygonStrokeArrowPrimitive2D, and
the css::drawing::LineCap inside drawinglayer::attribute::LineAttribute
always set to css::drawing::LineCap_BUTT

Change-Id: I4be76e2dbefcb34154a1404c3b57dc4b7f7ada85
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/133299
Tested-by: Jenkins
Reviewed-by: Bartosz Kosiorek <gang65@poczta.onet.pl>
diff --git a/drawinglayer/source/tools/emfphelperdata.cxx b/drawinglayer/source/tools/emfphelperdata.cxx
index c405a4c..c5b2829 100644
--- a/drawinglayer/source/tools/emfphelperdata.cxx
+++ b/drawinglayer/source/tools/emfphelperdata.cxx
@@ -29,6 +29,7 @@
#include "emfpstringformat.hxx"
#include <basegfx/curve/b2dcubicbezier.hxx>
#include <wmfemfhelper.hxx>
#include <drawinglayer/primitive2d/PolygonStrokeArrowPrimitive2D.hxx>
#include <drawinglayer/primitive2d/unifiedtransparenceprimitive2d.hxx>
#include <drawinglayer/primitive2d/PolyPolygonStrokePrimitive2D.hxx>
#include <drawinglayer/primitive2d/PolyPolygonColorPrimitive2D.hxx>
@@ -519,7 +520,72 @@ namespace emfplushelper
        }
    }

    void EmfPlusHelperData::EMFPPlusDrawPolygon(const ::basegfx::B2DPolyPolygon& polygon, sal_uInt32 penIndex)
    drawinglayer::attribute::LineStartEndAttribute
    EmfPlusHelperData::CreateLineEnd(const sal_Int32 aCap, const float aPenWidth) const
    {
        const double pw = mdExtractedYScale * aPenWidth;
        if (aCap == LineCapTypeSquare)
        {
            basegfx::B2DPolygon aCapPolygon(
                { {-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0} });
            aCapPolygon.setClosed(true);
            return drawinglayer::attribute::LineStartEndAttribute(
                pw, basegfx::B2DPolyPolygon(aCapPolygon), true);
        }
        else if (aCap == LineCapTypeRound)
        {
            basegfx::B2DPolygon aCapPolygon(
                { {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.9236, -0.3827},
                  {0.7071, -0.7071}, {0.3827, -0.9236}, {0.0, -1.0}, {-0.3827, -0.9236},
                  {-0.7071, -0.7071}, {-0.9236, -0.3827}, {-1.0, 0.0} });
            aCapPolygon.setClosed(true);
            return drawinglayer::attribute::LineStartEndAttribute(
                pw, basegfx::B2DPolyPolygon(aCapPolygon), true);
        }
        else if (aCap == LineCapTypeTriangle)
        {
            basegfx::B2DPolygon aCapPolygon(
                { {-1.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, -1.0}, {-1.0, 0.0} });
            aCapPolygon.setClosed(true);
            return drawinglayer::attribute::LineStartEndAttribute(
                pw, basegfx::B2DPolyPolygon(aCapPolygon), true);
        }
        else if (aCap == LineCapTypeSquareAnchor)
        {
            basegfx::B2DPolygon aCapPolygon(
                { {-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0} });
            aCapPolygon.setClosed(true);
            return drawinglayer::attribute::LineStartEndAttribute(
                1.5 * pw, basegfx::B2DPolyPolygon(aCapPolygon), true);
        }
        else if (aCap == LineCapTypeRoundAnchor)
        {
            const basegfx::B2DPolygon aCapPolygon
                = ::basegfx::utils::createPolygonFromEllipse(::basegfx::B2DPoint(0.0, 0.0), 1.0, 1.0);
            return drawinglayer::attribute::LineStartEndAttribute(
                2.0 * pw, basegfx::B2DPolyPolygon(aCapPolygon), true);
        }
        else if (aCap == LineCapTypeDiamondAnchor)
        {
            basegfx::B2DPolygon aCapPolygon({ {0.0, -1.0}, {1.0, 0.0}, {0.5, 0.5},
                                              {0.5, 1.0}, {-0.5, 1.0}, {-0.5, 0.5},
                                              {-1.0, 0.0} });
            aCapPolygon.setClosed(true);
            return drawinglayer::attribute::LineStartEndAttribute(
                2.0 * pw, basegfx::B2DPolyPolygon(aCapPolygon), true);
        }
        else if (aCap == LineCapTypeArrowAnchor)
        {
            basegfx::B2DPolygon aCapPolygon({ {0.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0} });
            aCapPolygon.setClosed(true);
            return drawinglayer::attribute::LineStartEndAttribute(
                2.0 * pw, basegfx::B2DPolyPolygon(aCapPolygon), true);
        }
        return drawinglayer::attribute::LineStartEndAttribute();
    }

    void EmfPlusHelperData::EMFPPlusDrawPolygon(const ::basegfx::B2DPolyPolygon& polygon,
                                                sal_uInt32 penIndex)
    {
        const EMFPPen* pen = dynamic_cast<EMFPPen*>(maEMFPObjects[penIndex & 0xff].get());
        SAL_WARN_IF(!pen, "drawinglayer.emf", "emf+ missing pen");
@@ -527,40 +593,56 @@ namespace emfplushelper
        if (!(pen && polygon.count()))
            return;

        // we need a line cap attribute
        css::drawing::LineCap lineCap = css::drawing::LineCap_BUTT;
        if (pen->penDataFlags & EmfPlusPenDataStartCap) // additional line cap information
        {
            lineCap = static_cast<css::drawing::LineCap>(EMFPPen::lcl_convertStrokeCap(pen->startCap));
            SAL_WARN_IF(pen->startCap != pen->endCap, "drawinglayer.emf", "emf+ pen uses different start and end cap");
        }

        const double transformedPenWidth = mdExtractedYScale * pen->penWidth;
        drawinglayer::attribute::LineAttribute lineAttribute(pen->GetColor().getBColor(),
                                                             transformedPenWidth,
                                                             pen->GetLineJoinType(),
                                                             lineCap,
                                                             basegfx::deg2rad(15.0)); // TODO Add MiterLimit support
        if (!pen->GetColor().IsTransparent())
        drawinglayer::attribute::LineAttribute lineAttribute(
            pen->GetColor().getBColor(), transformedPenWidth, pen->GetLineJoinType(),
            css::drawing::LineCap_BUTT,
            basegfx::deg2rad(15.0)); // TODO Add MiterLimit support

        drawinglayer::attribute::LineStartEndAttribute aStart;
        if (pen->penDataFlags & EmfPlusPenDataStartCap)
            aStart = EmfPlusHelperData::CreateLineEnd(pen->startCap, pen->penWidth);

        drawinglayer::attribute::LineStartEndAttribute aEnd;
        if (pen->penDataFlags & EmfPlusPenDataEndCap)
            aEnd = EmfPlusHelperData::CreateLineEnd(pen->endCap, pen->penWidth);

        if (pen->GetColor().IsTransparent())
        {
            drawinglayer::primitive2d::Primitive2DContainer aContainer;
            if ((pen->penDataFlags & EmfPlusPenDataStartCap)
                || (pen->penDataFlags & EmfPlusPenDataEndCap))
            {
                aContainer.resize(polygon.count());
                for (sal_uInt32 i = 0; i < polygon.count(); i++)
                    aContainer[i] = drawinglayer::primitive2d::Primitive2DReference(
                        new drawinglayer::primitive2d::PolygonStrokeArrowPrimitive2D(
                            polygon.getB2DPolygon(i), lineAttribute,
                            pen->GetStrokeAttribute(mdExtractedXScale), aStart, aEnd));
            }
            else
                aContainer.append(drawinglayer::primitive2d::Primitive2DReference(
                    new drawinglayer::primitive2d::PolyPolygonStrokePrimitive2D(
                        polygon, lineAttribute, pen->GetStrokeAttribute(mdExtractedXScale))));
            mrTargetHolders.Current().append(
                new drawinglayer::primitive2d::PolyPolygonStrokePrimitive2D(
                    polygon,
                    lineAttribute,
                    pen->GetStrokeAttribute(mdExtractedXScale)));
                new drawinglayer::primitive2d::UnifiedTransparencePrimitive2D(
                    std::move(aContainer), (255 - pen->GetColor().GetAlpha()) / 255.0));
        }
        else
        {
            const drawinglayer::primitive2d::Primitive2DReference aPrimitive(
                        new drawinglayer::primitive2d::PolyPolygonStrokePrimitive2D(
                            polygon,
                            lineAttribute,
                            pen->GetStrokeAttribute(mdExtractedXScale)));

            mrTargetHolders.Current().append(
                        new drawinglayer::primitive2d::UnifiedTransparencePrimitive2D(
                            drawinglayer::primitive2d::Primitive2DContainer { aPrimitive },
                            (255 - pen->GetColor().GetAlpha()) / 255.0));
            if ((pen->penDataFlags & EmfPlusPenDataStartCap)
                || (pen->penDataFlags & EmfPlusPenDataEndCap))
                for (sal_uInt32 i = 0; i < polygon.count(); i++)
                {
                    mrTargetHolders.Current().append(
                        new drawinglayer::primitive2d::PolygonStrokeArrowPrimitive2D(
                            polygon.getB2DPolygon(i), lineAttribute,
                            pen->GetStrokeAttribute(mdExtractedXScale), aStart, aEnd));
                }
            else
                mrTargetHolders.Current().append(
                    new drawinglayer::primitive2d::PolyPolygonStrokePrimitive2D(
                        polygon, lineAttribute, pen->GetStrokeAttribute(mdExtractedXScale)));
        }

        if ((pen->penDataFlags & EmfPlusPenDataCustomStartCap) && (pen->customStartCap->polygon.begin()->count() > 1))
@@ -604,7 +686,7 @@ namespace emfplushelper
                            new drawinglayer::primitive2d::PolyPolygonStrokePrimitive2D(
                                startCapPolygon,
                                lineAttribute,
                                pen->GetStrokeAttribute(maMapTransform.get(1, 1))));
                                pen->GetStrokeAttribute(mdExtractedXScale)));
            }
        }

@@ -649,7 +731,7 @@ namespace emfplushelper
                            new drawinglayer::primitive2d::PolyPolygonStrokePrimitive2D(
                                endCapPolygon,
                                lineAttribute,
                                pen->GetStrokeAttribute(maMapTransform.get(1, 1))));
                                pen->GetStrokeAttribute(mdExtractedXScale)));
            }
        }

diff --git a/drawinglayer/source/tools/emfphelperdata.hxx b/drawinglayer/source/tools/emfphelperdata.hxx
index 600f666..796cbff 100644
--- a/drawinglayer/source/tools/emfphelperdata.hxx
+++ b/drawinglayer/source/tools/emfphelperdata.hxx
@@ -21,6 +21,7 @@

#include <wmfemfhelper.hxx>
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <drawinglayer/attribute/linestartendattribute.hxx>
#include <tools/stream.hxx>
#include <basegfx/point/b2dpoint.hxx>
#include <map>
@@ -232,6 +233,9 @@ namespace emfplushelper
        void GraphicStatePush(GraphicStateMap& map, sal_Int32 index);
        void GraphicStatePop(GraphicStateMap& map, sal_Int32 index);

        drawinglayer::attribute::LineStartEndAttribute CreateLineEnd(const sal_Int32 aCap,
                                                                     const float aPenWidth) const;

        // primitive creators
        void EMFPPlusDrawPolygon(const ::basegfx::B2DPolyPolygon& polygon, sal_uInt32 penIndex);
        void EMFPPlusFillPolygon(const ::basegfx::B2DPolyPolygon& polygon, const bool isColor, const sal_uInt32 brushIndexOrColor);
diff --git a/drawinglayer/source/tools/emfppen.cxx b/drawinglayer/source/tools/emfppen.cxx
index b927ac1..b0408f8 100644
--- a/drawinglayer/source/tools/emfppen.cxx
+++ b/drawinglayer/source/tools/emfppen.cxx
@@ -173,23 +173,6 @@ namespace emfplushelper
        return "";
    }

    /// Convert stroke caps between EMF+ and rendering API
    sal_Int8 EMFPPen::lcl_convertStrokeCap(sal_uInt32 nEmfStroke)
    {
        switch (nEmfStroke)
        {
            case EmfPlusLineCapTypeSquare:
                return rendering::PathCapType::SQUARE;
            // we have no mapping for EmfPlusLineCapTypeTriangle,
            // but it is similar to Round
            case EmfPlusLineCapTypeTriangle: // fall-through
            case EmfPlusLineCapTypeRound:
                return rendering::PathCapType::ROUND;
        }

        return rendering::PathCapType::BUTT;
    }

    basegfx::B2DLineJoin EMFPPen::GetLineJoinType() const
    {
        if (penDataFlags & EmfPlusPenDataJoin) // additional line join information
diff --git a/drawinglayer/source/tools/emfppen.hxx b/drawinglayer/source/tools/emfppen.hxx
index a22ae14..cad849e 100644
--- a/drawinglayer/source/tools/emfppen.hxx
+++ b/drawinglayer/source/tools/emfppen.hxx
@@ -123,7 +123,6 @@ namespace emfplushelper

        void Read(SvStream& s, EmfPlusHelperData const & rR);

        static sal_Int8 lcl_convertStrokeCap(sal_uInt32 nEmfStroke);
        drawinglayer::attribute::StrokeAttribute GetStrokeAttribute(const double aTransformation) const;
        basegfx::B2DLineJoin GetLineJoinType() const;
    };
diff --git a/drawinglayer/source/tools/primitive2dxmldump.cxx b/drawinglayer/source/tools/primitive2dxmldump.cxx
index 3074ad3..886ffbf 100644
--- a/drawinglayer/source/tools/primitive2dxmldump.cxx
+++ b/drawinglayer/source/tools/primitive2dxmldump.cxx
@@ -23,6 +23,7 @@
#include <drawinglayer/primitive2d/Tools.hxx>
#include <drawinglayer/primitive2d/transformprimitive2d.hxx>
#include <drawinglayer/primitive2d/PolygonHairlinePrimitive2D.hxx>
#include <drawinglayer/primitive2d/PolygonStrokeArrowPrimitive2D.hxx>
#include <drawinglayer/primitive2d/PolygonStrokePrimitive2D.hxx>
#include <drawinglayer/primitive2d/PolyPolygonStrokePrimitive2D.hxx>
#include <drawinglayer/primitive2d/PolyPolygonColorPrimitive2D.hxx>
@@ -722,6 +723,49 @@ void Primitive2dXmlDump::decomposeAndWrite(
                rWriter.endElement();
            }
            break;

            case PRIMITIVE2D_ID_POLYGONSTROKEARROWPRIMITIVE2D:
            {
                const PolygonStrokeArrowPrimitive2D& rPolygonStrokeArrowPrimitive2D
                    = dynamic_cast<const PolygonStrokeArrowPrimitive2D&>(*pBasePrimitive);
                rWriter.startElement("polygonstrokearrow");

                rWriter.startElement("polygon");
                rWriter.content(basegfx::utils::exportToSvgPoints(
                    rPolygonStrokeArrowPrimitive2D.getB2DPolygon()));
                rWriter.endElement();

                if (rPolygonStrokeArrowPrimitive2D.getStart().getB2DPolyPolygon().count())
                {
                    rWriter.startElement("linestartattribute");
                    rWriter.attribute("width",
                                      rPolygonStrokeArrowPrimitive2D.getStart().getWidth());
                    rWriter.attribute("centered",
                                      static_cast<sal_Int32>(
                                          rPolygonStrokeArrowPrimitive2D.getStart().isCentered()));
                    writePolyPolygon(rWriter,
                                     rPolygonStrokeArrowPrimitive2D.getStart().getB2DPolyPolygon());
                    rWriter.endElement();
                }

                if (rPolygonStrokeArrowPrimitive2D.getEnd().getB2DPolyPolygon().count())
                {
                    rWriter.startElement("lineendattribute");
                    rWriter.attribute("width", rPolygonStrokeArrowPrimitive2D.getEnd().getWidth());
                    rWriter.attribute("centered",
                                      static_cast<sal_Int32>(
                                          rPolygonStrokeArrowPrimitive2D.getEnd().isCentered()));
                    writePolyPolygon(rWriter,
                                     rPolygonStrokeArrowPrimitive2D.getEnd().getB2DPolyPolygon());
                    rWriter.endElement();
                }

                writeLineAttribute(rWriter, rPolygonStrokeArrowPrimitive2D.getLineAttribute());
                writeStrokeAttribute(rWriter, rPolygonStrokeArrowPrimitive2D.getStrokeAttribute());
                rWriter.endElement();
            }
            break;

            case PRIMITIVE2D_ID_POLYGONSTROKEPRIMITIVE2D:
            {
                const PolygonStrokePrimitive2D& rPolygonStrokePrimitive2D
diff --git a/emfio/qa/cppunit/emf/EmfImportTest.cxx b/emfio/qa/cppunit/emf/EmfImportTest.cxx
index 5891799..f836887 100644
--- a/emfio/qa/cppunit/emf/EmfImportTest.cxx
+++ b/emfio/qa/cppunit/emf/EmfImportTest.cxx
@@ -51,6 +51,7 @@ class Test : public test::BootstrapFixture, public XmlTestTools, public unotest:
    void TestDrawStringTransparent();
    void TestDrawStringWithBrush();
    void TestDrawLine();
    void TestDrawLineWithCaps();
    void TestDrawLineWithDash();
    void TestLinearGradient();
    void TestTextMapMode();
@@ -98,6 +99,7 @@ public:
    CPPUNIT_TEST(TestDrawStringTransparent);
    CPPUNIT_TEST(TestDrawStringWithBrush);
    CPPUNIT_TEST(TestDrawLine);
    CPPUNIT_TEST(TestDrawLineWithCaps);
    CPPUNIT_TEST(TestDrawLineWithDash);
    CPPUNIT_TEST(TestLinearGradient);
    CPPUNIT_TEST(TestTextMapMode);
@@ -155,6 +157,7 @@ Primitive2DSequence Test::parseEmf(std::u16string_view aSource)

    SvFileStream aFileStream(aUrl, StreamMode::READ);
    std::size_t nSize = aFileStream.remainingSize();
    CPPUNIT_ASSERT_MESSAGE("Unable to open file", nSize);
    std::unique_ptr<sal_Int8[]> pBuffer(new sal_Int8[nSize + 1]);
    aFileStream.ReadBytes(pBuffer.get(), nSize);
    pBuffer[nSize] = 0;
@@ -379,30 +382,91 @@ void Test::TestDrawLine()
{
    // EMF+ with records: DrawLine
    // The line is colored and has a specified width, therefore a polypolygonstroke primitive is the optimal choice
    Primitive2DSequence aSequence = parseEmf(u"/emfio/qa/cppunit/emf/data/TestDrawLine.emf");
    Primitive2DSequence aSequence = parseEmf(u"emfio/qa/cppunit/emf/data/TestDrawLine.emf");
    CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(aSequence.getLength()));
    drawinglayer::Primitive2dXmlDump dumper;
    xmlDocUniquePtr pDocument = dumper.dumpAndParse(Primitive2DContainer(aSequence));
    CPPUNIT_ASSERT(pDocument);

    // check correct import of the DrawLine: color and width of the line
    assertXPath(pDocument, aXPathPrefix + "polypolygonstroke/line", "color", "#000000");
    assertXPath(pDocument, aXPathPrefix + "polypolygonstroke/line", "width", "23");
    assertXPath(pDocument, aXPathPrefix + "mask/unifiedtransparence", "transparence", "14");
    assertXPath(pDocument, aXPathPrefix + "mask/unifiedtransparence/polypolygonstroke/line",
                "color", "#c01002");
    assertXPath(pDocument, aXPathPrefix + "mask/unifiedtransparence/polypolygonstroke/line",
                "width", "115");
    assertXPath(pDocument, aXPathPrefix + "mask/unifiedtransparence/polypolygonstroke/line",
                "linecap", "BUTT");
    assertXPath(pDocument, aXPathPrefix + "mask/unifiedtransparence/polypolygonstroke/polypolygon",
                "path", "m55.5192348773662 403.573503917507 874.352660545936-345.821325648415");
}

void Test::TestDrawLineWithCaps()
{
    // EMF+ with records: DrawLine
    // Test lines with different caps styles and arrows
    Primitive2DSequence aSequence
        = parseEmf(u"emfio/qa/cppunit/emf/data/TestEmfPlusDrawLineWithCaps.emf");
    CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(aSequence.getLength()));
    drawinglayer::Primitive2dXmlDump dumper;
    xmlDocUniquePtr pDocument = dumper.dumpAndParse(Primitive2DContainer(aSequence));
    CPPUNIT_ASSERT(pDocument);
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow", 3);

    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[1]/line", "width", "211");
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[1]/stroke", 0);
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[1]/linestartattribute/polypolygon",
                "path", "m0-1 1 2h-2z");
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[1]/lineendattribute", 0);

    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[2]/line", "width", "211");
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[2]/stroke", 0);
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[2]/linestartattribute/polypolygon",
                "path", "m0-1 1 2h-2z");
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[2]/lineendattribute", 0);

    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[3]/line", "width", "423");
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[3]/stroke", 0);
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[3]/linestartattribute", 0);
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow[3]/lineendattribute/polypolygon",
                "path", "m-1-1h2v2h-2z");

    assertXPath(pDocument, aXPathPrefix + "unifiedtransparence", 3);
    assertXPath(pDocument, aXPathPrefix + "unifiedtransparence[1]", "transparence", "39");
    assertXPath(pDocument,
                aXPathPrefix
                    + "unifiedtransparence[1]/polygonstrokearrow/linestartattribute/polypolygon",
                "path",
                "m-1 1h2v-1l-0.0764-0.3827-0.2165-0.3244-0.3244-0.2165-0.3827-0.0764-0.3827 "
                "0.0764-0.3244 0.2165-0.2165 0.3244-0.0764 0.3827z");
    assertXPath(pDocument,
                aXPathPrefix
                    + "unifiedtransparence[1]/polygonstrokearrow/lineendattribute/polypolygon",
                "path", "m-1 1h2v-1l-1-1-1 1z");
    assertXPath(pDocument,
                aXPathPrefix + "unifiedtransparence[2]/polygonstrokearrow/linestartattribute", 0);
    assertXPath(pDocument,
                aXPathPrefix
                    + "unifiedtransparence[2]/polygonstrokearrow/lineendattribute/polypolygon",
                "path", "m-1-1h2v2h-2z");
    assertXPath(pDocument,
                aXPathPrefix
                    + "unifiedtransparence[3]/polygonstrokearrow/lineendattribute/polypolygon",
                "path", "m0-1 1 1-0.5 0.5v0.5h-1v-0.5l-0.5-0.5z");
}

void Test::TestDrawLineWithDash()
{
    // EMF+ with records: DrawLine, ScaleWorldTransform, RotateWorldTransform
    // Test lines with different dash styles and different World Rotation
    // Test lines with different dash styles, different line arrows and different World Rotation
    Primitive2DSequence aSequence
        = parseEmf(u"/emfio/qa/cppunit/emf/data/TestEmfPlusDrawLineWithDash.emf");
        = parseEmf(u"emfio/qa/cppunit/emf/data/TestEmfPlusDrawLineWithDash.emf");
    CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(aSequence.getLength()));
    drawinglayer::Primitive2dXmlDump dumper;
    xmlDocUniquePtr pDocument = dumper.dumpAndParse(Primitive2DContainer(aSequence));
    CPPUNIT_ASSERT(pDocument);

    // check correct import of the DrawLine: color and width of the line
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke", 12);
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke", 10);
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[1]/line", "color", "#000000");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[1]/line", "width", "185");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[1]/stroke", 0);
@@ -419,14 +483,30 @@ void Test::TestDrawLineWithDash()
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[5]/line", "width", "370");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[5]/stroke", "dotDashArray",
                "556 185 185 185 185 185 ");

    assertXPath(pDocument, aXPathPrefix + "mask/polygonstrokearrow", 2);
    //TODO polypolygonstroke[6-9]/stroke add support for PenDataDashedLineOffset
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[10]/line", "width", "370");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[10]/stroke", "dotDashArray",
    assertXPath(pDocument, aXPathPrefix + "mask/polygonstrokearrow[1]/line", "width", "370");
    assertXPath(pDocument, aXPathPrefix + "mask/polygonstrokearrow[1]/stroke", "dotDashArray",
                "1851 741 5554 1481 ");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[11]/line", "width", "370");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[11]/stroke", "dotDashArray",
                "1851 741 5554 1481 ");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke[12]/line", "width", "370");
    // Arrows on both ends
    assertXPath(pDocument,
                aXPathPrefix + "mask/polygonstrokearrow[1]/linestartattribute/polypolygon", "path",
                "m0-1 1 2h-2z");
    assertXPath(pDocument, aXPathPrefix + "mask/polygonstrokearrow[1]/lineendattribute/polypolygon",
                "path", "m0-1 1 2h-2z");

    assertXPath(pDocument, aXPathPrefix + "mask/polygonstrokearrow[2]/line", "width", "370");
    assertXPath(pDocument, aXPathPrefix + "mask/polygonstrokearrow[2]/stroke", "dotDashArray",
                "1852 741 5555 1481 ");
    assertXPath(pDocument,
                aXPathPrefix + "mask/polygonstrokearrow[2]/linestartattribute/polypolygon", "path",
                "m-1 1h2v-1l-0.0764-0.3827-0.2165-0.3244-0.3244-0.2165-0.3827-0.0764-0.3827 "
                "0.0764-0.3244 0.2165-0.2165 0.3244-0.0764 0.3827z");
    assertXPath(pDocument, aXPathPrefix + "mask/polygonstrokearrow[2]/lineendattribute/polypolygon",
                "path",
                "m-1 1h2v-1l-0.0764-0.3827-0.2165-0.3244-0.3244-0.2165-0.3827-0.0764-0.3827 "
                "0.0764-0.3244 0.2165-0.2165 0.3244-0.0764 0.3827z");
}

void Test::TestLinearGradient()
@@ -878,7 +958,7 @@ void Test::TestPolylinetoCloseStroke()
void Test::TestEmfPlusGetDC()
{
    // tdf#147818 EMF+ records: GetDC, DrawPath, FillRects
    Primitive2DSequence aSequence = parseEmf(u"/emfio/qa/cppunit/emf/data/TestEmfPlusGetDC.emf");
    Primitive2DSequence aSequence = parseEmf(u"emfio/qa/cppunit/emf/data/TestEmfPlusGetDC.emf");
    CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(aSequence.getLength()));
    drawinglayer::Primitive2dXmlDump dumper;
    xmlDocUniquePtr pDocument = dumper.dumpAndParse(Primitive2DContainer(aSequence));
@@ -910,7 +990,8 @@ void Test::TestEmfPlusGetDC()
                "m19428.4895833333 6632.22222222222h317.34375v-2398.88888888889h-317.34375z");
    assertXPath(pDocument, aXPathPrefix + "polypolygoncolor[6]", "color", "#fcf2e3");

    assertXPath(pDocument, aXPathPrefix + "polypolygonstroke", 15);
    assertXPath(pDocument, aXPathPrefix + "polypolygonstroke", 4);
    assertXPath(pDocument, aXPathPrefix + "polygonstrokearrow", 13);
}

void Test::TestEmfPlusSave()
@@ -932,9 +1013,10 @@ void Test::TestEmfPlusSave()
                "m10853.4145539602 7321.41354709201h41952690v29630720h-41952690z");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygoncolor", "color", "#00ffad");

    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke/line", "color", "#000000");
    assertXPath(pDocument, aXPathPrefix + "mask/polypolygonstroke/polypolygon", "path",
                "m10853.4145539602 7321.41354709201v-2413.87029012044h1979.24116969109");
    assertXPath(pDocument, aXPathPrefix + "mask/polygonstrokearrow/line", "color", "#000000");
    assertXPathContent(pDocument, aXPathPrefix + "mask/polygonstrokearrow/polygon",
                       "10853.4145539602,7321.41354709201 10853.4145539602,4907.54325697157 "
                       "12832.6557236512,4907.54325697157");
}

void Test::TestExtTextOutOpaqueAndClipTransform()
diff --git a/emfio/qa/cppunit/emf/data/TestDrawLine.emf b/emfio/qa/cppunit/emf/data/TestDrawLine.emf
index 89946c5..8d8c620 100644
--- a/emfio/qa/cppunit/emf/data/TestDrawLine.emf
+++ b/emfio/qa/cppunit/emf/data/TestDrawLine.emf
Binary files differ
diff --git a/emfio/qa/cppunit/emf/data/TestEmfPlusDrawLineWithCaps.emf b/emfio/qa/cppunit/emf/data/TestEmfPlusDrawLineWithCaps.emf
new file mode 100644
index 0000000..1b4cedb
--- /dev/null
+++ b/emfio/qa/cppunit/emf/data/TestEmfPlusDrawLineWithCaps.emf
Binary files differ