configure.ac                         |    2 +-
 vcl/inc/font/PhysicalFontFace.hxx    |    3 ++-
 vcl/inc/pdf/pdfwriter_impl.hxx       |   31 +++++++++++++++++++++++++++++--
 vcl/source/font/PhysicalFontFace.cxx |   17 +++++++++++++++--
 vcl/source/pdf/pdfwriter_impl.cxx    |   35 ++++++++++++++++++-----------------
 5 files changed, 65 insertions(+), 23 deletions(-)

New commits:
commit 7ffb13159276c0a74ce4fffc49bb0da4a1950f1d
Author:     Khaled Hosny <[email protected]>
AuthorDate: Wed Feb 18 16:40:20 2026 +0200
Commit:     Khaled Hosny <[email protected]>
CommitDate: Fri Feb 20 22:31:41 2026 +0100

    Use HarfBuzz for instancing variable fonts
    
    Instance variable fonts into static instances for embedding in PDF as
    regular fonts, instead of drawing the glyph outlines in PDF Type 3
    fonts. This should be more efficient and preserve most font data like
    hinting.
    
    Variable fonts with CFF2 table (which are rare) continue to be drawn as
    Type 3 fonts as we would also need to to convert CFF2 to CFF and current
    versions of HarfBuzz has no support for that yet.
    
    The new APIs we use require HarfBuzz 8.3.1
    
    Change-Id: Ic1e0b0ad445cb5918ad8f0b04d4bf9d364d870d8
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199626
    Tested-by: Jenkins
    Reviewed-by: Khaled Hosny <[email protected]>

diff --git a/configure.ac b/configure.ac
index 9d02bbce9900..a962aba3ec24 100644
--- a/configure.ac
+++ b/configure.ac
@@ -11679,7 +11679,7 @@ AC_SUBST(SYSTEM_LIBORCUS)
 dnl ===================================================================
 dnl HarfBuzz
 dnl ===================================================================
-harfbuzz_required_version=7.3.0
+harfbuzz_required_version=8.3.1
 
 GRAPHITE_CFLAGS_internal="-I${WORKDIR}/UnpackedTarball/graphite/include 
-DGRAPHITE2_STATIC"
 HARFBUZZ_CFLAGS_internal="-I${WORKDIR}/UnpackedTarball/harfbuzz/src"
diff --git a/vcl/inc/font/PhysicalFontFace.hxx 
b/vcl/inc/font/PhysicalFontFace.hxx
index 5efdc4e2be41..2b7e159dd56f 100644
--- a/vcl/inc/font/PhysicalFontFace.hxx
+++ b/vcl/inc/font/PhysicalFontFace.hxx
@@ -161,7 +161,8 @@ public:
     // implementation note: encoding 0 with glyph id 0 should be added 
implicitly
     // as "undefined character"
     SAL_DLLPRIVATE bool CreateFontSubset(std::vector<sal_uInt8>&, const 
sal_GlyphId*,
-                                         const sal_uInt8*, const int, 
FontSubsetInfo&) const;
+                                         const sal_uInt8*, const int, 
FontSubsetInfo&,
+                                         const std::vector<hb_variation_t>& 
rVariations = {}) const;
 
     bool IsColorFont() const { return HasColorLayers() || HasColorBitmaps(); }
 
diff --git a/vcl/inc/pdf/pdfwriter_impl.hxx b/vcl/inc/pdf/pdfwriter_impl.hxx
index 62f86e0a92f5..c762a6fb0959 100644
--- a/vcl/inc/pdf/pdfwriter_impl.hxx
+++ b/vcl/inc/pdf/pdfwriter_impl.hxx
@@ -348,6 +348,33 @@ struct FontSubset
     std::map<sal_GlyphId, Glyph>         m_aMapping;
 };
 
+struct FontSubsetKey
+{
+    const vcl::font::PhysicalFontFace* m_pFace;
+    const std::vector<hb_variation_t>& m_rVariations;
+    size_t m_nHash;
+
+    FontSubsetKey(const vcl::font::PhysicalFontFace* pFace, const 
LogicalFontInstance* pFont)
+        : m_pFace(pFace)
+        , m_rVariations(pFace->GetVariations(*pFont))
+        , m_nHash(0)
+    {
+        o3tl::hash_combine(m_nHash, m_rVariations.size());
+        for (const auto& rVar : m_rVariations)
+        {
+            o3tl::hash_combine(m_nHash, rVar.tag);
+            o3tl::hash_combine(m_nHash, rVar.value);
+        }
+    }
+
+    bool operator<(const FontSubsetKey& rOther) const
+    {
+        if (m_pFace != rOther.m_pFace)
+            return m_pFace < rOther.m_pFace;
+        return m_nHash < rOther.m_nHash;
+    }
+};
+
 struct EmbedFont
 {
     sal_Int32                       m_nNormalFontID;
@@ -786,7 +813,7 @@ private:
     std::vector< TilingEmit >           m_aTilings;
     std::vector< TransparencyEmit >     m_aTransparentObjects;
     /*  contains all font subsets in use */
-    std::map<const vcl::font::PhysicalFontFace*, FontSubset> m_aSubsets;
+    std::map<FontSubsetKey, FontSubset> m_aSubsets;
     std::map<const vcl::font::PhysicalFontFace*, EmbedFont> m_aSystemFonts;
     std::map<const vcl::font::PhysicalFontFace*, FontSubset> m_aType3Fonts;
     sal_Int32                           m_nNextFID;
@@ -866,7 +893,7 @@ private:
 private:
     /* creates fonts and subsets that will be emitted later */
     void registerGlyph(const sal_GlyphId, const vcl::font::PhysicalFontFace*, 
const LogicalFontInstance* pFont, const std::vector<sal_Ucs>&, sal_Int32, 
sal_uInt8&, sal_Int32&);
-    void registerSimpleGlyph(const sal_GlyphId, const 
vcl::font::PhysicalFontFace*, const std::vector<sal_Ucs>&, sal_Int32, 
sal_uInt8&, sal_Int32&);
+    void registerSimpleGlyph(const sal_GlyphId, const 
vcl::font::PhysicalFontFace*, const LogicalFontInstance*, const 
std::vector<sal_Ucs>&, sal_Int32, sal_uInt8&, sal_Int32&);
 
     /*  emits a text object according to the passed layout */
     /* TODO: remove rText as soon as SalLayout will change so that rText is 
not necessary anymore */
diff --git a/vcl/source/font/PhysicalFontFace.cxx 
b/vcl/source/font/PhysicalFontFace.cxx
index 3f965ac5852f..8569a35ecb85 100644
--- a/vcl/source/font/PhysicalFontFace.cxx
+++ b/vcl/source/font/PhysicalFontFace.cxx
@@ -307,7 +307,8 @@ constexpr auto DESCENT_HHEA = 
static_cast<hb_ot_metrics_tag_t>(HB_TAG('H', 'd',
 
 bool PhysicalFontFace::CreateFontSubset(std::vector<sal_uInt8>& rOutBuffer,
                                         const sal_GlyphId* pGlyphIds, const 
sal_uInt8* pEncoding,
-                                        const int nGlyphCount, FontSubsetInfo& 
rInfo) const
+                                        const int nGlyphCount, FontSubsetInfo& 
rInfo,
+                                        const std::vector<hb_variation_t>& 
rVariations) const
 {
     // Create subset input
     hb_subset_input_t* pInput = hb_subset_input_create_or_fail();
@@ -343,8 +344,20 @@ bool 
PhysicalFontFace::CreateFontSubset(std::vector<sal_uInt8>& rOutBuffer,
     for (auto nKeep : aKeepTables)
         hb_set_del(pDropTableSet, nKeep);
 
+    hb_face_t* pHbFace = GetHbFace();
+    bool bIsVariableFont = hb_ot_var_has_data(pHbFace);
+    if (bIsVariableFont)
+    {
+        // Instance variable font. We first pin all axes to their default 
values, so we don’t have to
+        // enumerate all axes in the font. Then we pin the axes we want to 
instance to their specified
+        // values.
+        hb_subset_input_pin_all_axes_to_default(pInput, pHbFace);
+        for (const auto& rVariation : rVariations)
+            hb_subset_input_pin_axis_location(pInput, pHbFace, rVariation.tag, 
rVariation.value);
+    }
+
     // Perform the subsettting
-    hb_face_t* pSubsetFace = hb_subset_or_fail(GetHbFace(), pInput);
+    hb_face_t* pSubsetFace = hb_subset_or_fail(pHbFace, pInput);
     comphelper::ScopeGuard aSubsetFaceGuard([&]() { 
hb_face_destroy(pSubsetFace); });
     if (!pSubsetFace)
         return false;
diff --git a/vcl/source/pdf/pdfwriter_impl.cxx 
b/vcl/source/pdf/pdfwriter_impl.cxx
index 4442f15fbba4..f893ae47f910 100644
--- a/vcl/source/pdf/pdfwriter_impl.cxx
+++ b/vcl/source/pdf/pdfwriter_impl.cxx
@@ -2083,8 +2083,9 @@ bool PDFWriterImpl::emitFonts()
 
             std::vector<sal_uInt8> aBuffer;
             FontSubsetInfo aSubsetInfo;
-            const auto* pFace = subset.first;
-            if (pFace->CreateFontSubset(aBuffer, pGlyphIds, pEncoding, 
nGlyphs, aSubsetInfo))
+            const auto* pFace = subset.first.m_pFace;
+            if (pFace->CreateFontSubset(aBuffer, pGlyphIds, pEncoding, 
nGlyphs, aSubsetInfo,
+                                        subset.first.m_rVariations))
             {
                 // create font stream
                 if (g_bDebugDisableCompression)
@@ -2185,7 +2186,7 @@ bool PDFWriterImpl::emitFonts()
                 if ( !writeBuffer( aLine ) ) return false;
 
                 // write font descriptor
-                sal_Int32 nFontDescriptor = emitFontDescriptor( subset.first, 
aSubsetInfo, s_subset.m_nFontID, nFontStream );
+                sal_Int32 nFontDescriptor = emitFontDescriptor( 
subset.first.m_pFace, aSubsetInfo, s_subset.m_nFontID, nFontStream );
 
                 if( nToUnicodeStream )
                     nToUnicodeStream = createToUnicodeCMap( pEncoding, 
aCodeUnits, pCodeUnitsPerGlyph, pEncToUnicodeIndex, nGlyphs );
@@ -5456,12 +5457,14 @@ sal_Int32 PDFWriterImpl::getSystemFont( const 
vcl::Font& i_rFont )
 
 void PDFWriterImpl::registerSimpleGlyph(const sal_GlyphId nFontGlyphId,
                                   const vcl::font::PhysicalFontFace* pFace,
+                                  const LogicalFontInstance* pFont,
                                   const std::vector<sal_Ucs>& rCodeUnits,
                                   sal_Int32 nGlyphWidth,
                                   sal_uInt8& nMappedGlyph,
                                   sal_Int32& nMappedFontObject)
 {
-    FontSubset& rSubset = m_aSubsets[ pFace ];
+    FontSubset& rSubset = m_aSubsets[ { pFace, pFont } ];
+
     // search for font specific glyphID
     auto it = rSubset.m_aMapping.find( nFontGlyphId );
     if( it != rSubset.m_aMapping.end() )
@@ -5504,21 +5507,19 @@ void PDFWriterImpl::registerGlyph(const sal_GlyphId 
nFontGlyphId,
                                   const std::vector<sal_Ucs>& rCodeUnits, 
sal_Int32 nGlyphWidth,
                                   sal_uInt8& nMappedGlyph, sal_Int32& 
nMappedFontObject)
 {
-    auto bVariations = !pFace->GetVariations(*pFont).empty();
     // tdf#155161
-    // PDF doesn’t support CFF2 table and we currently don’t convert them to
-    // Type 1 (like we do with CFF table), so treat it like fonts with
-    // variations and embed as Type 3 fonts.
-    if (!pFace->GetRawFontData(HB_TAG('C', 'F', 'F', '2')).empty())
-        bVariations = true;
+    // PDF doesn't support CFF2 table and we currently don't convert them to
+    // Type 1 (like we do with CFF table), so embed as Type 3 fonts.
+    // Non-CFF2 variable fonts are instanced via hb-subset and embedded 
normally.
+    bool bCFF2 = !pFace->GetRawFontData(HB_TAG('C', 'F', 'F', '2')).empty();
 
-    if (pFace->IsColorFont() || bVariations)
+    if (pFace->IsColorFont() || bCFF2)
     {
         // Font has colors, check if this glyph has color layers or bitmap.
         tools::Rectangle aRect;
         auto aLayers = pFace->GetGlyphColorLayers(nFontGlyphId);
         auto aBitmap = pFace->GetGlyphColorBitmap(nFontGlyphId, aRect);
-        if (!aLayers.empty() || !aBitmap.empty() || bVariations)
+        if (!aLayers.empty() || !aBitmap.empty() || bCFF2)
         {
             auto& rSubset = m_aType3Fonts[pFace];
             auto it = rSubset.m_aMapping.find(nFontGlyphId);
@@ -5557,8 +5558,8 @@ void PDFWriterImpl::registerGlyph(const sal_GlyphId 
nFontGlyphId,
                     {
                         sal_uInt8 nLayerGlyph;
                         sal_Int32 nLayerFontID;
-                        registerSimpleGlyph(aLayer.nGlyphIndex, pFace, 
rCodeUnits, nGlyphWidth,
-                                            nLayerGlyph, nLayerFontID);
+                        registerSimpleGlyph(aLayer.nGlyphIndex, pFace, pFont, 
rCodeUnits,
+                                            nGlyphWidth, nLayerGlyph, 
nLayerFontID);
 
                         rNewGlyphEmit.addColorLayer(
                             { nLayerFontID, nLayerGlyph, aLayer.nColorIndex });
@@ -5566,7 +5567,7 @@ void PDFWriterImpl::registerGlyph(const sal_GlyphId 
nFontGlyphId,
                 }
                 else if (!aBitmap.empty())
                     rNewGlyphEmit.setColorBitmap(aBitmap, aRect);
-                else if (bVariations)
+                else if (bCFF2)
                     
rNewGlyphEmit.setOutline(pFont->GetGlyphOutlineUntransformed(nFontGlyphId));
 
                 // add new glyph to font mapping
@@ -5579,7 +5580,7 @@ void PDFWriterImpl::registerGlyph(const sal_GlyphId 
nFontGlyphId,
     }
 
     // If we reach here then the glyph has no color layers.
-    registerSimpleGlyph(nFontGlyphId, pFace, rCodeUnits, nGlyphWidth, 
nMappedGlyph,
+    registerSimpleGlyph(nFontGlyphId, pFace, pFont, rCodeUnits, nGlyphWidth, 
nMappedGlyph,
                         nMappedFontObject);
 }
 
@@ -6013,7 +6014,7 @@ void PDFWriterImpl::drawLayout( SalLayout& rLayout, const 
OUString& rText, bool
         // instead.
         if (!aCodeUnits.empty() && !bUseActualText)
         {
-            for (const auto& rSubset : m_aSubsets[pFace].m_aSubsets)
+            for (const auto& rSubset : m_aSubsets[{ pFace, pGlyphFont 
}].m_aSubsets)
             {
                 const auto it = rSubset.m_aMapping.find(nGlyphId);
                 if (it != rSubset.m_aMapping.cend() && it->second.codes() != 
aCodeUnits)

Reply via email to