tdf#163240 sw: subdivide a Link by Spans

The commit "tdf#163240 vcl,sw: keep one Link tag for a wrapped link"
made language, baseline shift and decoration come from the link's first
portion for the whole link, and noted subdividing by Spans as the fix:
those three are attributes of the Link tag, and there is now one tag per
hyperlink, so a change inside it had nowhere to go.

Open a Span in the Link instead. A span waits inside its link, so two
tags can be open at once, and an annotation goes to the link's own tag -
vcl takes it on a Link only, and a styled run crossing a line break
would otherwise leave that line's annotation without a StructParent.

Signed-off-by: Mike Kaganski <mike.kaganski@collabora.com>
Change-Id: I302d32f66fcb4e3b1eaf63b136bdcf96aaea17b7
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/11543
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
(cherry picked from commit 8f52d63f897aaf710dfe4cd9771f39689471abfd)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211101
Tested-by: Jenkins
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
Mike Kaganski
2026-09-18 14:21:04 +02:00
committed by Xisco Fauli
parent 1d901c0707
commit 6e785d7b0b
6 changed files with 434 additions and 67 deletions
+10 -1
View File
@@ -167,10 +167,19 @@ class SwTaggedPDFHelper
void BeginInlineStructureElements();
void EndStructureElements();
// what an open link or span leaves for this portion to do
enum class Continuation
{
None, // nothing is open that covers it
Whole, // the open tag covers it, so no new tag
SpanInLink // the link goes on, but its properties do not
};
void EndCurrentAll();
void EndCurrentSpan();
void CreateCurrentSpan(SwTextPaintInfo const& rInf, OUString const& rStyleName);
bool CheckContinueSpan(SwTextPaintInfo const& rInf, std::u16string_view rStyleName, SwTextAttr const* pInetFormatAttr);
Continuation CheckContinuation(SwTextPaintInfo const& rInf, OUString const& rStyleName,
SwTextAttr const* pInetFormatAttr);
bool CheckReopenTag();
void CheckRestoreTag() const;
+106 -66
View File
@@ -164,6 +164,8 @@ struct SwEnhancedPDFState
SwFontScript nScript;
LanguageType nLang;
OUString StyleName;
bool operator==(const Span&) const = default;
};
struct Link
@@ -172,6 +174,8 @@ struct SwEnhancedPDFState
sal_Int32 nStructElement;
// the annotations already nested in it
std::vector<sal_Int32> AnnotIds;
// the properties the Link's own tag carries
Span aSpan;
};
::std::optional<Span> m_oCurrentSpan;
@@ -732,7 +736,7 @@ void SwTaggedPDFHelper::EndTag()
void SwTaggedPDFHelper::DeferTag()
{
assert(!m_aOpenedTags.empty());
assert(mpPDFExtOutDevData->GetSwPDFState()->m_DeferredTags.empty());
assert(mpPDFExtOutDevData->GetSwPDFState()->m_DeferredTags.size() <= 1); // a span in a link
assert(mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan
|| mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink);
mpPDFExtOutDevData->GetSwPDFState()->m_DeferredTags.push_back(m_aOpenedTags.back());
@@ -750,9 +754,18 @@ void SwTaggedPDFHelper::EndDeferredTag()
{
assert(IsDeferredTagCurrent());
auto& rState(*mpPDFExtOutDevData->GetSwPDFState());
assert(!rState.m_oCurrentSpan || !rState.m_oCurrentLink); // one reset covers both
rState.m_oCurrentSpan.reset();
rState.m_oCurrentLink.reset();
// one deferred tag per open span or link
assert(rState.m_DeferredTags.size()
== (rState.m_oCurrentSpan ? 1u : 0u) + (rState.m_oCurrentLink ? 1u : 0u));
// a span is inside the link, so it is the innermost of the two
if (rState.m_oCurrentSpan)
{
rState.m_oCurrentSpan.reset();
}
else
{
rState.m_oCurrentLink.reset();
}
rState.m_DeferredTags.pop_back();
EndTag();
}
@@ -787,7 +800,11 @@ namespace {
return;
sal_Int32 nLinkId = (*aIter).second;
if (roCurrentLink && roCurrentLink->nStructElement == nCurrentSE) // not a nested SE
// a Link of its own, for a footnote or a fly, is not the link being subdivided
const auto& rDeferredTags(rPDFExtOutDevData.GetSwPDFState()->m_DeferredTags);
const bool bInSpanOfLink(roCurrentLink && rPDFExtOutDevData.GetSwPDFState()->m_oCurrentSpan
&& !rDeferredTags.empty() && rDeferredTags.back() == nCurrentSE);
if (roCurrentLink && (roCurrentLink->nStructElement == nCurrentSE || bInSpanOfLink))
{
// every portion of a line finds that line's annotation again
if (std::find(roCurrentLink->AnnotIds.begin(), roCurrentLink->AnnotIds.end(), nLinkId)
@@ -796,6 +813,15 @@ namespace {
return;
}
roCurrentLink->AnnotIds.push_back(nLinkId);
if (bInSpanOfLink)
{
// vcl accepts the attribute on a Link, so set it on the link
rPDFExtOutDevData.SetCurrentStructureElement(roCurrentLink->nStructElement);
rPDFExtOutDevData.SetStructureAttributeNumerical(
vcl::pdf::PDFWriter::LinkAnnotation, nLinkId);
rPDFExtOutDevData.SetCurrentStructureElement(nCurrentSE);
return;
}
}
rPDFExtOutDevData.SetStructureAttributeNumerical(vcl::pdf::PDFWriter::LinkAnnotation,
nLinkId);
@@ -1975,73 +2001,78 @@ void SwTaggedPDFHelper::EndCurrentSpan()
EndDeferredTag();
}
namespace
{
// the properties a Span tag would carry for this portion
SwEnhancedPDFState::Span MakeSpan(const SwTextPaintInfo& rInf, const OUString& rStyleName)
{
return { .eUnderline = rInf.GetFont()->GetUnderline(),
.eOverline = rInf.GetFont()->GetOverline(),
.eStrikeout = rInf.GetFont()->GetStrikeout(),
.eFontEmphasis = rInf.GetFont()->GetEmphasisMark(),
.nEscapement = rInf.GetFont()->GetEscapement(),
.nScript = rInf.GetFont()->GetActual(),
.nLang = rInf.GetFont()->GetLanguage(),
.StyleName = rStyleName };
}
}
void SwTaggedPDFHelper::CreateCurrentSpan(
SwTextPaintInfo const& rInf, OUString const& rStyleName)
{
assert(!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan);
mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan.emplace(
SwEnhancedPDFState::Span{
rInf.GetFont()->GetUnderline(),
rInf.GetFont()->GetOverline(),
rInf.GetFont()->GetStrikeout(),
rInf.GetFont()->GetEmphasisMark(),
rInf.GetFont()->GetEscapement(),
rInf.GetFont()->GetActual(),
rInf.GetFont()->GetLanguage(),
rStyleName});
mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan.emplace(MakeSpan(rInf, rStyleName));
}
bool SwTaggedPDFHelper::CheckContinueSpan(
SwTextPaintInfo const& rInf, std::u16string_view const rStyleName,
SwTextAttr const*const pInetFormatAttr)
SwTaggedPDFHelper::Continuation
SwTaggedPDFHelper::CheckContinuation(SwTextPaintInfo const& rInf, OUString const& rStyleName,
SwTextAttr const* const pInetFormatAttr)
{
// for now, don't create span inside of link - this should be very rare
// situation and it looks complicated to implement.
assert(!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan
|| !mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink);
if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink)
auto& rState(*mpPDFExtOutDevData->GetSwPDFState());
if (rState.m_oCurrentLink)
{
if (pInetFormatAttr
&& pInetFormatAttr == mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink->pAttr)
if (!pInetFormatAttr || pInetFormatAttr != rState.m_oCurrentLink->pAttr)
{
return true;
}
else
{
if (IsDeferredTagCurrent()) // a ruby leaves it to the paragraph
// the span is inside the link, so it closes first
if (rState.m_oCurrentSpan && IsDeferredTagCurrent())
{
EndCurrentSpan();
}
if (IsDeferredTagCurrent())
{
EndDeferredTag();
}
return false;
return Continuation::None;
}
}
if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan && pInetFormatAttr)
{
if (IsDeferredTagCurrent()) // a fly leaves it to the paragraph
// the link goes on; a property change subdivides it instead of splitting it
const SwEnhancedPDFState::Span aSpan(MakeSpan(rInf, rStyleName));
if (rState.m_oCurrentSpan)
{
EndCurrentSpan();
if (aSpan == *rState.m_oCurrentSpan)
{
return Continuation::Whole;
}
if (IsDeferredTagCurrent())
{
EndCurrentSpan();
}
}
return false;
return aSpan == rState.m_oCurrentLink->aSpan ? Continuation::Whole
: Continuation::SpanInLink;
}
if (!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan)
return false;
SwEnhancedPDFState::Span const& rCurrent(*mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan);
bool const ret(rCurrent.eUnderline == rInf.GetFont()->GetUnderline()
&& rCurrent.eOverline == rInf.GetFont()->GetOverline()
&& rCurrent.eStrikeout == rInf.GetFont()->GetStrikeout()
&& rCurrent.eFontEmphasis == rInf.GetFont()->GetEmphasisMark()
&& rCurrent.nEscapement == rInf.GetFont()->GetEscapement()
&& rCurrent.nScript == rInf.GetFont()->GetActual()
&& rCurrent.nLang == rInf.GetFont()->GetLanguage()
&& rCurrent.StyleName == rStyleName);
if (!ret && IsDeferredTagCurrent()) // a fly leaves it to the paragraph
if (!rState.m_oCurrentSpan)
{
return Continuation::None;
}
if (!pInetFormatAttr && MakeSpan(rInf, rStyleName) == *rState.m_oCurrentSpan)
{
return Continuation::Whole;
}
if (IsDeferredTagCurrent()) // a fly in the span leaves it to the paragraph
{
EndCurrentSpan();
}
return ret;
return Continuation::None;
}
void SwTaggedPDFHelper::BeginInlineStructureElements()
@@ -2072,12 +2103,14 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
SwStyleNameMapper::FillProgName( pCharFormat->GetName(), sStyleName, SwGetPoolIdFromName::TxtColl );
}
const OUString aStyleName(sStyleName.toString());
// note: ILSE may be nested, so only end the span if needed to start new one
bool const isContinueSpan(CheckContinueSpan(rInf, sStyleName.toString(), pInetFormatAttr));
const Continuation eContinuation(CheckContinuation(rInf, aStyleName, pInetFormatAttr));
const sal_Int32 nParentSE(mpPDFExtOutDevData->GetCurrentStructureElement());
const auto& rDeferredTags(mpPDFExtOutDevData->GetSwPDFState()->m_DeferredTags);
// only one tag waits for the next portion; a ruby closes the rest with itself
const bool bCanDefer(rDeferredTags.empty());
// a span may be deferred inside its link, but not inside a ruby
const bool bCanDefer(rDeferredTags.empty() || rDeferredTags.back() == nParentSE);
sal_uInt16 nPDFType = USHRT_MAX;
OUString aPDFType;
bool bDeferTag(false);
@@ -2113,7 +2146,7 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
// Check for Link:
if( pInetFormatAttr )
{
if (!isContinueSpan)
if (eContinuation == Continuation::None)
{
nPDFType = sal_uInt16(vcl::pdf::StructElement::Link);
aPDFType = aLinkString;
@@ -2121,7 +2154,8 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
{
assert(!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink);
mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.emplace(
SwEnhancedPDFState::Link{ pInetFormatAttr, -1, {} });
SwEnhancedPDFState::Link{
pInetFormatAttr, -1, {}, MakeSpan(rInf, aStyleName) });
bDeferTag = true;
}
}
@@ -2131,12 +2165,18 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
SwRect aPorRect;
rInf.CalcRect(*pPor, &aPorRect);
LinkLinkLink(*mpPDFExtOutDevData, aPorRect);
if (eContinuation == Continuation::SpanInLink)
{
nPDFType = sal_uInt16(vcl::pdf::StructElement::Span);
aPDFType = aSpanString;
bDeferSpan = bCanDefer;
}
}
}
// Emphasis
else if (sStyleName == constEmphasisStyleName)
{
if (!isContinueSpan)
if (eContinuation == Continuation::None)
{
nPDFType = sal_uInt16(vcl::pdf::StructElement::Emphasis);
aPDFType = constEmphasisStyleName;
@@ -2146,7 +2186,7 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
// Strong
else if (sStyleName == constStrongEmphasisStyleName)
{
if (!isContinueSpan)
if (eContinuation == Continuation::None)
{
nPDFType = sal_uInt16(vcl::pdf::StructElement::Strong);
aPDFType = constStrongEmphasisStyleName;
@@ -2156,7 +2196,7 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
// Check for Quote/Code character style:
else if (sStyleName == aQuotation)
{
if (!isContinueSpan)
if (eContinuation == Continuation::None)
{
nPDFType = sal_uInt16(vcl::pdf::StructElement::Quote);
aPDFType = aQuoteString;
@@ -2165,14 +2205,14 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
}
else if (sStyleName == aSourceText)
{
if (!isContinueSpan)
if (eContinuation == Continuation::None)
{
nPDFType = sal_uInt16(vcl::pdf::StructElement::Code);
aPDFType = aCodeString;
bDeferSpan = bCanDefer;
}
}
else if (!isContinueSpan)
else if (eContinuation == Continuation::None)
{
const LanguageType nCurrentLanguage = rInf.GetFont()->GetLanguage();
const SwFontScript nFont = rInf.GetFont()->GetActual();
@@ -2274,7 +2314,7 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
// for FootnoteNum, is called twice: outer generates Lbl, inner Link
case PortionType::FootnoteNum:
assert(!isContinueSpan); // is at start
assert(eContinuation == Continuation::None); // is at start
if (mpPorInfo->m_Mode == 0)
{ // tdf#152218 link both directions
nPDFType = sal_uInt16(vcl::pdf::StructElement::Link);
@@ -2285,7 +2325,7 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
case PortionType::Number:
case PortionType::Bullet:
case PortionType::GrfNum:
assert(!isContinueSpan); // is at start
assert(eContinuation == Continuation::None); // is at start
if (mpPorInfo->m_Mode == 1)
{ // only works for multiple lines via wrapper from PaintSwFrame
nPDFType = sal_uInt16(vcl::pdf::StructElement::LILabel);
@@ -2307,7 +2347,7 @@ void SwTaggedPDFHelper::BeginInlineStructureElements()
BeginTag( static_cast<vcl::pdf::StructElement>(nPDFType), aPDFType );
if (bDeferSpan)
{
CreateCurrentSpan(rInf, sStyleName.toString());
CreateCurrentSpan(rInf, aStyleName);
bDeferTag = true;
}
if (bDeferTag)
@@ -0,0 +1,32 @@
<?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:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" 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 with a span and an as-character frame</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:style style:name="T1" style:family="text">
<style:text-properties fo:language="de" fo:country="DE"/>
</style:style>
<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">plain <text:a xlink:type="simple" xlink:href="https://www.example.com/" office:name="Fly link" text:style-name="Internet_20_link">before <text:span text:style-name="T1">deutsch <draw:frame text:anchor-type="as-char" svg:width="2cm" svg:height="1cm" draw:z-index="0"><draw:text-box><text:p text:style-name="Standard">inner</text:p></draw:text-box></draw:frame> weiter</text:span> after</text:a></text:p>
</office:text>
</office:body>
</office:document>
@@ -0,0 +1,32 @@
<?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 with a wrapped language change</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:style style:name="T1" style:family="text">
<style:text-properties fo:language="de" fo:country="DE"/>
</style:style>
<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="Span link" text:style-name="Internet_20_link">This hyperlink starts in the document language and then <text:span text:style-name="T1">continues in German for long enough that the run itself has to break onto a second line, and the link ends with it</text:span></text:a></text:p>
</office:text>
</office:body>
</office:document>
@@ -0,0 +1,32 @@
<?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 with a language change</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:style style:name="T1" style:family="text">
<style:text-properties fo:language="de" fo:country="DE"/>
</style:style>
<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="Span link" text:style-name="Internet_20_link">before <text:span text:style-name="T1">deutsch</text:span> after</text:a></text:p>
</office:text>
</office:body>
</office:document>
+222
View File
@@ -3183,6 +3183,139 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest, testLinkWithRuby)
CPPUNIT_ASSERT(std::find(aRubyKids.begin(), aRubyKids.end(), "RT"_ostr) != aRubyKids.end());
}
CPPUNIT_TEST_FIXTURE(PdfExportTest, testLinkWithSpan)
{
uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) };
vcl::filter::PDFDocument aDocument;
loadFromFile(u"LinkWithSpan.fodt");
save(TestFilter::PDF_WRITER,
{ comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) });
SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
CPPUNIT_ASSERT(aDocument.Read(aStream));
vcl::filter::PDFObjectElement* pLinkSE(nullptr);
for (const auto& rDocElement : aDocument.GetElements())
{
auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
if (!pObject)
continue;
auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
if (pType && pType->GetValue() == "StructElem" && pS && pS->GetValue() == "Link")
{
CPPUNIT_ASSERT_MESSAGE("a language change must not split the link", !pLinkSE);
pLinkSE = pObject;
}
}
CPPUNIT_ASSERT(pLinkSE);
// the link keeps the language of its first portion, so the German must not be on it
auto pLinkLang
= dynamic_cast<vcl::filter::PDFLiteralStringElement*>(pLinkSE->Lookup("Lang"_ostr));
if (pLinkLang)
{
CPPUNIT_ASSERT(pLinkLang->GetValue() != "de-DE"_ostr);
}
// the run that differs is a Span inside it, not a second Link
auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pLinkSE->Lookup("K"_ostr));
CPPUNIT_ASSERT(pKids);
size_t nMCID(0);
size_t nMCIDBeforeSpan(0);
vcl::filter::PDFObjectElement* pSpanSE(nullptr);
for (const auto pElement : pKids->GetElements())
{
if (dynamic_cast<vcl::filter::PDFNumberElement*>(pElement))
{
++nMCID;
continue;
}
auto pRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(pElement);
if (!pRef)
continue; // the OBJR for the annotation
auto pKid = pRef->LookupObject();
CPPUNIT_ASSERT(pKid);
auto pKidS = dynamic_cast<vcl::filter::PDFNameElement*>(pKid->Lookup("S"_ostr));
CPPUNIT_ASSERT(pKidS);
CPPUNIT_ASSERT_EQUAL("Span"_ostr, pKidS->GetValue());
CPPUNIT_ASSERT(!pSpanSE);
pSpanSE = pKid;
nMCIDBeforeSpan = nMCID;
}
CPPUNIT_ASSERT(pSpanSE);
CPPUNIT_ASSERT_EQUAL(size_t(2), nMCID);
// it reads between the text before it and the text after it
CPPUNIT_ASSERT_EQUAL(size_t(1), nMCIDBeforeSpan);
auto pSpanLang
= dynamic_cast<vcl::filter::PDFLiteralStringElement*>(pSpanSE->Lookup("Lang"_ostr));
CPPUNIT_ASSERT(pSpanLang);
CPPUNIT_ASSERT_EQUAL("de-DE"_ostr, pSpanLang->GetValue());
}
CPPUNIT_TEST_FIXTURE(PdfExportTest, testLinkSpanWithFly)
{
uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) };
vcl::filter::PDFDocument aDocument;
loadFromFile(u"LinkSpanWithFly.fodt");
save(TestFilter::PDF_WRITER,
{ comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) });
SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
CPPUNIT_ASSERT(aDocument.Read(aStream));
// the frame's own paragraph is current when the link ends, so neither the span nor
// the link closes there - one link, one span, and the frame nested in the span
vcl::filter::PDFObjectElement* pLinkSE(nullptr);
vcl::filter::PDFObjectElement* pSpanSE(nullptr);
for (const auto& rDocElement : aDocument.GetElements())
{
auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
if (!pObject)
continue;
auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
if (!pType || pType->GetValue() != "StructElem" || !pS)
continue;
if (pS->GetValue() == "Link")
{
CPPUNIT_ASSERT_MESSAGE("a frame must not split the link", !pLinkSE);
pLinkSE = pObject;
}
else if (pS->GetValue() == "Span")
{
CPPUNIT_ASSERT_MESSAGE("a frame must not split the span", !pSpanSE);
pSpanSE = pObject;
}
}
CPPUNIT_ASSERT(pLinkSE);
CPPUNIT_ASSERT(pSpanSE);
// the span is a kid of the link, and the frame a kid of the span
auto pSpanParent = dynamic_cast<vcl::filter::PDFReferenceElement*>(pSpanSE->Lookup("P"_ostr));
CPPUNIT_ASSERT(pSpanParent);
CPPUNIT_ASSERT_EQUAL(pLinkSE, pSpanParent->LookupObject());
auto pSpanKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pSpanSE->Lookup("K"_ostr));
CPPUNIT_ASSERT(pSpanKids);
size_t nDiv(0);
for (const auto pElement : pSpanKids->GetElements())
{
auto pRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(pElement);
if (!pRef)
continue;
auto pKid = pRef->LookupObject();
CPPUNIT_ASSERT(pKid);
auto pKidS = dynamic_cast<vcl::filter::PDFNameElement*>(pKid->Lookup("S"_ostr));
CPPUNIT_ASSERT(pKidS);
CPPUNIT_ASSERT_EQUAL("Div"_ostr, pKidS->GetValue());
++nDiv;
}
CPPUNIT_ASSERT_EQUAL(size_t(1), nDiv);
}
CPPUNIT_TEST_FIXTURE(PdfExportTest, testSpanWithFly)
{
uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) };
@@ -3238,6 +3371,95 @@ CPPUNIT_TEST_FIXTURE(PdfExportTest, testSpanWithFly)
CPPUNIT_ASSERT_GREATER(size_t(1), nMCID);
}
CPPUNIT_TEST_FIXTURE(PdfExportTest, testLinkSpanWraps)
{
uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) };
vcl::filter::PDFDocument aDocument;
loadFromFile(u"LinkSpanWraps.fodt");
save(TestFilter::PDF_WRITER,
{ comphelper::makePropertyValue(u"FilterData"_ustr, aFilterData) });
SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
CPPUNIT_ASSERT(aDocument.Read(aStream));
std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages();
CPPUNIT_ASSERT_EQUAL(size_t(1), aPages.size());
auto pAnnots = dynamic_cast<vcl::filter::PDFArrayElement*>(aPages[0]->Lookup("Annots"_ostr));
CPPUNIT_ASSERT(pAnnots);
// how many lines the link takes does not matter, only that the styled run breaks
CPPUNIT_ASSERT_GREATER(size_t(1), pAnnots->GetElements().size());
vcl::filter::PDFObjectElement* pLinkSE(nullptr);
for (const auto& rDocElement : aDocument.GetElements())
{
auto pObject = dynamic_cast<vcl::filter::PDFObjectElement*>(rDocElement.get());
if (!pObject)
continue;
auto pType = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("Type"_ostr));
auto pS = dynamic_cast<vcl::filter::PDFNameElement*>(pObject->Lookup("S"_ostr));
if (pType && pType->GetValue() == "StructElem" && pS && pS->GetValue() == "Link")
{
CPPUNIT_ASSERT_MESSAGE("a wrapped span must not split the link", !pLinkSE);
pLinkSE = pObject;
}
}
CPPUNIT_ASSERT(pLinkSE);
// both lines' annotations reach the Link, although the span is what is open
// across the break
auto pKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pLinkSE->Lookup("K"_ostr));
CPPUNIT_ASSERT(pKids);
std::vector<vcl::filter::PDFObjectElement*> aKidAnnots;
for (const auto pElement : pKids->GetElements())
{
auto pObjR = dynamic_cast<vcl::filter::PDFDictionaryElement*>(pElement);
if (!pObjR)
continue;
auto pOType = dynamic_cast<vcl::filter::PDFNameElement*>(pObjR->LookupElement("Type"_ostr));
CPPUNIT_ASSERT(pOType);
CPPUNIT_ASSERT_EQUAL("OBJR"_ostr, pOType->GetValue());
auto pRef
= dynamic_cast<vcl::filter::PDFReferenceElement*>(pObjR->LookupElement("Obj"_ostr));
CPPUNIT_ASSERT(pRef);
aKidAnnots.push_back(pRef->LookupObject());
}
CPPUNIT_ASSERT_EQUAL(pAnnots->GetElements().size(), aKidAnnots.size());
CPPUNIT_ASSERT_EQUAL(aKidAnnots.size(), std::set(aKidAnnots.begin(), aKidAnnots.end()).size());
// the span is what straddles the break, so it holds a run of text on each line
vcl::filter::PDFObjectElement* pSpanSE(nullptr);
for (const auto pElement : pKids->GetElements())
{
auto pRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(pElement);
if (!pRef)
continue;
auto pKid = pRef->LookupObject();
CPPUNIT_ASSERT(pKid);
auto pKidS = dynamic_cast<vcl::filter::PDFNameElement*>(pKid->Lookup("S"_ostr));
CPPUNIT_ASSERT(pKidS);
CPPUNIT_ASSERT_EQUAL("Span"_ostr, pKidS->GetValue());
CPPUNIT_ASSERT(!pSpanSE);
pSpanSE = pKid;
}
CPPUNIT_ASSERT(pSpanSE);
auto pSpanKids = dynamic_cast<vcl::filter::PDFArrayElement*>(pSpanSE->Lookup("K"_ostr));
CPPUNIT_ASSERT(pSpanKids);
CPPUNIT_ASSERT_GREATER(size_t(1), pSpanKids->GetElements().size());
// and every annotation carries a StructParent and is one of the Link's OBJR kids
for (const auto pElement : pAnnots->GetElements())
{
auto pAnnotRef = dynamic_cast<vcl::filter::PDFReferenceElement*>(pElement);
CPPUNIT_ASSERT(pAnnotRef);
auto pAnnot = pAnnotRef->LookupObject();
CPPUNIT_ASSERT(pAnnot);
CPPUNIT_ASSERT_MESSAGE("every annotation needs a StructParent",
pAnnot->Lookup("StructParent"_ostr));
CPPUNIT_ASSERT(std::find(aKidAnnots.begin(), aKidAnnots.end(), pAnnot) != aKidAnnots.end());
}
}
CPPUNIT_TEST_FIXTURE(PdfExportTest, testRubyWithSpan)
{
uno::Sequence aFilterData{ comphelper::makePropertyValue(u"PDFUACompliance"_ustr, true) };