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 ) )
         {

Reply via email to