emfio/inc/mtftools.hxx | 2 emfio/qa/cppunit/emf/EmfImportTest.cxx | 49 +++++++++- emfio/qa/cppunit/emf/data/TestExtTextOutScaleGM_COMPATIBLE.emf |binary emfio/source/reader/emfreader.cxx | 16 +-- emfio/source/reader/mtftools.cxx | 41 ++++++++ 5 files changed, 97 insertions(+), 11 deletions(-)
New commits: commit b6857c1f1628caf3f2489390e84e9b5c65c3e0b1 Author: Bartosz Kosiorek <[email protected]> AuthorDate: Fri Feb 20 00:23:01 2026 +0100 Commit: Bartosz Kosiorek <[email protected]> CommitDate: Sat Feb 21 08:30:48 2026 +0100 tdf#138087 tdf#142548 EMF Fix font scaling and orientation in GM_COMPATIBLE When importing EMF files, certain records (like EXTTEXTOUTW) in GM_COMPATIBLE mode were not respecting non-proportional scaling or single-axis mirroring. This resulted in text that was too wide/narrow or rotated in the wrong direction. Key changes: - Added font width scaling based on the ratio between fXScale and fYScale. - Handled 180-degree rotation when both axes are negative. - Corrected font orientation for single-axis mirroring by inverting the angle (3600 - orientation) to compensate for the reversed chirality of the coordinate system. Change-Id: Ib1126c192a80b973f343f25bf20ec119d94d71a8 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199770 Tested-by: Jenkins Reviewed-by: Bartosz Kosiorek <[email protected]> diff --git a/emfio/inc/mtftools.hxx b/emfio/inc/mtftools.hxx index 50e206618625..e264c32f7140 100644 --- a/emfio/inc/mtftools.hxx +++ b/emfio/inc/mtftools.hxx @@ -810,6 +810,8 @@ namespace emfio OUString const & rString, KernArray* pDXArry = nullptr, tools::Long* pDYArry = nullptr, + const float fXScale = 1.0, + const float fYScale = 1.0, bool bRecordPath = false, GraphicsMode nGraphicsMode = GraphicsMode::GM_COMPATIBLE); diff --git a/emfio/qa/cppunit/emf/EmfImportTest.cxx b/emfio/qa/cppunit/emf/EmfImportTest.cxx index 4d91a7eb0afa..30ed80b631c1 100644 --- a/emfio/qa/cppunit/emf/EmfImportTest.cxx +++ b/emfio/qa/cppunit/emf/EmfImportTest.cxx @@ -1317,6 +1317,49 @@ CPPUNIT_TEST_FIXTURE(Test, testExtTextOutOpaqueAndClipTransform) u"#000000"); } +CPPUNIT_TEST_FIXTURE(Test, testExtTextOutScaleGM_COMPATIBLE) +{ + // tdf#142495 EMF records: EXTTEXTOUTW with GM_COMPATIBLE. + Primitive2DSequence aSequence + = parseEmf(u"/emfio/qa/cppunit/emf/data/TestExtTextOutScaleGM_COMPATIBLE.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 + "textsimpleportion", 4); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[1]", "text", u"Obliquité (ºC)"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[1]", "fontcolor", u"#202020"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[1]", "width", u"317"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[1]", "height", u"317"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[1]", "dx0", u"254"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[1]", "dx1", u"450"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[1]", "dx2", u"544"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[1]", "dx3", u"638"); + + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[2]", "text", u"23"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[2]", "fontcolor", u"#000000"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[2]", "width", u"161"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[2]", "height", u"317"); + + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[3]", "text", u"24"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[3]", "fontcolor", u"#000000"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[3]", "width", u"201"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[3]", "height", u"317"); + + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[4]", "text", u"25"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[4]", "fontcolor", u"#000000"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[4]", "width", u"268"); + assertXPath(pDocument, aXPathPrefix + "textsimpleportion[4]", "height", u"317"); + + assertXPath(pDocument, aXPathPrefix + "polygonstroke", 9); + assertXPath(pDocument, aXPathPrefix + "polypolygoncolor", 3); + assertXPath(pDocument, aXPathPrefix + "polypolygoncolor[1]/polypolygon", "path", + u"m0 0v21589h27944v-21589z"); + assertXPath(pDocument, aXPathPrefix + "polypolygoncolor[2]/polypolygon", "path", + u"m24258 16413v264h383v-264z"); +} + CPPUNIT_TEST_FIXTURE(Test, testUnderlineTransparentBackground) { // EMF with SETBKMODE=TRANSPARENT, SETBKCOLOR=black, underlined font, and EXTTEXTOUTW "TEST". @@ -1660,13 +1703,15 @@ CPPUNIT_TEST_FIXTURE(Test, testCreatePen) assertXPath(pDocument, aXPathPrefix + "mask/polygonhairline", 10); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion", 69); - assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "width", u"374"); + assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "height", u"374"); + assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "width", u"310"); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "x", u"28124"); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "y", u"16581"); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "text", u"0.0"); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[1]", "fontcolor", u"#000000"); - assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[10]", "width", u"266"); + assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[10]", "height", u"266"); + assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[10]", "width", u"221"); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[10]", "x", u"28000"); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[10]", "y", u"428"); assertXPath(pDocument, aXPathPrefix + "mask/textsimpleportion[10]", "text", u"-6"); diff --git a/emfio/qa/cppunit/emf/data/TestExtTextOutScaleGM_COMPATIBLE.emf b/emfio/qa/cppunit/emf/data/TestExtTextOutScaleGM_COMPATIBLE.emf new file mode 100644 index 000000000000..7df2e3d92077 Binary files /dev/null and b/emfio/qa/cppunit/emf/data/TestExtTextOutScaleGM_COMPATIBLE.emf differ diff --git a/emfio/source/reader/emfreader.cxx b/emfio/source/reader/emfreader.cxx index 310033fd1763..cbcbd9efb508 100644 --- a/emfio/source/reader/emfreader.cxx +++ b/emfio/source/reader/emfreader.cxx @@ -1884,16 +1884,16 @@ namespace emfio { sal_Int32 ptlReferenceX, ptlReferenceY; sal_uInt32 nLen, nOptions, nGfxMode; - float nXScale, nYScale; + float fXScale, fYScale; mpInputStream->ReadInt32( ptlReferenceX ).ReadInt32( ptlReferenceY ) .ReadUInt32( nLen ).ReadUInt32( nOptions ) - .ReadUInt32( nGfxMode ).ReadFloat( nXScale ).ReadFloat( nYScale ); + .ReadUInt32( nGfxMode ).ReadFloat( fXScale ).ReadFloat( fYScale ); SAL_INFO("emfio", " Reference: (" << ptlReferenceX << ", " << ptlReferenceY << ")"); SAL_INFO("emfio", " cChars: " << nLen); SAL_INFO("emfio", " fuOptions: 0x" << std::hex << nOptions << std::dec); SAL_INFO("emfio", " iGraphicsMode: 0x" << std::hex << nGfxMode << std::dec); - SAL_INFO("emfio", " Scale: " << nXScale << " x " << nYScale); + SAL_INFO("emfio", " Scale: " << fXScale << " x " << fYScale); // Read optional bounding rectangle (present only if ETO_NO_RECT is NOT set) tools::Rectangle aRect; @@ -1947,7 +1947,7 @@ namespace emfio Push(); IntersectClipRect( aRect ); } - DrawText(aPos, aText, nullptr, nullptr, mbRecordPath, static_cast<GraphicsMode>(nGfxMode)); + DrawText(aPos, aText, nullptr, nullptr, fXScale, fYScale, mbRecordPath, static_cast<GraphicsMode>(nGfxMode)); if ( nOptions & ETO_CLIPPED ) Pop(); mnBkMode = mnBkModeBackup; @@ -1964,7 +1964,7 @@ namespace emfio { sal_Int32 nLeft, nTop, nRight, nBottom; sal_uInt32 nGfxMode; - float nXScale, nYScale; + float fXScale, fYScale; sal_uInt32 ncStrings( 1 ); sal_Int32 ptlReferenceX, ptlReferenceY; sal_uInt32 nLen, nOffString, nOptions, offDx; @@ -1973,10 +1973,10 @@ namespace emfio nCurPos = mpInputStream->Tell() - 8; mpInputStream->ReadInt32( nLeft ).ReadInt32( nTop ).ReadInt32( nRight ).ReadInt32( nBottom ) - .ReadUInt32( nGfxMode ).ReadFloat( nXScale ).ReadFloat( nYScale ); + .ReadUInt32( nGfxMode ).ReadFloat( fXScale ).ReadFloat( fYScale ); SAL_INFO("emfio", " Bounds: " << nLeft << ", " << nTop << ", " << nRight << ", " << nBottom); SAL_INFO("emfio", " iGraphicsMode: 0x" << std::hex << nGfxMode << std::dec); - SAL_INFO("emfio", " Scale: " << nXScale << " x " << nYScale); + SAL_INFO("emfio", " Scale: " << fXScale << " x " << fYScale); if ( ( nRecType == EMR_POLYTEXTOUTA ) || ( nRecType == EMR_POLYTEXTOUTW ) ) { mpInputStream->ReadUInt32( ncStrings ); @@ -2101,7 +2101,7 @@ namespace emfio Push(); // Save the current clip. It will be restored after text drawing IntersectClipRect( aRect ); } - DrawText(aPos, aText, aDXAry.empty() ? nullptr : &aDXAry, pDYAry.get(), mbRecordPath, static_cast<GraphicsMode>(nGfxMode)); + DrawText(aPos, aText, aDXAry.empty() ? nullptr : &aDXAry, pDYAry.get(), fXScale, fYScale, mbRecordPath, static_cast<GraphicsMode>(nGfxMode)); if ( nOptions & ETO_CLIPPED ) Pop(); } diff --git a/emfio/source/reader/mtftools.cxx b/emfio/source/reader/mtftools.cxx index 5e2783e603be..153524665485 100644 --- a/emfio/source/reader/mtftools.cxx +++ b/emfio/source/reader/mtftools.cxx @@ -1686,7 +1686,7 @@ namespace emfio } } - void MtfTools::DrawText( Point& rPosition, OUString const & rText, KernArray* pDXArry, tools::Long* pDYArry, bool bRecordPath, GraphicsMode nGfxMode ) + void MtfTools::DrawText(Point& rPosition, OUString const & rText, KernArray* pDXArry, tools::Long* pDYArry, const float fXScale, const float fYScale, bool bRecordPath, GraphicsMode nGfxMode) { UpdateClipRegion(); rPosition = ImplMap( rPosition ); @@ -1772,6 +1772,8 @@ namespace emfio bChangeFont = true; mpGDIMetaFile->AddAction( new MetaTextFillColorAction( maFont.GetFillColor(), !maFont.IsTransparent() ) ); } + // Create a local copy of the current font to apply transient modifications + // (such as color, alignment, and custom scaling) before recording it to the metafile. vcl::Font aTmp( maFont ); aTmp.SetColor( maTextColor ); @@ -1807,6 +1809,43 @@ namespace emfio aTmp.SetOrientation( aTmp.GetOrientation() + Degree10( static_cast<sal_Int16>(fOrientation) ) ); } } + else if (nGfxMode == GraphicsMode::GM_COMPATIBLE) + { + if (fXScale != 0.0) + { + // As we changing only font width, we are skippin if scales have the same values + const bool bNeedsWidthScale = (std::fabs(fYScale) != std::fabs(fXScale)); + if (bNeedsWidthScale) + { + Size aFontSize = aTmp.GetFontSize(); + const float fTestWidthScale = std::fabs(fYScale / fXScale); + + // If Width is 0, the font is scaled proportionally based on Height. + if (aFontSize.Width() == 0) + aFontSize.setWidth(basegfx::fround<tools::Long>(aFontSize.Height() * fTestWidthScale)); + else + aFontSize.setWidth(basegfx::fround<tools::Long>(aFontSize.Width() * fTestWidthScale)); + aTmp.SetFontSize(aFontSize); + bChangeFont = true; + } + } + + if ((fYScale < 0.0) && (fXScale < 0.0)) + { + // Both scales negative = 180 degree rotation. vcl::Font handles this perfectly. + aTmp.SetOrientation(aTmp.GetOrientation() + Degree10(1800)); + } + else if ((fYScale < 0.0) || (fXScale < 0.0)) + { + // Single-axis mirroring. + // In GM_COMPATIBLE, text glyphs are NOT physically mirrored. + // However, the flipped coordinate system reverses the rotation direction. + // Inverting the angle (360 degrees - current angle) fixes the text direction. + aTmp.SetOrientation(Degree10(3600) - aTmp.GetOrientation()); + + // TODO: True single-axis glyph mirroring would require a MapMode transform here + } + } if( mnTextAlign & ( TA_UPDATECP | TA_RIGHT_CENTER ) ) {
