svx/source/unodraw/unoshape.cxx | 5 + 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 + sw/source/core/unocore/unoframe.cxx | 2 xmloff/source/text/txtparae.cxx | 6 - 11 files changed, 160 insertions(+), 7 deletions(-)
New commits: commit 370dc64fc7a49e2cb29767e7e70c8f3ce0abcb95 Author: Miklos Vajna <[email protected]> AuthorDate: Mon Jan 19 13:34:32 2026 +0100 Commit: Xisco Fauli <[email protected]> CommitDate: Thu Jan 22 19:55:15 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]> (cherry picked from commit 58d677055d9f6da976bf4fe34c1d89dd6871050d) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197695 Reviewed-by: Xisco Fauli <[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 ffe3699d2c7a..cf0f37b19f57 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> @@ -146,6 +149,92 @@ CPPUNIT_TEST_FIXTURE(Test, testRedlineRenderModeOmitInsertDelete) aColor3 = getXPath(pXmlDoc, "(//textarray)[3]/preceding-sibling::textcolor[1]", "color"); CPPUNIT_ASSERT_EQUAL(120, GetColorHue(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 077edc081e06..e9881f1bbfb2 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 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 289d081f9445..5d9f140adc7f 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; @@ -3473,6 +3474,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 cddf1e100fbf..6b0fe4c6b9cb 100644 --- a/sw/source/core/layout/paintfrm.cxx +++ b/sw/source/core/layout/paintfrm.cxx @@ -4487,7 +4487,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); } } } commit caa37aeb9ce69dc6fd9f68c9db848d3ff2222f7f Author: Michael Stahl <[email protected]> AuthorDate: Fri Jan 16 19:20:05 2026 +0100 Commit: Xisco Fauli <[email protected]> CommitDate: Thu Jan 22 19:55:07 2026 +0100 xmloff,svx,sw: ODF export: prevent spurious z-index="0" Currently any object anchored in a Writer header / footer which isn't actually used in the document isn't put on the SdrPage and thus doesn't have a ZOrder; this results in a spurious z-index="0" attribute, where 0 is a perfectly valid value that is typically already used by a visible shape and is thus duplicated, which is invalid. Fix SvxShape::getPropertyValueImpl() and SwXFrame::getPropertyValue() to check that it has a parent, and XMLTextParagraphExport::addTextFrameAttributes() to properly check that the property has a valid value. There is one possible use case for producing a z-index for something that isn't visible: when it's anchored in a delete tracked change, and we want to preserve the order relative to other shapes in the document. However, it turns out that in SdXMLShapeContext::AddShape() and XMLTextFrameContext_Impl::Create() the z-index of anything anchored in a delete redline is already explicitly ignored, so it's a non-issue. Change-Id: I37e461ebcd3e4546c60f421054ee39c053919267 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197474 Tested-by: Jenkins CollaboraOffice <[email protected]> Reviewed-by: Miklos Vajna <[email protected]> (cherry picked from commit 7ffd4f7a27b9b2bc55ac3ddb7333dbaf3d48109c) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197622 Reviewed-by: Michael Stahl <[email protected]> Tested-by: Jenkins (cherry picked from commit c455986626dacfcc4300c33f0ce223944e700037) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197659 Reviewed-by: Xisco Fauli <[email protected]> diff --git a/svx/source/unodraw/unoshape.cxx b/svx/source/unodraw/unoshape.cxx index 8299a96fb985..c06cf971c3b1 100644 --- a/svx/source/unodraw/unoshape.cxx +++ b/svx/source/unodraw/unoshape.cxx @@ -2592,7 +2592,10 @@ bool SvxShape::getPropertyValueImpl( const OUString&, const SfxItemPropertyMapEn case OWN_ATTR_ZORDER: { - rValue <<= static_cast<sal_Int32>(GetSdrObject()->GetOrdNum()); + if (GetSdrObject()->getParentSdrObjListFromSdrObject()) + { + rValue <<= static_cast<sal_Int32>(GetSdrObject()->GetOrdNum()); + } break; } diff --git a/sw/source/core/unocore/unoframe.cxx b/sw/source/core/unocore/unoframe.cxx index 4ea5dbcb08c6..99b05427ea9a 100644 --- a/sw/source/core/unocore/unoframe.cxx +++ b/sw/source/core/unocore/unoframe.cxx @@ -2153,7 +2153,7 @@ uno::Any SwXFrame::getPropertyValue(const OUString& rPropertyName) const SdrObject* pObj = pFormat->FindRealSdrObject(); if( pObj == nullptr ) pObj = pFormat->FindSdrObject(); - if( pObj ) + if (pObj && pObj->getParentSdrObjListFromSdrObject()) { aAny <<= static_cast<sal_Int32>(pObj->GetOrdNum()); } diff --git a/xmloff/source/text/txtparae.cxx b/xmloff/source/text/txtparae.cxx index 60db48f76c59..65531cc07e0f 100644 --- a/xmloff/source/text/txtparae.cxx +++ b/xmloff/source/text/txtparae.cxx @@ -3103,10 +3103,10 @@ XMLShapeExportFlags XMLTextParagraphExport::addTextFrameAttributes( OUString sZOrder( u"ZOrder"_ustr ); if( xPropSetInfo->hasPropertyByName( sZOrder ) ) { - sal_Int32 nZIndex = 0; - rPropSet->getPropertyValue( sZOrder ) >>= nZIndex; - if( -1 != nZIndex ) + sal_Int32 nZIndex{-1}; + if (rPropSet->getPropertyValue(sZOrder) >>= nZIndex) { + assert(0 <= nZIndex); GetExport().AddAttribute( XML_NAMESPACE_DRAW, XML_ZINDEX, OUString::number( nZIndex ) ); }
