sw/qa/core/text/data/redline-image-anchored.docx |binary
 sw/qa/core/text/itrpaint.cxx                     |   90 +++++++++++++++++++++++
 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, 153 insertions(+), 2 deletions(-)

New commits:
commit a2f50ec5de1facc7375d55168b57978d4f501315
Author:     Miklos Vajna <[email protected]>
AuthorDate: Mon Jan 19 13:34:32 2026 +0100
Commit:     Caolán McNamara <[email protected]>
CommitDate: Tue Jan 20 09:33:48 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/+/197585
    Tested-by: Jenkins CollaboraOffice <[email protected]>
    Reviewed-by: Caolán McNamara <[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..9ecd2d4a5466 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,93 @@ 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 BitmapEx& rBitmap)
+{
+    Bitmap aBitmap = rBitmap.GetBitmap();
+    BitmapScopedReadAccess pReadAccess(aBitmap);
+    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<BitmapEx> 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->GetBitmapEx());
+    }
+    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->GetBitmapEx());
+    }
+    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->GetBitmapEx());
+    }
+    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 e1c29e52d2df..311ee8229145 100644
--- a/sw/source/core/docnode/node.cxx
+++ b/sw/source/core/docnode/node.cxx
@@ -933,6 +933,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 2477913e2872..277ed374c6d4 100644
--- a/sw/source/core/graphic/ndgrf.cxx
+++ b/sw/source/core/graphic/ndgrf.cxx
@@ -46,6 +46,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>
@@ -710,7 +714,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 fc1f46142745..a990323f4084 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 3647774f0bb8..b764338cbaa8 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;
@@ -3447,6 +3448,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 aa07433c24c9..df1458fb5566 100644
--- a/sw/source/core/layout/paintfrm.cxx
+++ b/sw/source/core/layout/paintfrm.cxx
@@ -4493,7 +4493,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 ce7303f85ec7..1206eebe4e3a 100644
--- a/sw/source/core/text/porlay.cxx
+++ b/sw/source/core/text/porlay.cxx
@@ -701,6 +701,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 )
@@ -714,9 +715,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