emfio: implement EMR_SETPOLYFILLMODE and W_META_SETPOLYFILLMODE

The polygon fill mode (ALTERNATE vs WINDING) was ignored in both EMF
and WMF readers.  ALTERNATE (even-odd) is the default; WINDING
(non-zero winding) appears in 2541/6939 EMF files and 135/467 WMF
files that set the mode.  Self-intersecting polygons like pentagram
stars render differently under the two rules.

Store the fill mode state in MtfTools with save/restore in Push/Pop,
wire up the record handlers, and fix the hardcoded fillNonZero in
DrawPolygon pattern fills to respect the current mode.

Add a unit test with a pentagram star EMF using WINDING mode.

Change-Id: I1d9127b79cc6c3b67f39bebe2c82e806c9db4ce8
Reviewed-on: https://gerrit.collaboraoffice.com/c/core/+/324
Tested-by: Andras Timar <andras.timar@collabora.com>
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Signed-off-by: Xisco Fauli <xiscofauli@libreoffice.org>
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/204388
Tested-by: Jenkins
This commit is contained in:
Andras Timar
2026-04-27 08:58:11 +02:00
committed by Xisco Fauli
parent d8f87cb61f
commit 857fd268e8
7 changed files with 177 additions and 17 deletions
+1 -1
View File
@@ -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
+3
View File
@@ -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);
+32 -3
View File
@@ -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<int>(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: */
+5
View File
@@ -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 :
+119 -5
View File
@@ -22,6 +22,7 @@
#include <cstdlib>
#include <memory>
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <basegfx/polygon/b2dpolypolygoncutter.hxx>
#include <basegfx/polygon/b2dpolypolygontools.hxx>
#include <tools/mapunit.hxx>
#include <vcl/metric.hxx>
@@ -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 (bWindingFill)
{
tools::PolyPolygon aFill = CreateWindingFillPolyPolygon(maPathObj);
mpGDIMetaFile->AddAction( new MetaPolyPolygonAction( aFill ) );
}
else
{
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();
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)
{
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,6 +1708,23 @@ namespace emfio
else
{
UpdateLineStyle();
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)
{
@@ -1625,6 +1736,7 @@ namespace emfio
}
}
}
}
void MtfTools::DrawPolyLine( tools::Polygon rPolygon, bool bTo, bool bRecordPath )
{
@@ -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;
+10 -1
View File
@@ -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: