diff --git a/include/vcl/pdfextoutdevdata.hxx b/include/vcl/pdfextoutdevdata.hxx
index fe332670c87a..2c7da2ae15bd 100644
--- a/include/vcl/pdfextoutdevdata.hxx
+++ b/include/vcl/pdfextoutdevdata.hxx
@@ -296,6 +296,8 @@ public:
the dest the link shall point to
*/
void SetLinkDest( sal_Int32 nLinkId, sal_Int32 nDestId );
+ /** Name the structure element a destination points at, for ISO 14289-2 8.8 */
+ void SetDestStructureElement(sal_Int32 nDestId, sal_Int32 nStructElementId);
/** Set the URL for a link
will change a dest type link to a URL type link if necessary
@param nLinkId
diff --git a/include/vcl/pdfwriter.hxx b/include/vcl/pdfwriter.hxx
index 800b4eba3d48..a6d4bebc2cf8 100644
--- a/include/vcl/pdfwriter.hxx
+++ b/include/vcl/pdfwriter.hxx
@@ -1031,6 +1031,8 @@ public:
the dest the link shall point to
*/
void SetLinkDest( sal_Int32 nLinkId, sal_Int32 nDestId );
+ /** Name the structure element a destination points at, for ISO 14289-2 8.8 */
+ void SetDestStructureElement(sal_Int32 nDestId, sal_Int32 nStructElementId);
/** Set the URL for a link
will change a dest type link to a URL type link if necessary
@param nLinkId
diff --git a/sw/CppunitTest_sw_core_text.mk b/sw/CppunitTest_sw_core_text.mk
index 38cd6882b0ee..78d7e850cf3b 100644
--- a/sw/CppunitTest_sw_core_text.mk
+++ b/sw/CppunitTest_sw_core_text.mk
@@ -14,6 +14,7 @@ $(eval $(call gb_CppunitTest_CppunitTest,sw_core_text))
$(eval $(call gb_CppunitTest_use_common_precompiled_header,sw_core_text))
$(eval $(call gb_CppunitTest_add_exception_objects,sw_core_text, \
+ sw/qa/core/text/EnhancedPDFExportHelper \
sw/qa/core/text/frmform \
sw/qa/core/text/itratr \
sw/qa/core/text/itrpaint \
diff --git a/sw/inc/EnhancedPDFExportHelper.hxx b/sw/inc/EnhancedPDFExportHelper.hxx
index a71cea0c8ec6..3d4c63199c6c 100644
--- a/sw/inc/EnhancedPDFExportHelper.hxx
+++ b/sw/inc/EnhancedPDFExportHelper.hxx
@@ -41,6 +41,7 @@ class SwEditShell;
class StringRangeEnumerator;
class SwTextAttr;
class SwTextNode;
+struct SwPosition;
class SwTextPaintInfo;
class SwTextFrame;
@@ -232,6 +233,13 @@ class SwEnhancedPDFExportHelper
/// Exports bibliography entry links.
void ExportAuthorityEntryLinks();
+ /// Creates a destination, and remembers the node it points at, to name it once tagging is done.
+ /// The target is either the node the cursor jumped to since rBeforeJump, or one known outright.
+ sal_Int32 CreateDestination(const SwPageFrame* pCurrPage, const SwRect& rRect,
+ sal_Int32 nPageNum, const SwPosition& rBeforeJump);
+ sal_Int32 CreateDestination(const SwPageFrame* pCurrPage, const SwRect& rRect,
+ sal_Int32 nPageNum, const SwTextNode* pTarget);
+
sal_Int32 CalcOutputPageNum( const SwRect& rRect ) const;
std::vector< sal_Int32 > CalcOutputPageNums( const SwRect& rRect ) const;
diff --git a/sw/qa/core/text/EnhancedPDFExportHelper.cxx b/sw/qa/core/text/EnhancedPDFExportHelper.cxx
new file mode 100644
index 000000000000..2442be2039cb
--- /dev/null
+++ b/sw/qa/core/text/EnhancedPDFExportHelper.cxx
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
+/*
+ * Copyright the LibreOffice contributors.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+namespace
+{
+/// Covers sw/source/core/text/EnhancedPDFExportHelper.cxx fixes.
+class Test : public SwModelTestBase
+{
+public:
+ Test()
+ : SwModelTestBase(u"/sw/qa/core/text/data/"_ustr)
+ {
+ }
+};
+
+CPPUNIT_TEST_FIXTURE(Test, testTdf171022)
+{
+ createSwDoc("structure-destinations.fodt");
+
+ uno::Sequence aFilterData{ comphelper::makePropertyValue(u"UseTaggedPDF"_ustr, true),
+ comphelper::makePropertyValue(u"SelectPdfVersion"_ustr,
+ sal_Int32(20)) };
+ save(TestFilter::PDF_WRITER,
+ { comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) });
+
+ vcl::filter::PDFDocument aDocument;
+ CPPUNIT_ASSERT(aDocument.Read(*maTempFile.GetStream(StreamMode::READ)));
+
+ // the outline item, the footnote's two links, the shape's link and the citation's
+ std::vector aOutlineTypes;
+ std::vector aLinkTypes;
+ for (const auto& rDocElement : aDocument.GetElements())
+ {
+ auto pObject = dynamic_cast(rDocElement.get());
+ if (!pObject)
+ continue;
+ auto pAction = dynamic_cast(pObject->Lookup("A"_ostr));
+ if (!pAction)
+ continue;
+ auto pType = dynamic_cast(pAction->LookupElement("S"_ostr));
+ if (!pType || pType->GetValue() != "GoTo")
+ continue;
+ // ISO 14289-2 8.8: the destination inside the document is a structure destination, so
+ // it names the element and not only the page. veraPDF passes a structure destination
+ // that points at nothing, so check what it points at.
+ auto pPage = dynamic_cast(pAction->LookupElement("D"_ostr));
+ auto pStructure
+ = dynamic_cast(pAction->LookupElement("SD"_ostr));
+ CPPUNIT_ASSERT(pPage);
+ CPPUNIT_ASSERT(pStructure);
+ CPPUNIT_ASSERT(!pPage->GetElements().empty());
+ CPPUNIT_ASSERT(!pStructure->GetElements().empty());
+
+ auto pPageRef = dynamic_cast(pPage->GetElements()[0]);
+ auto pStructureRef
+ = dynamic_cast(pStructure->GetElements()[0]);
+ CPPUNIT_ASSERT(pPageRef);
+ CPPUNIT_ASSERT(pStructureRef);
+
+ CPPUNIT_ASSERT(pPageRef->LookupObject());
+ CPPUNIT_ASSERT(pStructureRef->LookupObject());
+ auto pPageType = dynamic_cast(
+ pPageRef->LookupObject()->Lookup("Type"_ostr));
+ auto pStructureType = dynamic_cast(
+ pStructureRef->LookupObject()->Lookup("Type"_ostr));
+ CPPUNIT_ASSERT(pPageType);
+ CPPUNIT_ASSERT(pStructureType);
+ CPPUNIT_ASSERT_EQUAL("Page"_ostr, pPageType->GetValue());
+ CPPUNIT_ASSERT_EQUAL("StructElem"_ostr, pStructureType->GetValue());
+
+ // which element it is, not merely that it is one: a jump that leaves the cursor behind
+ // names the paragraph the link sits in, and that reads as a plausible structure
+ // destination to a validator
+ auto pStructureS = dynamic_cast(
+ pStructureRef->LookupObject()->Lookup("S"_ostr));
+ CPPUNIT_ASSERT(pStructureS);
+ // an outline item has a Title, a link annotation does not
+ if (pObject->Lookup("Title"_ostr))
+ aOutlineTypes.push_back(pStructureS->GetValue());
+ else
+ aLinkTypes.push_back(pStructureS->GetValue());
+ }
+
+ // the outline item names the heading it was made from
+ CPPUNIT_ASSERT_EQUAL(size_t(1), aOutlineTypes.size());
+ CPPUNIT_ASSERT_EQUAL("H1"_ostr, aOutlineTypes[0]);
+
+ // the shape's bookmark sits at the document's first position on purpose: the cursor is
+ // already there, and nothing moves
+ std::sort(aLinkTypes.begin(), aLinkTypes.end());
+ CPPUNIT_ASSERT_EQUAL(size_t(4), aLinkTypes.size());
+ CPPUNIT_ASSERT_EQUAL("Bibliography 1"_ostr, aLinkTypes[0]);
+ CPPUNIT_ASSERT_EQUAL("Footnote"_ostr, aLinkTypes[1]);
+ CPPUNIT_ASSERT_EQUAL("H1"_ostr, aLinkTypes[2]);
+ CPPUNIT_ASSERT_EQUAL("Standard"_ostr, aLinkTypes[3]);
+}
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/qa/core/text/data/structure-destinations.fodt b/sw/qa/core/text/data/structure-destinations.fodt
new file mode 100644
index 000000000000..2aa91f366601
--- /dev/null
+++ b/sw/qa/core/text/data/structure-destinations.fodt
@@ -0,0 +1,47 @@
+
+
+ Structure destinations
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A heading
+ Jump to the heading
+ Body text with a footnote.1The footnote body.
+ A citation [AAA]
+
+
+
+
+
+
+
+ AAA
+
+
+
+
+
diff --git a/sw/source/core/text/EnhancedPDFExportHelper.cxx b/sw/source/core/text/EnhancedPDFExportHelper.cxx
index ffe98c7b350a..ff2e1bafec75 100644
--- a/sw/source/core/text/EnhancedPDFExportHelper.cxx
+++ b/sw/source/core/text/EnhancedPDFExportHelper.cxx
@@ -20,6 +20,7 @@
#include
#include
+#include
#include
#include
@@ -151,6 +152,8 @@ struct SwEnhancedPDFState
NumListIdMap m_NumListIdMap;
NumListBodyIdMap m_NumListBodyIdMap;
FrameTagSet m_FrameTagSet;
+ std::unordered_map m_DestNodeMap;
+ std::unordered_map m_NodeTagIdMap;
LanguageType m_eLanguageDefault;
@@ -442,6 +445,22 @@ bool lcl_HasPreviousParaSameNumRule(SwTextFrame const& rTextFrame, const SwTextN
return bRet;
}
+// a jump that selects an object, or finds nothing, moves the cursor nowhere and names no node
+const SwTextNode* lcl_JumpedToNode(const SwEditShell& rSh, const SwPosition& rBeforeJump)
+{
+ const SwPosition& rPoint = *rSh.GetCursor_()->GetPoint();
+ return rPoint == rBeforeJump ? nullptr : rPoint.GetNode().GetTextNode();
+}
+
+// a destination is made before its target is tagged, so remember the node
+void lcl_RememberDestNode(vcl::PDFExtOutDevData& rPDFExtOutDevData, const SwTextNode* pTextNode,
+ const sal_Int32 nDestId)
+{
+ SwEnhancedPDFState* pState = rPDFExtOutDevData.GetSwPDFState();
+ if (pState && nDestId >= 0 && pTextNode)
+ pState->m_DestNodeMap.emplace(nDestId, pTextNode);
+}
+
bool lcl_TryMoveToNonHiddenField(SwEditShell& rShell, const SwTextNode& rNd, const SwFormatField& rField)
{
// 1. Check if the whole paragraph is hidden
@@ -694,6 +713,16 @@ void SwTaggedPDFHelper::BeginTag(vcl::pdf::StructElement eType, const OUString&
sal_Int32 const nId = BeginTagImpl(pKey, eType, rString);
+ // which tag a destination pointing at this node names
+ if (mpFrameInfo && mpFrameInfo->mrFrame.IsTextFrame())
+ {
+ const SwTextFrame& rTextFrame(static_cast(mpFrameInfo->mrFrame));
+ if (const SwTextNode* pTextNode = rTextFrame.GetTextNodeFirst())
+ {
+ mpPDFExtOutDevData->GetSwPDFState()->m_NodeTagIdMap.emplace(pTextNode, nId);
+ }
+ }
+
// Store the id of the current structure element if
// - it is a list structure element
// - it is a list body element with children
@@ -2446,6 +2475,27 @@ tools::Rectangle SwEnhancedPDFExportHelper::MapSwRectToPDFRect(const SwPageFrame
return aRect;
}
+sal_Int32 SwEnhancedPDFExportHelper::CreateDestination(const SwPageFrame* pCurrPage,
+ const SwRect& rRect,
+ const sal_Int32 nPageNum,
+ const SwPosition& rBeforeJump)
+{
+ return CreateDestination(pCurrPage, rRect, nPageNum, lcl_JumpedToNode(mrSh, rBeforeJump));
+}
+
+sal_Int32 SwEnhancedPDFExportHelper::CreateDestination(const SwPageFrame* pCurrPage,
+ const SwRect& rRect,
+ const sal_Int32 nPageNum,
+ const SwTextNode* pTarget)
+{
+ assert(dynamic_cast(mrOut.GetExtOutDevData()));
+ auto& rPDFExtOutDevData = *static_cast(mrOut.GetExtOutDevData());
+ const sal_Int32 nDestId
+ = rPDFExtOutDevData.CreateDest(SwRectToPDFRect(pCurrPage, rRect.SVRect()), nPageNum);
+ lcl_RememberDestNode(rPDFExtOutDevData, pTarget, nDestId);
+ return nDestId;
+}
+
void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDefault)
{
vcl::PDFExtOutDevData* pPDFExtOutDevData =
@@ -2600,6 +2650,7 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
{
aURL = aURL.copy( 1 );
mrSh.SwCursorShell::ClearMark();
+ const SwPosition aBeforeJump(*mrSh.GetCursor_()->GetPoint());
if (! JumpToSwMark( &mrSh, SwMarkName(aURL) ))
{
continue; // target deleted
@@ -2617,8 +2668,8 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
// Destination Export
if ( -1 != nDestPageNum )
{
- tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect()));
- nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum);
+ nDestId = CreateDestination(pCurrPage, rDestRect, nDestPageNum,
+ aBeforeJump);
}
}
@@ -2688,6 +2739,7 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
{
aURL = aURL.copy( 1 );
mrSh.SwCursorShell::ClearMark();
+ const SwPosition aBeforeJump(*mrSh.GetCursor_()->GetPoint());
if (! JumpToSwMark( &mrSh, SwMarkName(aURL) ))
{
continue; // target deleted
@@ -2704,8 +2756,8 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
// Destination Export
if ( -1 != nDestPageNum )
{
- tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect()));
- nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum);
+ nDestId
+ = CreateDestination(pCurrPage, rDestRect, nDestPageNum, aBeforeJump);
}
}
@@ -2838,6 +2890,7 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
// Destination Rectangle
const SwGetRefField* pField = static_cast(pFormatField->GetField());
const SwMarkName& rRefName = pField->GetSetRefName();
+ const SwPosition aBeforeJump(*mrSh.GetCursor_()->GetPoint());
mrSh.GotoRefMark( rRefName, pField->GetSubType(), pField->GetSeqNo(), pField->GetFlags() );
const SwRect& rDestRect = mrSh.GetCharRect();
@@ -2849,8 +2902,8 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
if ( -1 != nDestPageNum )
{
// Destination Export
- tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect()));
- const sal_Int32 nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum);
+ const sal_Int32 nDestId
+ = CreateDestination(pCurrPage, rDestRect, nDestPageNum, aBeforeJump);
// #i44368# Links in Header/Footer
const bool bHeaderFooter = pDoc->IsInHeaderFooter( *pTNd );
@@ -2868,7 +2921,8 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
for (sal_Int32 aLinkPageNum : aLinkPageNums)
{
// Link Export
- aRect = SwRectToPDFRect(pCurrPage, rLinkRect.SVRect());
+ const tools::Rectangle aRect(
+ SwRectToPDFRect(pCurrPage, rLinkRect.SVRect()));
const sal_Int32 nLinkId =
pPDFExtOutDevData->CreateLink(aRect, rRefName.toString(), aLinkPageNum);
@@ -2932,6 +2986,7 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
const SwRect aLinkRect( aTmp[ 0 ] );
// Goto footnote text:
+ const SwPosition aBeforeFootnote(*mrSh.GetCursor_()->GetPoint());
if ( mrSh.GotoFootnoteText() )
{
// Destination Rectangle
@@ -2940,8 +2995,6 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
if ( -1 != nDestPageNum )
{
const SwPageFrame* pCurrPage = static_cast( mrSh.GetLayout()->Lower() );
- // Destination PageNum
- tools::Rectangle aRect = SwRectToPDFRect(pCurrPage, rDestRect.SVRect());
// Back link rectangle calculation
const SwPageFrame* fnBodyPage = pCurrPage->getRootFrame()->GetPageByPageNum(nDestPageNum+1);
SwRect fnSymbolRect;
@@ -2968,16 +3021,19 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
// Export back link
const sal_Int32 nBackLinkId = pPDFExtOutDevData->CreateLink(aFootnoteSymbolRect, numStrRef, nDestPageNum);
// Destination Export
- const sal_Int32 nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum);
+ const sal_Int32 nDestId
+ = CreateDestination(pCurrPage, rDestRect, nDestPageNum, aBeforeFootnote);
+ const SwPosition aBeforeAnchor(*mrSh.GetCursor_()->GetPoint());
mrSh.GotoFootnoteAnchor();
// Link PageNums
sal_Int32 aLinkPageNum = CalcOutputPageNum( aLinkRect );
pCurrPage = static_cast( mrSh.GetLayout()->Lower() );
// Link Export
- aRect = SwRectToPDFRect(pCurrPage, aLinkRect.SVRect());
+ const tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, aLinkRect.SVRect()));
const sal_Int32 nLinkId = pPDFExtOutDevData->CreateLink(aRect, numStrSymbol, aLinkPageNum);
// Back link destination Export
- const sal_Int32 nBackDestId = pPDFExtOutDevData->CreateDest(aRect, aLinkPageNum);
+ const sal_Int32 nBackDestId
+ = CreateDestination(pCurrPage, aLinkRect, aLinkPageNum, aBeforeAnchor);
// Store link info for tagged pdf output:
const IdMapEntry aLinkEntry( aLinkRect, nLinkId );
pPDFExtOutDevData->GetSwPDFState()->m_LinkIdMap.push_back(aLinkEntry);
@@ -3042,6 +3098,7 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
const sal_Int32 nParent = aOutlineStack.top().second;
// Destination rectangle
+ const SwPosition aBeforeJump(*mrSh.GetCursor_()->GetPoint());
mrSh.GotoOutline(i);
const SwRect& rDestRect = mrSh.GetCharRect();
@@ -3054,9 +3111,8 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
if ( -1 != nDestPageNum )
{
// Destination Export
- tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect()));
- const sal_Int32 nDestId =
- pPDFExtOutDevData->CreateDest(aRect, nDestPageNum);
+ const sal_Int32 nDestId
+ = CreateDestination(pCurrPage, rDestRect, nDestPageNum, aBeforeJump);
// Outline entry text
const OUString aEntry = mrSh.getIDocumentOutlineNodesAccess()->getOutlineText(
@@ -3183,7 +3239,8 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
if ( bInternal )
{
aBookmarkName = aBookmarkName.copy( 1 );
- JumpToSwMark( &mrSh, SwMarkName(aBookmarkName) );
+ const SwPosition aBeforeJump(*mrSh.GetCursor_()->GetPoint());
+ const bool bJumped = JumpToSwMark(&mrSh, SwMarkName(aBookmarkName));
// Destination Rectangle
const SwRect& rDestRect = mrSh.GetCharRect();
@@ -3196,18 +3253,32 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
if ( -1 != nDestPageNum )
{
- tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect()));
if ( rBookmark.nLinkId != -1 )
{
+ const SwTextNode* pTarget = lcl_JumpedToNode(mrSh, aBeforeJump);
+ if (!pTarget && bJumped)
+ {
+ // this pass can begin with the cursor on the target already, where a
+ // successful jump shows no move, and the mark names its own node
+ const IDocumentMarkAccess& rMarks = *pDoc->getIDocumentMarkAccess();
+ const auto ppMark = rMarks.findMark(SwMarkName(INetURLObject::decode(
+ aBookmarkName, INetURLObject::DecodeMechanism::WithCharset)));
+ if (ppMark != rMarks.getAllMarksEnd())
+ pTarget = (*ppMark)->GetMarkStart().GetNode().GetTextNode();
+ }
+
// Destination Export
- const sal_Int32 nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum);
+ const sal_Int32 nDestId
+ = CreateDestination(pCurrPage, rDestRect, nDestPageNum, pTarget);
// Connect Link and Destination:
pPDFExtOutDevData->SetLinkDest( rBookmark.nLinkId, nDestId );
}
else
{
- pPDFExtOutDevData->DescribeRegisteredDest(rBookmark.nDestId, aRect, nDestPageNum);
+ pPDFExtOutDevData->DescribeRegisteredDest(
+ rBookmark.nDestId, SwRectToPDFRect(pCurrPage, rDestRect.SVRect()),
+ nDestPageNum);
}
}
}
@@ -3217,6 +3288,18 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe
rBookmarks.clear();
assert(pPDFExtOutDevData->GetSwPDFState());
assert(pPDFExtOutDevData->GetSwPDFState()->m_DeferredTags.empty());
+
+ // everything is tagged now, so each destination can name its element
+ SwEnhancedPDFState& rState(*pPDFExtOutDevData->GetSwPDFState());
+ for (const auto& rDest : rState.m_DestNodeMap)
+ {
+ const auto it(rState.m_NodeTagIdMap.find(rDest.second));
+ if (it != rState.m_NodeTagIdMap.end())
+ {
+ pPDFExtOutDevData->SetDestStructureElement(rDest.first, it->second);
+ }
+ }
+
delete pPDFExtOutDevData->GetSwPDFState();
pPDFExtOutDevData->SetSwPDFState(nullptr);
}
@@ -3265,9 +3348,10 @@ void SwEnhancedPDFExportHelper::ExportAuthorityEntryLinks()
// Destination Export
if ( -1 != nDestPageNum )
{
- tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect()));
- const sal_Int32 nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum);
- const OUString* vNodeText = &static_cast(&rCurrentNode)->GetText();
+ auto& rTextNode = static_cast(rCurrentNode);
+ const sal_Int32 nDestId
+ = CreateDestination(pCurrPage, rDestRect, nDestPageNum, &rTextNode);
+ const OUString* vNodeText = &rTextNode.GetText();
vDestinations.emplace_back(pIteratedTOX, vNodeText, nDestId);
}
}
diff --git a/vcl/inc/pdf/pdfwriter_impl.hxx b/vcl/inc/pdf/pdfwriter_impl.hxx
index 200a3aa074e0..7bb32c1196c5 100644
--- a/vcl/inc/pdf/pdfwriter_impl.hxx
+++ b/vcl/inc/pdf/pdfwriter_impl.hxx
@@ -386,6 +386,8 @@ struct PDFDest
sal_Int32 m_nPage;
PDFWriter::DestAreaType m_eType;
tools::Rectangle m_aRect;
+ /// index into m_aStructure, -1 when the target is not tagged
+ sal_Int32 m_nStructElement = -1;
};
//--->i56629
@@ -995,6 +997,10 @@ private:
sal_Int32 emitResources();
// appends a dest
bool appendDest( sal_Int32 nDestID, OStringBuffer& rBuffer );
+ /// ISO 14289-2 requires a GoTo action to name the structure element, not only the page
+ bool appendStructureDest(sal_Int32 nDestID, OStringBuffer& rBuffer);
+ /// writes /Dest, or the GoTo action carrying /SD when the target is tagged
+ void appendDestOrGoTo(sal_Int32 nDestID, OStringBuffer& rBuffer);
// write all links
bool emitLinkAnnotations();
// Write all screen annotations.
@@ -1314,6 +1320,7 @@ public:
sal_Int32 createDest( const tools::Rectangle& rRect, sal_Int32 nPageNr, PDFWriter::DestAreaType eType );
sal_Int32 registerDestReference( sal_Int32 nDestId, const tools::Rectangle& rRect, sal_Int32 nPageNr, PDFWriter::DestAreaType eType );
void setLinkDest( sal_Int32 nLinkId, sal_Int32 nDestId );
+ void setDestStructureElement(sal_Int32 nDestId, sal_Int32 nStructElementId);
void setLinkURL( sal_Int32 nLinkId, const OUString& rURL );
void setLinkPropertyId( sal_Int32 nLinkId, sal_Int32 nPropertyId );
diff --git a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
index 17bfaaf2ea13..6722949175ad 100644
--- a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
+++ b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx
@@ -2200,9 +2200,9 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTdf173162)
loadFromFile(u"StructureNamespaces.fodt");
// Tagged rather than PDF/UA, because the harness validates a PDF/UA export and this document
- // still fails ISO 14289-2 8.8 (a link destination is not a structure destination, tdf#171022),
- // 8.2.5.8 (a TOCI needs Ref) and 8.2.5.14 (PDF/UA-2 wants FENote, not Note). TODO: ask for
- // PDF/UA here once those are fixed. The namespaces below depend on the version alone.
+ // still fails ISO 14289-2 8.2.5.8 (a TOCI needs Ref) and 8.2.5.14 (PDF/UA-2 wants FENote,
+ // not Note). TODO: ask for PDF/UA here once those are fixed. The namespaces below depend on
+ // the version alone.
uno::Sequence aFilterData{ comphelper::makePropertyValue(u"UseTaggedPDF"_ustr, true),
comphelper::makePropertyValue(u"SelectPdfVersion"_ustr,
sal_Int32(20)) };
diff --git a/vcl/source/pdf/pdfextoutdevdata.cxx b/vcl/source/pdf/pdfextoutdevdata.cxx
index 80a21ef49aa9..7cebc72f5ec0 100644
--- a/vcl/source/pdf/pdfextoutdevdata.cxx
+++ b/vcl/source/pdf/pdfextoutdevdata.cxx
@@ -75,6 +75,11 @@ struct SetLinkDest {
sal_Int32 mnLinkId;
sal_Int32 mnDestId;
};
+struct SetDestStructureElement
+{
+ sal_Int32 mnDestId;
+ sal_Int32 mnStructElementId;
+};
struct SetLinkURL {
OUString maLinkURL;
sal_Int32 mnLinkId;
@@ -144,6 +149,7 @@ typedef std::variant(action))
+ {
+ const vcl::SetDestStructureElement& rSet = std::get(action);
+ rWriter.SetDestStructureElement(GetMappedId(rSet.mnDestId), rSet.mnStructElementId);
+ }
else if (std::holds_alternative(action)) {
const vcl::SetLinkURL& rSetLinkURL = std::get(action);
sal_Int32 nLinkId = GetMappedId(rSetLinkURL.mnLinkId);
@@ -703,6 +714,11 @@ sal_Int32 PDFExtOutDevData::CreateScreen(const tools::Rectangle& rRect,
return it->second;
}
+void PDFExtOutDevData::SetDestStructureElement(sal_Int32 nDestId, sal_Int32 nStructElementId)
+{
+ mpGlobalSyncData->mActions.push_back(vcl::SetDestStructureElement{ nDestId, nStructElementId });
+}
+
void PDFExtOutDevData::SetLinkDest( sal_Int32 nLinkId, sal_Int32 nDestId )
{
mpGlobalSyncData->mActions.push_back( vcl::SetLinkDest{ nLinkId, nDestId } );
diff --git a/vcl/source/pdf/pdfwriter.cxx b/vcl/source/pdf/pdfwriter.cxx
index 69c8d59224f6..935444a4214f 100644
--- a/vcl/source/pdf/pdfwriter.cxx
+++ b/vcl/source/pdf/pdfwriter.cxx
@@ -353,6 +353,11 @@ void PDFWriter::SetLinkDest( sal_Int32 nLinkId, sal_Int32 nDestId )
xImplementation->setLinkDest( nLinkId, nDestId );
}
+void PDFWriter::SetDestStructureElement(sal_Int32 nDestId, sal_Int32 nStructElementId)
+{
+ xImplementation->setDestStructureElement(nDestId, nStructElementId);
+}
+
void PDFWriter::SetLinkURL( sal_Int32 nLinkId, const OUString& rURL )
{
xImplementation->setLinkURL( nLinkId, rURL );
diff --git a/vcl/source/pdf/pdfwriter_impl.cxx b/vcl/source/pdf/pdfwriter_impl.cxx
index 2da2b01a7e89..67ac22dd328a 100644
--- a/vcl/source/pdf/pdfwriter_impl.cxx
+++ b/vcl/source/pdf/pdfwriter_impl.cxx
@@ -2552,8 +2552,7 @@ sal_Int32 PDFWriterImpl::emitOutline()
// Dest is not required
if( rItem.m_nDestID >= 0 && o3tl::make_unsigned(rItem.m_nDestID) < m_aDests.size() )
{
- aLine.append( "/Dest" );
- appendDest( rItem.m_nDestID, aLine );
+ appendDestOrGoTo(rItem.m_nDestID, aLine);
}
aLine.append( "/Parent "
+ OString::number( rItem.m_nParentObject )
@@ -2579,6 +2578,47 @@ sal_Int32 PDFWriterImpl::emitOutline()
return m_aOutline[0].m_nObject;
}
+bool PDFWriterImpl::appendStructureDest(sal_Int32 nDestID, OStringBuffer& rBuffer)
+{
+ if (nDestID < 0 || o3tl::make_unsigned(nDestID) >= m_aDests.size())
+ return false;
+
+ const PDFDest& rDest = m_aDests[nDestID];
+ if (rDest.m_nStructElement < 0
+ || o3tl::make_unsigned(rDest.m_nStructElement) >= m_aStructure.size())
+ return false;
+
+ const sal_Int32 nObject(m_aStructure[rDest.m_nStructElement].m_nObject);
+ if (nObject <= 0) // not emitted: NonStruct, or no structure at all
+ return false;
+
+ // same as the page destination, bar the first entry
+ OStringBuffer aPageDest;
+ if (!appendDest(nDestID, aPageDest))
+ return false;
+ const OString aPage(aPageDest.makeStringAndClear());
+ const sal_Int32 nAfterPage(aPage.indexOf(" 0 R") + 4);
+
+ rBuffer.append("[" + OString::number(nObject) + " 0 R" + aPage.subView(nAfterPage));
+ return true;
+}
+
+void PDFWriterImpl::appendDestOrGoTo(sal_Int32 nDestID, OStringBuffer& rBuffer)
+{
+ OStringBuffer aStructure;
+ if (!appendStructureDest(nDestID, aStructure))
+ {
+ rBuffer.append("/Dest");
+ appendDest(nDestID, rBuffer);
+ return;
+ }
+
+ // ISO 14289-2 8.8: a destination inside the document shall be a structure destination
+ rBuffer.append("/A<>");
+}
+
bool PDFWriterImpl::appendDest( sal_Int32 nDestID, OStringBuffer& rBuffer )
{
if( nDestID < 0 || o3tl::make_unsigned(nDestID) >= m_aDests.size() )
@@ -2826,8 +2866,7 @@ bool PDFWriterImpl::emitLinkAnnotations()
}
if( rLink.m_nDest >= 0 )
{
- aLine.append( "/Dest" );
- appendDest( rLink.m_nDest, aLine );
+ appendDestOrGoTo(rLink.m_nDest, aLine);
}
else
{
@@ -9932,6 +9971,14 @@ sal_Int32 PDFWriterImpl::registerDestReference( sal_Int32 nDestId, const tools::
return m_aDestinationIdTranslation[ nDestId ];
}
+void PDFWriterImpl::setDestStructureElement(sal_Int32 nDestId, sal_Int32 nStructElementId)
+{
+ if (nDestId < 0 || o3tl::make_unsigned(nDestId) >= m_aDests.size())
+ return;
+
+ m_aDests[nDestId].m_nStructElement = nStructElementId;
+}
+
void PDFWriterImpl::setLinkDest( sal_Int32 nLinkId, sal_Int32 nDestId )
{
if( nLinkId < 0 || o3tl::make_unsigned(nLinkId) >= m_aLinks.size() )