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]);