sw/qa/filter/md/md.cxx        |   33 ++++++++++++++
 sw/source/filter/md/wrtmd.cxx |   93 ++++++++++++++++++++++++++++++++++++++++++
 sw/source/filter/md/wrtmd.hxx |   14 ++++++
 3 files changed, 140 insertions(+)

New commits:
commit 8bcdc8f326752176efbe2dfe6cfabfed6f9280b2
Author:     Miklos Vajna <[email protected]>
AuthorDate: Fri Oct 3 09:01:21 2025 +0200
Commit:     Caolán McNamara <[email protected]>
CommitDate: Fri Oct 3 15:12:48 2025 +0200

    tdf#168662 sw markdown export: handle anchored images
    
    The bugdoc has a to-char anchored image, exporting that to markdown the
    image is lost.
    
    This happens because anchored images are not represented in the doc
    model as a SwFormatFlyCnt on a dummy character.
    
    Fix the problem similar to what the HTML export does with its
    SwHTMLWriter::OutFlyFrame() code:
    1) Collect the position of all fly formats at the start
    2) In OutMarkdown_SwTextNode(), take the flys anchored to a given text
       node out of that container
    3) In OutFormattingChange(), emit the anchored images using the markdown
       image markup, which will turn them into inline images
    
    Draw shapes are ignored for now, since the current fly export code takes
    the object title / description directly from the fly frame format.
    
    Change-Id: I8061a4ea51b2c24a7d48daca0b3c62d79d3e7a3e
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/191811
    Tested-by: Jenkins CollaboraOffice <[email protected]>
    Reviewed-by: Caolán McNamara <[email protected]>

diff --git a/sw/qa/filter/md/md.cxx b/sw/qa/filter/md/md.cxx
index 32bcefc99c28..ac3be4b87c29 100644
--- a/sw/qa/filter/md/md.cxx
+++ b/sw/qa/filter/md/md.cxx
@@ -865,6 +865,39 @@ CPPUNIT_TEST_FIXTURE(Test, testEmbeddedImageMdExport)
     CPPUNIT_ASSERT(aActual.ends_with(") B" SAL_NEWLINE_STRING));
 }
 
+CPPUNIT_TEST_FIXTURE(Test, testEmbeddedAnchoredImageMdExport)
+{
+    // Given a document with an embedded anchored image:
+    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_AT_CHAR);
+    aFrameSet.Put(aAnchor);
+    OUString aImageURL = createFileURL(u"test.png");
+    SvFileStream aImageStream(aImageURL, StreamMode::READ);
+    GraphicFilter& rFilter = GraphicFilter::GetGraphicFilter();
+    Graphic aGraphic = rFilter.ImportUnloadedGraphic(aImageStream);
+    IDocumentContentOperations& rIDCO = pDoc->getIDocumentContentOperations();
+    SwCursor* pCursor = pWrtShell->GetCursor();
+    SwFlyFrameFormat* pFlyFormat
+        = rIDCO.InsertGraphic(*pCursor, /*rGrfName=*/OUString(), OUString(), 
&aGraphic, &aFrameSet,
+                              /*pGrfAttrSet=*/nullptr, 
/*SwFrameFormat=*/nullptr);
+    pFlyFormat->SetObjDescription(u"mydesc"_ustr);
+    pWrtShell->Insert(u" B"_ustr);
+
+    // When saving that to markdown:
+    save(mpFilter);
+
+    // Then make sure that the embedded image is exported:
+    std::string aActual = TempFileToString();
+    // Without the accompanying fix in place, this test would have failed, 
aActual was 'A  B
'.
+    CPPUNIT_ASSERT(aActual.starts_with("A ![mydesc](data:image/png;base64,"));
+    CPPUNIT_ASSERT(aActual.ends_with(") B" SAL_NEWLINE_STRING));
+}
+
 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/wrtmd.cxx b/sw/source/filter/md/wrtmd.cxx
index 81ee40177a4b..5707a1e0db0d 100644
--- a/sw/source/filter/md/wrtmd.cxx
+++ b/sw/source/filter/md/wrtmd.cxx
@@ -50,6 +50,7 @@
 #include <ndgrf.hxx>
 #include <ndole.hxx>
 #include <fmturl.hxx>
+#include <fmtanchr.hxx>
 #include "wrtmd.hxx"
 
 #include <algorithm>
@@ -105,6 +106,7 @@ struct FormattingStatus
     std::set<SwMDImageInfo> aImages;
     // Checked/unchecked -> increment (1 or -1) map.
     std::unordered_map<bool, int> aTaskListItemChanges;
+    std::set<const SwFrameFormat*> aFlys;
 };
 
 template <typename T> struct PosData
@@ -126,6 +128,7 @@ struct NodePositions
     PosData<SfxPoolItem> hintEnds;
     PosData<SwRangeRedline> redlineStarts;
     PosData<SwRangeRedline> redlineEnds;
+    PosData<SwFrameFormat> flys;
 
     sal_Int32 getEndOfCurrent(sal_Int32 end)
     {
@@ -136,6 +139,7 @@ struct NodePositions
             pos_of(hintStarts.current()),
             pos_of(redlineEnds.current()),
             pos_of(redlineStarts.current()),
+            pos_of(flys.current()),
         });
     }
 };
@@ -281,6 +285,11 @@ void ApplyItem(FormattingStatus& rChange, const 
SwRangeRedline* pItem, int incre
     rChange.aRedlineChanges[pItem] += increment;
 }
 
+void ApplyItem(FormattingStatus& rChange, const SwFrameFormat* pItem)
+{
+    rChange.aFlys.insert(pItem);
+}
+
 FormattingStatus CalculateFormattingChange(SwMDWriter& rWrt, NodePositions& 
positions,
                                            sal_Int32 pos, const 
FormattingStatus& currentFormatting)
 {
@@ -305,6 +314,12 @@ FormattingStatus CalculateFormattingChange(SwMDWriter& 
rWrt, NodePositions& posi
          p = positions.redlineStarts.next())
         ApplyItem(result, p->second, +1);
 
+    // Output anchored flys.
+    for (auto it = positions.flys.current(); it && it->first == pos; it = 
positions.flys.next())
+    {
+        ApplyItem(result, it->second);
+    }
+
     return result;
 }
 
@@ -400,6 +415,29 @@ void OutFormattingChange(SwMDWriter& rWrt, NodePositions& 
positions, sal_Int32 p
         }
     }
 
+    // Write flys anchored at this position.
+    FormattingStatus aChange;
+    for (const SwFrameFormat* pFrameFormat : result.aFlys)
+    {
+        if (current.aFlys.contains(pFrameFormat))
+        {
+            // 'current' is the old state and 'result' is the new state, so 
don't write images which were written already.
+            continue;
+        }
+
+        if (pFrameFormat->Which() != RES_FLYFRMFMT)
+        {
+            continue;
+        }
+
+        const auto& rFlyFrameFormat = static_cast<const 
SwFlyFrameFormat&>(*pFrameFormat);
+        ApplyFlyFrameFormat(rFlyFrameFormat, rWrt, aChange);
+    }
+    for (const SwMDImageInfo& rImage : aChange.aImages)
+    {
+        OutMarkdown_SwMDImageInfo(rImage, rWrt);
+    }
+
     // Not in CommonMark
     if (ShouldOpenIt(current.nCrossedOutChange, result.nCrossedOutChange))
         rWrt.Strm().WriteUnicodeOrByteText(u"~~");
@@ -733,6 +771,24 @@ void OutMarkdown_SwTextNode(SwMDWriter& rWrt, const 
SwTextNode& rNode, bool bFir
 
         positions.redlineEnds.sort();
 
+        // Collect flys anchored to this text node.
+        for (size_t nFly = 0; nFly < rWrt.GetFlys().size(); ++nFly)
+        {
+            const SwMDFly& rFly = rWrt.GetFlys()[nFly];
+            if (rFly.m_nAnchorNodeOffset < rNode.GetIndex())
+            {
+                continue;
+            }
+            if (rFly.m_nAnchorNodeOffset > rNode.GetIndex())
+            {
+                break;
+            }
+
+            SwMDFly aFly = rWrt.GetFlys().erase_extract(nFly);
+            --nFly;
+            positions.flys.add(aFly.m_nAnchorContentOffset, 
aFly.m_pFrameFormat);
+        }
+
         FormattingStatus currentStatus;
         while (nStrPos < nEnd)
         {
@@ -874,8 +930,45 @@ void OutMarkdown_SwTableNode(SwMDWriter& rWrt, const 
SwTableNode& rTableNode)
 
 SwMDWriter::SwMDWriter(const OUString& rBaseURL) { SetBaseURL(rBaseURL); }
 
+bool SwMDFly::operator<(const SwMDFly& rFly) const
+{
+    if (m_nAnchorNodeOffset != rFly.m_nAnchorNodeOffset)
+    {
+        return m_nAnchorNodeOffset < rFly.m_nAnchorNodeOffset;
+    }
+
+    if (m_nAnchorContentOffset != rFly.m_nAnchorContentOffset)
+    {
+        return m_nAnchorContentOffset < rFly.m_nAnchorContentOffset;
+    }
+
+    return m_pFrameFormat->GetAnchor().GetOrder() < 
rFly.m_pFrameFormat->GetAnchor().GetOrder();
+}
+
+void SwMDWriter::CollectFlys()
+{
+    // ApplyItem() already handles as-char flys.
+    SwPosFlyFrames aFrames(m_pDoc->GetAllFlyFormats(m_bWriteAll ? nullptr : 
m_pCurrentPam.get(),
+                                                    /*bDrawAlso=*/false));
+    for (const SwPosFlyFrame& rFrame : aFrames)
+    {
+        const SwFrameFormat& rFrameFormat = rFrame.GetFormat();
+        const SwFormatAnchor& rAnchor = rFrameFormat.GetAnchor();
+        SwContentNode* pAnchorNode = rAnchor.GetAnchorContentNode();
+        if (!pAnchorNode)
+        {
+            continue;
+        }
+
+        m_aFlys.insert(
+            { pAnchorNode->GetIndex(), rAnchor.GetAnchorContentOffset(), 
&rFrameFormat });
+    }
+}
+
 ErrCode SwMDWriter::WriteStream()
 {
+    CollectFlys();
+
     Strm().SetStreamCharSet(RTL_TEXTENCODING_UTF8);
     if (m_bShowProgress)
         ::StartProgress(STR_STATSTR_W4WWRITE, 0, 
sal_Int32(m_pDoc->GetNodes().Count()),
diff --git a/sw/source/filter/md/wrtmd.hxx b/sw/source/filter/md/wrtmd.hxx
index 78a83af4c0bb..fbc984e191f8 100644
--- a/sw/source/filter/md/wrtmd.hxx
+++ b/sw/source/filter/md/wrtmd.hxx
@@ -51,6 +51,16 @@ struct SwMDTableInfo
     const SwEndNode* pEndNode = nullptr;
 };
 
+/// Tracks an anchored object to be exported at a specific document model 
position.
+struct SwMDFly
+{
+    SwNodeOffset m_nAnchorNodeOffset = {};
+    sal_Int32 m_nAnchorContentOffset = {};
+    const SwFrameFormat* m_pFrameFormat = nullptr;
+
+    bool operator<(const SwMDFly&) const;
+};
+
 class SwMDWriter : public Writer
 {
 public:
@@ -64,12 +74,14 @@ public:
 
     void SetTaskListItems(int nTaskListItems) { m_nTaskListItems = 
nTaskListItems; }
     int GetTaskListItems() const { return m_nTaskListItems; }
+    o3tl::sorted_vector<SwMDFly>& GetFlys() { return m_aFlys; }
 
 protected:
     ErrCode WriteStream() override;
 
 private:
     void Out_SwDoc(SwPaM* pPam);
+    void CollectFlys();
 
     bool m_bOutTable = false;
     SwNodeOffset m_nStartNodeIndex{ 0 };
@@ -78,6 +90,8 @@ private:
     std::stack<SwMDTableInfo> m_aTableInfos;
     /// Number of currently open task list items.
     int m_nTaskListItems = 0;
+    /// Anchored fly frames, e.g. images.
+    o3tl::sorted_vector<SwMDFly> m_aFlys;
 };
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */

Reply via email to