include/vcl/font.hxx                                           |    3 
 vcl/inc/font/FontSelectPattern.hxx                             |    1 
 vcl/inc/font/LogicalFontInstance.hxx                           |   16 +++
 vcl/inc/impfont.hxx                                            |    3 
 vcl/qa/cppunit/complextext.cxx                                 |   50 
++++++++++
 vcl/qa/cppunit/data/Fraunces-VariableFont_opsz,wght.ttf        |binary
 vcl/qa/cppunit/data/Fraunces-VariableFont_opsz,wght.ttf.readme |    8 +
 vcl/source/font/FontSelectPattern.cxx                          |    7 +
 vcl/source/font/LogicalFontInstance.cxx                        |   12 ++
 vcl/source/font/font.cxx                                       |   18 +++
 vcl/source/outdev/font.cxx                                     |   11 ++
 11 files changed, 126 insertions(+), 3 deletions(-)

New commits:
commit 9e67072a21bdbdfa9dc8da826654bf6fb4ec6116
Author:     Khaled Hosny <[email protected]>
AuthorDate: Mon Mar 2 02:02:17 2026 +0200
Commit:     Khaled Hosny <[email protected]>
CommitDate: Mon Mar 2 21:44:53 2026 +0100

    tdf#153368: Support optical size for variable fonts, part 1
    
    Font plumbing to enable opsz axis and set it to font’s point size.
    
    Change-Id: I3a4941591e47788dc659ba55d37c9f28d2f267d6
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/200497
    Tested-by: Jenkins
    Reviewed-by: Khaled Hosny <[email protected]>

diff --git a/include/vcl/font.hxx b/include/vcl/font.hxx
index a7c1cc3c9659..2e4b20d5d037 100644
--- a/include/vcl/font.hxx
+++ b/include/vcl/font.hxx
@@ -137,6 +137,9 @@ public:
     short               GetFixKerning() const;
     bool                IsFixKerning() const;
 
+    void                SetOpticalSizing( bool bOpticalSizing );
+    bool                GetOpticalSizing() const;
+
     void                SetOutline( bool bOutline );
     bool                IsOutline() const;
     void                SetShadow( bool bShadow );
diff --git a/vcl/inc/font/FontSelectPattern.hxx 
b/vcl/inc/font/FontSelectPattern.hxx
index 8655ec7c4106..9dae3b41b810 100644
--- a/vcl/inc/font/FontSelectPattern.hxx
+++ b/vcl/inc/font/FontSelectPattern.hxx
@@ -68,6 +68,7 @@ public:
     LanguageType    meLanguage;                 // text language
     bool            mbVertical;                 // vertical mode of requested 
font
     bool            mbNonAntialiased;           // true if antialiasing is 
disabled
+    bool            mbOpticalSizing;            // true if optical sizing is 
enabled
 
     bool            mbEmbolden;                 // Force emboldening
     ItalicMatrix    maItalicMatrix;             // Force matrix for slant
diff --git a/vcl/inc/font/LogicalFontInstance.hxx 
b/vcl/inc/font/LogicalFontInstance.hxx
index 34ce611685b6..a11ed8607910 100644
--- a/vcl/inc/font/LogicalFontInstance.hxx
+++ b/vcl/inc/font/LogicalFontInstance.hxx
@@ -108,6 +108,20 @@ public: // TODO: make data members private
     }
     const std::vector<hb_variation_t>& GetVariations() const;
 
+    void SetOpticalSizing(bool bOpticalSizing)
+    {
+        m_bOpticalSizing = bOpticalSizing;
+        mxVariations.reset();
+    }
+    bool GetOpticalSizing() const { return m_bOpticalSizing; }
+
+    void SetPointSize(float fPointSize)
+    {
+        m_fPointSize = fPointSize;
+        mxVariations.reset();
+    }
+    float GetPointSize() const { return m_fPointSize; }
+
     const vcl::font::PhysicalFontFace* GetFontFace() const { return 
m_pFontFace.get(); }
     vcl::font::PhysicalFontFace* GetFontFace() { return m_pFontFace.get(); }
     const ImplFontCache* GetFontCache() const { return mpFontCache; }
@@ -157,6 +171,8 @@ private:
     std::optional<bool> m_xbIsGraphiteFont;
     std::vector<hb_variation_t> m_aVariations;
     mutable std::optional<std::vector<hb_variation_t>> mxVariations;
+    bool m_bOpticalSizing = false;
+    float m_fPointSize = 0;
 
     mutable hb_draw_funcs_t* m_pHbDrawFuncs = nullptr;
     basegfx::B2DPolygon m_aDrawPolygon;
diff --git a/vcl/inc/impfont.hxx b/vcl/inc/impfont.hxx
index 1e697b9ee3d1..0d04e2bc5447 100644
--- a/vcl/inc/impfont.hxx
+++ b/vcl/inc/impfont.hxx
@@ -128,7 +128,8 @@ private:
                         mbConfigLookup:1,   // config lookup should only be 
done once
                         mbShadow:1,
                         mbVertical:1,
-                        mbTransparent:1;    // compatibility, now on output 
device
+                        mbTransparent:1,    // compatibility, now on output 
device
+                        mbOpticalSizing:1;
 
     // deprecated variables - device independent
     Color               maColor;            // compatibility, now on output 
device
diff --git a/vcl/qa/cppunit/complextext.cxx b/vcl/qa/cppunit/complextext.cxx
index 5b4e36ac88ca..68ba2359f3d0 100644
--- a/vcl/qa/cppunit/complextext.cxx
+++ b/vcl/qa/cppunit/complextext.cxx
@@ -24,6 +24,7 @@ static std::ostream& operator<<(std::ostream& rStream, const 
std::vector<double>
 #include <vcl/virdev.hxx>
 // workaround MSVC2015 issue with std::unique_ptr
 #include <sallayout.hxx>
+#include <tools/mapunit.hxx>
 
 
 #if HAVE_MORE_FONTS
@@ -872,4 +873,53 @@ CPPUNIT_TEST_FIXTURE(VclComplexTextTest, testTdf163761)
     }
 }
 
+// Load a variable font with opsz axis, and enable optical sizing.
+// In this particular font, at the two ends of the opsz axis, some characters 
produce different
+// glyphs. So we check that the glyphs are different for different point sizes.
+CPPUNIT_TEST_FIXTURE(VclComplexTextTest, testOpticalSizing)
+{
+#if HAVE_MORE_FONTS
+    ScopedVclPtrInstance<VirtualDevice> pOutDev;
+    pOutDev->SetMapMode(MapMode(MapUnit::MapPoint));
+
+    bool bAdded = addFont(pOutDev, u"Fraunces-VariableFont_opsz,wght.ttf", 
u"Fraunces");
+    CPPUNIT_ASSERT_EQUAL(true, bAdded);
+
+    auto aText = u"nh"_ustr;
+
+    // Test with small font size
+    vcl::Font aFont1{ u"Fraunces"_ustr, u"Regular"_ustr, Size{ 0, 9 } };
+    aFont1.SetOpticalSizing(true);
+    pOutDev->SetFont(aFont1);
+
+    auto pLayout1 = pOutDev->ImplLayout(aText, /*nIndex*/ 0, /*nLen*/ 
aText.getLength());
+
+    std::vector<sal_GlyphId> aGlyphs1;
+    const GlyphItem* pGlyph = nullptr;
+    basegfx::B2DPoint stPos;
+    int nCurrPos = 0;
+    while (pLayout1->GetNextGlyph(&pGlyph, stPos, nCurrPos))
+        aGlyphs1.push_back(pGlyph->glyphId());
+
+    // Test with large font size
+    vcl::Font aFont2{ u"Fraunces"_ustr, u"Regular"_ustr, Size{ 0, 144 } };
+    aFont2.SetOpticalSizing(true);
+    pOutDev->SetFont(aFont2);
+
+    auto pLayout2 = pOutDev->ImplLayout(aText, /*nIndex*/ 0, /*nLen*/ 
aText.getLength());
+
+    std::vector<sal_GlyphId> aGlyphs2;
+    pGlyph = nullptr;
+    nCurrPos = 0;
+    while (pLayout2->GetNextGlyph(&pGlyph, stPos, nCurrPos))
+        aGlyphs2.push_back(pGlyph->glyphId());
+
+    // Check that the glyphs are different for different point sizes
+    CPPUNIT_ASSERT_EQUAL(size_t(2), aGlyphs1.size());
+    CPPUNIT_ASSERT_EQUAL(size_t(2), aGlyphs2.size());
+    CPPUNIT_ASSERT(aGlyphs1.at(0) != aGlyphs2.at(0));
+    CPPUNIT_ASSERT(aGlyphs1.at(1) != aGlyphs2.at(1));
+#endif
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/qa/cppunit/data/Fraunces-VariableFont_opsz,wght.ttf 
b/vcl/qa/cppunit/data/Fraunces-VariableFont_opsz,wght.ttf
new file mode 100644
index 000000000000..0837b2af7f98
Binary files /dev/null and 
b/vcl/qa/cppunit/data/Fraunces-VariableFont_opsz,wght.ttf differ
diff --git a/vcl/qa/cppunit/data/Fraunces-VariableFont_opsz,wght.ttf.readme 
b/vcl/qa/cppunit/data/Fraunces-VariableFont_opsz,wght.ttf.readme
new file mode 100644
index 000000000000..aa1fa27bd18c
--- /dev/null
+++ b/vcl/qa/cppunit/data/Fraunces-VariableFont_opsz,wght.ttf.readme
@@ -0,0 +1,8 @@
+This is a subset copy of Fraunces font licensed under Open Font License and
+obtained from:
+
+  https://fonts.google.com/specimen/Fraunces
+
+And subset using hb-subset to keep only the opsz and wght axes:
+
+  hb-subset --keep-everything --variations=SOFT=drop,WONK=drop 
Fraunces-VariableFont_SOFT,WONK,opsz,wght.ttf -o 
Fraunces-VariableFont_opsz,wght.ttf
diff --git a/vcl/source/font/FontSelectPattern.cxx 
b/vcl/source/font/FontSelectPattern.cxx
index 30c2c9aaa574..482ed2d02c90 100644
--- a/vcl/source/font/FontSelectPattern.cxx
+++ b/vcl/source/font/FontSelectPattern.cxx
@@ -44,6 +44,7 @@ FontSelectPattern::FontSelectPattern( const vcl::Font& rFont,
     , meLanguage( rFont.GetLanguage() )
     , mbVertical( rFont.IsVertical() )
     , mbNonAntialiased(bNonAntialias)
+    , mbOpticalSizing(rFont.GetOpticalSizing())
     , mbEmbolden( false )
 {
     maTargetName = GetFamilyName();
@@ -79,6 +80,7 @@ FontSelectPattern::FontSelectPattern( const PhysicalFontFace& 
rFontData,
     , meLanguage( 0 )
     , mbVertical( bVertical )
     , mbNonAntialiased( false )
+    , mbOpticalSizing( false )
     , mbEmbolden( false )
 {
     maTargetName = maSearchName = GetFamilyName();
@@ -108,6 +110,8 @@ size_t FontSelectPattern::hashCode() const
     nHash += 41 * static_cast<sal_uInt16>(meLanguage);
     if( mbVertical )
         nHash += 53;
+    if( mbOpticalSizing )
+        nHash += 61;
     return nHash;
 }
 
@@ -143,6 +147,9 @@ bool FontSelectPattern::operator==(const FontSelectPattern& 
rOther) const
     if (mbNonAntialiased != rOther.mbNonAntialiased)
         return false;
 
+    if (mbOpticalSizing != rOther.mbOpticalSizing)
+        return false;
+
     if (mbEmbolden != rOther.mbEmbolden)
         return false;
 
diff --git a/vcl/source/font/LogicalFontInstance.cxx 
b/vcl/source/font/LogicalFontInstance.cxx
index 626c9b3f5fbf..f6f46d461526 100644
--- a/vcl/source/font/LogicalFontInstance.cxx
+++ b/vcl/source/font/LogicalFontInstance.cxx
@@ -41,6 +41,7 @@ LogicalFontInstance::LogicalFontInstance(const 
vcl::font::PhysicalFontFace& rFon
     , m_pHbFont(nullptr)
     , m_nAveWidthFactor(1.0f)
     , m_pFontFace(&const_cast<vcl::font::PhysicalFontFace&>(rFontFace))
+    , m_bOpticalSizing(rFontSelData.mbOpticalSizing)
 {
 }
 
@@ -67,6 +68,9 @@ const std::vector<hb_variation_t>& 
LogicalFontInstance::GetVariations() const
         mxVariations = GetFontFace()->GetVariations(*this);
         hb_face_t* pHbFace = GetFontFace()->GetHbFace();
         auto aVariations = m_aVariations;
+        if (m_bOpticalSizing && m_fPointSize > 0)
+            aVariations.push_back({ HB_TAG('o', 'p', 's', 'z'), m_fPointSize 
});
+
         for (auto& rVariation : aVariations)
         {
             hb_ot_var_axis_info_t info;
diff --git a/vcl/source/font/font.cxx b/vcl/source/font/font.cxx
index 296dff6b2e08..f1a70e424fea 100644
--- a/vcl/source/font/font.cxx
+++ b/vcl/source/font/font.cxx
@@ -243,6 +243,17 @@ void Font::SetWeight( FontWeight eWeight )
         mpImplFont->SetWeight( eWeight );
 }
 
+void Font::SetOpticalSizing( bool bOpticalSizing )
+{
+    if (GetOpticalSizing() != bOpticalSizing)
+        mpImplFont->mbOpticalSizing = bOpticalSizing;
+}
+
+bool Font::GetOpticalSizing() const
+{
+    return mpImplFont->mbOpticalSizing;
+}
+
 void Font::SetWidthType( FontWidth eWidth )
 {
     if (std::as_const(mpImplFont)->GetWidthTypeNoAsk() != eWidth)
@@ -404,6 +415,7 @@ void Font::Merge( const vcl::Font& rFont )
     SetKerning( rFont.IsKerning() ? FontKerning::FontSpecific : 
FontKerning::NONE );
     SetOutline( rFont.IsOutline() );
     SetShadow( rFont.IsShadow() );
+    SetOpticalSizing( rFont.GetOpticalSizing() );
     SetRelief( rFont.GetRelief() );
 }
 
@@ -975,6 +987,7 @@ ImplFont::ImplFont() :
     mbShadow( false ),
     mbVertical( false ),
     mbTransparent( true ),
+    mbOpticalSizing( false ),
     maColor( COL_TRANSPARENT ),
     maFillColor( COL_TRANSPARENT ),
     mbWordLine( false ),
@@ -1008,6 +1021,7 @@ ImplFont::ImplFont( const ImplFont& rImplFont ) :
     mbShadow( rImplFont.mbShadow ),
     mbVertical( rImplFont.mbVertical ),
     mbTransparent( rImplFont.mbTransparent ),
+    mbOpticalSizing( rImplFont.mbOpticalSizing ),
     maColor( rImplFont.maColor ),
     maFillColor( rImplFont.maFillColor ),
     mbWordLine( rImplFont.mbWordLine ),
@@ -1062,7 +1076,8 @@ bool ImplFont::EqualIgnoreColor( const ImplFont& rOther ) 
const
     ||  (mbShadow       != rOther.mbShadow)
     ||  (meKerning      != rOther.meKerning)
     ||  (mnSpacing      != rOther.mnSpacing)
-    ||  (mbTransparent  != rOther.mbTransparent) )
+    ||  (mbTransparent  != rOther.mbTransparent)
+    ||  (mbOpticalSizing!= rOther.mbOpticalSizing) )
         return false;
 
     return true;
@@ -1108,6 +1123,7 @@ size_t ImplFont::GetHashValueIgnoreColor() const
     o3tl::hash_combine( hash, meKerning );
     o3tl::hash_combine( hash, mnSpacing );
     o3tl::hash_combine( hash, mbTransparent );
+    o3tl::hash_combine( hash, mbOpticalSizing );
 
     return hash;
 }
diff --git a/vcl/source/outdev/font.cxx b/vcl/source/outdev/font.cxx
index c9b51908680f..a7d97fc36f12 100644
--- a/vcl/source/outdev/font.cxx
+++ b/vcl/source/outdev/font.cxx
@@ -22,6 +22,7 @@
 #include <rtl/ustrbuf.hxx>
 #include <sal/log.hxx>
 #include <tools/debug.hxx>
+#include <tools/mapunit.hxx>
 #include <i18nlangtag/mslangid.hxx>
 #include <i18nlangtag/lang.h>
 #include <comphelper/configuration.hxx>
@@ -728,6 +729,16 @@ bool OutputDevice::ImplNewFont() const
         return false;
     }
 
+
+    // Compute font size in points for optical sizing.
+    if (!pFontInstance->GetPointSize())
+    {
+        auto nHeight = maFont.GetFontHeight();
+        auto eFrom = MapToO3tlLength(GetMapMode().GetMapUnit());
+        float fPointSize = o3tl::convert(float(nHeight), eFrom, 
o3tl::Length::pt);
+        pFontInstance->SetPointSize(fPointSize);
+    }
+
     // mark when lower layers need to get involved
     mbNewFont = false;
     if( bNewFontInstance )
commit 89fe022707aa1d59b0486587d572f575839aac1a
Author:     Khaled Hosny <[email protected]>
AuthorDate: Mon Mar 2 02:02:15 2026 +0200
Commit:     Khaled Hosny <[email protected]>
CommitDate: Mon Mar 2 21:44:41 2026 +0100

    Clamp variation values to axis min and max
    
    Avoids creating needless PDF subsets for instances that are effectively
    the same.
    
    Change-Id: I9ce04f99a77cb4d6fde5456d6de928b9f46670e2
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/200626
    Reviewed-by: Khaled Hosny <[email protected]>
    Tested-by: Jenkins

diff --git a/vcl/source/font/LogicalFontInstance.cxx 
b/vcl/source/font/LogicalFontInstance.cxx
index ae418ccfad3f..626c9b3f5fbf 100644
--- a/vcl/source/font/LogicalFontInstance.cxx
+++ b/vcl/source/font/LogicalFontInstance.cxx
@@ -65,8 +65,14 @@ const std::vector<hb_variation_t>& 
LogicalFontInstance::GetVariations() const
     if (!mxVariations)
     {
         mxVariations = GetFontFace()->GetVariations(*this);
-        for (const auto& rVariation : m_aVariations)
+        hb_face_t* pHbFace = GetFontFace()->GetHbFace();
+        auto aVariations = m_aVariations;
+        for (auto& rVariation : aVariations)
         {
+            hb_ot_var_axis_info_t info;
+            if (hb_ot_var_find_axis_info(pHbFace, rVariation.tag, &info))
+                rVariation.value = std::clamp(rVariation.value, 
info.min_value, info.max_value);
+
             auto it = std::find_if(mxVariations->begin(), mxVariations->end(),
                                    [&rVariation](const hb_variation_t& rOther) 
{
                                        return rOther.tag == rVariation.tag;

Reply via email to