/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "transparencygroupaction.hxx" #include #include "mtftools.hxx" #include #if OSL_DEBUG_LEVEL > 2 #include #endif using namespace ::com::sun::star; namespace cppcanvas::internal { // free support functions // ====================== namespace { class TransparencyGroupAction : public Action { public: /** Create new transparency group action. @param rGroupMtf Metafile that groups all actions to be rendered transparent. @param rAlphaGradient VCL gradient, to be rendered into the action's alpha channel. @param rDstPoint Left, top edge of destination, in current state coordinate system @param rDstSize Size of the transparency group object, in current state coordinate system. */ TransparencyGroupAction( std::unique_ptr< GDIMetaFile >&& rGroupMtf, std::optional< Gradient >&& rAlphaGradient, const ::basegfx::B2DPoint& rDstPoint, const ::basegfx::B2DVector& rDstSize, const CanvasSharedPtr& rCanvas, const OutDevState& rState ); TransparencyGroupAction(const TransparencyGroupAction&) = delete; const TransparencyGroupAction& operator=(const TransparencyGroupAction&) = delete; virtual bool render( const ::basegfx::B2DHomMatrix& rTransformation ) const override; virtual bool renderSubset( const ::basegfx::B2DHomMatrix& rTransformation, const Subset& rSubset ) const override; virtual ::basegfx::B2DRange getBounds( const ::basegfx::B2DHomMatrix& rTransformation ) const override; virtual ::basegfx::B2DRange getBounds( const ::basegfx::B2DHomMatrix& rTransformation, const Subset& rSubset ) const override; virtual sal_Int32 getActionCount() const override; private: std::unique_ptr< GDIMetaFile > mpGroupMtf; std::optional< Gradient > mpAlphaGradient; const ::basegfx::B2DSize maDstSize; mutable uno::Reference< rendering::XBitmap > mxBufferBitmap; // contains last rendered version mutable ::basegfx::B2DHomMatrix maLastTransformation; // contains last active transformation mutable Subset maLastSubset; // contains last effective subset // transformation for // mxBufferBitmap content CanvasSharedPtr mpCanvas; rendering::RenderState maState; }; /** Setup transformation such that the next render call is moved rPoint away, and scaled according to the ratio given by src and dst size. */ void implSetupTransform( rendering::RenderState& rRenderState, const ::basegfx::B2DPoint& rDstPoint ) { ::basegfx::B2DHomMatrix aLocalTransformation; aLocalTransformation.translate( rDstPoint.getX(), rDstPoint.getY() ); ::canvas::tools::appendToRenderState( rRenderState, aLocalTransformation ); } TransparencyGroupAction::TransparencyGroupAction( std::unique_ptr< GDIMetaFile >&& rGroupMtf, std::optional< Gradient >&& rAlphaGradient, const ::basegfx::B2DPoint& rDstPoint, const ::basegfx::B2DVector& rDstSize, const CanvasSharedPtr& rCanvas, const OutDevState& rState ) : mpGroupMtf( std::move(rGroupMtf) ), mpAlphaGradient( std::move(rAlphaGradient) ), maDstSize(rDstSize.getX(), rDstSize.getY()), mpCanvas( rCanvas ) { tools::initRenderState(maState,rState); implSetupTransform( maState, rDstPoint ); // correct clip (which is relative to original transform) tools::modifyClip( maState, rState, rCanvas, rDstPoint, nullptr, nullptr ); maLastSubset.mnSubsetBegin = 0; maLastSubset.mnSubsetEnd = -1; } // TODO(P3): The whole float transparency handling is a mess, // this should be refactored. What's more, the old idea of // having only internal 'metaactions', and not the original // GDIMetaFile now looks a lot less attractive. Try to move // into the direction of having a direct GDIMetaFile2XCanvas // renderer, and maybe a separate metafile XCanvas // implementation. bool TransparencyGroupAction::renderSubset( const ::basegfx::B2DHomMatrix& rTransformation, const Subset& rSubset ) const { SAL_INFO( "cppcanvas.emf", "::cppcanvas::internal::TransparencyGroupAction::renderSubset()" ); SAL_INFO( "cppcanvas.emf", "::cppcanvas::internal::TransparencyGroupAction: 0x" << std::hex << this ); // determine overall transformation matrix (render, view, // and passed transformation) ::basegfx::B2DHomMatrix aTransform; ::canvas::tools::getRenderStateTransform( aTransform, maState ); aTransform = rTransformation * aTransform; ::basegfx::B2DHomMatrix aTotalTransform; ::canvas::tools::getViewStateTransform( aTotalTransform, mpCanvas->getViewState() ); aTotalTransform = aTotalTransform * aTransform; // since pure translational changes to the transformation // does not matter, remove them before comparing aTotalTransform.set( 0, 2, 0.0 ); aTotalTransform.set( 1, 2, 0.0 ); // determine total scaling factor of the // transformation matrix - need to make the bitmap // large enough ::basegfx::B2DTuple aScale; ::basegfx::B2DTuple aTranslate; double nRotate; double nShearX; if( !aTotalTransform.decompose( aScale, aTranslate, nRotate, nShearX ) ) { SAL_WARN( "cppcanvas.emf", "TransparencyGroupAction::renderSubset(): non-decomposable transformation" ); return false; } ::Point aMtfOffsetPoint; // if there's no buffer bitmap, or as soon as the // total transformation changes, we've got to // re-render the bitmap if( !mxBufferBitmap.is() || aTotalTransform != maLastTransformation || rSubset.mnSubsetBegin != maLastSubset.mnSubsetBegin || rSubset.mnSubsetEnd != maLastSubset.mnSubsetEnd ) { DBG_TESTSOLARMUTEX(); // tdf#150610 fix broken rendering of text meta actions // Even when drawing to a VirtualDevice where antialiasing // is disabled, text will still be drawn with some // antialiased pixels on HiDPI displays. So, expand the // size of the VirtualDevice slightly to capture any of // the pixels drawn past the edges of the destination // bounds. bool bHasTextActions = false; MetaAction* pCurrAct; int nCurrActionIndex; for( nCurrActionIndex=0, pCurrAct=mpGroupMtf->FirstAction(); pCurrAct && !bHasTextActions; ++nCurrActionIndex, pCurrAct = mpGroupMtf->NextAction() ) { switch( pCurrAct->GetType() ) { case MetaActionType::TEXT: case MetaActionType::TEXTARRAY: case MetaActionType::STRETCHTEXT: case MetaActionType::TEXTRECT: if( ( rSubset.mnSubsetBegin == 0 && rSubset.mnSubsetEnd == -1 ) || ( rSubset.mnSubsetBegin <= nCurrActionIndex && rSubset.mnSubsetEnd > nCurrActionIndex ) ) bHasTextActions = true; break; default: break; } } // output size of metafile ::Size aOutputSizePixel( ::basegfx::fround( aScale.getX() * maDstSize.getWidth() ), ::basegfx::fround( aScale.getY() * maDstSize.getHeight() ) ); sal_Int32 nBitmapExtra; if ( bHasTextActions ) { nBitmapExtra = 10; aMtfOffsetPoint = ::Point( nBitmapExtra / 2, nBitmapExtra / 2 ); } else { // Related tdf#150610 assume antialiasing is enabled // Although antialiasing is normally disabled in the // VirtualDevice, lines in tdf#150610 will draw past // the edge of the VirtualDevice when running a // slideshow so always add an extra pixel on the // right and bottom edges. nBitmapExtra = 1; } // pixel size of cache bitmap: round up to nearest int ::Point aBitmapPoint; ::Size aBitmapSizePixel( static_cast( aScale.getX() * maDstSize.getWidth() ) + nBitmapExtra, static_cast( aScale.getY() * maDstSize.getHeight() ) + nBitmapExtra ); // render our content into an appropriately sized // VirtualDevice with alpha channel ScopedVclPtrInstance aVDev( *::Application::GetDefaultDevice(), DeviceFormat::WITH_ALPHA ); aVDev->SetOutputSizePixel( aBitmapSizePixel, true, true ); aVDev->SetMapMode(); if( rSubset.mnSubsetBegin != 0 || rSubset.mnSubsetEnd != -1 ) { // true subset - extract referenced // metaactions from mpGroupMtf GDIMetaFile aMtf; // extract subset actions for( nCurrActionIndex=0, pCurrAct=mpGroupMtf->FirstAction(); pCurrAct; ++nCurrActionIndex, pCurrAct = mpGroupMtf->NextAction() ) { switch( pCurrAct->GetType() ) { case MetaActionType::PUSH: case MetaActionType::POP: case MetaActionType::CLIPREGION: case MetaActionType::ISECTRECTCLIPREGION: case MetaActionType::ISECTREGIONCLIPREGION: case MetaActionType::MOVECLIPREGION: case MetaActionType::LINECOLOR: case MetaActionType::FILLCOLOR: case MetaActionType::TEXTCOLOR: case MetaActionType::TEXTFILLCOLOR: case MetaActionType::TEXTLINECOLOR: case MetaActionType::TEXTALIGN: case MetaActionType::FONT: case MetaActionType::RASTEROP: case MetaActionType::REFPOINT: case MetaActionType::LAYOUTMODE: // state-changing action - copy as-is aMtf.AddAction( pCurrAct->Clone() ); break; case MetaActionType::GRADIENT: case MetaActionType::HATCH: case MetaActionType::EPS: case MetaActionType::COMMENT: case MetaActionType::POINT: case MetaActionType::PIXEL: case MetaActionType::LINE: case MetaActionType::RECT: case MetaActionType::ROUNDRECT: case MetaActionType::ELLIPSE: case MetaActionType::ARC: case MetaActionType::PIE: case MetaActionType::CHORD: case MetaActionType::POLYLINE: case MetaActionType::POLYGON: case MetaActionType::POLYPOLYGON: case MetaActionType::BMP: case MetaActionType::BMPSCALE: case MetaActionType::BMPSCALEPART: case MetaActionType::BMPEX: case MetaActionType::BMPEXSCALE: case MetaActionType::BMPEXSCALEPART: case MetaActionType::MASK: case MetaActionType::MASKSCALE: case MetaActionType::MASKSCALEPART: case MetaActionType::GRADIENTEX: case MetaActionType::WALLPAPER: case MetaActionType::Transparent: case MetaActionType::FLOATTRANSPARENT: case MetaActionType::TEXT: case MetaActionType::TEXTARRAY: case MetaActionType::TEXTLINE: case MetaActionType::TEXTRECT: case MetaActionType::STRETCHTEXT: // output-generating action - only // copy, if we're within the // requested subset if( rSubset.mnSubsetBegin <= nCurrActionIndex && rSubset.mnSubsetEnd > nCurrActionIndex ) { aMtf.AddAction( pCurrAct->Clone() ); } break; default: SAL_WARN( "cppcanvas.emf", "Unknown meta action type encountered" ); break; } } aVDev->DrawTransparent( aMtf, aBitmapPoint, aBitmapSizePixel, aMtfOffsetPoint, aOutputSizePixel, *mpAlphaGradient ); } else { // no subsetting - render whole mtf aVDev->DrawTransparent( *mpGroupMtf, aBitmapPoint, aBitmapSizePixel, aMtfOffsetPoint, aOutputSizePixel, *mpAlphaGradient ); } // update buffered bitmap and transformation BitmapSharedPtr aBmp( VCLFactory::createBitmap( mpCanvas, aVDev->GetBitmapEx( aBitmapPoint, aBitmapSizePixel ) ) ); mxBufferBitmap = aBmp->getUNOBitmap(); maLastTransformation = aTotalTransform; maLastSubset = rSubset; } // determine target transformation (we can't simply pass // aTotalTransform as assembled above, since we must take // the canvas' view state as is, it might contain clipping // (which, in turn, is relative to the view // transformation)) // given that aTotalTransform is the identity // transformation, we could simply render our bitmap // as-is. Now, since the mxBufferBitmap content already // accounts for scale changes in the overall // transformation, we must factor this out // before. Generally, the transformation matrix should be // structured like this: // Translation*Rotation*Shear*Scale. Thus, to neutralize // the contained scaling, we've got to right-multiply with // the inverse. ::basegfx::B2DHomMatrix aScaleCorrection; aScaleCorrection.translate( -aMtfOffsetPoint.X(), -aMtfOffsetPoint.Y() ); aScaleCorrection.scale( 1/aScale.getX(), 1/aScale.getY() ); aTransform = aTransform * aScaleCorrection; rendering::RenderState aLocalState( maState ); ::canvas::tools::setRenderStateTransform(aLocalState, aTransform); if(aLocalState.Clip.is()) { // tdf#95709 // Adjust renderstate clip to modified scale from above ::basegfx::B2DPolyPolygon aClip = ::basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(aLocalState.Clip); aClip.transform(basegfx::utils::createScaleB2DHomMatrix(aScale)); aLocalState.Clip = ::basegfx::unotools::xPolyPolygonFromB2DPolyPolygon(mpCanvas->getUNOCanvas()->getDevice(), aClip); } #if OSL_DEBUG_LEVEL > 2 aLocalState.Clip.clear(); aLocalState.DeviceColor = vcl::unotools::colorToDoubleSequence( ::Color( 0x80FF0000 ), mpCanvas->getUNOCanvas()->getDevice()->getDeviceColorSpace() ); if( maState.Clip.is() ) mpCanvas->getUNOCanvas()->fillPolyPolygon( maState.Clip, mpCanvas->getViewState(), aLocalState ); aLocalState.DeviceColor = maState.DeviceColor; #endif // no further alpha changes necessary -> draw directly mpCanvas->getUNOCanvas()->drawBitmap( mxBufferBitmap, mpCanvas->getViewState(), aLocalState ); return true; } // TODO(P3): The whole float transparency handling is a mess, // this should be refactored. What's more, the old idea of // having only internal 'metaactions', and not the original // GDIMetaFile now looks a lot less attractive. Try to move // into the direction of having a direct GDIMetaFile2XCanvas // renderer, and maybe a separate metafile XCanvas // implementation. bool TransparencyGroupAction::render( const ::basegfx::B2DHomMatrix& rTransformation ) const { Subset aSubset; aSubset.mnSubsetBegin = 0; aSubset.mnSubsetEnd = -1; return renderSubset( rTransformation, aSubset ); } ::basegfx::B2DRange TransparencyGroupAction::getBounds( const ::basegfx::B2DHomMatrix& rTransformation ) const { rendering::RenderState aLocalState( maState ); ::canvas::tools::prependToRenderState(aLocalState, rTransformation); return tools::calcDevicePixelBounds( ::basegfx::B2DRange( 0,0, maDstSize.getWidth(), maDstSize.getHeight() ), mpCanvas->getViewState(), aLocalState ); } ::basegfx::B2DRange TransparencyGroupAction::getBounds( const ::basegfx::B2DHomMatrix& rTransformation, const Subset& rSubset ) const { // TODO(F3): Currently, the bounds for // TransparencyGroupAction subsets equal those of the // full set, although this action is able to render // true subsets. // polygon only contains a single action, empty bounds // if subset requests different range if( rSubset.mnSubsetBegin != 0 || rSubset.mnSubsetEnd != 1 ) return ::basegfx::B2DRange(); return getBounds( rTransformation ); } sal_Int32 TransparencyGroupAction::getActionCount() const { return mpGroupMtf ? mpGroupMtf->GetActionSize() : 0; } } std::shared_ptr TransparencyGroupActionFactory::createTransparencyGroupAction( std::unique_ptr< GDIMetaFile >&& rGroupMtf, std::optional< Gradient >&& rAlphaGradient, const ::basegfx::B2DPoint& rDstPoint, const ::basegfx::B2DVector& rDstSize, const CanvasSharedPtr& rCanvas, const OutDevState& rState ) { return std::make_shared(std::move(rGroupMtf), std::move(rAlphaGradient), rDstPoint, rDstSize, rCanvas, rState ); } } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */