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