tdf#163240 vcl,sw: keep one Link tag for a wrapped link

Regression after commit a71da3b7a8
(tdf#156565 sw: PDF/UA export: split Link SE at line break,
2023-11-01), which closed the tag at each line break so that every
annotation had a tag to nest in.

A structure element could reference one annotation only, the
LinkAnnotation attribute being a value in a std::map; give it a vector
and emit an OBJR kid and a parent tree entry per annotation, so each
StructParent resolves to that tag. A page or column break still ends
the tag.

Language, baseline shift and decoration now come from the link's first
portion, at a line break as much as within a line - splitting the tag
at a break would only make the result depend on where the line ends.
LATER: subdivide the Link by Spans, which covers both.

testTdf157816Link and testTdf142806 expected a tag per annotation.

Signed-off-by: Mike Kaganski <mike.kaganski@collabora.com>
Change-Id: I493439a33b2cbc0eecf1af6b300859cd78ee87ba
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/11541
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
(cherry picked from commit ade287e6a4edf7476e667f48082261deeddd96d5)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211077
Tested-by: Jenkins
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
Mike Kaganski
2026-09-18 08:45:00 +02:00
committed by Xisco Fauli
parent 692482b522
commit 79b06ca748
7 changed files with 446 additions and 522 deletions
-2
View File
@@ -180,8 +180,6 @@ class SwTaggedPDFHelper
SwTaggedPDFHelper( const Num_Info* pNumInfo, const Frame_Info* pFrameInfo, const Por_Info* pPorInfo,
OutputDevice const & rOut );
~SwTaggedPDFHelper();
static void EndCurrentLink(OutputDevice const&);
};
/*
+44 -31
View File
@@ -19,6 +19,8 @@
#include <EnhancedPDFExportHelper.hxx>
#include <algorithm>
#include <com/sun/star/embed/XEmbeddedObject.hpp>
#include <com/sun/star/i18n/ScriptType.hpp>
#include <com/sun/star/drawing/XShape.hpp>
@@ -164,8 +166,16 @@ struct SwEnhancedPDFState
OUString StyleName;
};
struct Link
{
const SwTextAttr* pAttr;
sal_Int32 nStructElement;
// the annotations already nested in it
std::vector<sal_Int32> AnnotIds;
};
::std::optional<Span> m_oCurrentSpan;
::std::optional<SwTextAttr const*> m_oCurrentLink;
std::optional<Link> m_oCurrentLink;
SwEnhancedPDFState(LanguageType const eLanguageDefault)
: m_eLanguageDefault(eLanguageDefault)
@@ -722,15 +732,34 @@ namespace {
// link the link annotation to the link structured element
void LinkLinkLink(vcl::PDFExtOutDevData & rPDFExtOutDevData, SwRect const& rRect)
{
const sal_Int32 nCurrentSE(rPDFExtOutDevData.GetCurrentStructureElement());
auto& roCurrentLink(rPDFExtOutDevData.GetSwPDFState()->m_oCurrentLink);
if (roCurrentLink && roCurrentLink->nStructElement == -1)
{
// the first call after opening a link is for the link's own SE
roCurrentLink->nStructElement = nCurrentSE;
}
const LinkIdMap& rLinkIdMap(rPDFExtOutDevData.GetSwPDFState()->m_LinkIdMap);
const Point aCenter = rRect.Center();
auto aIter = std::find_if(rLinkIdMap.begin(), rLinkIdMap.end(),
[&aCenter](const IdMapEntry& rEntry) { return rEntry.first.Contains(aCenter); });
if (aIter != rLinkIdMap.end())
if (aIter == rLinkIdMap.end())
return;
sal_Int32 nLinkId = (*aIter).second;
if (roCurrentLink && roCurrentLink->nStructElement == nCurrentSE) // not a nested SE
{
sal_Int32 nLinkId = (*aIter).second;
rPDFExtOutDevData.SetStructureAttributeNumerical(vcl::pdf::PDFWriter::LinkAnnotation, nLinkId);
// every portion of a line finds that line's annotation again
if (std::find(roCurrentLink->AnnotIds.begin(), roCurrentLink->AnnotIds.end(), nLinkId)
!= roCurrentLink->AnnotIds.end())
{
return;
}
roCurrentLink->AnnotIds.push_back(nLinkId);
}
rPDFExtOutDevData.SetStructureAttributeNumerical(vcl::pdf::PDFWriter::LinkAnnotation,
nLinkId);
}
}
@@ -1900,20 +1929,6 @@ void SwTaggedPDFHelper::EndStructureElements()
CheckRestoreTag();
}
void SwTaggedPDFHelper::EndCurrentLink(OutputDevice const& rOut)
{
vcl::PDFExtOutDevData *const pPDFExtOutDevData(
dynamic_cast<vcl::PDFExtOutDevData *>(rOut.GetExtOutDevData()));
if (pPDFExtOutDevData && pPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink)
{
pPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.reset();
pPDFExtOutDevData->EndStructureElement();
#if OSL_DEBUG_LEVEL > 1
aStructStack.pop_back();
#endif
}
}
void SwTaggedPDFHelper::EndCurrentAll()
{
if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan)
@@ -1960,7 +1975,8 @@ bool SwTaggedPDFHelper::CheckContinueSpan(
|| !mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink);
if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink)
{
if (pInetFormatAttr && pInetFormatAttr == *mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink)
if (pInetFormatAttr
&& pInetFormatAttr == mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink->pAttr)
{
return true;
}
@@ -2050,17 +2066,6 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
aPDFType = aSpanString;
break;
case PortionType::Fly:
// if a link is split by a fly overlap, then there will be multiple
// annotations for the link, and hence there must be multiple SEs,
// so every annotation has its own SE.
if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink)
{
mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.reset();
EndTag();
}
break;
case PortionType::Lay :
case PortionType::Text :
case PortionType::Para :
@@ -2077,10 +2082,18 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
nPDFType = sal_uInt16(vcl::pdf::StructElement::Link);
aPDFType = aLinkString;
assert(!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink);
mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.emplace(pInetFormatAttr);
mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.emplace(
SwEnhancedPDFState::Link{ pInetFormatAttr, -1, {} });
// leave it open to let next portion decide to merge or close
--m_nEndStructureElement;
}
else
{
// attach this portion's annotation
SwRect aPorRect;
rInf.CalcRect(*pPor, &aPorRect);
LinkLinkLink(*mpPDFExtOutDevData, aPorRect);
}
}
// Emphasis
else if (sStyleName == constEmphasisStyleName)
-2
View File
@@ -263,8 +263,6 @@ void SwTextPainter::DrawTextLine( const SwRect &rPaint, SwSaveClip &rClip,
roTaggedParagraph.emplace(nullptr, &aFrameInfo, nullptr, *GetInfo().GetOut());
}
SwTaggedPDFHelper::EndCurrentLink(*GetInfo().GetOut());
// Optimization!
SwTwips nMaxRight = std::min<SwTwips>( rPaint.Right(), Right() );
const SwTwips nTmpLeft = GetInfo().X();
+2
View File
@@ -602,6 +602,8 @@ struct PDFStructureElement
std::map<PDFWriter::StructAttribute, PDFStructureAttribute >
m_aAttributes;
::std::vector<sal_Int32> m_AnnotIds;
// a Link has one annotation per line, and per fly gap
std::vector<sal_Int32> m_LinkAnnotIds;
tools::Rectangle m_aBBox;
OUString m_aActualText;
OUString m_aAltText;
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta><dc:title>Link on several lines</dc:title></office:meta>
<office:font-face-decls>
<style:font-face style:name="Liberation Serif" svg:font-family="'Liberation Serif'" style:font-family-generic="roman" style:font-pitch="variable"/>
</office:font-face-decls>
<office:styles>
<style:default-style style:family="paragraph">
<style:text-properties style:font-name="Liberation Serif" fo:font-size="12pt" fo:language="en" fo:country="US" style:font-name-asian="Liberation Serif" style:font-size-asian="12pt" style:font-name-complex="Liberation Serif" style:font-size-complex="12pt"/>
</style:default-style>
<style:style style:name="Standard" style:family="paragraph" style:class="text"/>
<style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text">
<style:text-properties fo:color="#000080" style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="font-color"/>
</style:style>
</office:styles>
<office:automatic-styles>
<style:page-layout style:name="pm1">
<style:page-layout-properties fo:page-width="21cm" fo:page-height="29.7cm" style:print-orientation="portrait" fo:margin-top="2cm" fo:margin-bottom="2cm" fo:margin-left="2cm" fo:margin-right="2cm" style:writing-mode="lr-tb"/>
</style:page-layout>
</office:automatic-styles>
<office:master-styles>
<style:master-page style:name="Standard" style:page-layout-name="pm1"/>
</office:master-styles>
<office:body>
<office:text>
<text:p text:style-name="Standard"><text:a xlink:type="simple" xlink:href="https://www.example.com/" office:name="Example link" text:style-name="Internet_20_link">This is a hyperlink whose text is deliberately long enough that the Writer layout has to break it onto a second line of the same paragraph, which used to produce one Link structure element per line.</text:a></text:p>
</office:text>
</office:body>
</office:document>
File diff suppressed because it is too large Load Diff
+33 -26
View File
@@ -940,30 +940,6 @@ OString PDFWriterImpl::emitStructureAttributes( PDFStructureElement& i_rEle )
{
appendStructureAttributeLine( attribute.first, attribute.second, aTable, false );
}
else if( attribute.first == PDFWriter::LinkAnnotation )
{
sal_Int32 nLink = attribute.second.nValue;
std::map< sal_Int32, sal_Int32 >::const_iterator link_it =
m_aLinkPropertyMap.find( nLink );
if( link_it != m_aLinkPropertyMap.end() )
nLink = link_it->second;
if( nLink >= 0 && o3tl::make_unsigned(nLink) < m_aLinks.size() )
{
AppendAnnotKid(i_rEle, m_aLinks[nLink]);
}
else
{
OSL_FAIL( "unresolved link id for Link structure" );
SAL_INFO("vcl.pdfwriter", "unresolved link id " << nLink << " for Link structure");
if (g_bDebugDisableCompression)
{
OString aLine = "unresolved link id " +
OString::number( nLink ) +
" for Link structure";
emitComment( aLine.getStr() );
}
}
}
else if (attribute.first == PDFWriter::NoteAnnotation)
{
sal_Int32 nNote = attribute.second.nValue;
@@ -1250,6 +1226,27 @@ sal_Int32 PDFWriterImpl::emitStructure( PDFStructureElement& rEle )
}
}
}
for (const auto id : rEle.m_LinkAnnotIds)
{
sal_Int32 nLink(id);
const auto it(m_aLinkPropertyMap.find(nLink));
if (it != m_aLinkPropertyMap.end())
nLink = it->second;
if (0 <= nLink && o3tl::make_unsigned(nLink) < m_aLinks.size())
{
AppendAnnotKid(rEle, m_aLinks[nLink]);
}
else
{
SAL_WARN("vcl.pdfwriter", "unresolved link id " << nLink << " for Link structure");
if (g_bDebugDisableCompression)
{
const OString aComment
= "unresolved link id " + OString::number(nLink) + " for Link structure";
emitComment(aComment.getStr());
}
}
}
if( ! rEle.m_aKids.empty() )
{
unsigned int i = 0;
@@ -10997,8 +10994,18 @@ bool PDFWriterImpl::setStructureAttributeNumerical( enum PDFWriter::StructAttrib
}
}
if( bInsert )
m_aStructure[ m_nCurrentStructElement ].m_aAttributes[ eAttr ] = PDFStructureAttribute( nValue );
if (bInsert)
{
if (eAttr == PDFWriter::LinkAnnotation)
{
m_aStructure[m_nCurrentStructElement].m_LinkAnnotIds.push_back(nValue);
}
else
{
m_aStructure[m_nCurrentStructElement].m_aAttributes[eAttr]
= PDFStructureAttribute(nValue);
}
}
else if( m_nCurrentStructElement > 0 && m_bEmitStructure )
SAL_INFO("vcl.pdfwriter",
"rejecting setStructureAttributeNumerical( " << getAttributeTag( eAttr )