cppcanvas/source/mtfrenderer/implrenderer.cxx |   20 +-------
 editeng/source/editeng/impedit.hxx            |    1 
 editeng/source/editeng/impedit3.cxx           |   18 +------
 include/vcl/svapp.hxx                         |    8 ++-
 vcl/CppunitTest_vcl_localize_digits.mk        |   26 ++++++++++
 vcl/Module_vcl.mk                             |    1 
 vcl/qa/cppunit/LocalizeDigits.cxx             |   64 ++++++++++++++++++++++++++
 vcl/source/gdi/sallayout.cxx                  |   47 +++++++++++++++++++
 vcl/source/outdev/text.cxx                    |   26 +---------
 9 files changed, 154 insertions(+), 57 deletions(-)

New commits:
commit 95621a71f9ff4257b5c6f4be1eb11fc7328a2fa4
Author:     Neil Roberts <[email protected]>
AuthorDate: Wed Nov 5 14:05:08 2025 +0100
Commit:     Noel Grandin <[email protected]>
CommitDate: Wed Nov 5 19:21:18 2025 +0100

    Add a unit test for LocalizeDigitsInString
    
    Change-Id: I51fb04dbc542b4052a31e8c96552406a1d6947e7
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/193461
    Tested-by: Jenkins
    Reviewed-by: Noel Grandin <[email protected]>

diff --git a/vcl/CppunitTest_vcl_localize_digits.mk 
b/vcl/CppunitTest_vcl_localize_digits.mk
new file mode 100644
index 000000000000..b077dbca060e
--- /dev/null
+++ b/vcl/CppunitTest_vcl_localize_digits.mk
@@ -0,0 +1,26 @@
+# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t; fill-column: 
100 -*-
+#
+# This file is part of the LibreOffice project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+
+$(eval $(call gb_CppunitTest_CppunitTest,vcl_localize_digits))
+
+$(eval $(call gb_CppunitTest_set_include,vcl_localize_digits,\
+    $$(INCLUDE) \
+    -I$(SRCDIR)/vcl/inc \
+))
+
+$(eval $(call gb_CppunitTest_add_exception_objects,vcl_localize_digits, \
+    vcl/qa/cppunit/LocalizeDigits \
+))
+
+$(eval $(call gb_CppunitTest_use_libraries,vcl_localize_digits, \
+    sal \
+    vcl \
+))
+
+# vim: set noet sw=4 ts=4:
diff --git a/vcl/Module_vcl.mk b/vcl/Module_vcl.mk
index a80744aad17a..517bdf723d39 100644
--- a/vcl/Module_vcl.mk
+++ b/vcl/Module_vcl.mk
@@ -254,6 +254,7 @@ $(eval $(call gb_Module_add_check_targets,vcl,\
         CppunitTest_vcl_skia) \
     CppunitTest_vcl_filter_igif \
     CppunitTest_vcl_unit_conversion \
+    CppunitTest_vcl_localize_digits \
 ))
 
 ifeq ($(USING_X11),TRUE)
diff --git a/vcl/qa/cppunit/LocalizeDigits.cxx 
b/vcl/qa/cppunit/LocalizeDigits.cxx
new file mode 100644
index 000000000000..6062bacd0286
--- /dev/null
+++ b/vcl/qa/cppunit/LocalizeDigits.cxx
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; 
fill-column: 100 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <sal/config.h>
+
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/plugin/TestPlugIn.h>
+
+#include <vcl/svapp.hxx>
+
+namespace
+{
+CPPUNIT_TEST_FIXTURE(CppUnit::TestFixture, LocalDigits)
+{
+    // Test the case where there are no digits to convert
+    OUString sNoDigits = "hello";
+    sal_Int32 nLen = sNoDigits.getLength();
+    // It should return the same string
+    CPPUNIT_ASSERT_EQUAL(reinterpret_cast<sal_uIntPtr>(sNoDigits.getStr()),
+                         reinterpret_cast<sal_uIntPtr>(
+                             LocalizeDigitsInString(sNoDigits, LANGUAGE_FARSI, 
0, nLen).getStr()));
+    // The length should not change
+    CPPUNIT_ASSERT_EQUAL(sNoDigits.getLength(), nLen);
+
+    // Test the case where there are digits but they are already correct for 
the locale.
+    OUString sDozen = "There are 12 eggs in a dozen.";
+    nLen = sDozen.getLength();
+    // It should return the same string
+    CPPUNIT_ASSERT_EQUAL(reinterpret_cast<sal_uIntPtr>(sDozen.getStr()),
+                         reinterpret_cast<sal_uIntPtr>(
+                             LocalizeDigitsInString(sDozen, LANGUAGE_SYSTEM, 
0, nLen).getStr()));
+    // The length should not change
+    CPPUNIT_ASSERT_EQUAL(sDozen.getLength(), nLen);
+
+    // Test an actual conversion
+    CPPUNIT_ASSERT_EQUAL(u"There are Ϋ±Ϋ² eggs in a dozen."_ustr,
+                         LocalizeDigitsInString(sDozen, LANGUAGE_FARSI, 0, 
nLen));
+    CPPUNIT_ASSERT_EQUAL(sDozen.getLength(), nLen);
+
+    // Test converting a subrange
+    nLen = 12;
+    CPPUNIT_ASSERT_EQUAL(
+        u"There are Ϋ±Ϋ² eggs in a dozen but 13 in a baker's dozen."_ustr,
+        LocalizeDigitsInString("There are 12 eggs in a dozen but 13 in a 
baker's dozen.",
+                               LANGUAGE_FARSI, 2, nLen));
+    CPPUNIT_ASSERT_EQUAL(sal_Int32(12), nLen);
+
+    // Test with characters outside of the bmp
+    CPPUNIT_ASSERT_EQUAL(
+        u"π‘žπ‘Ί 𐑸 Ϋ±Ϋ² π‘§π‘œπ‘Ÿ 𐑦𐑯 𐑩 π‘šπ‘±π‘’π‘Όπ‘Ÿ π‘›π‘³π‘Ÿπ‘©π‘―."_ustr,
+        LocalizeDigitsInString(u"π‘žπ‘Ί 𐑸 12 π‘§π‘œπ‘Ÿ 𐑦𐑯 𐑩 π‘šπ‘±π‘’π‘Όπ‘Ÿ π‘›π‘³π‘Ÿπ‘©π‘―."_ustr, 
LANGUAGE_FARSI));
+}
+} // namespace
+
+CPPUNIT_PLUGIN_IMPLEMENT();
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
commit 117d9624a3b861ead575394f6dfc974b4f4eeee6
Author:     Neil Roberts <[email protected]>
AuthorDate: Wed Nov 5 11:08:25 2025 +0100
Commit:     Noel Grandin <[email protected]>
CommitDate: Wed Nov 5 19:21:07 2025 +0100

    Consolidate calls to GetLocalizedChar into one helper function
    
    The three places that call GetLocalizedChar were all functions that were
    doing the same thing, ie, localizing the digits in a string. Instead of
    having three separate copies of this similar code, this patch stops
    exporting GetLocalizedChar and instead exports a utility function that
    operates on a string.
    
    One of the three versions of the code avoided allocating a new string if
    nothing changes. This new consolidated helper uses the same approach
    so that the other two places can take advantage of this very likely
    optimisation.
    
    The main motivation to do this was to avoid truncating the sal_UCS4
    returned by GetLocalizedChar into a sal_Unicode. The consolidated
    implementation adds the converted characters with appendUtf32 instead so
    that if GetLocalizedChar ever returns characters outside of the BMP then
    it would work. In practice this doesn’t currently happen so it’s not an
    actual problem. However the commit below makes the compiler warn when
    sal_UCS4 is truncated, so this patch fixes a truncation that was marked
    as a TODO in that commit.
    
        commit 82f055e5cb35501db06b4996909759f686de9631
        Author: Noel Grandin <[email protected]>
        Date:   Tue Nov 4 10:12:18 2025 +0200
            new loplugin:narrow
    
    Change-Id: I628a29520a0896cd0f4c7c2ff131ac763e07925b
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/193460
    Tested-by: Jenkins
    Reviewed-by: Noel Grandin <[email protected]>

diff --git a/cppcanvas/source/mtfrenderer/implrenderer.cxx 
b/cppcanvas/source/mtfrenderer/implrenderer.cxx
index d11f9cdada40..e7b04397507b 100644
--- a/cppcanvas/source/mtfrenderer/implrenderer.cxx
+++ b/cppcanvas/source/mtfrenderer/implrenderer.cxx
@@ -215,20 +215,6 @@ namespace
 
         return Bitmap( aSolid, aMask );
     }
-
-    OUString convertToLocalizedNumerals(std::u16string_view rStr,
-        LanguageType eTextLanguage)
-    {
-        OUStringBuffer aBuf(rStr);
-        for (sal_Int32 i = 0; i < aBuf.getLength(); ++i)
-        {
-            sal_Unicode nChar = aBuf[i];
-            if (nChar >= '0' && nChar <= '9')
-                // TODO: are the localized digit surrogates?
-                aBuf[i] = static_cast<sal_Unicode>(GetLocalizedChar(nChar, 
eTextLanguage));
-        }
-        return aBuf.makeStringAndClear();
-    }
 }
 
 namespace cppcanvas::internal
@@ -2438,7 +2424,7 @@ namespace cppcanvas::internal
                         OUString sText = pAct->GetText();
 
                         if (rVDev.GetDigitLanguage())
-                            sText = convertToLocalizedNumerals(sText, 
rVDev.GetDigitLanguage());
+                            sText = LocalizeDigitsInString(sText, 
rVDev.GetDigitLanguage());
 
                         const sal_Int32 nLen = std::min(pAct->GetLen(), 
pAct->GetText().getLength() - pAct->GetIndex());
 
@@ -2460,7 +2446,7 @@ namespace cppcanvas::internal
                         OUString sText = pAct->GetText();
 
                         if (rVDev.GetDigitLanguage())
-                            sText = convertToLocalizedNumerals(sText, 
rVDev.GetDigitLanguage());
+                            sText = LocalizeDigitsInString(sText, 
rVDev.GetDigitLanguage());
 
                         const sal_Int32 nLen = std::min(pAct->GetLen(), 
pAct->GetText().getLength() - pAct->GetIndex());
 
@@ -2540,7 +2526,7 @@ namespace cppcanvas::internal
                         OUString sText = pAct->GetText();
 
                         if (rVDev.GetDigitLanguage())
-                            sText = convertToLocalizedNumerals(sText, 
rVDev.GetDigitLanguage());
+                            sText = LocalizeDigitsInString(sText, 
rVDev.GetDigitLanguage());
 
                         const sal_Int32 nLen = std::min(pAct->GetLen(), 
pAct->GetText().getLength() - pAct->GetIndex());
 
diff --git a/editeng/source/editeng/impedit.hxx 
b/editeng/source/editeng/impedit.hxx
index 338145db8973..72eadc1b79ac 100644
--- a/editeng/source/editeng/impedit.hxx
+++ b/editeng/source/editeng/impedit.hxx
@@ -771,7 +771,6 @@ private:
     void                ImplInitLayoutMode(OutputDevice& rOutDev, sal_Int32 
nPara, sal_Int32 nIndex);
     static LanguageType ImplCalcDigitLang(LanguageType eCurLang);
     static void         ImplInitDigitMode(OutputDevice& rOutDev, LanguageType 
eLang);
-    static OUString     convertDigits(std::u16string_view rString, sal_Int32 
nStt, sal_Int32 nLen, LanguageType eDigitLang);
 
     EditPaM             ReadText( SvStream& rInput, EditSelection aSel );
     EditPaM             ReadRTF( SvStream& rInput, EditSelection aSel );
diff --git a/editeng/source/editeng/impedit3.cxx 
b/editeng/source/editeng/impedit3.cxx
index 1298eb314b91..000d959c7b89 100644
--- a/editeng/source/editeng/impedit3.cxx
+++ b/editeng/source/editeng/impedit3.cxx
@@ -3906,8 +3906,9 @@ void ImpEditEngine::StripAllPortions( OutputDevice& 
rOutDev, tools::Rectangle aC
                                 const Color 
aTextLineColor(rOutDev.GetTextLineColor());
 
                                 // Unicode code points conversion according to 
ctl text numeral setting
-                                aText = convertDigits(aText, nTextStart, 
nTextLen,
-                                    ImplCalcDigitLang(aTmpFont.GetLanguage()));
+                                aText = LocalizeDigitsInString(aText,
+                                    ImplCalcDigitLang(aTmpFont.GetLanguage()),
+                                    nTextStart, nTextLen);
 
                                 // StripPortions() data callback
                                 const DrawPortionInfo aInfo(
@@ -4668,19 +4669,6 @@ LanguageType 
ImpEditEngine::ImplCalcDigitLang(LanguageType eCurLang)
     return eLang;
 }
 
-OUString ImpEditEngine::convertDigits(std::u16string_view rString, sal_Int32 
nStt, sal_Int32 nLen, LanguageType eDigitLang)
-{
-    OUStringBuffer aBuf(rString);
-    for (sal_Int32 nIdx = nStt, nEnd = nStt + nLen; nIdx < nEnd; ++nIdx)
-    {
-        sal_Unicode cChar = aBuf[nIdx];
-        if (cChar >= '0' && cChar <= '9')
-            // TODO: are the localized digit surrogates?
-            aBuf[nIdx] = static_cast<sal_Unicode>(GetLocalizedChar(cChar, 
eDigitLang));
-    }
-    return aBuf.makeStringAndClear();
-}
-
 // Either sets the digit mode at the output device
 void ImpEditEngine::ImplInitDigitMode(OutputDevice& rOutDev, LanguageType 
eCurLang)
 {
diff --git a/include/vcl/svapp.hxx b/include/vcl/svapp.hxx
index ef908f61931b..43cb3f9535b0 100644
--- a/include/vcl/svapp.hxx
+++ b/include/vcl/svapp.hxx
@@ -84,7 +84,13 @@ namespace com::sun::star::awt {
     class XWindow;
 }
 
-VCL_DLLPUBLIC sal_UCS4 GetLocalizedChar( sal_UCS4, LanguageType );
+VCL_DLLPUBLIC OUString LocalizeDigitsInString( const OUString&, LanguageType,
+                                               sal_Int32 nStart, sal_Int32& 
nLen );
+inline OUString LocalizeDigitsInString( const OUString& sStr, LanguageType 
eTextLanguage )
+{
+    sal_Int32 nLen = sStr.getLength();
+    return LocalizeDigitsInString(sStr, eTextLanguage, 0, nLen);
+}
 
 enum class SystemWindowFlags {
     NOAUTOMODE    = 0x0001,
diff --git a/vcl/source/gdi/sallayout.cxx b/vcl/source/gdi/sallayout.cxx
index aa9dca1d6e06..67b52fba8d39 100644
--- a/vcl/source/gdi/sallayout.cxx
+++ b/vcl/source/gdi/sallayout.cxx
@@ -47,6 +47,8 @@
 #define GF_FONTMASK  0xF0000000
 #define GF_FONTSHIFT 28
 
+namespace
+{
 
 sal_UCS4 GetLocalizedChar( sal_UCS4 nChar, LanguageType eLang )
 {
@@ -121,6 +123,51 @@ sal_UCS4 GetLocalizedChar( sal_UCS4 nChar, LanguageType 
eLang )
     return nChar;
 }
 
+}
+
+OUString LocalizeDigitsInString( const OUString& sStr, LanguageType 
eTextLanguage,
+                                 sal_Int32 nStart, sal_Int32& nLen )
+{
+    sal_Int32 nextPos, nEnd = nStart + nLen;
+
+    for (sal_Int32 i = nStart; i < nEnd; i = nextPos)
+    {
+        nextPos = i;
+        sal_uInt32 nChar = sStr.iterateCodePoints(&nextPos);
+
+        sal_UCS4 nReplacementChar = GetLocalizedChar(nChar, eTextLanguage);
+
+        // The first time we encounter a character that needs to change we’ll 
make a copy of the
+        // string so we can return a new modified one
+        if (nReplacementChar != nChar)
+        {
+            // The new string is very likely to have the same length as the 
old one
+            OUStringBuffer xTmpStr(sStr.getLength());
+            xTmpStr.append(sStr.subView(0, i));
+            xTmpStr.appendUtf32(nReplacementChar);
+
+            // Convert the remainder of the range
+            for (i = nextPos; i < nEnd;)
+            {
+                nReplacementChar = 
GetLocalizedChar(sStr.iterateCodePoints(&i), eTextLanguage);
+                xTmpStr.appendUtf32(nReplacementChar);
+            }
+
+            // Add the rest of the string outside of the range
+            xTmpStr.append(sStr.subView(nEnd));
+
+            // The length of the string might have changed if GetLocalizedChar 
converts between BMP
+            // and surrogate pairs
+            nLen += xTmpStr.getLength() - sStr.getLength();
+
+            return xTmpStr.makeStringAndClear();
+        }
+    }
+
+    // Nothing changed so we can just return the original string
+    return sStr;
+}
+
 SalLayout::SalLayout()
 :   mnMinCharPos( -1 ),
     mnEndCharPos( -1 ),
diff --git a/vcl/source/outdev/text.cxx b/vcl/source/outdev/text.cxx
index ba190b047cfe..4b1afddcfc14 100644
--- a/vcl/source/outdev/text.cxx
+++ b/vcl/source/outdev/text.cxx
@@ -994,29 +994,9 @@ vcl::text::ImplLayoutArgs 
OutputDevice::ImplPrepareLayoutArgs( OUString& rStr,
 
     if( meTextLanguage ) //TODO: (mnTextLayoutMode & 
vcl::text::ComplexTextLayoutFlags::SubstituteDigits)
     {
-        // disable character localization when no digits used
-        const sal_Unicode* pBase = rStr.getStr();
-        const sal_Unicode* pStr = pBase + nMinIndex;
-        const sal_Unicode* pEnd = pBase + nEndIndex;
-        std::optional<OUStringBuffer> xTmpStr;
-        for( ; pStr < pEnd; ++pStr )
-        {
-            // TODO: are there non-digit localizations?
-            if( (*pStr >= '0') && (*pStr <= '9') )
-            {
-                // translate characters to local preference
-                sal_UCS4 cChar = GetLocalizedChar( *pStr, meTextLanguage );
-                if( cChar != *pStr )
-                {
-                    if (!xTmpStr)
-                        xTmpStr = OUStringBuffer(rStr);
-                    // TODO: are the localized digit surrogates?
-                    (*xTmpStr)[pStr - pBase] = static_cast<sal_Unicode>(cChar);
-                }
-            }
-        }
-        if (xTmpStr)
-            rStr = (*xTmpStr).makeStringAndClear();
+        sal_Int32 nSubstringLen = nEndIndex - nMinIndex;
+        rStr = LocalizeDigitsInString(rStr, meTextLanguage, nMinIndex, 
nSubstringLen);
+        nEndIndex = nMinIndex + nSubstringLen;
     }
 
     // right align for RTL text, DRAWPOS_REVERSED, RTL window style

Reply via email to