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() )