From 04543c13fd79149c91b14649ee29f32605295ffa Mon Sep 17 00:00:00 2001 From: Dennis Francis Date: Wed, 19 Aug 2026 14:01:50 +0000 Subject: [PATCH] sw: docx: recode a list bullet from the old StarOffice symbol fonts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps to reproduce: 1) Open a document whose list bullet uses the StarBats font, such as sw/qa/extras/ooxmlexport/data/starbats-bullet.docx. 2) Save it as DOCX. 3) Open the saved file in an office suite that does not have that font. Both bullets draw as empty rectangles. The level goes out as character U+F095 in the font starbats. StarBats is one of the old StarOffice symbol fonts, and its characters sit in the Unicode private use area, so a reader without the font gets no glyph from any fallback either. Writer itself draws the bullet, because substituting the missing font installs the table that recodes the character to the place OpenSymbol keeps it at. Nothing did that on the way out, so the writer put the old font name and the private use character straight back. MSWordExportBase::SubstituteBullet already maps an OpenSymbol or a StarSymbol bullet to a character in a font that other suites have. Recode a StarBats or a StarMath character to OpenSymbol first and that mapping then covers it as well. U+F095 becomes U+2022, and from there U+F0B7 in the font Symbol, which is what other writers produce for a plain bullet. CreateFontToSubsFontConverter with the import flag names exactly those two fonts and no others, so a bullet that already came in as Symbol, Wingdings or Webdings keeps the character it had. The font test moves from the caller into SubstituteBullet, which keeps the new recode behind the m_bSubstituteBullets switch. The RTF writer clears that switch and goes on writing the OpenSymbol names its own reader expects. Add a test that saves such a document and reads the bullet level back out of the numbering part. Change-Id: Id487d6ec216cdf293b645e9749d1b3f81eb6347d Signed-off-by: Dennis Francis Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/9566 Tested-by: Jenkins CPCI Reviewed-by: Tomaž Vajngerl (cherry picked from commit dc625bf7ece1baf29201e2622c4b4c61750203d2) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/210418 Reviewed-by: Xisco Fauli Tested-by: Jenkins --- .../ooxmlexport/data/starbats-bullet.docx | Bin 0 -> 8041 bytes sw/qa/extras/ooxmlexport/ooxmlexport27.cxx | 15 ++++++++++ sw/source/filter/ww8/wrtw8num.cxx | 27 ++++++++++++++---- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 sw/qa/extras/ooxmlexport/data/starbats-bullet.docx diff --git a/sw/qa/extras/ooxmlexport/data/starbats-bullet.docx b/sw/qa/extras/ooxmlexport/data/starbats-bullet.docx new file mode 100644 index 0000000000000000000000000000000000000000..75cf8354366fedbb491773666bb44110ebea696b GIT binary patch literal 8041 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 On4l9teAEdp?*0$WPKi1I literal 0 HcmV?d00001 diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport27.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport27.cxx index cd63da17c196..f9905e7782ec 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport27.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport27.cxx @@ -106,6 +106,21 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf171527_flyInFramePr) assertXPath(pXmlDoc, "//w:body/w:p/w:pPr/w:framePr", 1); } +CPPUNIT_TEST_FIXTURE(Test, testStarBatsBulletKeepsAVisibleGlyph) +{ + createSwDoc("starbats-bullet.docx"); + + save(TestFilter::DOCX); + + xmlDocUniquePtr pXmlDoc = parseExport(u"word/numbering.xml"_ustr); + // The bullet comes out in a font that is widely available, at the character that font holds + // the bullet at. A reader without the old StarBats font still gets a bullet rather than an + // empty rectangle. + assertXPath(pXmlDoc, "/w:numbering/w:abstractNum[1]/w:lvl[1]/w:rPr/w:rFonts", "ascii", + u"Symbol"); + assertXPath(pXmlDoc, "/w:numbering/w:abstractNum[1]/w:lvl[1]/w:lvlText", "val", u"\xF0B7"); +} + CPPUNIT_TEST_FIXTURE(Test, testTdf171433_equation) { // given a document with formula diff --git a/sw/source/filter/ww8/wrtw8num.cxx b/sw/source/filter/ww8/wrtw8num.cxx index f0a0734dc0d8..f48dc1465d2a 100644 --- a/sw/source/filter/ww8/wrtw8num.cxx +++ b/sw/source/filter/ww8/wrtw8num.cxx @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -439,8 +440,7 @@ MSWordExportBase::GetNumberingLevelBulletStringAndFont(const SwNumFormat& rLevel OUString sFontName = pBulletFont->GetFamilyName(); FontFamily eFamily = pBulletFont->GetFamilyTypeMaybeAskConfig(); - if (IsOpenSymbol(sFontName)) - SubstituteBullet(sNumStr, eChrSet, sFontName); + SubstituteBullet(sNumStr, eChrSet, sFontName); if (sFontName.isEmpty()) sFontName = pBulletFont->GetFamilyName(); @@ -688,15 +688,32 @@ void MSWordExportBase::SubstituteBullet( OUString& rNumStr, { if (!m_bSubstituteBullets) return; - OUString sFontName = rFontName; // If Bullet char is "", don't change - if (rNumStr[0] != u'\0') + if (rNumStr[0] == u'\0') + return; + + OUString sFontName = rFontName; + + // StarBats and StarMath are the old StarOffice symbol fonts. Their characters sit in the + // Unicode private use area, so a reader without those fonts installed gets no glyph at all. + // Recode the character to the matching OpenSymbol one, and the mapping below then finds a + // font that other office suites do have. The IMPORT flag picks the direction that goes from + // one of those two fonts to OpenSymbol, and it covers no other font. + if (FontToSubsFontConverter hConverter + = CreateFontToSubsFontConverter(sFontName, FontToSubsFontFlags::IMPORT)) { rNumStr = rNumStr.replaceAt(0, 1, rtl::OUStringChar( - msfilter::util::bestFitOpenSymbolToMSFont(rNumStr[0], rChrSet, sFontName))); + ConvertFontToSubsFontChar(hConverter, rNumStr[0]))); + sFontName = GetFontToSubsFontName(hConverter); } + if (!IsOpenSymbol(sFontName)) + return; + + rNumStr = rNumStr.replaceAt(0, 1, rtl::OUStringChar( + msfilter::util::bestFitOpenSymbolToMSFont(rNumStr[0], rChrSet, sFontName))); + rFontName = sFontName; }