include/vcl/pdfextoutdevdata.hxx | 5 + include/vcl/pdfwriter.hxx | 8 ++- sw/source/core/text/EnhancedPDFExportHelper.cxx | 61 +++++++++++++++++++++++ vcl/inc/pdf/pdfwriter_impl.hxx | 5 + vcl/qa/cppunit/pdfexport/data/tdf162359.odt |binary vcl/qa/cppunit/pdfexport/pdfexport.cxx | 62 ++++++++++++++++++++++++ vcl/source/gdi/pdfextoutdevdata.cxx | 14 +++-- vcl/source/gdi/pdfwriter.cxx | 5 + vcl/source/gdi/pdfwriter_impl.cxx | 58 +++++++++++++++++++++- 9 files changed, 205 insertions(+), 13 deletions(-)
New commits: commit 43549566f3785065375cdf345993bd91c14e749d Author: Tibor Nagy <tibor.nagy.ext...@allotropia.de> AuthorDate: Thu Sep 5 00:25:19 2024 +0200 Commit: Michael Stahl <michael.st...@allotropia.de> CommitDate: Fri Sep 20 12:32:50 2024 +0200 tdf#162359 sw: fix the export of comments as PDF annotations Change-Id: If4a8a1a73c382f496b2c6dd4d52271dc6bc87dda Reviewed-on: https://gerrit.libreoffice.org/c/core/+/172877 Tested-by: Jenkins Reviewed-by: Nagy Tibor <tibor.nagy.ext...@allotropia.de> Signed-off-by: Xisco Fauli <xiscofa...@libreoffice.org> Reviewed-on: https://gerrit.libreoffice.org/c/core/+/173671 Reviewed-by: Michael Stahl <michael.st...@allotropia.de> diff --git a/include/vcl/pdfextoutdevdata.hxx b/include/vcl/pdfextoutdevdata.hxx index e6bf7e5aed08..6ffd88ddce15 100644 --- a/include/vcl/pdfextoutdevdata.hxx +++ b/include/vcl/pdfextoutdevdata.hxx @@ -342,11 +342,14 @@ public: @param rNote specifies the contents of the note + @param rPopupRect + specifies the rectangle of the popup window for the note + @param nPageNr number of page the note is on (as returned by NewPage) or -1 in which case the current page is used */ - void CreateNote( const tools::Rectangle& rRect, const vcl::pdf::PDFNote& rNote, sal_Int32 nPageNr = -1 ); + sal_Int32 CreateNote(const tools::Rectangle& rRect, const vcl::pdf::PDFNote& rNote, const tools::Rectangle& rPopupRect = tools::Rectangle(), sal_Int32 nPageNr = -1); /** begin a new logical structure element diff --git a/include/vcl/pdfwriter.hxx b/include/vcl/pdfwriter.hxx index 044d18dece10..90d1a48754d1 100644 --- a/include/vcl/pdfwriter.hxx +++ b/include/vcl/pdfwriter.hxx @@ -148,6 +148,9 @@ public: // case the arbitrary id has to be passed again when the // actual link annotation is created via SetLinkPropertyID LinkAnnotation, + // note destination is an artificial attribute that sets + // the note annotation ID of a Note element + NoteAnnotation, // Language currently sets a LanguageType (see i18nlangtag/lang.h) // which will be internally changed to a corresponding locale Language @@ -1042,6 +1045,9 @@ The following structure describes the permissions used in PDF security active rectangle of the note (that is the area that has to be hit to popup the annotation) + @param rPopupRect + specifies the rectangle of the popup window for the note + @param rNote specifies the contents of the note @@ -1049,7 +1055,7 @@ The following structure describes the permissions used in PDF security number of page the note is on (as returned by NewPage) or -1 in which case the current page is used */ - void CreateNote( const tools::Rectangle& rRect, const vcl::pdf::PDFNote& rNote, sal_Int32 nPageNr ); + sal_Int32 CreateNote( const tools::Rectangle& rRect, const tools::Rectangle& rPopupRect, const vcl::pdf::PDFNote& rNote, sal_Int32 nPageNr ); /** begin a new logical structure element diff --git a/sw/source/core/text/EnhancedPDFExportHelper.cxx b/sw/source/core/text/EnhancedPDFExportHelper.cxx index cbbc083228a0..c95a84fb9e92 100644 --- a/sw/source/core/text/EnhancedPDFExportHelper.cxx +++ b/sw/source/core/text/EnhancedPDFExportHelper.cxx @@ -87,6 +87,8 @@ #include <tblafmt.hxx> #include <authfld.hxx> #include <dcontact.hxx> +#include <PostItMgr.hxx> +#include <AnnotationWin.hxx> #include <tools/globname.hxx> #include <svx/svdobj.hxx> @@ -130,6 +132,7 @@ void lcl_DBGCheckStack() typedef std::set< tools::Long, lt_TableColumn > TableColumnsMapEntry; typedef std::pair< SwRect, sal_Int32 > IdMapEntry; typedef std::vector< IdMapEntry > LinkIdMap; +typedef std::vector< IdMapEntry > NoteIdMap; typedef std::map< const SwTable*, TableColumnsMapEntry > TableColumnsMap; typedef std::map< const SwNumberTreeNode*, sal_Int32 > NumListIdMap; typedef std::map< const SwNumberTreeNode*, sal_Int32 > NumListBodyIdMap; @@ -139,6 +142,7 @@ struct SwEnhancedPDFState { TableColumnsMap m_TableColumnsMap; LinkIdMap m_LinkIdMap; + NoteIdMap m_NoteIdMap; NumListIdMap m_NumListIdMap; NumListBodyIdMap m_NumListBodyIdMap; FrameTagSet m_FrameTagSet; @@ -212,6 +216,7 @@ constexpr OUStringLiteral aFigureString = u"Figure"; constexpr OUStringLiteral aFormulaString = u"Formula"; constexpr OUString aLinkString = u"Link"_ustr; constexpr OUStringLiteral aNoteString = u"Note"; +constexpr OUStringLiteral aAnnotString = u"Annot"; // returns true if first paragraph in cell frame has 'table heading' style bool lcl_IsHeadlineCell( const SwCellFrame& rCellFrame ) @@ -946,6 +951,7 @@ void SwTaggedPDFHelper::SetAttributes( vcl::PDFWriter::StructElement eType ) bool bBaselineShift = false; bool bTextDecorationType = false; bool bLinkAttribute = false; + bool bAnnotAttribute = false; bool bLanguage = false; // Check which attributes to set: @@ -1025,6 +1031,11 @@ void SwTaggedPDFHelper::SetAttributes( vcl::PDFWriter::StructElement eType ) } break; + case vcl::PDFWriter::Annot: + bAnnotAttribute = + bLanguage = true; + break; + default: break; } @@ -1081,6 +1092,23 @@ void SwTaggedPDFHelper::SetAttributes( vcl::PDFWriter::StructElement eType ) rInf.CalcRect( *pPor, &aPorRect ); LinkLinkLink(*mpPDFExtOutDevData, aPorRect); } + + if (bAnnotAttribute) + { + SwRect aPorRect; + rInf.CalcRect(*pPor, &aPorRect); + const NoteIdMap& rNoteIdMap(mpPDFExtOutDevData->GetSwPDFState()->m_NoteIdMap); + const Point aCenter = aPorRect.Center(); + auto aIter = std::find_if(rNoteIdMap.begin(), rNoteIdMap.end(), + [&aCenter](const IdMapEntry& rEntry) + { return rEntry.first.Contains(aCenter); }); + if (aIter != rNoteIdMap.end()) + { + sal_Int32 nNoteId = (*aIter).second; + mpPDFExtOutDevData->SetStructureAttributeNumerical(vcl::PDFWriter::NoteAnnotation, + nNoteId); + } + } } else if (mpNumInfo && eType == vcl::PDFWriter::List) { @@ -1844,6 +1872,14 @@ void SwTaggedPDFHelper::BeginInlineStructureElements() switch ( pPor->GetWhichPor() ) { + case PortionType::PostIts: + if (!mpPDFExtOutDevData->GetSwPDFState()->m_NoteIdMap.empty()) + { + nPDFType = vcl::PDFWriter::Annot; + aPDFType = aAnnotString; + } + break; + case PortionType::Hyphen : case PortionType::SoftHyphen : // Check for alternative spelling: @@ -2200,9 +2236,32 @@ void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDe // Guess what the contents contains... aNote.maContents = pField->GetText(); + tools::Rectangle aPopupRect(0, 0); + SwPostItMgr* pPostItMgr = pDoc->GetEditShell()->GetPostItMgr(); + for (auto it = pPostItMgr->begin(); it != pPostItMgr->end(); ++it) + { + sw::annotation::SwAnnotationWin* pWin = it->get()->mpPostIt; + if (pWin) + { + const SwRect& aAnnotRect = pWin->GetAnchorRect(); + if (aAnnotRect.Contains(rNoteRect)) + { + Point aPt(pDoc->GetEditShell()->GetWin()->PixelToLogic(pWin->GetPosPixel())); + Size aSize(pDoc->GetEditShell()->GetWin()->PixelToLogic(pWin->GetSizePixel())); + aPopupRect = tools::Rectangle(aPt, aSize); + } + } + } + // Link Export tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rNoteRect.SVRect())); - pPDFExtOutDevData->CreateNote(aRect, aNote, aNotePageNum); + sal_Int32 nNoteId = pPDFExtOutDevData->CreateNote(aRect, aNote, aPopupRect, aNotePageNum); + + if (mrPrintData.GetPrintPostIts() != SwPostItMode::InMargins) + { + const IdMapEntry aNoteEntry(aRect, nNoteId); + pPDFExtOutDevData->GetSwPDFState()->m_NoteIdMap.push_back(aNoteEntry); + } } mrSh.SwCursorShell::ClearMark(); } diff --git a/vcl/inc/pdf/pdfwriter_impl.hxx b/vcl/inc/pdf/pdfwriter_impl.hxx index 9b2db532e6c0..b92be41c9622 100644 --- a/vcl/inc/pdf/pdfwriter_impl.hxx +++ b/vcl/inc/pdf/pdfwriter_impl.hxx @@ -463,7 +463,10 @@ struct PDFNoteEntry : public PDFAnnotation PDFPopupAnnotation m_aPopUpAnnotation; + sal_Int32 m_nStructParent; + PDFNoteEntry() + : m_nStructParent(-1) {} }; @@ -1337,7 +1340,7 @@ public: void setOutlineItemDest( sal_Int32 nItem, sal_Int32 nDestID ); // notes - void createNote( const tools::Rectangle& rRect, const PDFNote& rNote, sal_Int32 nPageNr ); + sal_Int32 createNote(const tools::Rectangle& rRect, const tools::Rectangle& rPopupRect, const PDFNote& rNote, sal_Int32 nPageNr); // structure elements sal_Int32 ensureStructureElement(); void initStructureElement(sal_Int32 id, PDFWriter::StructElement eType, std::u16string_view rAlias); diff --git a/vcl/qa/cppunit/pdfexport/data/tdf162359.odt b/vcl/qa/cppunit/pdfexport/data/tdf162359.odt new file mode 100644 index 000000000000..b92290df9e54 Binary files /dev/null and b/vcl/qa/cppunit/pdfexport/data/tdf162359.odt differ diff --git a/vcl/qa/cppunit/pdfexport/pdfexport.cxx b/vcl/qa/cppunit/pdfexport/pdfexport.cxx index 2e047f27fdff..d114103c1f07 100644 --- a/vcl/qa/cppunit/pdfexport/pdfexport.cxx +++ b/vcl/qa/cppunit/pdfexport/pdfexport.cxx @@ -66,6 +66,68 @@ void PdfExportTest::load(std::u16string_view rFile, vcl::filter::PDFDocument& rD CPPUNIT_ASSERT(rDocument.Read(aStream)); } +CPPUNIT_TEST_FIXTURE(PdfExportTest, testCommentAnnotation) +{ + // Enable PDF/UA and Comment as PDF annotations + uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence( + { { "PDFUACompliance", uno::Any(true) }, { "ExportNotes", uno::Any(true) } })); + aMediaDescriptor[u"FilterData"_ustr] <<= aFilterData; + + vcl::filter::PDFDocument aDocument; + load(u"tdf162359.odt", aDocument); + + // The document has one page. + std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages(); + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aPages.size()); + + vcl::filter::PDFObjectElement* pAnnot(nullptr); + for (const auto& aElement : aDocument.GetElements()) + { + auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(aElement.get()); + if (!pObject) + continue; + auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr)); + if (pType && pType->GetValue() == "StructElem") + { + auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr)); + if (pS && pS->GetValue() == "Annot") + { + pAnnot = pObject; + } + } + } + CPPUNIT_ASSERT(pAnnot); + auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pAnnot->Lookup("K"_ostr)); + CPPUNIT_ASSERT(pKids); + auto pObj = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pKids->GetElement(0)); + CPPUNIT_ASSERT(pObj); + auto pOType = dynamic_cast<vcl::filter::PDFNameElement*>(pObj->LookupElement("Type"_ostr)); + CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue()); + + // Parse the export result with pdfium. + std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport(); + std::unique_ptr<vcl::pdf::PDFiumPage> pPdfPage = pPdfDocument->openPage(/*nIndex=*/0); + CPPUNIT_ASSERT(pPdfPage); + + // The page has two annotation. + CPPUNIT_ASSERT_EQUAL(2, pPdfPage->getAnnotationCount()); + // Text annotation + { + auto pAnnotation = pPdfPage->getAnnotation(0); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Text, pAnnotation->getSubType()); + CPPUNIT_ASSERT(pAnnotation->hasKey("StructParent"_ostr)); + } + + // Popup annotation + { + auto pAnnotation = pPdfPage->getAnnotation(1); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Popup, pAnnotation->getSubType()); + CPPUNIT_ASSERT(!pAnnotation->getRectangle().isEmpty()); + } +} + CPPUNIT_TEST_FIXTURE(PdfExportTest, testFigurePlacement) { aMediaDescriptor[u"FilterName"_ustr] <<= u"impress_pdf_Export"_ustr; diff --git a/vcl/source/gdi/pdfextoutdevdata.cxx b/vcl/source/gdi/pdfextoutdevdata.cxx index c81868c97a59..ab8875990e05 100644 --- a/vcl/source/gdi/pdfextoutdevdata.cxx +++ b/vcl/source/gdi/pdfextoutdevdata.cxx @@ -94,6 +94,7 @@ struct CreateNote { MapMode maParaMapMode; vcl::pdf::PDFNote maParaPDFNote; tools::Rectangle maParaRect; + tools::Rectangle maPopupRect; sal_Int32 mnPage; }; @@ -306,7 +307,9 @@ void GlobalSyncData::PlayGlobalActions( PDFWriter& rWriter ) const vcl::CreateNote& rCreateNote = std::get<CreateNote>(action); rWriter.Push( PushFlags::MAPMODE ); rWriter.SetMapMode( rCreateNote.maParaMapMode ); - rWriter.CreateNote( rCreateNote.maParaRect, rCreateNote.maParaPDFNote, rCreateNote.mnPage ); + mParaIds.push_back(rWriter.CreateNote(rCreateNote.maParaRect, rCreateNote.maPopupRect, rCreateNote.maParaPDFNote, rCreateNote.mnPage)); + rWriter.SetLinkPropertyID(mParaIds.back(), sal_Int32(mParaIds.size() - 1)); + rWriter.Pop(); } else if (std::holds_alternative<SetPageTransition>(action)) { const vcl::SetPageTransition& rSetPageTransition = std::get<SetPageTransition>(action); @@ -723,10 +726,13 @@ sal_Int32 PDFExtOutDevData::CreateOutlineItem( sal_Int32 nParent, const OUString mpGlobalSyncData->mActions.push_back( vcl::CreateOutlineItem{ rText, nParent, nDestID } ); return mpGlobalSyncData->mCurId++; } -void PDFExtOutDevData::CreateNote( const tools::Rectangle& rRect, const vcl::pdf::PDFNote& rNote, sal_Int32 nPageNr ) +sal_Int32 PDFExtOutDevData::CreateNote(const tools::Rectangle& rRect, + const vcl::pdf::PDFNote& rNote, + const tools::Rectangle& rPopupRect, sal_Int32 nPageNr) { - mpGlobalSyncData->mActions.push_back( - vcl::CreateNote{ mrOutDev.GetMapMode(), rNote, rRect, nPageNr == -1 ? mnPage : nPageNr } ); + mpGlobalSyncData->mActions.push_back(vcl::CreateNote{ + mrOutDev.GetMapMode(), rNote, rRect, rPopupRect, nPageNr == -1 ? mnPage : nPageNr }); + return mpGlobalSyncData->mCurId++; } void PDFExtOutDevData::SetPageTransition( PDFWriter::PageTransition eType, sal_uInt32 nMilliSec ) { diff --git a/vcl/source/gdi/pdfwriter.cxx b/vcl/source/gdi/pdfwriter.cxx index 3fb8b6015570..999e8a7b708e 100644 --- a/vcl/source/gdi/pdfwriter.cxx +++ b/vcl/source/gdi/pdfwriter.cxx @@ -382,9 +382,10 @@ sal_Int32 PDFWriter::CreateOutlineItem( sal_Int32 nParent, std::u16string_view r return xImplementation->createOutlineItem( nParent, rText, nDestID ); } -void PDFWriter::CreateNote( const tools::Rectangle& rRect, const PDFNote& rNote, sal_Int32 nPageNr ) +sal_Int32 PDFWriter::CreateNote(const tools::Rectangle& rRect, const tools::Rectangle& rPopupRect, + const PDFNote& rNote, sal_Int32 nPageNr) { - xImplementation->createNote( rRect, rNote, nPageNr ); + return xImplementation->createNote(rRect, rPopupRect, rNote, nPageNr); } sal_Int32 PDFWriter::EnsureStructureElement() diff --git a/vcl/source/gdi/pdfwriter_impl.cxx b/vcl/source/gdi/pdfwriter_impl.cxx index 8439ec124229..9b8775a15faa 100644 --- a/vcl/source/gdi/pdfwriter_impl.cxx +++ b/vcl/source/gdi/pdfwriter_impl.cxx @@ -1887,7 +1887,8 @@ const char* PDFWriterImpl::getAttributeTag( PDFWriter::StructAttribute eAttr ) { PDFWriter::RubyPosition, "RubyPosition" }, { PDFWriter::Type, "Type" }, { PDFWriter::Subtype, "Subtype" }, - { PDFWriter::LinkAnnotation, "LinkAnnotation" } + { PDFWriter::LinkAnnotation, "LinkAnnotation" }, + { PDFWriter::NoteAnnotation, "NoteAnnotation" } }); auto it = aAttributeStrings.find( eAttr ); @@ -2036,6 +2037,28 @@ OString PDFWriterImpl::emitStructureAttributes( PDFStructureElement& i_rEle ) } } } + else if (attribute.first == PDFWriter::NoteAnnotation) + { + sal_Int32 nNote = attribute.second.nValue; + std::map<sal_Int32, sal_Int32>::const_iterator link_it = m_aLinkPropertyMap.find(nNote); + if (link_it != m_aLinkPropertyMap.end()) + nNote = link_it->second; + if (nNote >= 0 && o3tl::make_unsigned(nNote) < m_aNotes.size()) + { + AppendAnnotKid(i_rEle, m_aNotes[nNote]); + } + else + { + OSL_FAIL("unresolved note id for Note structure"); + SAL_INFO("vcl.pdfwriter", "unresolved note id " << nNote << " for Note structure"); + if (g_bDebugDisableCompression) + { + OString aLine + = "unresolved note id " + OString::number(nNote) + " for Note structure"; + emitComment(aLine.getStr()); + } + } + } else appendStructureAttributeLine( attribute.first, attribute.second, aLayout, true ); } @@ -4067,6 +4090,14 @@ void PDFWriterImpl::emitTextAnnotationLine(OStringBuffer & aLine, PDFNoteEntry c appendUnicodeTextStringEncrypt(rNote.m_aContents.maTitle, rNote.m_nObject, aLine); aLine.append(" "); } + + if (-1 != rNote.m_nStructParent) + { + aLine.append("/StructParent "); + aLine.append(rNote.m_nStructParent); + aLine.append(" "); + } + aLine.append(">> "); aLine.append("endobj "); } @@ -4075,6 +4106,15 @@ void PDFWriterImpl::emitPopupAnnotationLine(OStringBuffer & aLine, PDFPopupAnnot { appendObjectID(rPopUp.m_nObject, aLine); aLine.append("<</Type /Annot /Subtype /Popup "); + aLine.append("/Rect["); + appendFixedInt(rPopUp.m_aRect.Left(), aLine); + aLine.append(' '); + appendFixedInt(rPopUp.m_aRect.Top(), aLine); + aLine.append(' '); + appendFixedInt(rPopUp.m_aRect.Right(), aLine); + aLine.append(' '); + appendFixedInt(rPopUp.m_aRect.Bottom(), aLine); + aLine.append("]"); aLine.append("/Parent "); appendObjectReference(rPopUp.m_nParentObject, aLine); aLine.append(">> "); @@ -10477,28 +10517,36 @@ void PDFWriterImpl::intersectClipRegion( const basegfx::B2DPolyPolygon& rRegion } } -void PDFWriterImpl::createNote( const tools::Rectangle& rRect, const pdf::PDFNote& rNote, sal_Int32 nPageNr ) +sal_Int32 PDFWriterImpl::createNote(const tools::Rectangle& rRect, + const tools::Rectangle& rPopupRect, const pdf::PDFNote& rNote, + sal_Int32 nPageNr) { if (nPageNr < 0) nPageNr = m_nCurrentPage; if (nPageNr < 0 || o3tl::make_unsigned(nPageNr) >= m_aPages.size()) - return; + return -1; + + sal_Int32 nRet = m_aNotes.size(); m_aNotes.emplace_back(); auto & rNoteEntry = m_aNotes.back(); rNoteEntry.m_nObject = createObject(); rNoteEntry.m_aPopUpAnnotation.m_nObject = createObject(); rNoteEntry.m_aPopUpAnnotation.m_nParentObject = rNoteEntry.m_nObject; + rNoteEntry.m_aPopUpAnnotation.m_aRect = rPopupRect; rNoteEntry.m_aContents = rNote; rNoteEntry.m_aRect = rRect; rNoteEntry.m_nPage = nPageNr; // convert to default user space now, since the mapmode may change m_aPages[nPageNr].convertRect(rNoteEntry.m_aRect); + m_aPages[nPageNr].convertRect(rNoteEntry.m_aPopUpAnnotation.m_aRect); // insert note to page's annotation list m_aPages[nPageNr].m_aAnnotations.push_back(rNoteEntry.m_nObject); m_aPages[nPageNr].m_aAnnotations.push_back(rNoteEntry.m_aPopUpAnnotation.m_nObject); + + return nRet; } sal_Int32 PDFWriterImpl::createLink(const tools::Rectangle& rRect, sal_Int32 nPageNr, OUString const& rAltText) @@ -11586,6 +11634,10 @@ bool PDFWriterImpl::setStructureAttributeNumerical( enum PDFWriter::StructAttrib if( eType == PDFWriter::Link ) bInsert = true; break; + case PDFWriter::NoteAnnotation: + if (eType == PDFWriter::Annot) + bInsert = true; + break; default: break; } }