From 64b5464df4e4aa0e1982e59cb626f1ff832f19e5 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Thu, 27 Aug 2026 14:25:38 +0000 Subject: [PATCH] cool#15668 sw: keep adjacent caption links clickable in a pdf Exporting several cross-reference links placed right next to each other, separated only by punctuation, kept only the last one clickable. The export checks every link rectangle for the text it covers, by hit-testing the middle of the rectangle. For a link only one digit wide that point falls exactly where the hit test advances to the next character, which belongs to the neighbouring link, so the rectangle was dropped. Ask which character contains that point, so a one-glyph link is matched against its own position. Signed-off-by: Samuel Mehrbrodt Change-Id: I722aa2c83af50e99b8c06e051ff0bff9b0f76e0a Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/10251 Reviewed-by: Mike Kaganski Tested-by: Jenkins CPCI (cherry picked from commit 1284d1a3e3b93f02bd31068c9898743fa65e6e64) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/210922 Tested-by: Jenkins Reviewed-by: Xisco Fauli --- .../core/text/EnhancedPDFExportHelper.cxx | 6 +- vcl/qa/cppunit/pdfexport/pdfexport2.cxx | 126 ++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/sw/source/core/text/EnhancedPDFExportHelper.cxx b/sw/source/core/text/EnhancedPDFExportHelper.cxx index 59f7ef16b86c..d65f46064005 100644 --- a/sw/source/core/text/EnhancedPDFExportHelper.cxx +++ b/sw/source/core/text/EnhancedPDFExportHelper.cxx @@ -444,6 +444,8 @@ bool lcl_TryMoveToNonHiddenField(SwEditShell& rShell, const SwTextNode& rNd, con ::std::vector ret; SwRects rects; rShell.GetLayout()->CalcFrameRects(*rShell.GetCursor_(), rects, SwRootFrame::RectsMode::NoAnchoredFlys); + auto const [pStart, pEnd] = rShell.GetCursor_()->StartEnd(); + for (SwRect const& rRect : rects) { Point center(rRect.Center()); @@ -451,8 +453,10 @@ bool lcl_TryMoveToNonHiddenField(SwEditShell& rShell, const SwTextNode& rNd, con SwCursorMoveState cms(CursorMoveState::NONE); cms.m_pSpecialPos = &special; cms.m_bFieldInfo = true; + // the centre of a one-glyph rectangle falls in that glyph's second half, so the hit + // test below returns the character that contains the point + cms.m_bPosMatchesBounds = true; SwPosition pos(rShell.GetDoc()->GetNodes()); - auto const [pStart, pEnd] = rShell.GetCursor_()->StartEnd(); if (rShell.GetLayout()->GetModelPositionForViewPoint(&pos, center, &cms) && *pStart <= pos && pos <= *pEnd) { diff --git a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx index be08bb47a7eb..58b238adb029 100644 --- a/vcl/qa/cppunit/pdfexport/pdfexport2.cxx +++ b/vcl/qa/cppunit/pdfexport/pdfexport2.cxx @@ -20,16 +20,24 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -511,6 +519,124 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest2, testTocLink) CPPUNIT_ASSERT(pPdfPage->hasLinks()); } +// Inserts three cross-reference fields to a one-digit bookmark target, separated only by +// ", ", the way consecutive caption numbers are. +void lcl_insertAdjacentReferenceFields(const uno::Reference& xComponent, + float fCharHeight) +{ + uno::Reference xTextDocument(xComponent, uno::UNO_QUERY); + uno::Reference xText = xTextDocument->getText(); + uno::Reference xCursor = xText->createTextCursor(); + uno::Reference xFactory(xComponent, uno::UNO_QUERY); + uno::Reference xCursorProps(xCursor, uno::UNO_QUERY); + xCursorProps->setPropertyValue(u"CharHeight"_ustr, uno::Any(fCharHeight)); + + xText->insertString(xCursor, u"1"_ustr, /*bAbsorb=*/false); + xCursor->goLeft(1, /*bExpand=*/true); + uno::Reference xBookmark( + xFactory->createInstance(u"com.sun.star.text.Bookmark"_ustr), uno::UNO_QUERY); + uno::Reference xBookmarkName(xBookmark, uno::UNO_QUERY); + xBookmarkName->setName(u"Target"_ustr); + xText->insertTextContent(xCursor, xBookmark, /*bAbsorb=*/true); + + xCursor->gotoEnd(/*bExpand=*/false); + xText->insertControlCharacter(xCursor, text::ControlCharacter::PARAGRAPH_BREAK, + /*bAbsorb=*/false); + + auto insertReferenceField = [&]() { + xCursor->gotoEnd(/*bExpand=*/false); + uno::Reference xField( + xFactory->createInstance(u"com.sun.star.text.TextField.GetReference"_ustr), + uno::UNO_QUERY); + uno::Reference xFieldProps(xField, uno::UNO_QUERY); + xFieldProps->setPropertyValue(u"ReferenceFieldSource"_ustr, + uno::Any(sal_Int16(text::ReferenceFieldSource::BOOKMARK))); + xFieldProps->setPropertyValue(u"ReferenceFieldPart"_ustr, + uno::Any(sal_Int16(text::ReferenceFieldPart::TEXT))); + xFieldProps->setPropertyValue(u"SourceName"_ustr, uno::Any(u"Target"_ustr)); + xField->attach(xCursor); + }; + + insertReferenceField(); + xCursor->gotoEnd(/*bExpand=*/false); + xText->insertString(xCursor, u", "_ustr, /*bAbsorb=*/false); + insertReferenceField(); + xCursor->gotoEnd(/*bExpand=*/false); + xText->insertString(xCursor, u", "_ustr, /*bAbsorb=*/false); + insertReferenceField(); +} + +int lcl_countAnnotations(const std::unique_ptr& pPdfDocument) +{ + std::unique_ptr pPdfPage = pPdfDocument->openPage(/*nIndex=*/0); + CPPUNIT_ASSERT(pPdfPage); + return pPdfPage->getAnnotationCount(); +} + +CPPUNIT_TEST_FIXTURE(PdfExportTest2, testAdjacentReferenceFieldLinks_12) +{ + // Every one of three adjacent one-digit cross-reference fields gets its own clickable + // area in the exported PDF, at every font size. + loadFromURL(u"private:factory/swriter"_ustr); + lcl_insertAdjacentReferenceFields(mxComponent, 12.0f); + + save(TestFilter::PDF_WRITER); + + OString aMessage("font size " + OString::number(12.0f)); + CPPUNIT_ASSERT_EQUAL_MESSAGE( + aMessage.getStr(), 3, lcl_countAnnotations(parsePDFExport(vcl::pdf::PDFiumLibrary::get()))); +} + +CPPUNIT_TEST_FIXTURE(PdfExportTest2, testAdjacentReferenceFieldLinks_10) +{ + // Every one of three adjacent one-digit cross-reference fields gets its own clickable + // area in the exported PDF, at every font size. + loadFromURL(u"private:factory/swriter"_ustr); + lcl_insertAdjacentReferenceFields(mxComponent, 10.0f); + + save(TestFilter::PDF_WRITER); + + OString aMessage("font size " + OString::number(10.0f)); + CPPUNIT_ASSERT_EQUAL_MESSAGE( + aMessage.getStr(), 3, lcl_countAnnotations(parsePDFExport(vcl::pdf::PDFiumLibrary::get()))); +} + +CPPUNIT_TEST_FIXTURE(PdfExportTest2, testAdjacentReferenceFieldLinks_8) +{ + // Every one of three adjacent one-digit cross-reference fields gets its own clickable + // area in the exported PDF, at every font size. + loadFromURL(u"private:factory/swriter"_ustr); + lcl_insertAdjacentReferenceFields(mxComponent, 8.0f); + + save(TestFilter::PDF_WRITER); + + OString aMessage("font size " + OString::number(8.0f)); + CPPUNIT_ASSERT_EQUAL_MESSAGE( + aMessage.getStr(), 3, lcl_countAnnotations(parsePDFExport(vcl::pdf::PDFiumLibrary::get()))); +} + +CPPUNIT_TEST_FIXTURE(PdfExportTest2, testAdjacentReferenceFieldLinksWideCaret) +{ + // The width of the text caret is a desktop setting, and can be set wider than a small + // glyph. The clickable areas stay independent of it. + AllSettings aOldSettings(Application::GetSettings()); + comphelper::ScopeGuard aSettingsGuard( + [&aOldSettings] { Application::SetSettings(aOldSettings); }); + AllSettings aSettings(aOldSettings); + StyleSettings aStyleSettings(aSettings.GetStyleSettings()); + // 6 pixels is 90 twips at 96 dots per inch, wider than an 8 point digit. + aStyleSettings.SetCursorSize(6); + aSettings.SetStyleSettings(aStyleSettings); + Application::SetSettings(aSettings); + + loadFromURL(u"private:factory/swriter"_ustr); + lcl_insertAdjacentReferenceFields(mxComponent, 8.0f); + + save(TestFilter::PDF_WRITER); + + CPPUNIT_ASSERT_EQUAL(3, lcl_countAnnotations(parsePDFExport(vcl::pdf::PDFiumLibrary::get()))); +} + CPPUNIT_TEST_FIXTURE(PdfExportTest2, testReduceSmallImage) { // Load the Writer document.