/* -*- 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 // helper class for animated graphics #include #include #include #include #include #include #include namespace drawinglayer::primitive2d { namespace { class AnimatedGraphicPrimitive2D : public AnimatedSwitchPrimitive2D { private: /// the geometric definition basegfx::B2DHomMatrix maTransform; /** the Graphic with all its content possibilities, here only animated is allowed and gets checked by isValidData(). an instance of Graphic is used here since it's ref-counted and thus a safe copy for now */ const Graphic maGraphic; /// local animation processing data, excerpt from maGraphic ::Animation maAnimation; /// the on-demand created VirtualDevices for frame creation ScopedVclPtrInstance< VirtualDevice > maVirtualDevice; ScopedVclPtrInstance< VirtualDevice > maVirtualDeviceMask; // index of the next frame that would be regularly prepared sal_uInt32 mnNextFrameToPrepare; /// buffering of 1st frame (always active) Primitive2DReference maBufferedFirstFrame; /// buffering of all frames std::vector maBufferedPrimitives; bool mbBufferingAllowed; /// set if the animation is huge so that just always the next frame /// is used instead of using timing bool mbHugeSize; /// helper methods bool isValidData() const { return (GraphicType::Bitmap == maGraphic.GetType() && maGraphic.IsAnimated() && maAnimation.Count()); } void ensureVirtualDeviceSizeAndState() { if (!isValidData()) return; const Size aCurrent(maVirtualDevice->GetOutputSizePixel()); const Size aTarget(maAnimation.GetDisplaySizePixel()); if (aCurrent != aTarget) { maVirtualDevice->EnableMapMode(false); maVirtualDeviceMask->EnableMapMode(false); maVirtualDevice->SetOutputSizePixel(aTarget); maVirtualDeviceMask->SetOutputSizePixel(aTarget); // tdf#156630 make erase calls fill with transparency maVirtualDevice->SetBackground(COL_BLACK); maVirtualDeviceMask->SetBackground(COL_ALPHA_TRANSPARENT); } maVirtualDevice->Erase(); maVirtualDeviceMask->Erase(); const ::tools::Rectangle aRect(Point(0, 0), aTarget); maVirtualDeviceMask->SetFillColor(COL_BLACK); maVirtualDeviceMask->SetLineColor(); maVirtualDeviceMask->DrawRect(aRect); } sal_uInt32 generateStepTime(sal_uInt32 nIndex) const { const AnimationFrame& rAnimationFrame = maAnimation.Get(sal_uInt16(nIndex)); sal_uInt32 nWaitTime(rAnimationFrame.mnWait * 10); // Take care of special value for MultiPage TIFFs. ATM these shall just // show their first page. Later we will offer some switching when object // is selected. if (ANIMATION_TIMEOUT_ON_CLICK == rAnimationFrame.mnWait) { // ATM the huge value would block the timer, so // use a long time to show first page (whole day) nWaitTime = 100 * 60 * 60 * 24; } // Bad trap: There are animated gifs with no set WaitTime (!). // In that case use a default value. if (0 == nWaitTime) { nWaitTime = 100; } return nWaitTime; } void createAndSetAnimationTiming() { if (!isValidData()) return; animation::AnimationEntryLoop aAnimationLoop(maAnimation.GetLoopCount() ? maAnimation.GetLoopCount() : 0xffff); const sal_uInt32 nCount(maAnimation.Count()); for (sal_uInt32 a(0); a < nCount; a++) { const sal_uInt32 aStepTime(generateStepTime(a)); const animation::AnimationEntryFixed aTime(static_cast(aStepTime), static_cast(a) / static_cast(nCount)); aAnimationLoop.append(aTime); } animation::AnimationEntryList aAnimationEntryList; aAnimationEntryList.append(aAnimationLoop); setAnimationEntry(aAnimationEntryList); } Primitive2DReference createFromBuffer() const { // create BitmapEx by extracting from VirtualDevices const Bitmap aMainBitmap(maVirtualDevice->GetBitmap(Point(), maVirtualDevice->GetOutputSizePixel())); bool useAlphaMask = false; #if defined(MACOSX) || defined(IOS) useAlphaMask = true; #else // GetBitmap()-> AlphaMask is optimized with SkiaSalBitmap::InterpretAs8Bit(), 1bpp mask is not. if( SkiaHelper::isVCLSkiaEnabled()) useAlphaMask = true; #endif BitmapEx bitmap; if( useAlphaMask ) { const AlphaMask aMaskBitmap(maVirtualDeviceMask->GetBitmap(Point(), maVirtualDeviceMask->GetOutputSizePixel())); bitmap = BitmapEx(aMainBitmap, aMaskBitmap); } else { Bitmap aMaskBitmap(maVirtualDeviceMask->GetBitmap(Point(), maVirtualDeviceMask->GetOutputSizePixel())); // tdf#156630 invert the alpha mask aMaskBitmap.Invert(); // convert from transparency to alpha bitmap = BitmapEx(aMainBitmap, aMaskBitmap); } return Primitive2DReference( new BitmapPrimitive2D( bitmap, getTransform())); } void checkSafeToBuffer(sal_uInt32 nIndex) { if (mbBufferingAllowed) { // all frames buffered if (!maBufferedPrimitives.empty() && nIndex < maBufferedPrimitives.size()) { if (!maBufferedPrimitives[nIndex].is()) { maBufferedPrimitives[nIndex] = createFromBuffer(); // check if buffering is complete bool bBufferingComplete(true); for (auto const & a: maBufferedPrimitives) { if (!a.is()) { bBufferingComplete = false; break; } } if (bBufferingComplete) { maVirtualDevice.disposeAndClear(); maVirtualDeviceMask.disposeAndClear(); } } } } else { // always buffer first frame if (0 == nIndex && !maBufferedFirstFrame.is()) { maBufferedFirstFrame = createFromBuffer(); } } } void createFrame(sal_uInt32 nTarget) { // mnNextFrameToPrepare is the target frame to create next (which implies that // mnNextFrameToPrepare-1 *is* currently in the VirtualDevice when // 0 != mnNextFrameToPrepare. nTarget is the target frame. if (!isValidData()) return; if (mnNextFrameToPrepare > nTarget) { // we are ahead request, reset mechanism to start at frame zero ensureVirtualDeviceSizeAndState(); mnNextFrameToPrepare = 0; } while (mnNextFrameToPrepare <= nTarget) { // prepare step const AnimationFrame& rAnimationFrame = maAnimation.Get(sal_uInt16(mnNextFrameToPrepare)); bool bSourceBlending = rAnimationFrame.meBlend == Blend::Source; if (bSourceBlending) { tools::Rectangle aArea(rAnimationFrame.maPositionPixel, rAnimationFrame.maBitmapEx.GetSizePixel()); maVirtualDevice->Erase(aArea); maVirtualDeviceMask->Erase(aArea); } switch (rAnimationFrame.meDisposal) { case Disposal::Not: { maVirtualDevice->DrawBitmapEx(rAnimationFrame.maPositionPixel, rAnimationFrame.maBitmapEx); AlphaMask aAlphaMask = rAnimationFrame.maBitmapEx.GetAlphaMask(); if (aAlphaMask.IsEmpty()) { const Point aEmpty; const ::tools::Rectangle aRect(aEmpty, maVirtualDeviceMask->GetOutputSizePixel()); const Wallpaper aWallpaper(COL_BLACK); maVirtualDeviceMask->DrawWallpaper(aRect, aWallpaper); } else { BitmapEx aExpandVisibilityMask(aAlphaMask.GetBitmap(), aAlphaMask); maVirtualDeviceMask->DrawBitmapEx(rAnimationFrame.maPositionPixel, aExpandVisibilityMask); } break; } case Disposal::Back: { // #i70772# react on no mask, for primitives, too. const AlphaMask & rMask(rAnimationFrame.maBitmapEx.GetAlphaMask()); maVirtualDeviceMask->Erase(); maVirtualDevice->DrawBitmapEx(rAnimationFrame.maPositionPixel, rAnimationFrame.maBitmapEx); if (rMask.IsEmpty()) { const ::tools::Rectangle aRect(rAnimationFrame.maPositionPixel, rAnimationFrame.maBitmapEx.GetSizePixel()); maVirtualDeviceMask->SetFillColor(COL_BLACK); maVirtualDeviceMask->SetLineColor(); maVirtualDeviceMask->DrawRect(aRect); } else { BitmapEx aExpandVisibilityMask(rMask.GetBitmap(), rMask); maVirtualDeviceMask->DrawBitmapEx(rAnimationFrame.maPositionPixel, aExpandVisibilityMask); } break; } case Disposal::Previous: { maVirtualDevice->DrawBitmapEx(rAnimationFrame.maPositionPixel, rAnimationFrame.maBitmapEx); BitmapEx aExpandVisibilityMask(rAnimationFrame.maBitmapEx.GetAlphaMask().GetBitmap(), rAnimationFrame.maBitmapEx.GetAlphaMask()); maVirtualDeviceMask->DrawBitmapEx(rAnimationFrame.maPositionPixel, aExpandVisibilityMask); break; } } // to not waste created data, check adding to buffers checkSafeToBuffer(mnNextFrameToPrepare); mnNextFrameToPrepare++; } } Primitive2DReference tryTogetFromBuffer(sal_uInt32 nIndex) const { if (mbBufferingAllowed) { // all frames buffered, check if available if (!maBufferedPrimitives.empty() && nIndex < maBufferedPrimitives.size()) { if (maBufferedPrimitives[nIndex].is()) { return maBufferedPrimitives[nIndex]; } } } else { // always buffer first frame, it's sometimes requested out-of-order if (0 == nIndex && maBufferedFirstFrame.is()) { return maBufferedFirstFrame; } } return Primitive2DReference(); } public: /// constructor AnimatedGraphicPrimitive2D( const Graphic& rGraphic, basegfx::B2DHomMatrix aTransform); /// data read access const basegfx::B2DHomMatrix& getTransform() const { return maTransform; } /// compare operator virtual bool operator==(const BasePrimitive2D& rPrimitive) const override; /// override to deliver the correct expected frame dependent of timing virtual void get2DDecomposition(Primitive2DDecompositionVisitor& rVisitor, const geometry::ViewInformation2D& rViewInformation) const override; }; } AnimatedGraphicPrimitive2D::AnimatedGraphicPrimitive2D( const Graphic& rGraphic, basegfx::B2DHomMatrix aTransform) : AnimatedSwitchPrimitive2D( animation::AnimationEntryList(), Primitive2DContainer(), false), maTransform(std::move(aTransform)), maGraphic(rGraphic), maAnimation(rGraphic.GetAnimation()), maVirtualDevice(*Application::GetDefaultDevice()), maVirtualDeviceMask(*Application::GetDefaultDevice()), mnNextFrameToPrepare(SAL_MAX_UINT32), mbBufferingAllowed(false), mbHugeSize(false) { // initialize AnimationTiming, needed to detect which frame is requested // in get2DDecomposition createAndSetAnimationTiming(); // check if we allow buffering if (isValidData()) { // allow buffering up to a size of: // - 64 frames // - sizes of 256x256 pixels // This may be offered in option values if needed static const sal_uInt64 nAllowedSize(64 * 256 * 256); static const sal_uInt64 nHugeSize(10000000); const Size aTarget(maAnimation.GetDisplaySizePixel()); const sal_uInt64 nUsedSize(static_cast(maAnimation.Count()) * aTarget.Width() * aTarget.Height()); if (nUsedSize < nAllowedSize) { mbBufferingAllowed = true; } if (nUsedSize > nHugeSize) { mbHugeSize = true; } } // prepare buffer space if (mbBufferingAllowed && isValidData()) { maBufferedPrimitives.resize(maAnimation.Count()); } } bool AnimatedGraphicPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const { // do not use 'GroupPrimitive2D::operator==' here, that would compare // the children. Also do not use 'BasePrimitive2D::operator==', that would // check the ID-Type. Since we are a simple derivation without own ID, // use the dynamic_cast RTTI directly const AnimatedGraphicPrimitive2D* pCompare = dynamic_cast(&rPrimitive); // use operator== of Graphic - if that is equal, the basic definition is equal return (nullptr != pCompare && getTransform() == pCompare->getTransform() && maGraphic == pCompare->maGraphic); } void AnimatedGraphicPrimitive2D::get2DDecomposition(Primitive2DDecompositionVisitor& rVisitor, const geometry::ViewInformation2D& rViewInformation) const { if (!isValidData()) return; Primitive2DReference aRetval; const double fState(getAnimationEntry().getStateAtTime(rViewInformation.getViewTime())); const sal_uInt32 nLen(maAnimation.Count()); sal_uInt32 nIndex(basegfx::fround(fState * static_cast(nLen))); // nIndex is the requested frame - it is in range [0..nLen[ // create frame representation in VirtualDevices if (nIndex >= nLen) { nIndex = nLen - 1; } // check buffering shortcuts, may already be created aRetval = tryTogetFromBuffer(nIndex); if (aRetval.is()) { rVisitor.visit(aRetval); return; } // if huge size (and not the buffered 1st frame) simply // create next frame if (mbHugeSize && 0 != nIndex && mnNextFrameToPrepare <= nIndex) { nIndex = mnNextFrameToPrepare % nLen; } // frame not (yet) buffered or no buffering allowed, create it const_cast(this)->createFrame(nIndex); // try to get from buffer again, may have been added from createFrame aRetval = tryTogetFromBuffer(nIndex); if (aRetval.is()) { rVisitor.visit(aRetval); return; } // did not work (not buffered and not 1st frame), create from buffer aRetval = createFromBuffer(); rVisitor.visit(aRetval); } } // end of namespace namespace drawinglayer::primitive2d { void create2DDecompositionOfGraphic( Primitive2DContainer& rContainer, const Graphic& rGraphic, const basegfx::B2DHomMatrix& rTransform) { Primitive2DContainer aRetval; switch(rGraphic.GetType()) { case GraphicType::Bitmap : { if(rGraphic.IsAnimated()) { // prepare specialized AnimatedGraphicPrimitive2D aRetval.resize(1); aRetval[0] = new AnimatedGraphicPrimitive2D( rGraphic, rTransform); } else if(rGraphic.getVectorGraphicData()) { // embedded Vector Graphic Data fill, create embed transform const basegfx::B2DRange& rSvgRange(rGraphic.getVectorGraphicData()->getRange()); if(basegfx::fTools::more(rSvgRange.getWidth(), 0.0) && basegfx::fTools::more(rSvgRange.getHeight(), 0.0)) { // translate back to origin, scale to unit coordinates basegfx::B2DHomMatrix aEmbedVectorGraphic( basegfx::utils::createTranslateB2DHomMatrix( -rSvgRange.getMinX(), -rSvgRange.getMinY())); aEmbedVectorGraphic.scale( 1.0 / rSvgRange.getWidth(), 1.0 / rSvgRange.getHeight()); // apply created object transformation aEmbedVectorGraphic = rTransform * aEmbedVectorGraphic; // add Vector Graphic Data primitives embedded aRetval.resize(1); aRetval[0] = new TransformPrimitive2D( aEmbedVectorGraphic, Primitive2DContainer(rGraphic.getVectorGraphicData()->getPrimitive2DSequence())); } } else { aRetval.resize(1); aRetval[0] = new BitmapPrimitive2D( rGraphic.GetBitmapEx(), rTransform); } break; } case GraphicType::GdiMetafile : { // create MetafilePrimitive2D const GDIMetaFile& rMetafile = rGraphic.GetGDIMetaFile(); aRetval.resize(1); aRetval[0] = new MetafilePrimitive2D( rTransform, rMetafile); // #i100357# find out if clipping is needed for this primitive. Unfortunately, // there exist Metafiles who's content is bigger than the proposed PrefSize set // at them. This is an error, but we need to work around this const Size aMetaFilePrefSize(rMetafile.GetPrefSize()); const Size aMetaFileRealSize( rMetafile.GetBoundRect( *Application::GetDefaultDevice()).GetSize()); if(aMetaFileRealSize.getWidth() > aMetaFilePrefSize.getWidth() || aMetaFileRealSize.getHeight() > aMetaFilePrefSize.getHeight()) { // clipping needed. Embed to MaskPrimitive2D. Create children and mask polygon basegfx::B2DPolygon aMaskPolygon(basegfx::utils::createUnitPolygon()); aMaskPolygon.transform(rTransform); aRetval = Primitive2DContainer { Primitive2DReference(new MaskPrimitive2D( basegfx::B2DPolyPolygon(aMaskPolygon), std::move(aRetval))) }; } break; } default: { // nothing to create break; } } rContainer.append(std::move(aRetval)); } Primitive2DContainer create2DColorModifierEmbeddingsAsNeeded( Primitive2DContainer&& rChildren, GraphicDrawMode aGraphicDrawMode, double fLuminance, double fContrast, double fRed, double fGreen, double fBlue, double fGamma, bool bInvert) { Primitive2DContainer aRetval; if(rChildren.empty()) { // no child content, done return aRetval; } // set child content as retval; that is what will be used as child content in all // embeddings from here aRetval = std::move(rChildren); if(GraphicDrawMode::Watermark == aGraphicDrawMode) { // this is solved by applying fixed values additionally to luminance // and contrast, do it here and reset DrawMode to GraphicDrawMode::Standard // original in svtools uses: // #define WATERMARK_LUM_OFFSET 50 // #define WATERMARK_CON_OFFSET -70 fLuminance = std::clamp(fLuminance + 0.5, -1.0, 1.0); fContrast = std::clamp(fContrast - 0.7, -1.0, 1.0); aGraphicDrawMode = GraphicDrawMode::Standard; } // DrawMode (GraphicDrawMode::Watermark already handled) switch(aGraphicDrawMode) { case GraphicDrawMode::Greys: { // convert to grey const Primitive2DReference aPrimitiveGrey( new ModifiedColorPrimitive2D( std::move(aRetval), std::make_shared())); aRetval = Primitive2DContainer { aPrimitiveGrey }; break; } case GraphicDrawMode::Mono: { // convert to mono (black/white with threshold 0.5) const Primitive2DReference aPrimitiveBlackAndWhite( new ModifiedColorPrimitive2D( std::move(aRetval), std::make_shared(0.5))); aRetval = Primitive2DContainer { aPrimitiveBlackAndWhite }; break; } default: // case GraphicDrawMode::Standard: { assert( aGraphicDrawMode != GraphicDrawMode::Watermark && "OOps, GraphicDrawMode::Watermark should already be handled (see above)"); // nothing to do break; } } // mnContPercent, mnLumPercent, mnRPercent, mnGPercent, mnBPercent // handled in a single call if(!basegfx::fTools::equalZero(fLuminance) || !basegfx::fTools::equalZero(fContrast) || !basegfx::fTools::equalZero(fRed) || !basegfx::fTools::equalZero(fGreen) || !basegfx::fTools::equalZero(fBlue)) { const Primitive2DReference aPrimitiveRGBLuminannceContrast( new ModifiedColorPrimitive2D( std::move(aRetval), std::make_shared( fRed, fGreen, fBlue, fLuminance, fContrast))); aRetval = Primitive2DContainer { aPrimitiveRGBLuminannceContrast }; } // gamma (boolean) if(!basegfx::fTools::equal(fGamma, 1.0)) { const Primitive2DReference aPrimitiveGamma( new ModifiedColorPrimitive2D( std::move(aRetval), std::make_shared( fGamma))); aRetval = Primitive2DContainer { aPrimitiveGamma }; } // invert (boolean) if(bInvert) { const Primitive2DReference aPrimitiveInvert( new ModifiedColorPrimitive2D( std::move(aRetval), std::make_shared())); aRetval = Primitive2DContainer { aPrimitiveInvert }; } return aRetval; } } // end of namespace /* vim:set shiftwidth=4 softtabstop=4 expandtab: */