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;
