diff --git a/emfio/README.md b/emfio/README.md index babdfef16510..b24f3e8a4c3e 100644 --- a/emfio/README.md +++ b/emfio/README.md @@ -63,7 +63,7 @@ Currently, these records are not implemented for WMF (specified in `wmfreader.cxx`): ``` -W_META_SETRELABS W_META_SETPOLYFILLMODE W_META_SETSTRETCHBLTMODE +W_META_SETRELABS W_META_SETSTRETCHBLTMODE W_META_SETTEXTCHAREXTRA W_META_SETTEXTJUSTIFICATION W_META_FLOODFILL W_META_FILLREGION W_META_FRAMEREGION W_META_INVERTREGION W_META_PAINTREGION W_META_DRAWTEXT W_META_SETMAPPERFLAGS diff --git a/emfio/inc/mtftools.hxx b/emfio/inc/mtftools.hxx index 706506099572..f6ecd7f8268f 100644 --- a/emfio/inc/mtftools.hxx +++ b/emfio/inc/mtftools.hxx @@ -593,6 +593,7 @@ namespace emfio bool bClockWiseArcDirection; bool bFillStyleSelected; + sal_uInt32 nPolyFillMode; }; struct BSaveStruct @@ -699,6 +700,7 @@ namespace emfio bool mbNopMode : 1; bool mbClockWiseArcDirection : 1; bool mbFillStyleSelected : 1; + sal_uInt32 mnPolyFillMode; // ALTERNATE(1) or WINDING(2) bool mbClipNeedsUpdate : 1; bool mbComplexClip : 1; bool mbIsMapWinSet : 1; @@ -756,6 +758,7 @@ namespace emfio void SetGfxMode(GraphicsMode nGfxMode) { meGfxMode = nGfxMode; }; GraphicsMode GetGfxMode() const { return meGfxMode; }; void SetBkMode(BackgroundMode nMode); + void SetPolyFillMode(sal_uInt32 nPolyFillMode); void SetBkColor(const Color& rColor); void SetTextColor(const Color& rColor); void SetTextAlign(sal_uInt32 nAlign); diff --git a/emfio/qa/cppunit/emf/EmfImportTest.cxx b/emfio/qa/cppunit/emf/EmfImportTest.cxx index 284eb025c0ff..cfaa8391e4a7 100644 --- a/emfio/qa/cppunit/emf/EmfImportTest.cxx +++ b/emfio/qa/cppunit/emf/EmfImportTest.cxx @@ -683,13 +683,16 @@ CPPUNIT_TEST_FIXTURE(Test, testEnglishMapMode) assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "width", u"424"); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "height", u"424"); - assertXPath(pDocument, aXPathPrefix + "mask/polygonhairline", 5); + // WINDING fill mode: fill and stroke are separated; strokes come from + // MetaPolyLineAction instead of MetaPolygonAction, one per shape. + assertXPath(pDocument, aXPathPrefix + "mask/polygonhairline", 3); assertXPathContent(pDocument, aXPathPrefix + "mask/polygonhairline[1]/polygon", u"-1,-1 29699,-1 29699,21000 -1,21000"); assertXPathContent(pDocument, aXPathPrefix + "mask/polygonhairline[2]/polygon", u"1058,7937 1058,13230 4233,13230 4233,12171 2115,12171 2115,7937"); - assertXPathContent(pDocument, aXPathPrefix + "mask/polygonhairline[3]/polygon", - u"1058,7937 1058,13230 4233,13230 4233,12171 2115,12171 2115,7937"); + assertXPathContent( + pDocument, aXPathPrefix + "mask/polygonhairline[3]/polygon", + u"12699,1058 16933,1058 16933,2118 15346,2118 15346,6349 14287,6349 14287,2118 12699,2118"); } CPPUNIT_TEST_FIXTURE(Test, testRectangleWithModifyWorldTransform) @@ -2004,6 +2007,32 @@ CPPUNIT_TEST_FIXTURE(Test, testExcludeClipRect) assertXPath(pDocument, aXPathPrefix + "group/mask/polypolygoncolor", "color", u"#ff0000"); } +CPPUNIT_TEST_FIXTURE(Test, testPolyFillModeWinding) +{ + // EMR_SETPOLYFILLMODE sets WINDING mode, then draws a self-intersecting + // pentagram (5-pointed star) filled with red. With WINDING the entire + // star including the center is filled; with ALTERNATE the center pentagon + // would be a hole. The geometry is converted by CreateWindingFillPolyPolygon() + // so that even-odd rendering fills the star completely. + Primitive2DSequence aSequence + = parseEmf(u"/emfio/qa/cppunit/emf/data/TestPolyFillModeWinding.emf"); + CPPUNIT_ASSERT_EQUAL(1, static_cast(aSequence.getLength())); + drawinglayer::Primitive2dXmlDump dumper; + xmlDocUniquePtr pDocument = dumper.dumpAndParse(Primitive2DContainer(aSequence)); + CPPUNIT_ASSERT(pDocument); + + // The pentagram is filled with red. The self-intersecting polygon is + // resolved into a non-self-intersecting outer boundary via union of + // sub-regions, so even-odd fill covers the entire star including the center. + assertXPath(pDocument, aXPathPrefix + "polypolygoncolor", "color", u"#ff0000"); + assertXPath(pDocument, aXPathPrefix + "polypolygoncolor/polypolygon/polygon", 1); + + // The stroke preserves the original 5-vertex star shape. + assertXPath(pDocument, aXPathPrefix + "polygonhairline", 1); + assertXPathContent(pDocument, aXPathPrefix + "polygonhairline/polygon", + u"2646,264 4048,4565 370,1900 4921,1900 1244,4565"); +} + CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/emfio/qa/cppunit/emf/data/TestPolyFillModeWinding.emf b/emfio/qa/cppunit/emf/data/TestPolyFillModeWinding.emf new file mode 100644 index 000000000000..aeb45b6a917f Binary files /dev/null and b/emfio/qa/cppunit/emf/data/TestPolyFillModeWinding.emf differ diff --git a/emfio/source/reader/emfreader.cxx b/emfio/source/reader/emfreader.cxx index 3e42537a1e87..8996b8951f2f 100644 --- a/emfio/source/reader/emfreader.cxx +++ b/emfio/source/reader/emfreader.cxx @@ -1146,6 +1146,11 @@ namespace emfio break; case EMR_SETPOLYFILLMODE : + { + mpInputStream->ReadUInt32( nDat32 ); + SAL_INFO("emfio", "\t\tPolyFillMode: 0x" << std::hex << nDat32 << std::dec); + SetPolyFillMode( nDat32 ); + } break; case EMR_SETROP2 : diff --git a/emfio/source/reader/mtftools.cxx b/emfio/source/reader/mtftools.cxx index bfc49a3aad74..135150b7a234 100644 --- a/emfio/source/reader/mtftools.cxx +++ b/emfio/source/reader/mtftools.cxx @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -883,6 +884,11 @@ namespace emfio mnBkMode = nMode; } + void MtfTools::SetPolyFillMode( sal_uInt32 nPolyFillMode ) + { + mnPolyFillMode = nPolyFillMode; + } + void MtfTools::SetBkColor( const Color& rColor ) { maBkColor = rColor; @@ -903,12 +909,60 @@ namespace emfio mvGDIObj.resize(nNewEntrys); } + // Convert a PolyPolygon so that nonzero/winding fill rule gives the same visual + // result when rendered with even-odd fill rule (which is all VCL supports). + static tools::PolyPolygon CreateWindingFillPolyPolygon(const tools::PolyPolygon& rPolyPoly) + { + basegfx::B2DPolyPolygon aB2D = rPolyPoly.getB2DPolyPolygon(); + + if (aB2D.count() == 1) + { + // Single self-intersecting polygon (e.g. pentagram star): resolve + // self-intersections, then union (OR) all resulting sub-regions. + // This gives the outer boundary of the filled area, which even-odd + // fill renders correctly. createNonzeroConform() fails here because + // solveCrossovers() produces overlapping loops for odd-crossing + // polygons, and the center ends up inside an even number of loops. + basegfx::B2DPolyPolygon aResolved + = basegfx::utils::solveCrossovers(aB2D.getB2DPolygon(0)); + aResolved = basegfx::utils::stripNeutralPolygons(aResolved); + + if (aResolved.count() > 1) + { + basegfx::B2DPolyPolygonVector aInput; + for (sal_uInt32 i = 0; i < aResolved.count(); ++i) + aInput.push_back(basegfx::B2DPolyPolygon(aResolved.getB2DPolygon(i))); + aB2D = basegfx::utils::mergeToSinglePolyPolygon(aInput); + } + else + { + aB2D = aResolved; + } + } + else + { + // Multiple polygons: use createNonzeroConform (handles nested + // same-orientation polygons by removing inner duplicates). + aB2D = basegfx::utils::createNonzeroConform(aB2D); + } + + return tools::PolyPolygon(aB2D); + } + void MtfTools::ImplDrawClippedPolyPolygon( const tools::PolyPolygon& rPolyPoly ) { if ( !rPolyPoly.Count() ) return; ImplSetNonPersistentLineColorTransparenz(); + + if (mnPolyFillMode == 2) // WINDING + { + tools::PolyPolygon aFill = CreateWindingFillPolyPolygon(rPolyPoly); + mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( aFill ) ); + return; + } + if ( rPolyPoly.Count() == 1 ) { if ( rPolyPoly.IsRect() ) @@ -1113,6 +1167,7 @@ namespace emfio mbNopMode(false), mbClockWiseArcDirection(false), mbFillStyleSelected(false), + mnPolyFillMode(1), // ALTERNATE mbClipNeedsUpdate(true), mbComplexClip(false), mbIsMapWinSet(false), @@ -1292,26 +1347,39 @@ namespace emfio UpdateClipRegion(); UpdateLineStyle(); UpdateFillStyle(); + bool bWindingFill = (mnPolyFillMode == 2); if ( bFill ) { - if ( !bStroke ) + // When using WINDING fill mode, we convert the geometry so it renders + // correctly with even-odd fill. We must also disable line color to avoid + // stroking internal edges of the converted fill geometry. + if ( !bStroke || bWindingFill ) { mpGDIMetaFile->AddAction( new MetaPushAction( vcl::PushFlags::LINECOLOR ) ); mpGDIMetaFile->AddAction( new MetaLineColorAction( Color(), false ) ); } - if ( maPathObj.Count() == 1 ) - mpGDIMetaFile->AddAction( new MetaPolygonAction( maPathObj.GetObject( 0 ) ) ); + if (bWindingFill) + { + tools::PolyPolygon aFill = CreateWindingFillPolyPolygon(maPathObj); + mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( aFill ) ); + } else - mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( maPathObj ) ); + { + if ( maPathObj.Count() == 1 ) + mpGDIMetaFile->AddAction( new MetaPolygonAction( maPathObj.GetObject( 0 ) ) ); + else + mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( maPathObj ) ); + } - if ( !bStroke ) + if ( !bStroke || bWindingFill ) mpGDIMetaFile->AddAction( new MetaPopAction() ); } // tdf#142014 By default the stroke is made with hairline. If width is bigger, we need to use PolyLineAction if ( bStroke ) { // bFill is drawing hairstyle line. So we need to draw it only when the width is different than 0 - if ( !bFill || maLineStyle.aLineInfo.GetWidth() || ( maLineStyle.aLineInfo.GetStyle() == LineStyle::Dash ) ) + // When using WINDING fill mode, line was disabled for the fill, so always draw stroke + if ( !bFill || bWindingFill || maLineStyle.aLineInfo.GetWidth() || ( maLineStyle.aLineInfo.GetStyle() == LineStyle::Dash ) ) { sal_uInt16 i, nCount = maPathObj.Count(); for ( i = 0; i < nCount; i++ ) @@ -1552,21 +1620,47 @@ namespace emfio } } ImplSetNonPersistentLineColorTransparenz(); - mpGDIMetaFile->AddAction( new MetaPolygonAction( rPolygon ) ); + if (mnPolyFillMode == 2) // WINDING + { + tools::PolyPolygon aFill = CreateWindingFillPolyPolygon(tools::PolyPolygon(rPolygon)); + mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( aFill ) ); + } + else + { + mpGDIMetaFile->AddAction( new MetaPolygonAction( rPolygon ) ); + } UpdateLineStyle(); - mpGDIMetaFile->AddAction( new MetaPolyLineAction( std::move(rPolygon), maLineStyle.aLineInfo ) ); + mpGDIMetaFile->AddAction( new MetaPolyLineAction( rPolygon, maLineStyle.aLineInfo ) ); } else { UpdateLineStyle(); if (maLatestFillStyle.aType != WinMtfFillStyleType::Pattern) - mpGDIMetaFile->AddAction( new MetaPolygonAction( std::move(rPolygon) ) ); + { + if (mnPolyFillMode == 2) // WINDING + { + tools::PolyPolygon aFill = CreateWindingFillPolyPolygon(tools::PolyPolygon(rPolygon)); + ImplSetNonPersistentLineColorTransparenz(); + mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( aFill ) ); + UpdateLineStyle(); + // Stroke with original polygon, closing it for the polyline + sal_uInt16 nCount = rPolygon.GetSize(); + if (nCount && rPolygon[nCount - 1] != rPolygon[0]) + { + Point aPoint(rPolygon[0]); + rPolygon.Insert(nCount, aPoint); + } + mpGDIMetaFile->AddAction(new MetaPolyLineAction(rPolygon, maLineStyle.aLineInfo)); + } + else + mpGDIMetaFile->AddAction( new MetaPolygonAction( std::move(rPolygon) ) ); + } else { SvtGraphicFill aFill( tools::PolyPolygon( rPolygon ), Color(), 0.0, - SvtGraphicFill::fillNonZero, + mnPolyFillMode == 2 ? SvtGraphicFill::fillNonZero : SvtGraphicFill::fillEvenOdd, SvtGraphicFill::fillTexture, SvtGraphicFill::Transform(), true, @@ -1614,14 +1708,32 @@ namespace emfio else { UpdateLineStyle(); - mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( rPolyPolygon ) ); - if (maLineStyle.aLineInfo.GetWidth() > 0 || maLineStyle.aLineInfo.GetStyle() == LineStyle::Dash) + if (mnPolyFillMode == 2) // WINDING { + // Convert geometry so even-odd rendering matches winding fill. + // Disable line to avoid stroking internal resolved edges. + tools::PolyPolygon aFill = CreateWindingFillPolyPolygon(rPolyPolygon); + mpGDIMetaFile->AddAction( new MetaPushAction( vcl::PushFlags::LINECOLOR ) ); + mpGDIMetaFile->AddAction( new MetaLineColorAction( Color(), false ) ); + mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( aFill ) ); + mpGDIMetaFile->AddAction( new MetaPopAction() ); + // Stroke with original geometry for (sal_uInt16 nPoly = 0; nPoly < rPolyPolygon.Count(); ++nPoly) { mpGDIMetaFile->AddAction(new MetaPolyLineAction(rPolyPolygon[nPoly], maLineStyle.aLineInfo)); } } + else + { + mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( rPolyPolygon ) ); + if (maLineStyle.aLineInfo.GetWidth() > 0 || maLineStyle.aLineInfo.GetStyle() == LineStyle::Dash) + { + for (sal_uInt16 nPoly = 0; nPoly < rPolyPolygon.Count(); ++nPoly) + { + mpGDIMetaFile->AddAction(new MetaPolyLineAction(rPolyPolygon[nPoly], maLineStyle.aLineInfo)); + } + } + } } } } @@ -2782,6 +2894,7 @@ namespace emfio pSave->aBkColor = maBkColor; pSave->bClockWiseArcDirection = mbClockWiseArcDirection; pSave->bFillStyleSelected = mbFillStyleSelected; + pSave->nPolyFillMode = mnPolyFillMode; pSave->aActPos = maActPos; pSave->aXForm = maXForm; @@ -2844,6 +2957,7 @@ namespace emfio maBkColor = pSave->aBkColor; mbClockWiseArcDirection = pSave->bClockWiseArcDirection; mbFillStyleSelected = pSave->bFillStyleSelected; + mnPolyFillMode = pSave->nPolyFillMode; maActPos = pSave->aActPos; maXForm = pSave->aXForm; diff --git a/emfio/source/reader/wmfreader.cxx b/emfio/source/reader/wmfreader.cxx index cb3e130cd551..89b12a516ce6 100644 --- a/emfio/source/reader/wmfreader.cxx +++ b/emfio/source/reader/wmfreader.cxx @@ -1390,8 +1390,17 @@ namespace emfio } break; - case W_META_SETRELABS: case W_META_SETPOLYFILLMODE: + { + if ((nRecordSize != 4) && (nRecordSize != 5)) + bRecordOk = false; + sal_uInt16 nDat = 0; + mpInputStream->ReadUInt16( nDat ); + SetPolyFillMode( nDat ); + } + break; + + case W_META_SETRELABS: case W_META_SETSTRETCHBLTMODE: case W_META_SETTEXTCHAREXTRA: case W_META_SETTEXTJUSTIFICATION: