From ea52d24b5a19bb54f91cd679a977332ec330880d Mon Sep 17 00:00:00 2001 From: Miklos Vajna Date: Wed, 8 Apr 2020 17:47:24 +0200 Subject: [PATCH] SVG export: fix lost semi-transparent line shapes The line shape itself didn't really have a height, rather it had a stroke. For some reason, the SVG mask then decides that nothing has to be painted there, so unless the line is entirely opaque, the line shape gets lost on export. Fix the problem by handling transparency similar to the PDF export, which detects if the whole purpose of the transparency gradient is to pass around a transparency percentage. We don't need a mask in that case, we can just use opacity as described at e.g. . Change-Id: I0355b9b09b6dd48bbacc5b7cc54fb71866304ef1 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/91932 Reviewed-by: Miklos Vajna Tested-by: Jenkins --- filter/qa/unit/data/semi-transparent-line.odg | Bin 0 -> 8874 bytes filter/qa/unit/svg.cxx | 34 +++++++++-- filter/source/svg/svgwriter.cxx | 57 +++++++++++------- test/source/xmltesttools.cxx | 5 +- 4 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 filter/qa/unit/data/semi-transparent-line.odg diff --git a/filter/qa/unit/data/semi-transparent-line.odg b/filter/qa/unit/data/semi-transparent-line.odg new file mode 100644 index 0000000000000000000000000000000000000000..2d28a694cea559a3dc2f023b1c3bb4f1227bb1da GIT binary patch literal 8874 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 literal 0 HcmV?d00001 diff --git a/filter/qa/unit/svg.cxx b/filter/qa/unit/svg.cxx index d6bb68283b74..8e6524c688ba 100644 --- a/filter/qa/unit/svg.cxx +++ b/filter/qa/unit/svg.cxx @@ -20,9 +20,7 @@ using namespace ::com::sun::star; -#if !defined MACOSX char const DATA_DIRECTORY[] = "/filter/qa/unit/data/"; -#endif /// SVG filter tests. class SvgFilterTest : public test::BootstrapFixture, public unotest::MacrosTest, public XmlTestTools @@ -34,10 +32,8 @@ public: void setUp() override; void tearDown() override; void registerNamespaces(xmlXPathContextPtr& pXmlXpathCtx) override; -#if !defined MACOSX uno::Reference& getComponent() { return mxComponent; } void load(const OUString& rURL); -#endif }; void SvgFilterTest::setUp() @@ -55,13 +51,11 @@ void SvgFilterTest::tearDown() test::BootstrapFixture::tearDown(); } -#if !defined MACOSX void SvgFilterTest::load(const OUString& rFileName) { OUString aURL = m_directories.getURLFromSrc(DATA_DIRECTORY) + rFileName; mxComponent = loadFromDesktop(aURL); } -#endif void SvgFilterTest::registerNamespaces(xmlXPathContextPtr& pXmlXpathCtx) { @@ -100,6 +94,34 @@ CPPUNIT_TEST_FIXTURE(SvgFilterTest, testPreserveJpg) #endif } +CPPUNIT_TEST_FIXTURE(SvgFilterTest, testSemiTransparentLine) +{ + // Load a document with a semi-transparent line shape. + load("semi-transparent-line.odg"); + + // Export it to SVG. + uno::Reference xStorable(getComponent(), uno::UNO_QUERY_THROW); + SvMemoryStream aStream; + uno::Reference xOut = new utl::OOutputStreamWrapper(aStream); + utl::MediaDescriptor aMediaDescriptor; + aMediaDescriptor["FilterName"] <<= OUString("draw_svg_Export"); + aMediaDescriptor["OutputStream"] <<= xOut; + xStorable->storeToURL("private:stream", aMediaDescriptor.getAsConstPropertyValueList()); + aStream.Seek(STREAM_SEEK_TO_BEGIN); + + // Get the style of the group around the actual element. + xmlDocPtr pXmlDoc = parseXmlStream(&aStream); + OUString aStyle = getXPath( + pXmlDoc, "//svg:g[@class='com.sun.star.drawing.LineShape']/svg:g/svg:g", "style"); + OUString aPrefix("opacity: "); + // Without the accompanying fix in place, this test would have failed, as the style was + // "mask:url(#mask1)", not "opacity: ". + CPPUNIT_ASSERT(aStyle.startsWith(aPrefix)); + int nPercent = std::round(aStyle.copy(aPrefix.getLength()).toDouble() * 100); + // Make sure that the line is still 30% opaque, rather than completely invisible. + CPPUNIT_ASSERT_EQUAL(30, nPercent); +} + CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/filter/source/svg/svgwriter.cxx b/filter/source/svg/svgwriter.cxx index 8d491d122d0b..c276fd903048 100644 --- a/filter/source/svg/svgwriter.cxx +++ b/filter/source/svg/svgwriter.cxx @@ -2382,32 +2382,43 @@ void SVGActionWriter::ImplWriteMask( GDIMetaFile& rMtf, if( nMoveX || nMoveY ) rMtf.Move( nMoveX, nMoveY ); - OUString aMaskId = "mask" + OUString::number( mnCurMaskId++ ); - + OUString aStyle; + if (rGradient.GetStartColor() == rGradient.GetEndColor()) { - SvXMLElementExport aElemDefs( mrExport, XML_NAMESPACE_NONE, aXMLElemDefs, true, true ); - - mrExport.AddAttribute( XML_NAMESPACE_NONE, aXMLAttrId, aMaskId ); - { - SvXMLElementExport aElemMask( mrExport, XML_NAMESPACE_NONE, "mask", true, true ); - - const tools::PolyPolygon aPolyPolygon( tools::PolyPolygon( tools::Rectangle( rDestPt, rDestSize ) ) ); - Gradient aGradient( rGradient ); - - // swap gradient stops to adopt SVG mask - Color aTmpColor( aGradient.GetStartColor() ); - sal_uInt16 nTmpIntensity( aGradient.GetStartIntensity() ); - aGradient.SetStartColor( aGradient.GetEndColor() ); - aGradient.SetStartIntensity( aGradient.GetEndIntensity() ) ; - aGradient.SetEndColor( aTmpColor ); - aGradient.SetEndIntensity( nTmpIntensity ); - - ImplWriteGradientEx( aPolyPolygon, aGradient, nWriteFlags ); - } + // Special case: constant alpha value. + const Color& rColor = rGradient.GetStartColor(); + const double fOpacity = 1.0 - static_cast(rColor.GetLuminance()) / 255; + aStyle = "opacity: " + OUString::number(fOpacity); } + else + { + OUString aMaskId = "mask" + OUString::number(mnCurMaskId++); - OUString aMaskStyle = "mask:url(#" + aMaskId + ")"; - mrExport.AddAttribute( XML_NAMESPACE_NONE, aXMLAttrStyle, aMaskStyle ); + { + SvXMLElementExport aElemDefs(mrExport, XML_NAMESPACE_NONE, aXMLElemDefs, true, true); + + mrExport.AddAttribute(XML_NAMESPACE_NONE, aXMLAttrId, aMaskId); + { + SvXMLElementExport aElemMask(mrExport, XML_NAMESPACE_NONE, "mask", true, true); + + const tools::PolyPolygon aPolyPolygon(tools::PolyPolygon(tools::Rectangle(rDestPt, rDestSize))); + Gradient aGradient(rGradient); + + // swap gradient stops to adopt SVG mask + Color aTmpColor(aGradient.GetStartColor()); + sal_uInt16 nTmpIntensity(aGradient.GetStartIntensity()); + aGradient.SetStartColor(aGradient.GetEndColor()); + aGradient.SetStartIntensity(aGradient.GetEndIntensity()); + aGradient.SetEndColor(aTmpColor); + aGradient.SetEndIntensity(nTmpIntensity); + + ImplWriteGradientEx(aPolyPolygon, aGradient, nWriteFlags); + } + } + + aStyle = "mask:url(#" + aMaskId + ")"; + } + mrExport.AddAttribute(XML_NAMESPACE_NONE, aXMLAttrStyle, aStyle); { SvXMLElementExport aElemG( mrExport, XML_NAMESPACE_NONE, aXMLElemG, true, true ); diff --git a/test/source/xmltesttools.cxx b/test/source/xmltesttools.cxx index b4a74aa4a8b4..f89c127171bd 100644 --- a/test/source/xmltesttools.cxx +++ b/test/source/xmltesttools.cxx @@ -12,6 +12,7 @@ #include #include +#include namespace { @@ -53,7 +54,9 @@ xmlDocPtr XmlTestTools::parseXmlStream(SvStream* pStream) std::unique_ptr pBuffer(new sal_uInt8[nSize + 1]); pStream->ReadBytes(pBuffer.get(), nSize); pBuffer[nSize] = 0; - return xmlParseDoc(reinterpret_cast(pBuffer.get())); + auto pCharBuffer = reinterpret_cast(pBuffer.get()); + SAL_INFO("test", "XmlTestTools::parseXmlStream: pBuffer is '" << pCharBuffer << "'"); + return xmlParseDoc(pCharBuffer); } xmlDocPtr XmlTestTools::dumpAndParse(MetafileXmlDump& rDumper, const GDIMetaFile& rGDIMetaFile)