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); } } }
