diff --git a/sw/source/core/text/EnhancedPDFExportHelper.cxx b/sw/source/core/text/EnhancedPDFExportHelper.cxx index d65f46064005..c5cb29c41084 100644 --- a/sw/source/core/text/EnhancedPDFExportHelper.cxx +++ b/sw/source/core/text/EnhancedPDFExportHelper.cxx @@ -2061,6 +2061,10 @@ void SwTaggedPDFHelper::BeginInlineStructureElements() case PortionType::Lay : case PortionType::Text : case PortionType::Para : + // index, reference and metadata marks + case PortionType::Tox: + case PortionType::Ref: + case PortionType::Meta: { // Check for Link: if( pInetFormatAttr ) diff --git a/vcl/qa/cppunit/pdfexport/data/IndexMarkWithSpan.fodt b/vcl/qa/cppunit/pdfexport/data/IndexMarkWithSpan.fodt new file mode 100644 index 000000000000..faa09408c320 --- /dev/null +++ b/vcl/qa/cppunit/pdfexport/data/IndexMarkWithSpan.fodt @@ -0,0 +1,29 @@ + + + Index mark over a language change + + + + + + + + + + + + + + + + + + + + + + + alphadeutschgamma + + + diff --git a/vcl/qa/cppunit/pdfexport/data/LinkWithIndexMark.fodt b/vcl/qa/cppunit/pdfexport/data/LinkWithIndexMark.fodt new file mode 100644 index 000000000000..1380adc4103a --- /dev/null +++ b/vcl/qa/cppunit/pdfexport/data/LinkWithIndexMark.fodt @@ -0,0 +1,29 @@ + + + Link with an alphabetical index mark + + + + + + + + + + + + + + + + + + + + + + + alphabetagamma + + + diff --git a/vcl/qa/cppunit/pdfexport/data/LinkWithMeta.fodt b/vcl/qa/cppunit/pdfexport/data/LinkWithMeta.fodt new file mode 100644 index 000000000000..671aacf43fe1 --- /dev/null +++ b/vcl/qa/cppunit/pdfexport/data/LinkWithMeta.fodt @@ -0,0 +1,29 @@ + + + Link with an RDF metadata field + + + + + + + + + + + + + + + + + + + + + + + alphabetagamma + + + diff --git a/vcl/qa/cppunit/pdfexport/data/LinkWithRefMark.fodt b/vcl/qa/cppunit/pdfexport/data/LinkWithRefMark.fodt new file mode 100644 index 000000000000..6e2a2c83cfbf --- /dev/null +++ b/vcl/qa/cppunit/pdfexport/data/LinkWithRefMark.fodt @@ -0,0 +1,29 @@ + + + Link with a reference mark + + + + + + + + + + + + + + + + + + + + + + + alphabetagamma + + + diff --git a/vcl/qa/cppunit/pdfexport/pdfexport.cxx b/vcl/qa/cppunit/pdfexport/pdfexport.cxx index 1aba931d24f6..047385af1762 100644 --- a/vcl/qa/cppunit/pdfexport/pdfexport.cxx +++ b/vcl/qa/cppunit/pdfexport/pdfexport.cxx @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -2928,6 +2929,151 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest, testTdf157816) CPPUNIT_ASSERT_EQUAL(static_cast(5), pAnnots->GetElements().size()); } +// ISO 14289-1 7.18.5: every link annotation is an OBJR kid of a Link structure element +void AssertLinkAnnotsAreTagged(vcl::filter::PDFDocument& rDocument, const size_t nLinks, + const size_t nAnnots) +{ + std::vector aPages = rDocument.GetPages(); + CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size()); + auto pAnnots = dynamic_cast(aPages[0]->Lookup("Annots"_ostr)); + CPPUNIT_ASSERT(pAnnots); + CPPUNIT_ASSERT_EQUAL(nAnnots, pAnnots->GetElements().size()); + + // what the Link structure elements claim as their annotations + size_t nLinkSE(0); + std::set aKidAnnots; + for (const auto& rDocElement : rDocument.GetElements()) + { + auto pObject = dynamic_cast(rDocElement.get()); + if (!pObject) + continue; + auto pType = dynamic_cast(pObject->Lookup("Type"_ostr)); + auto pS = dynamic_cast(pObject->Lookup("S"_ostr)); + if (!pType || pType->GetValue() != "StructElem" || !pS || pS->GetValue() != "Link") + continue; + ++nLinkSE; + auto pKids = dynamic_cast(pObject->Lookup("K"_ostr)); + CPPUNIT_ASSERT(pKids); + for (const auto pElement : pKids->GetElements()) + { + auto pObjR = dynamic_cast(pElement); + if (!pObjR) + continue; + auto pOType + = dynamic_cast(pObjR->LookupElement("Type"_ostr)); + CPPUNIT_ASSERT(pOType); + // a kid on another page is an MCR dictionary, not an OBJR + if (pOType->GetValue() == "MCR") + continue; + CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue()); + auto pRef + = dynamic_cast(pObjR->LookupElement("Obj"_ostr)); + CPPUNIT_ASSERT(pRef); + auto pKidAnnot = pRef->LookupObject(); + CPPUNIT_ASSERT_MESSAGE("an OBJR must resolve to an annotation", pKidAnnot); + CPPUNIT_ASSERT_MESSAGE("an annotation is claimed by more than one OBJR", + aKidAnnots.insert(pKidAnnot).second); + } + } + CPPUNIT_ASSERT_EQUAL(nLinks, nLinkSE); + CPPUNIT_ASSERT_EQUAL(nAnnots, aKidAnnots.size()); + + for (const auto pElement : pAnnots->GetElements()) + { + auto pAnnotRef = dynamic_cast(pElement); + CPPUNIT_ASSERT(pAnnotRef); + auto pAnnot = pAnnotRef->LookupObject(); + CPPUNIT_ASSERT(pAnnot); + CPPUNIT_ASSERT_MESSAGE("every annotation needs a StructParent", + pAnnot->Lookup("StructParent"_ostr)); + CPPUNIT_ASSERT_MESSAGE("every annotation needs an OBJR in a Link", + aKidAnnots.contains(pAnnot)); + } +} + +CPPUNIT_TEST_FIXTURE(PdfExportTest, testTdf167121) +{ + loadFromFile(u"LinkWithIndexMark.fodt"); + + uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) }; + save(TestFilter::PDF_WRITER, + { comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) }); + + SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ); + vcl::filter::PDFDocument aDocument; + CPPUNIT_ASSERT(aDocument.Read(aStream)); + + // an index mark over one of three links must not orphan that link's annotation + AssertLinkAnnotsAreTagged(aDocument, /*nLinks=*/3, /*nAnnots=*/3); +} + +CPPUNIT_TEST_FIXTURE(PdfExportTest, testLinkWithRefMark) +{ + loadFromFile(u"LinkWithRefMark.fodt"); + + uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) }; + save(TestFilter::PDF_WRITER, + { comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) }); + + SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ); + vcl::filter::PDFDocument aDocument; + CPPUNIT_ASSERT(aDocument.Read(aStream)); + + // a reference mark shades a run the way an index mark does, and lost the tag with it + AssertLinkAnnotsAreTagged(aDocument, /*nLinks=*/3, /*nAnnots=*/3); +} + +CPPUNIT_TEST_FIXTURE(PdfExportTest, testLinkWithMeta) +{ + loadFromFile(u"LinkWithMeta.fodt"); + + uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) }; + save(TestFilter::PDF_WRITER, + { comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) }); + + SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ); + vcl::filter::PDFDocument aDocument; + CPPUNIT_ASSERT(aDocument.Read(aStream)); + + // an RDF metadata field shades a run the same way, and lost the tag with it + AssertLinkAnnotsAreTagged(aDocument, /*nLinks=*/3, /*nAnnots=*/3); +} + +CPPUNIT_TEST_FIXTURE(PdfExportTest, testIndexMarkWithSpan) +{ + loadFromFile(u"IndexMarkWithSpan.fodt"); + + uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) }; + save(TestFilter::PDF_WRITER, + { comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) }); + + SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ); + vcl::filter::PDFDocument aDocument; + CPPUNIT_ASSERT(aDocument.Read(aStream)); + + // the tag a marked run needs is not always a Link: this one carries the language + vcl::filter::PDFObjectElement* pSpanSE(nullptr); + for (const auto& rDocElement : aDocument.GetElements()) + { + auto pObject = dynamic_cast(rDocElement.get()); + if (!pObject) + continue; + auto pType = dynamic_cast(pObject->Lookup("Type"_ostr)); + auto pS = dynamic_cast(pObject->Lookup("S"_ostr)); + if (pType && pType->GetValue() == "StructElem" && pS && pS->GetValue() == "Span") + { + CPPUNIT_ASSERT(!pSpanSE); + pSpanSE = pObject; + } + } + CPPUNIT_ASSERT(pSpanSE); + + auto pSpanLang + = dynamic_cast(pSpanSE->Lookup("Lang"_ostr)); + CPPUNIT_ASSERT(pSpanLang); + CPPUNIT_ASSERT_EQUAL("de-DE"_ostr, pSpanLang->GetValue()); +} + CPPUNIT_TEST_FIXTURE(PdfExportTest, testTdf157816Link) { // Enable PDF/UA