sw/qa/core/text/itrpaint.cxx | 71 ++++++++++++++++++++++++++++++++----- sw/source/core/inc/flyfrm.hxx | 2 + sw/source/core/layout/fly.cxx | 42 +++++++++++++++++++++ sw/source/core/layout/paintfrm.cxx | 26 +++++++++++-- 4 files changed, 130 insertions(+), 11 deletions(-)
New commits: commit 9b47b7914242811fe69815485840f20bc7bd7887 Author: Miklos Vajna <[email protected]> AuthorDate: Tue Jan 27 08:48:48 2026 +0100 Commit: Miklos Vajna <[email protected]> CommitDate: Wed Jan 28 10:25:37 2026 +0100 cool#13988 sw redline render mode: add colored border for anchored images So far the non-standard redline render mode for images focused on graying out images when they are meant to be "omitted". In the meantime, text got red/green colors since commit 9bc163b5637572684ac6cc5985d276c4bc01679f (cool#13574 sw redline render mode: somewhat color ins/del as green/red, 2026-01-20), for the case when the inserted/deleted text is not omitted. Do the same for images: if an anchored image is not omitted, then provide a red/green border for deleted/inserted images. This is for anchored images, inline images need more work. Two alternatives would have been to draw this border at the end of SwFlyFrame::PaintSwFrame() (like the standard redline mode does its cross for deletes, but doesn't work due to clipping problems) or as part of SwLayoutFrame::PaintSubsidiaryLines() (like the gray "boundary" indicator does, also has clipping problems). Doing it in SwFrame::PaintSwFrameShadowAndBorder() is free from these clipping problems. Change-Id: I7489e5be5c7081a8e6dc243cb30d8fd2dc4f1917 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/198254 Reviewed-by: Miklos Vajna <[email protected]> Tested-by: Jenkins diff --git a/sw/qa/core/text/itrpaint.cxx b/sw/qa/core/text/itrpaint.cxx index 479ba5a628bf..14c1af1a02b4 100644 --- a/sw/qa/core/text/itrpaint.cxx +++ b/sw/qa/core/text/itrpaint.cxx @@ -150,17 +150,41 @@ CPPUNIT_TEST_FIXTURE(Test, testRedlineRenderModeOmitInsertDelete) CPPUNIT_ASSERT_EQUAL(120, GetColorHue(aColor3)); } -bool IsGrayScale(const Bitmap& rBitmap) +struct ImageInfo { - BitmapScopedReadAccess pReadAccess(rBitmap); - Size aSize = rBitmap.GetSizePixel(); + Bitmap m_aBitmap; + tools::Rectangle m_aRectangle; +}; + +bool IsGrayScale(const ImageInfo& rInfo) +{ + Bitmap aBitmap = rInfo.m_aBitmap; + BitmapScopedReadAccess pReadAccess(aBitmap); + Size aSize = rInfo.m_aBitmap.GetSizePixel(); Color aColor = pReadAccess->GetColor(aSize.getHeight() / 2, aSize.getWidth() / 2); return aColor.GetRed() == aColor.GetGreen() && aColor.GetRed() == aColor.GetBlue(); } -std::vector<Bitmap> GetMetaFileImages(const GDIMetaFile& rMetaFile) +bool RectangleContainsPolygons(const tools::Rectangle& rRectangle, + const std::vector<tools::Polygon>& rPolygons) +{ + static constexpr SwTwips nPixel = 15; + tools::Rectangle aRectangle(rRectangle.Left() - nPixel, rRectangle.Top() - nPixel, + rRectangle.Right() + nPixel, rRectangle.Bottom() + nPixel); + for (const auto& rPolygon : rPolygons) + { + if (!aRectangle.Contains(rPolygon.GetBoundRect())) + { + return false; + } + } + + return true; +} + +std::vector<ImageInfo> GetMetaFileImages(const GDIMetaFile& rMetaFile) { - std::vector<Bitmap> aImages; + std::vector<ImageInfo> aImages; for (size_t nAction = 0; nAction < rMetaFile.GetActionSize(); ++nAction) { MetaAction* pAction = rMetaFile.GetAction(nAction); @@ -170,11 +194,31 @@ std::vector<Bitmap> GetMetaFileImages(const GDIMetaFile& rMetaFile) } auto pAct = static_cast<MetaBmpExScaleAction*>(pAction); - aImages.push_back(pAct->GetBitmap()); + ImageInfo aInfo; + aInfo.m_aBitmap = pAct->GetBitmap(); + aInfo.m_aRectangle = { pAct->GetPoint(), pAct->GetSize() }; + aImages.push_back(aInfo); } return aImages; } +std::vector<tools::Polygon> GetMetaFilePolylines(const GDIMetaFile& rMetaFile) +{ + std::vector<tools::Polygon> aPolygons; + for (size_t nAction = 0; nAction < rMetaFile.GetActionSize(); ++nAction) + { + MetaAction* pAction = rMetaFile.GetAction(nAction); + if (pAction->GetType() != MetaActionType::POLYLINE) + { + continue; + } + + auto pAct = static_cast<MetaPolyLineAction*>(pAction); + aPolygons.push_back(pAct->GetPolygon()); + } + return aPolygons; +} + CPPUNIT_TEST_FIXTURE(Test, testAnchoredImageRedlineRenderModeOmitInsertDelete) { // Given a document with a normal, a deleted and an inserted image: @@ -185,11 +229,14 @@ CPPUNIT_TEST_FIXTURE(Test, testAnchoredImageRedlineRenderModeOmitInsertDelete) std::shared_ptr<GDIMetaFile> xMetaFile = pDocShell->GetPreviewMetaFile(); // Then make sure none of the images are grayscale: - std::vector<Bitmap> aImages = GetMetaFileImages(*xMetaFile); + std::vector<ImageInfo> aImages = GetMetaFileImages(*xMetaFile); 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])); + std::vector<tools::Polygon> aPolygons = GetMetaFilePolylines(*xMetaFile); + // No frames around images. + CPPUNIT_ASSERT(aPolygons.empty()); // Omit insert: default, default, grayscale. SwWrtShell* pWrtShell = pDocShell->GetWrtShell(); @@ -206,6 +253,10 @@ CPPUNIT_TEST_FIXTURE(Test, testAnchoredImageRedlineRenderModeOmitInsertDelete) // Without the accompanying fix in place, this test would have failed, the image's center pixel // wasn't gray. CPPUNIT_ASSERT(IsGrayScale(aImages[2])); + aPolygons = GetMetaFilePolylines(*xMetaFile); + // Frame around the deleted image. This failed, there was no frame around the deleted image. + CPPUNIT_ASSERT(!aPolygons.empty()); + CPPUNIT_ASSERT(RectangleContainsPolygons(aImages[1].m_aRectangle, aPolygons)); // Omit deletes: default, grayscale, default. aOpt.SetRedlineRenderMode(SwRedlineRenderMode::OmitDeletes); @@ -218,6 +269,10 @@ CPPUNIT_TEST_FIXTURE(Test, testAnchoredImageRedlineRenderModeOmitInsertDelete) CPPUNIT_ASSERT(!IsGrayScale(aImages[0])); CPPUNIT_ASSERT(IsGrayScale(aImages[1])); CPPUNIT_ASSERT(!IsGrayScale(aImages[2])); + aPolygons = GetMetaFilePolylines(*xMetaFile); + // Frame around the inserted image. + CPPUNIT_ASSERT(!aPolygons.empty()); + CPPUNIT_ASSERT(RectangleContainsPolygons(aImages[2].m_aRectangle, aPolygons)); } CPPUNIT_TEST_FIXTURE(Test, testInlineImageRedlineRenderModeOmitInsertDelete) @@ -230,7 +285,7 @@ CPPUNIT_TEST_FIXTURE(Test, testInlineImageRedlineRenderModeOmitInsertDelete) std::shared_ptr<GDIMetaFile> xMetaFile = pDocShell->GetPreviewMetaFile(); // Then make sure none of the images are grayscale: - std::vector<Bitmap> aImages = GetMetaFileImages(*xMetaFile); + std::vector<ImageInfo> aImages = GetMetaFileImages(*xMetaFile); CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), aImages.size()); CPPUNIT_ASSERT(!IsGrayScale(aImages[0])); CPPUNIT_ASSERT(!IsGrayScale(aImages[1])); diff --git a/sw/source/core/inc/flyfrm.hxx b/sw/source/core/inc/flyfrm.hxx index 6b0982abf314..d5f8124af10b 100644 --- a/sw/source/core/inc/flyfrm.hxx +++ b/sw/source/core/inc/flyfrm.hxx @@ -313,6 +313,8 @@ public: bool IsSplitButNotYetMovedFollow() const; + bool GetRedlineRenderModeFrame(SvxBoxItem& rBoxItem) const; + private: void UpdateUnfloatButton(SwWrtShell* pWrtSh, bool bShow) const; void PaintDecorators() const; diff --git a/sw/source/core/layout/fly.cxx b/sw/source/core/layout/fly.cxx index 0d4a799610fa..3a746fc009a4 100644 --- a/sw/source/core/layout/fly.cxx +++ b/sw/source/core/layout/fly.cxx @@ -2330,6 +2330,48 @@ bool SwFlyFrame::IsSplitButNotYetMovedFollow() const return false; } +bool SwFlyFrame::GetRedlineRenderModeFrame(SvxBoxItem& rBoxItem) const +{ + // If we're in non-standard redline mode, then color deletes and inserts depending on the + // redline render mode. Similar to what SwFntObj::DrawText() does for redlined text. + const SwViewShell* pViewShell = getRootFrame()->GetCurrShell(); + if (!pViewShell) + { + return false; + } + + const SwViewOption* pViewOptions = pViewShell->GetViewOptions(); + if (!pViewOptions) + { + return false; + } + + SwRedlineRenderMode eRedlineRenderMode = pViewOptions->GetRedlineRenderMode(); + std::optional<Color> oColor; + if (eRedlineRenderMode == SwRedlineRenderMode::OmitInserts && IsDeleted()) + { + oColor.emplace(COL_RED); + } + else if (eRedlineRenderMode == SwRedlineRenderMode::OmitDeletes && IsInserted()) + { + oColor.emplace(COL_GREEN); + } + if (!oColor) + { + return false; + } + + editeng::SvxBorderLine aBorderLine; + aBorderLine.SetWidth(1); + aBorderLine.SetBorderLineStyle(SvxBorderLineStyle::SOLID); + aBorderLine.SetColor(*oColor); + rBoxItem.SetLine(&aBorderLine, SvxBoxItemLine::LEFT); + rBoxItem.SetLine(&aBorderLine, SvxBoxItemLine::RIGHT); + rBoxItem.SetLine(&aBorderLine, SvxBoxItemLine::TOP); + rBoxItem.SetLine(&aBorderLine, SvxBoxItemLine::BOTTOM); + return true; +} + SwTwips SwFlyFrame::Grow_(SwTwips nDist, SwResizeLimitReason& reason, bool bTst) { if (!Lower()) diff --git a/sw/source/core/layout/paintfrm.cxx b/sw/source/core/layout/paintfrm.cxx index d77fe8aa13c2..cfc66466fd1e 100644 --- a/sw/source/core/layout/paintfrm.cxx +++ b/sw/source/core/layout/paintfrm.cxx @@ -4323,8 +4323,11 @@ void SwFlyFrame::PaintSwFrame(vcl::RenderContext& rRenderContext, SwRect const& } } // paint of margin needed. + const SwViewOption* pViewOptions = pShell ? pShell->GetViewOptions() : nullptr; + SwRedlineRenderMode eRedlineRenderMode = pViewOptions ? pViewOptions->GetRedlineRenderMode() + : SwRedlineRenderMode::Standard; const bool bPaintMarginOnly( !bPaintCompleteBack && - getFramePrintArea().SSize() != getFrameArea().SSize() ); + (getFramePrintArea().SSize() != getFrameArea().SSize() || eRedlineRenderMode != SwRedlineRenderMode::Standard)); // #i47804# - paint background of parent fly frame // for transparent graphics in layer Hell, if parent fly frame isn't @@ -5515,7 +5518,8 @@ void SwFrame::PaintSwFrameShadowAndBorder( if (GetType() & (SwFrameType::NoTxt|SwFrameType::Row|SwFrameType::Body|SwFrameType::Footnote|SwFrameType::Column|SwFrameType::Root)) return; - if (IsCellFrame() && !gProp.pSGlobalShell->GetViewOptions()->IsTable()) + const SwViewOption& rViewOptions = *gProp.pSGlobalShell->GetViewOptions(); + if (IsCellFrame() && !rViewOptions.IsTable()) return; // #i29550# @@ -5534,7 +5538,8 @@ void SwFrame::PaintSwFrameShadowAndBorder( return; } - const bool bLine = rAttrs.IsLine(); + SwRedlineRenderMode eRedlineRenderMode = rViewOptions.GetRedlineRenderMode(); + const bool bLine = (rAttrs.IsLine() || (IsFlyFrame() && eRedlineRenderMode != SwRedlineRenderMode::Standard)); const bool bShadow = rAttrs.GetShadow().GetLocation() != SvxShadowLocation::NONE; // - flag to control, @@ -5613,6 +5618,21 @@ void SwFrame::PaintSwFrameShadowAndBorder( const SvxBorderLine* pTopBorder(rBox.GetTop()); const SvxBorderLine* pBottomBorder(rBox.GetBottom()); + auto pFlyFrame = IsFlyFrame() ? static_cast<const SwFlyFrame*>(this) : nullptr; + SvxBoxItem aBoxItem(RES_BOX); + if (pFlyFrame) + { + // This is a fly frame, see if it wants to paint a custom border based on the redline + // mode and status. + if (pFlyFrame->GetRedlineRenderModeFrame(aBoxItem)) + { + pLeftBorder = aBoxItem.GetLeft(); + pRightBorder = aBoxItem.GetRight(); + pTopBorder = aBoxItem.GetTop(); + pBottomBorder = aBoxItem.GetBottom(); + } + } + // if R2L, exchange Right/Left const bool bR2L(IsCellFrame() && IsRightToLeft());
