sw/qa/core/text/data/redline-image-anchored.docx |binary
 sw/qa/core/text/itrpaint.cxx                     |   89 +++++++++++++++++++++++
 sw/source/core/docnode/node.cxx                  |    8 ++
 sw/source/core/graphic/ndgrf.cxx                 |   37 +++++++++
 sw/source/core/inc/flyfrm.hxx                    |    3 
 sw/source/core/layout/fly.cxx                    |    5 +
 sw/source/core/layout/paintfrm.cxx               |    6 +
 sw/source/core/text/porlay.cxx                   |    6 +
 8 files changed, 152 insertions(+), 2 deletions(-)

New commits:
commit 58d677055d9f6da976bf4fe34c1d89dd6871050d
Author:     Miklos Vajna <[email protected]>
AuthorDate: Mon Jan 19 13:34:32 2026 +0100
Commit:     Miklos Vajna <[email protected]>
CommitDate: Tue Jan 20 18:32:46 2026 +0100

    cool#13988 sw redline render mode: handle anchored images
    
    Once a non-standard redline render mode is set, either the inserts or
    the deletes are "omitted" (painted in a semi-transparent way), but
    nothing happens with images.
    
    The standard redline render mode already had a way to cross out deleted
    images, which gives us a starting point.
    
    So use that info to render deleted flys in grayscale, and do the same
    for inserted images, depending on if "omit of inserts" or "omit of
    deletes" was requested.
    
    The test simply asserts if the pixel at the center is gray-ish, which
    detects the unwanted colors. The alternative would be to go via
    BitmapEx::ModifyBitmapEx() and basegfx::BColorModifier_gray, but then
    the bitmap checksum didn't match for me, even if the result was visually
    the ~same. (I.e. the idea could have been to see if a 2nd "gray" filter
    has any effect: if no changes, then the input was grayscale.)
    
    Change-Id: I3484b3b122d42006b44617b6df5bc9a5631b9266
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197674
    Tested-by: Jenkins
    Reviewed-by: Miklos Vajna <[email protected]>

diff --git a/sw/qa/core/text/data/redline-image-anchored.docx 
b/sw/qa/core/text/data/redline-image-anchored.docx
new file mode 100644
index 000000000000..58e6aa1e7df3
Binary files /dev/null and b/sw/qa/core/text/data/redline-image-anchored.docx 
differ
diff --git a/sw/qa/core/text/itrpaint.cxx b/sw/qa/core/text/itrpaint.cxx
index a59960616d68..ebac3c14dfdd 100644
--- a/sw/qa/core/text/itrpaint.cxx
+++ b/sw/qa/core/text/itrpaint.cxx
@@ -13,6 +13,9 @@
 
 #include <o3tl/string_view.hxx>
 #include <svtools/colorcfg.hxx>
+#include <vcl/gdimtf.hxx>
+#include <vcl/metaact.hxx>
+#include <vcl/BitmapReadAccess.hxx>
 
 #include <docsh.hxx>
 #include <wrtsh.hxx>
@@ -135,6 +138,92 @@ CPPUNIT_TEST_FIXTURE(Test, 
testRedlineRenderModeOmitInsertDelete)
     aColor3 = getXPath(pXmlDoc, 
"(//textarray)[3]/preceding-sibling::textcolor[1]", "color");
     CPPUNIT_ASSERT_EQUAL(u"#000000"_ustr, aColor3);
 }
+
+bool IsGrayScale(const Bitmap& rBitmap)
+{
+    BitmapScopedReadAccess pReadAccess(rBitmap);
+    Size aSize = rBitmap.GetSizePixel();
+    Color aColor = pReadAccess->GetColor(aSize.getHeight() / 2, 
aSize.getWidth() / 2);
+    return aColor.GetRed() == aColor.GetGreen() && aColor.GetRed() == 
aColor.GetBlue();
+}
+
+CPPUNIT_TEST_FIXTURE(Test, testAnchoredImageRedlineRenderModeOmitInsertDelete)
+{
+    // Given a document with a normal, a deleted and an inserted image:
+    createSwDoc("redline-image-anchored.docx");
+
+    // When using the standard redline render mode:
+    SwDocShell* pDocShell = getSwDocShell();
+    std::shared_ptr<GDIMetaFile> xMetaFile = pDocShell->GetPreviewMetaFile();
+
+    // Then make sure none of the images are grayscale:
+    std::vector<Bitmap> aImages;
+    for (size_t nAction = 0; nAction < xMetaFile->GetActionSize(); ++nAction)
+    {
+        MetaAction* pAction = xMetaFile->GetAction(nAction);
+        if (pAction->GetType() != MetaActionType::BMPEXSCALE)
+        {
+            continue;
+        }
+
+        auto pAct = static_cast<MetaBmpExScaleAction*>(pAction);
+        aImages.push_back(pAct->GetBitmap());
+    }
+    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), aImages.size());
+    CPPUNIT_ASSERT(!IsGrayScale(aImages[0]));
+    CPPUNIT_ASSERT(!IsGrayScale(aImages[1]));
+    CPPUNIT_ASSERT(!IsGrayScale(aImages[2]));
+
+    // Omit insert: default, default, grayscale.
+    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();
+    SwViewOption aOpt(*pWrtShell->GetViewOptions());
+    aOpt.SetRedlineRenderMode(SwRedlineRenderMode::OmitInserts);
+    pWrtShell->ApplyViewOptions(aOpt);
+
+    xMetaFile = pDocShell->GetPreviewMetaFile();
+
+    aImages.clear();
+    for (size_t nAction = 0; nAction < xMetaFile->GetActionSize(); ++nAction)
+    {
+        MetaAction* pAction = xMetaFile->GetAction(nAction);
+        if (pAction->GetType() != MetaActionType::BMPEXSCALE)
+        {
+            continue;
+        }
+
+        auto pAct = static_cast<MetaBmpExScaleAction*>(pAction);
+        aImages.push_back(pAct->GetBitmap());
+    }
+    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), aImages.size());
+    CPPUNIT_ASSERT(!IsGrayScale(aImages[0]));
+    CPPUNIT_ASSERT(!IsGrayScale(aImages[1]));
+    // Without the accompanying fix in place, this test would have failed, the 
image's center pixel
+    // wasn't gray.
+    CPPUNIT_ASSERT(IsGrayScale(aImages[2]));
+
+    // Omit deletes: default, grayscale, default.
+    aOpt.SetRedlineRenderMode(SwRedlineRenderMode::OmitDeletes);
+    pWrtShell->ApplyViewOptions(aOpt);
+
+    xMetaFile = pDocShell->GetPreviewMetaFile();
+
+    aImages.clear();
+    for (size_t nAction = 0; nAction < xMetaFile->GetActionSize(); ++nAction)
+    {
+        MetaAction* pAction = xMetaFile->GetAction(nAction);
+        if (pAction->GetType() != MetaActionType::BMPEXSCALE)
+        {
+            continue;
+        }
+
+        auto pAct = static_cast<MetaBmpExScaleAction*>(pAction);
+        aImages.push_back(pAct->GetBitmap());
+    }
+    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), aImages.size());
+    CPPUNIT_ASSERT(!IsGrayScale(aImages[0]));
+    CPPUNIT_ASSERT(IsGrayScale(aImages[1]));
+    CPPUNIT_ASSERT(!IsGrayScale(aImages[2]));
+}
 }
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/docnode/node.cxx b/sw/source/core/docnode/node.cxx
index f42e96e9519c..8f5d6230f2ee 100644
--- a/sw/source/core/docnode/node.cxx
+++ b/sw/source/core/docnode/node.cxx
@@ -934,6 +934,14 @@ void SwNode::dumpAsXml(xmlTextWriterPtr pWriter) const
         case SwNodeType::Grf:
         {
             auto pNoTextNode = static_cast<const SwNoTextNode*>(this);
+
+            if (pNoTextNode->HasSwAttrSet())
+            {
+                (void)xmlTextWriterStartElement(pWriter, 
BAD_CAST("SwAttrSet"));
+                pNoTextNode->GetSwAttrSet().dumpAsXml(pWriter);
+                (void)xmlTextWriterEndElement(pWriter);
+            }
+
             const tools::PolyPolygon* pContour = pNoTextNode->HasContour();
             if (pContour)
             {
diff --git a/sw/source/core/graphic/ndgrf.cxx b/sw/source/core/graphic/ndgrf.cxx
index 759976673522..8fe83f8880dd 100644
--- a/sw/source/core/graphic/ndgrf.cxx
+++ b/sw/source/core/graphic/ndgrf.cxx
@@ -47,6 +47,10 @@
 #include <hints.hxx>
 #include <swbaslnk.hxx>
 #include <pagefrm.hxx>
+#include <flyfrm.hxx>
+#include <rootfrm.hxx>
+#include <viewsh.hxx>
+#include <viewopt.hxx>
 
 #include <rtl/ustring.hxx>
 #include <o3tl/deleter.hxx>
@@ -711,7 +715,38 @@ GraphicAttr& SwGrfNode::GetGraphicAttr( GraphicAttr& rGA,
 {
     const SwAttrSet& rSet = GetSwAttrSet();
 
-    rGA.SetDrawMode( rSet.GetDrawModeGrf().GetValue() );
+    bool bOmitPaint = false;
+    if (pFrame)
+    {
+        SwViewShell* pViewShell = pFrame->getRootFrame()->GetCurrShell();
+        const SwViewOption* pViewOptions = pViewShell ? 
pViewShell->GetViewOptions() : nullptr;
+        if (pViewOptions)
+        {
+            SwRedlineRenderMode eRedlineRenderMode = 
pViewOptions->GetRedlineRenderMode();
+            const SwFlyFrame* pFlyFrame = pFrame->FindFlyFrame();
+            if (eRedlineRenderMode == SwRedlineRenderMode::OmitDeletes && 
pFlyFrame
+                && pFlyFrame->IsDeleted())
+            {
+                // Want to omit deletes and this is a delete: omit paint.
+                bOmitPaint = true;
+            }
+            else if (eRedlineRenderMode == SwRedlineRenderMode::OmitInserts && 
pFlyFrame
+                     && pFlyFrame->IsInserted())
+            {
+                // Want to omit inserts and this is an insert: omit paint.
+                bOmitPaint = true;
+            }
+        }
+    }
+    if (bOmitPaint)
+    {
+        // Omit paint by drawing the image grayscale.
+        rGA.SetDrawMode(GraphicDrawMode::Greys);
+    }
+    else
+    {
+        rGA.SetDrawMode(rSet.GetDrawModeGrf().GetValue());
+    }
 
     const SwMirrorGrf & rMirror = rSet.GetMirrorGrf();
     BmpMirrorFlags nMirror = BmpMirrorFlags::NONE;
diff --git a/sw/source/core/inc/flyfrm.hxx b/sw/source/core/inc/flyfrm.hxx
index 9dc03bc323af..6b0982abf314 100644
--- a/sw/source/core/inc/flyfrm.hxx
+++ b/sw/source/core/inc/flyfrm.hxx
@@ -134,6 +134,7 @@ protected:
     bool m_bAutoPosition :1; ///< RndStdIds::FLY_AT_CHAR, anchored at character
     bool m_bDeleted :1;      ///< Anchored to a tracked deletion
     size_t m_nAuthor;        ///< Redline author index for colored crossing out
+    bool m_bInserted;        ///< Anchored to a tracked insertion
 
     friend class SwNoTextFrame; // is allowed to call NotifyBackground
 
@@ -221,6 +222,8 @@ public:
     void SetDeleted(bool bDeleted) { m_bDeleted = bDeleted; }
     void SetAuthor( size_t nAuthor ) { m_nAuthor = nAuthor; }
     size_t GetAuthor() const { return m_nAuthor; }
+    bool IsInserted() const { return m_bInserted; }
+    void SetInserted(bool bInserted) { m_bInserted = bInserted; }
 
     bool IsNotifyBack() const { return m_bNotifyBack; }
     void SetNotifyBack()      { m_bNotifyBack = true; }
diff --git a/sw/source/core/layout/fly.cxx b/sw/source/core/layout/fly.cxx
index 712d4f6a6408..0d4a799610fa 100644
--- a/sw/source/core/layout/fly.cxx
+++ b/sw/source/core/layout/fly.cxx
@@ -175,6 +175,7 @@ SwFlyFrame::SwFlyFrame( SwFlyFrameFormat *pFormat, SwFrame* 
pSib, SwFrame *pAnch
     m_bAutoPosition( false ),
     m_bDeleted( false ),
     m_nAuthor( std::string::npos ),
+    m_bInserted( false ),
     m_bValidContentPos( false )
 {
     mnFrameType = SwFrameType::Fly;
@@ -3471,6 +3472,10 @@ void SwFlyFrame::dumpAsXml(xmlTextWriterPtr writer) const
 {
     (void)xmlTextWriterStartElement(writer, reinterpret_cast<const 
xmlChar*>("fly"));
     dumpAsXmlAttributes(writer);
+    (void)xmlTextWriterWriteFormatAttribute(writer, BAD_CAST("deleted"), "%s",
+                                            
BAD_CAST(OString::boolean(m_bDeleted).getStr()));
+    (void)xmlTextWriterWriteFormatAttribute(writer, BAD_CAST("inserted"), "%s",
+                                            
BAD_CAST(OString::boolean(m_bInserted).getStr()));
 
     SwLayoutFrame::dumpAsXml(writer);
 
diff --git a/sw/source/core/layout/paintfrm.cxx 
b/sw/source/core/layout/paintfrm.cxx
index d44b11738a52..a8faf95c3e5c 100644
--- a/sw/source/core/layout/paintfrm.cxx
+++ b/sw/source/core/layout/paintfrm.cxx
@@ -4486,7 +4486,11 @@ void SwFlyFrame::PaintSwFrame(vcl::RenderContext& 
rRenderContext, SwRect const&
     PaintDecorators();
 
     // crossing out for tracked deletion
-    if ( GetAuthor() != std::string::npos && IsDeleted() )
+    const SwViewOption* pViewOptions = pShell->GetViewOptions();
+    SwRedlineRenderMode eRedlineRenderMode
+        = pViewOptions ? pViewOptions->GetRedlineRenderMode() : 
SwRedlineRenderMode::Standard;
+    if (GetAuthor() != std::string::npos && IsDeleted()
+        && eRedlineRenderMode == SwRedlineRenderMode::Standard)
     {
         tools::Long startX = aRect.Left(  ), endX = aRect.Right();
         tools::Long startY = aRect.Top(  ),  endY = aRect.Bottom();
diff --git a/sw/source/core/text/porlay.cxx b/sw/source/core/text/porlay.cxx
index e321a16160dc..de25fc8ccc99 100644
--- a/sw/source/core/text/porlay.cxx
+++ b/sw/source/core/text/porlay.cxx
@@ -702,6 +702,7 @@ void SwLineLayout::CalcLine( SwTextFormatter &rLine, 
SwTextFormatInfo &rInf )
                     if ( auto pFly = pAnchoredObj->DynCastFlyFrame() )
                     {
                         bool bDeleted = false;
+                        bool bInserted = false;
                         size_t nAuthor = std::string::npos;
                         const SwFormatAnchor& rAnchor = 
pAnchoredObj->GetFrameFormat()->GetAnchor();
                         if ( rAnchor.GetAnchorId() == RndStdIds::FLY_AT_CHAR )
@@ -715,9 +716,14 @@ void SwLineLayout::CalcLine( SwTextFormatter &rLine, 
SwTextFormatInfo &rInf )
                                 bDeleted = true;
                                 nAuthor = pFnd->GetAuthor();
                             }
+                            else if (pFnd && pFnd->GetType() == 
RedlineType::Insert)
+                            {
+                                bInserted = true;
+                            }
                         }
                         pFly->SetDeleted(bDeleted);
                         pFly->SetAuthor(nAuthor);
+                        pFly->SetInserted(bInserted);
                     }
                 }
             }

Reply via email to