From f0600a8d88fb419e9b2ba542ea11ee0db73eb8dc Mon Sep 17 00:00:00 2001 From: Mike Kaganski Date: Sun, 20 Sep 2026 23:40:14 +0500 Subject: [PATCH] tdf#163272 sw: give a paragraph its own /Lang Write a paragraph in a language other than the document's, then File - Export As - Export as PDF with Tagged PDF on. The paragraph's element said nothing about the language, and the Span holding its text said it instead: /StructElem /S/Standard /K[6 0 R] /StructElem /S/Span /Lang (en-GB) /K[1 2 3 4 5 6 7 8] A run of text was compared against the language of the document, so every run of a paragraph that declares one of its own differed from it and opened a Span. The comparison is against the paragraph now, and a paragraph unlike the document names its language itself. A run unlike its paragraph still opens a Span and names its own, which it did not where it was the document's language the paragraph had departed from. A Span that carries anything else - a strikeout, a script, a character style - still stands, without the language it was repeating, and so does a Link. A paragraph with something anchored in it says nothing, as before: what hangs there is tagged underneath it and would take the paragraph's language for its own. testSpans reads four list items, three of them en-GB in a de-DE document. It asserted the shape the old rule made - for one of them it looked for a single Span and took the language off that, and for the rest it counted the Spans that rule opened. It reads the language off the paragraph now, asserts that the item at the document's own language names none, and where an item holds a hyperlink and a footnote it finds the two Links alone. Signed-off-by: Mike Kaganski Change-Id: I4082891666598793f59d1bf62c33e883ebdebb8f Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/13117 Tested-by: Jenkins CPCI Reviewed-by: Miklos Vajna (cherry picked from commit 0d2d9e656d87647f904f1a463ed9b879c2bfb10c) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211375 Tested-by: Jenkins Reviewed-by: Xisco Fauli --- sw/inc/EnhancedPDFExportHelper.hxx | 5 +- sw/qa/core/text/EnhancedPDFExportHelper.cxx | 43 +++++ sw/qa/core/text/data/paragraph-language.fodt | 36 +++++ .../core/text/EnhancedPDFExportHelper.cxx | 58 +++++-- vcl/qa/cppunit/pdfexport/pdfexport2.cxx | 148 +++++++----------- 5 files changed, 186 insertions(+), 104 deletions(-) create mode 100644 sw/qa/core/text/data/paragraph-language.fodt diff --git a/sw/inc/EnhancedPDFExportHelper.hxx b/sw/inc/EnhancedPDFExportHelper.hxx index 3d4c63199c6c..9fbbf8428e23 100644 --- a/sw/inc/EnhancedPDFExportHelper.hxx +++ b/sw/inc/EnhancedPDFExportHelper.hxx @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include "swrect.hxx" #include "swtypes.hxx" @@ -32,6 +33,7 @@ namespace vcl class PDFExtOutDevData; } class OutputDevice; +class SvxLanguageItem; class SwFrame; class SwLinePortion; class SwPageFrame; @@ -228,7 +230,8 @@ class SwEnhancedPDFExportHelper const SwPrintData& mrPrintData; - void EnhancedPDFExport(LanguageType const eLanguageDefault); + void EnhancedPDFExport(LanguageType const eLanguageDefault, + const TypedWhichId nLanguageWhich); /// Exports bibliography entry links. void ExportAuthorityEntryLinks(); diff --git a/sw/qa/core/text/EnhancedPDFExportHelper.cxx b/sw/qa/core/text/EnhancedPDFExportHelper.cxx index cf0045d9ce00..f95aa27e6a1f 100644 --- a/sw/qa/core/text/EnhancedPDFExportHelper.cxx +++ b/sw/qa/core/text/EnhancedPDFExportHelper.cxx @@ -355,6 +355,49 @@ CPPUNIT_TEST_FIXTURE(Test, testLayoutAttributeUnits) // the 1.5in column the cell sits in CPPUNIT_ASSERT_DOUBLES_EQUAL(108.0, getLength(pCell, "Width"_ostr), 0.01); } + +CPPUNIT_TEST_FIXTURE(Test, testParagraphLanguage) +{ + createSwDoc("paragraph-language.fodt"); + + uno::Sequence aFilterData{ comphelper::makePropertyValue(u"UseTaggedPDF"_ustr, true) }; + save(TestFilter::PDF_WRITER, + { comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) }); + + vcl::filter::PDFDocument aDocument; + maTempFile.CloseStream(); + CPPUNIT_ASSERT(aDocument.Read(*maTempFile.GetStream(StreamMode::READ))); + + // every structure element that names a language, sorted: they are emitted by object and + // not by document order + std::vector aLanguages; + for (const auto& rDocElement : aDocument.GetElements()) + { + auto pObject = dynamic_cast(rDocElement.get()); + if (!pObject) + continue; + + auto pType = dynamic_cast(pObject->Lookup("S"_ostr)); + auto pLang + = dynamic_cast(pObject->Lookup("Lang"_ostr)); + if (!pType || !pLang) + continue; + + aLanguages.push_back(pType->GetValue() + "=" + pLang->GetValue()); + } + std::sort(aLanguages.begin(), aLanguages.end()); + + CPPUNIT_ASSERT_EQUAL(size_t(5), aLanguages.size()); + // the German words in an English paragraph, and the text of the paragraph holding a frame, + // which says nothing itself because what is anchored there hangs under it + CPPUNIT_ASSERT_EQUAL("Span=de-DE"_ostr, aLanguages[0]); + CPPUNIT_ASSERT_EQUAL("Span=de-DE"_ostr, aLanguages[1]); + // Without the fix this was de-DE: the run differs from its paragraph, not from the document + CPPUNIT_ASSERT_EQUAL("Span=en-US"_ostr, aLanguages[2]); + // and without it neither German paragraph named a language at all + CPPUNIT_ASSERT_EQUAL("Standard=de-DE"_ostr, aLanguages[3]); + CPPUNIT_ASSERT_EQUAL("Standard=de-DE"_ostr, aLanguages[4]); +} } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/qa/core/text/data/paragraph-language.fodt b/sw/qa/core/text/data/paragraph-language.fodt new file mode 100644 index 000000000000..ad7c53e52e12 --- /dev/null +++ b/sw/qa/core/text/data/paragraph-language.fodt @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A paragraph in the language of the document. + Ein Absatz, der ganz und gar deutsch ist. + English with ein paar deutsche Worte in it. + Ein deutscher Absatz mit a few English words darin. + Ein deutscher Absatz mit einem Rahmen.Text inside the frame. + + + diff --git a/sw/source/core/text/EnhancedPDFExportHelper.cxx b/sw/source/core/text/EnhancedPDFExportHelper.cxx index 4a8264bae5f4..e2d928ba5be7 100644 --- a/sw/source/core/text/EnhancedPDFExportHelper.cxx +++ b/sw/source/core/text/EnhancedPDFExportHelper.cxx @@ -159,6 +159,8 @@ struct SwEnhancedPDFState std::unordered_map m_NodeTagIdMap; LanguageType m_eLanguageDefault; + // the language item the application's script uses, which the default was read with + TypedWhichId m_nLanguageWhich; struct Span { @@ -192,8 +194,10 @@ struct SwEnhancedPDFState // left open for a following portion, innermost last std::vector m_DeferredTags; - SwEnhancedPDFState(LanguageType const eLanguageDefault) + SwEnhancedPDFState(LanguageType const eLanguageDefault, + const TypedWhichId nLanguageWhich) : m_eLanguageDefault(eLanguageDefault) + , m_nLanguageWhich(nLanguageWhich) { } }; @@ -456,6 +460,25 @@ const SwTextNode* lcl_JumpedToNode(const SwEditShell& rSh, const SwPosition& rBe return rPoint == rBeforeJump ? nullptr : rPoint.GetNode().GetTextNode(); } +// the language a paragraph declares for itself, as against the one a run of text sets +LanguageType lcl_GetParagraphLanguage(const SwTextFrame& rFrame, + const TypedWhichId nWhich) +{ + // what is anchored here is tagged inside the paragraph's element and would inherit a + // language put there; the node, not the frame, because a split paragraph is one element + if (!rFrame.GetTextNodeFirst()->GetAnchoredFlys().empty()) + return LANGUAGE_DONTKNOW; + return rFrame.GetTextNodeForParaProps()->GetSwAttrSet().Get(nWhich).GetLanguage(); +} + +// the language a run of text is measured against +LanguageType lcl_GetEnclosingLanguage(const SwTextPaintInfo& rInf, const SwEnhancedPDFState& rState) +{ + const LanguageType nParagraph( + lcl_GetParagraphLanguage(*rInf.GetTextFrame(), rState.m_nLanguageWhich)); + return LANGUAGE_DONTKNOW == nParagraph ? rState.m_eLanguageDefault : nParagraph; +} + // the StarMath source of the formula a frame holds, empty for anything else OUString lcl_GetFormulaSource(const SwFlyFrame& rFly) { @@ -955,6 +978,7 @@ void SwTaggedPDFHelper::SetAttributes(vcl::pdf::StructElement eType) bool bBox = false; bool bRowSpan = false; bool bAltText = false; + bool bLanguage = false; // Check which attributes to set: @@ -1019,7 +1043,8 @@ void SwTaggedPDFHelper::SetAttributes(vcl::pdf::StructElement eType) bStartIndent = bEndIndent = bTextIndent = - bTextAlign = true; + bTextAlign = + bLanguage = true; break; case vcl::pdf::StructElement::Formula: @@ -1134,6 +1159,18 @@ void SwTaggedPDFHelper::SetAttributes(vcl::pdf::StructElement eType) mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::pdf::PDFWriter::TextIndent, nVal ); } + if (bLanguage && pFrame->IsTextFrame()) + { + const SwEnhancedPDFState& rState(*mpPDFExtOutDevData->GetSwPDFState()); + const LanguageType nLanguage(lcl_GetParagraphLanguage( + static_cast(*pFrame), rState.m_nLanguageWhich)); + if (LANGUAGE_DONTKNOW != nLanguage && rState.m_eLanguageDefault != nLanguage) + { + mpPDFExtOutDevData->SetStructureAttributeNumerical( + vcl::pdf::PDFWriter::Language, static_cast(nLanguage)); + } + } + if ( bTextAlign ) { OSL_ENSURE( pFrame->IsTextFrame(), "Frame type <-> tag attribute mismatch" ); @@ -1377,9 +1414,10 @@ void SwTaggedPDFHelper::SetAttributes(vcl::pdf::StructElement eType) { const LanguageType nCurrentLanguage = rInf.GetFont()->GetLanguage(); - const LanguageType nDefaultLang(mpPDFExtOutDevData->GetSwPDFState()->m_eLanguageDefault); + const LanguageType nEnclosingLanguage( + lcl_GetEnclosingLanguage(rInf, *mpPDFExtOutDevData->GetSwPDFState())); - if ( nDefaultLang != nCurrentLanguage ) + if (nEnclosingLanguage != nCurrentLanguage) mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::pdf::PDFWriter::Language, static_cast(nCurrentLanguage) ); } @@ -2331,7 +2369,8 @@ void SwTaggedPDFHelper::BeginInlineStructureElements() { const LanguageType nCurrentLanguage = rInf.GetFont()->GetLanguage(); const SwFontScript nFont = rInf.GetFont()->GetActual(); - const LanguageType nDefaultLang(mpPDFExtOutDevData->GetSwPDFState()->m_eLanguageDefault); + const LanguageType nEnclosingLanguage( + lcl_GetEnclosingLanguage(rInf, *mpPDFExtOutDevData->GetSwPDFState())); if ( LINESTYLE_NONE != rInf.GetFont()->GetUnderline() || LINESTYLE_NONE != rInf.GetFont()->GetOverline() || @@ -2339,7 +2378,7 @@ void SwTaggedPDFHelper::BeginInlineStructureElements() FontEmphasisMark::NONE != rInf.GetFont()->GetEmphasisMark() || 0 != rInf.GetFont()->GetEscapement() || SwFontScript::Latin != nFont || - nCurrentLanguage != nDefaultLang || + nCurrentLanguage != nEnclosingLanguage || !sStyleName.isEmpty()) { nPDFType = sal_uInt16(vcl::pdf::StructElement::Span); @@ -2519,7 +2558,7 @@ SwEnhancedPDFExportHelper::SwEnhancedPDFExportHelper( SwEditShell& rSh, const SvxLanguageItem& rLangItem = mrSh.GetDoc()->GetDefault( nLangRes ); auto const eLanguageDefault = rLangItem.GetLanguage(); - EnhancedPDFExport(eLanguageDefault); + EnhancedPDFExport(eLanguageDefault, nLangRes); } SwEnhancedPDFExportHelper::~SwEnhancedPDFExportHelper() @@ -2582,7 +2621,8 @@ sal_Int32 SwEnhancedPDFExportHelper::CreateDestination(const SwPageFrame* pCurrP return nDestId; } -void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDefault) +void SwEnhancedPDFExportHelper::EnhancedPDFExport( + LanguageType const eLanguageDefault, const TypedWhichId nLanguageWhich) { vcl::PDFExtOutDevData* pPDFExtOutDevData = dynamic_cast< vcl::PDFExtOutDevData*>( mrOut.GetExtOutDevData() ); @@ -2613,7 +2653,7 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe if ( !mbEditEngineOnly ) { assert(pPDFExtOutDevData->GetSwPDFState() == nullptr); - pPDFExtOutDevData->SetSwPDFState(new SwEnhancedPDFState(eLanguageDefault)); + pPDFExtOutDevData->SetSwPDFState(new SwEnhancedPDFState(eLanguageDefault, nLanguageWhich)); // POSTITS diff --git a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx index e28737ae630c..3386486941a4 100644 --- a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx +++ b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx @@ -3178,25 +3178,18 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testSpans) auto pKids10010 = dynamic_cast(pObject10010->Lookup("K"_ostr)); CPPUNIT_ASSERT(pKids10010); - // assume there are no MCID ref at this level + // the paragraph is wholly in one language, so it names it itself + auto pLang10010 = dynamic_cast( + pObject10010->Lookup("Lang"_ostr)); + CPPUNIT_ASSERT(pLang10010); + CPPUNIT_ASSERT_EQUAL("en-GB"_ostr, pLang10010->GetValue()); + // and its content needs no span between auto vKids10010 = pKids10010->GetElements(); - // only one span - CPPUNIT_ASSERT_EQUAL(size_t(1), vKids10010.size()); - - auto pRefKid100100 = dynamic_cast(vKids10010[0]); - CPPUNIT_ASSERT(pRefKid100100); - auto pObject100100 = pRefKid100100->LookupObject(); - CPPUNIT_ASSERT(pObject100100); - auto pType100100 = dynamic_cast( - pObject100100->Lookup("Type"_ostr)); - CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType100100->GetValue()); - auto pS100100 - = dynamic_cast(pObject100100->Lookup("S"_ostr)); - CPPUNIT_ASSERT_EQUAL("Span"_ostr, pS100100->GetValue()); - // this span exists because of lang - auto pLang100100 = dynamic_cast( - pObject100100->Lookup("Lang"_ostr)); - CPPUNIT_ASSERT_EQUAL("en-GB"_ostr, pLang100100->GetValue()); + CPPUNIT_ASSERT_EQUAL(size_t(8), vKids10010.size()); + for (const auto pKid : vKids10010) + { + CPPUNIT_ASSERT(!dynamic_cast(pKid)); + } auto pRefKid101 = dynamic_cast(vKids10[1]); CPPUNIT_ASSERT(pRefKid101); @@ -3258,6 +3251,8 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testSpans) auto pKids10110 = dynamic_cast(pObject10110->Lookup("K"_ostr)); CPPUNIT_ASSERT(pKids10110); + // this paragraph is in the language of the document, so it names none + CPPUNIT_ASSERT(!pObject10110->Lookup("Lang"_ostr)); auto vKids10110 = pKids10110->GetElements(); // only MCIDs, no span for (size_t i = 0; i < vKids10110.size(); ++i) @@ -3326,89 +3321,48 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testSpans) auto pKids10210 = dynamic_cast(pObject10210->Lookup("K"_ostr)); CPPUNIT_ASSERT(pKids10210); - // assume there are no MCID ref at this level auto vKids10210 = pKids10210->GetElements(); - // 2 span and a hyperlink - CPPUNIT_ASSERT_EQUAL(size_t(3), vKids10210.size()); + CPPUNIT_ASSERT_EQUAL(size_t(8), vKids10210.size()); + // this paragraph too is wholly in one language and names it itself + auto pLang10210 = dynamic_cast( + pObject10210->Lookup("Lang"_ostr)); + CPPUNIT_ASSERT(pLang10210); + CPPUNIT_ASSERT_EQUAL("en-GB"_ostr, pLang10210->GetValue()); - auto pRefKid102100 = dynamic_cast(vKids10210[0]); - CPPUNIT_ASSERT(pRefKid102100); - auto pObject102100 = pRefKid102100->LookupObject(); - CPPUNIT_ASSERT(pObject102100); - auto pType102100 = dynamic_cast( - pObject102100->Lookup("Type"_ostr)); - CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType102100->GetValue()); - auto pS102100 - = dynamic_cast(pObject102100->Lookup("S"_ostr)); - CPPUNIT_ASSERT_EQUAL("Span"_ostr, pS102100->GetValue()); - auto pKids102100 - = dynamic_cast(pObject102100->Lookup("K"_ostr)); - CPPUNIT_ASSERT(pKids102100); - auto vKids102100 = pKids102100->GetElements(); - for (size_t i = 0; i < vKids102100.size(); ++i) + // its own text needs no span, so the only elements left are the two links, and + // neither repeats the language + std::vector aLinks; + for (const auto pKid : vKids10210) { - auto pKid = dynamic_cast(vKids102100[i]); - CPPUNIT_ASSERT(!pKid); + auto pRef = dynamic_cast(pKid); + if (!pRef) + continue; + auto pLinked = pRef->LookupObject(); + CPPUNIT_ASSERT(pLinked); + CPPUNIT_ASSERT_EQUAL( + "StructElem"_ostr, + dynamic_cast(pLinked->Lookup("Type"_ostr)) + ->GetValue()); + CPPUNIT_ASSERT_EQUAL("Link"_ostr, dynamic_cast( + pLinked->Lookup("S"_ostr)) + ->GetValue()); + CPPUNIT_ASSERT(!pLinked->Lookup("Lang"_ostr)); + aLinks.push_back(pLinked); } + CPPUNIT_ASSERT_EQUAL(size_t(2), aLinks.size()); - auto pRefKid102101 = dynamic_cast(vKids10210[1]); - CPPUNIT_ASSERT(pRefKid102101); - auto pObject102101 = pRefKid102101->LookupObject(); - CPPUNIT_ASSERT(pObject102101); - auto pType102101 = dynamic_cast( - pObject102101->Lookup("Type"_ostr)); - CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType102101->GetValue()); - auto pS102101 - = dynamic_cast(pObject102101->Lookup("S"_ostr)); - CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS102101->GetValue()); - auto pKids102101 - = dynamic_cast(pObject102101->Lookup("K"_ostr)); - CPPUNIT_ASSERT(pKids102101); - auto vKids102101 = pKids102101->GetElements(); - int nRef(0); - for (size_t i = 0; i < vKids102101.size(); ++i) + // the hyperlink comes before the footnote in the text, so it is the first + // of the two + auto pKidsHyperlink + = dynamic_cast(aLinks[0]->Lookup("K"_ostr)); + CPPUNIT_ASSERT(pKidsHyperlink); + int nAnnots(0); + for (const auto pKid : pKidsHyperlink->GetElements()) { - auto pKid = dynamic_cast(vKids102101[i]); - if (pKid) - { - ++nRef; // annotation - } + if (dynamic_cast(pKid)) + ++nAnnots; } - CPPUNIT_ASSERT_EQUAL(1, nRef); - - auto pRefKid102102 = dynamic_cast(vKids10210[2]); - CPPUNIT_ASSERT(pRefKid102102); - auto pObject102102 = pRefKid102102->LookupObject(); - CPPUNIT_ASSERT(pObject102102); - auto pType102102 = dynamic_cast( - pObject102102->Lookup("Type"_ostr)); - CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType102102->GetValue()); - auto pS102102 - = dynamic_cast(pObject102102->Lookup("S"_ostr)); - CPPUNIT_ASSERT_EQUAL("Span"_ostr, pS102102->GetValue()); - auto pKids102102 - = dynamic_cast(pObject102102->Lookup("K"_ostr)); - CPPUNIT_ASSERT(pKids102102); - auto vKids102102 = pKids102102->GetElements(); - // there is a footnote - int nFtn(0); - for (size_t i = 0; i < vKids102102.size(); ++i) - { - auto pKid = dynamic_cast(vKids102102[i]); - if (pKid) - { - auto pObject = pKid->LookupObject(); - CPPUNIT_ASSERT(pObject); - auto pType = dynamic_cast( - pObject->Lookup("Type"_ostr)); - CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pType->GetValue()); - auto pS - = dynamic_cast(pObject->Lookup("S"_ostr)); - CPPUNIT_ASSERT_EQUAL("Link"_ostr, pS->GetValue()); - ++nFtn; - } - } - CPPUNIT_ASSERT_EQUAL(1, nFtn); + CPPUNIT_ASSERT_EQUAL(1, nAnnots); auto pRefKid103 = dynamic_cast(vKids10[3]); CPPUNIT_ASSERT(pRefKid103); @@ -3467,6 +3421,10 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testSpans) auto pS10310 = dynamic_cast(pObject10310->Lookup("S"_ostr)); CPPUNIT_ASSERT_EQUAL("Standard"_ostr, pS10310->GetValue()); + auto pLang10310 = dynamic_cast( + pObject10310->Lookup("Lang"_ostr)); + CPPUNIT_ASSERT(pLang10310); + CPPUNIT_ASSERT_EQUAL("en-GB"_ostr, pLang10310->GetValue()); auto pKids10310 = dynamic_cast(pObject10310->Lookup("K"_ostr)); CPPUNIT_ASSERT(pKids10310); @@ -3498,6 +3456,8 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testSpans) dynamic_cast( pDictA103101->LookupElement("TextDecorationType"_ostr)) ->GetValue()); + // the span stands for the strikeout, and no longer repeats the language + CPPUNIT_ASSERT(!pObject103101->Lookup("Lang"_ostr)); // now the footnote container - following the list auto pRefKid11 = dynamic_cast(vKids1[1]);