sw/qa/filter/md/data/image-and-link.md |    1 
 sw/qa/filter/md/md.cxx                 |   61 +++++++++++++++++++++++++++++++++
 sw/source/filter/md/mdcallbcks.cxx     |   12 +++++-
 sw/source/filter/md/swmd.cxx           |   15 ++++++--
 sw/source/filter/md/swmd.hxx           |    3 +
 sw/source/filter/md/wrtmd.cxx          |   33 ++++++++++++++++-
 6 files changed, 117 insertions(+), 8 deletions(-)

New commits:
commit 01fd17ba378b7da7545d29ecbfd883c21a4c6fe4
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Thu Sep 11 08:36:04 2025 +0200
Commit:     Caolán McNamara <caolan.mcnam...@collabora.com>
CommitDate: Thu Sep 11 10:28:16 2025 +0200

    tdf#168341 sw markdown filter: handle links on images
    
    The bugdoc has an image which has a link: the link is lost on both
    import and export.
    
    The doc model for links is different for text an images: the text is
    covered by an SwFormatINetFormat hint, but images have their
    SwFlyFrameFormat, and that can contain an SwFormatURL.
    
    Fix the problem by looking at the currently pending attributes stack
    while inserting images: if a link is open, also set that on the image.
    The original link will be discarded, because it covers no text.
    
    Also fix the export to write markup when the fly format has an
    SwFormatURL.
    
    Change-Id: Ib94bcd0903835d6eb197ef2077075390305f37c8
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190787
    Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com>
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com>

diff --git a/sw/qa/filter/md/data/image-and-link.md 
b/sw/qa/filter/md/data/image-and-link.md
new file mode 100644
index 000000000000..75111ca945d5
--- /dev/null
+++ b/sw/qa/filter/md/data/image-and-link.md
@@ -0,0 +1 @@
+A [![alt](./myimage.png)](https://www.example.com) Z
diff --git a/sw/qa/filter/md/md.cxx b/sw/qa/filter/md/md.cxx
index 4de7d058ded0..5123f9172ecd 100644
--- a/sw/qa/filter/md/md.cxx
+++ b/sw/qa/filter/md/md.cxx
@@ -25,6 +25,7 @@
 #include <ndgrf.hxx>
 #include <itabenum.hxx>
 #include <ndtxt.hxx>
+#include <fmturl.hxx>
 
 namespace
 {
@@ -505,6 +506,66 @@ CPPUNIT_TEST_FIXTURE(Test, testTableColumnAdjustMdExport)
     CPPUNIT_ASSERT_EQUAL(aExpected, aActual);
 }
 
+CPPUNIT_TEST_FIXTURE(Test, testImageLinkMdImport)
+{
+    // Given a document with an image which has a link on it:
+    // When importing that document:
+    setImportFilterName("Markdown");
+    createSwDoc("image-and-link.md");
+
+    // Then make sure the link is not lost:
+    SwDocShell* pDocShell = getSwDocShell();
+    SwDoc* pDoc = pDocShell->GetDoc();
+    sw::FrameFormats<sw::SpzFrameFormat*>& rFlys = *pDoc->GetSpzFrameFormats();
+    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), rFlys.size());
+    sw::SpzFrameFormat& rFly = *rFlys[0];
+    const SwFormatURL& rURL = rFly.GetURL();
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: https://www.example.com/
+    // - Actual  :
+    // i.e. the image's item set didn't have a URL.
+    CPPUNIT_ASSERT_EQUAL(u"https://www.example.com/"_ustr, rURL.GetURL());
+}
+
+CPPUNIT_TEST_FIXTURE(Test, testImageLinkMdExport)
+{
+    // Given a document with an inline, linked image + link on it:
+    createSwDoc();
+    SwDocShell* pDocShell = getSwDocShell();
+    SwDoc* pDoc = pDocShell->GetDoc();
+    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();
+    pWrtShell->Insert(u"A "_ustr);
+    SfxItemSet aFrameSet(pDoc->GetAttrPool(), svl::Items<RES_FRMATR_BEGIN, 
RES_FRMATR_END - 1>);
+    SwFormatAnchor aAnchor(RndStdIds::FLY_AS_CHAR);
+    aFrameSet.Put(aAnchor);
+    Graphic aGraphic;
+    OUString aGraphicURL(u"./test.png"_ustr);
+    IDocumentContentOperations& rIDCO = pDoc->getIDocumentContentOperations();
+    SwCursor* pCursor = pWrtShell->GetCursor();
+    SwFlyFrameFormat* pFlyFormat
+        = rIDCO.InsertGraphic(*pCursor, aGraphicURL, OUString(), &aGraphic, 
&aFrameSet,
+                              /*pGrfAttrSet=*/nullptr, 
/*SwFrameFormat=*/nullptr);
+    SwNodeOffset nContentOffset = 
pFlyFormat->GetContent().GetContentIdx()->GetIndex();
+    SwGrfNode* pGrfNode = pDoc->GetNodes()[nContentOffset + 1]->GetGrfNode();
+    pGrfNode->SetTitle(u"mytitle"_ustr);
+    SwFormatURL aFormatURL;
+    aFormatURL.SetURL(u"https://x.com"_ustr, /*bServerMap=*/false);
+    pFlyFormat->SetFormatAttr(aFormatURL);
+    pWrtShell->Insert(u" B"_ustr);
+
+    // When saving that to markdown:
+    save(mpFilter);
+
+    // Then make sure the image is exported and the link is not lost:
+    std::string aActual = TempFileToString();
+    std::string aExpected("A [![mytitle](./test.png)](https://x.com) B" 
SAL_NEWLINE_STRING);
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: A [![mytitle](./test.png)](https://x.com) B
+    // - Actual  : A ![mytitle](./test.png) B
+    // i.e. the image link was lost.
+    CPPUNIT_ASSERT_EQUAL(aExpected, aActual);
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/sw/source/filter/md/mdcallbcks.cxx 
b/sw/source/filter/md/mdcallbcks.cxx
index f37a12ae5175..77878e387975 100644
--- a/sw/source/filter/md/mdcallbcks.cxx
+++ b/sw/source/filter/md/mdcallbcks.cxx
@@ -177,7 +177,17 @@ int SwMarkdownParser::enter_span_callback(MD_SPANTYPE 
type, void* detail, void*
                                                    RTL_TEXTENCODING_UTF8);
             OUString aTitle = 
rtl::OStringToOUString(std::string_view(rTitle.text, rTitle.size),
                                                      RTL_TEXTENCODING_UTF8);
-            parser->InsertImage(aURL, aTitle);
+
+            const SwFormatINetFormat* pINetFormat = nullptr;
+            if (!parser->m_aAttrStack.empty()
+                && parser->m_aAttrStack.back()->Which() == RES_TXTATR_INETFMT)
+            {
+                // Copy the link to the image, so it won't be lost for empty 
spans.
+                const SfxPoolItem* pTopItem = 
parser->m_aAttrStack.back().get();
+                pINetFormat = dynamic_cast<const 
SwFormatINetFormat*>(pTopItem);
+            }
+
+            parser->InsertImage(aURL, aTitle, pINetFormat);
             parser->m_bInsideImage = true;
             break;
         }
diff --git a/sw/source/filter/md/swmd.cxx b/sw/source/filter/md/swmd.cxx
index 8218a4047632..8b2c829989f9 100644
--- a/sw/source/filter/md/swmd.cxx
+++ b/sw/source/filter/md/swmd.cxx
@@ -46,6 +46,7 @@
 #include <ndgrf.hxx>
 #include <fmtcntnt.hxx>
 #include <swtypes.hxx>
+#include <fmturl.hxx>
 
 #include "swmd.hxx"
 
@@ -592,7 +593,8 @@ void SwMarkdownParser::SetAttrs(SwPaM& rRange)
 
 void SwMarkdownParser::ClearAttrs() { m_xDoc->ResetAttrs(*m_pPam, true); }
 
-void SwMarkdownParser::InsertImage(const OUString& aURL, const OUString& 
rTitle)
+void SwMarkdownParser::InsertImage(const OUString& aURL, const OUString& 
rTitle,
+                                   const SwFormatINetFormat* pINetFormat)
 {
     OUString sGrfNm = INetURLObject::GetAbsURL(m_sBaseURL, aURL);
 
@@ -636,8 +638,7 @@ void SwMarkdownParser::InsertImage(const OUString& aURL, 
const OUString& rTitle)
     }
 
     SfxItemSet aFlySet(
-        SfxItemSet::makeFixedSfxItemSet<RES_FRM_SIZE, RES_VERT_ORIENT, 
RES_HORI_ORIENT, RES_ANCHOR>(
-            m_xDoc->GetAttrPool()));
+        SfxItemSet::makeFixedSfxItemSet<RES_FRMATR_BEGIN, 
RES_FRMATR_END>(m_xDoc->GetAttrPool()));
 
     aFlySet.Put(SwFormatAnchor(RndStdIds::FLY_AS_CHAR));
     aFlySet.Put(SwFormatFrameSize(SwFrameSize::Fixed, nWidth, nHeight));
@@ -645,6 +646,14 @@ void SwMarkdownParser::InsertImage(const OUString& aURL, 
const OUString& rTitle)
     aFlySet.Put(
         SwFormatVertOrient(0, text::VertOrientation::CHAR_CENTER, 
text::RelOrientation::CHAR));
 
+    if (pINetFormat)
+    {
+        // Have a link, set that on the image.
+        SwFormatURL aFormatURL;
+        aFormatURL.SetURL(pINetFormat->GetValue(), /*bServerMap=*/false);
+        aFlySet.Put(aFormatURL);
+    }
+
     SanitizeAnchor(aFlySet);
 
     SwFlyFrameFormat* pFlyFormat = 
m_xDoc->getIDocumentContentOperations().InsertGraphic(
diff --git a/sw/source/filter/md/swmd.hxx b/sw/source/filter/md/swmd.hxx
index 508da0762d13..a2930de1feae 100644
--- a/sw/source/filter/md/swmd.hxx
+++ b/sw/source/filter/md/swmd.hxx
@@ -117,7 +117,8 @@ class SwMarkdownParser
     void SetAttrs(SwPaM& rRange);
     void ClearAttrs();
 
-    void InsertImage(const OUString& aURL, const OUString& rTitle);
+    void InsertImage(const OUString& aURL, const OUString& rTitle,
+                     const SwFormatINetFormat* pINetFormat);
 
     void StartTable(sal_Int32 nRow, sal_Int32 nCol);
     void EndTable();
diff --git a/sw/source/filter/md/wrtmd.cxx b/sw/source/filter/md/wrtmd.cxx
index 48134875b499..36a28b24289d 100644
--- a/sw/source/filter/md/wrtmd.cxx
+++ b/sw/source/filter/md/wrtmd.cxx
@@ -47,6 +47,7 @@
 #include <charatr.hxx>
 #include <fmtcntnt.hxx>
 #include <ndgrf.hxx>
+#include <fmturl.hxx>
 #include "wrtmd.hxx"
 
 #include <algorithm>
@@ -60,10 +61,12 @@ struct SwMDImageInfo
 {
     OUString aURL;
     OUString aTitle;
+    OUString aLink;
 
-    SwMDImageInfo(const OUString& rURL, const OUString& rTitle)
+    SwMDImageInfo(const OUString& rURL, const OUString& rTitle, const 
OUString& rLink)
         : aURL(rURL)
         , aTitle(rTitle)
+        , aLink(rLink)
     {
     }
 
@@ -73,7 +76,11 @@ struct SwMDImageInfo
             return true;
         if (rOther.aURL < aURL)
             return false;
-        return aTitle < rOther.aTitle;
+        if (aLink < rOther.aLink)
+            return true;
+        if (rOther.aLink < aLink)
+            return false;
+        return aLink < rOther.aLink;
     }
 };
 
@@ -209,7 +216,13 @@ void ApplyItem(SwMDWriter& rWrt, FormattingStatus& 
rChange, const SfxPoolItem& r
                 aGraphicURL = 
URIHelper::simpleNormalizedMakeRelative(rBaseURL, aGraphicURL);
             }
             OUString aTitle = pGrfNode->GetTitle();
-            rChange.aImages.emplace(aGraphicURL, aTitle);
+            OUString aLink;
+            if (rFrameFormat.GetAttrSet().HasItem(RES_URL))
+            {
+                const SwFormatURL& rLink = rFrameFormat.GetURL();
+                aLink = rLink.GetURL();
+            }
+            rChange.aImages.emplace(aGraphicURL, aTitle, aLink);
             break;
         }
     }
@@ -369,11 +382,25 @@ void OutFormattingChange(SwMDWriter& rWrt, NodePositions& 
positions, sal_Int32 p
             continue;
         }
 
+        if (!rImageInfo.aLink.isEmpty())
+        {
+            // Start image link.
+            rWrt.Strm().WriteUnicodeOrByteText(u"[");
+        }
+
         rWrt.Strm().WriteUnicodeOrByteText(u"![");
         OutEscapedChars(rWrt, rImageInfo.aTitle);
         rWrt.Strm().WriteUnicodeOrByteText(u"](");
         rWrt.Strm().WriteUnicodeOrByteText(rImageInfo.aURL);
         rWrt.Strm().WriteUnicodeOrByteText(u")");
+
+        if (!rImageInfo.aLink.isEmpty())
+        {
+            // End image link.
+            rWrt.Strm().WriteUnicodeOrByteText(u"](");
+            rWrt.Strm().WriteUnicodeOrByteText(rImageInfo.aLink);
+            rWrt.Strm().WriteUnicodeOrByteText(u")");
+        }
     }
 
     current = result;

Reply via email to