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