diff options
author | Armin Le Grand <Armin.Le.Grand@cib.de> | 2016-06-03 13:58:40 +0200 |
---|---|---|
committer | Caolán McNamara <caolanm@redhat.com> | 2016-06-10 13:39:10 +0000 |
commit | 285744fef87f4ca0278834b97d7f618bdba5f4c0 (patch) | |
tree | 3192a9683c8f5715b70087b116b8b916dfcf6563 /drawinglayer | |
parent | CygWin is also known terminal (diff) | |
download | core-285744fef87f4ca0278834b97d7f618bdba5f4c0.tar.gz core-285744fef87f4ca0278834b97d7f618bdba5f4c0.zip |
tdf#99519 Added more intelligent handling of animated GIFs
Isolated to a single Primitive2D class based on the AnimatedSwitch-
Primitive2D which does the specializing in one place. Buffers small
GIFs completely, handles 1st frame always buffered, huge GIFs get
animated by just playing he next frame.
To reach more with the current approach we would have to re-implement
AnimatedGIF import, replay it internally on a sys-specific Surface
and blit the current content (with alpha) to our display
Change-Id: I46c3325fa7936df73bea9a9284a0421f1475a34b
Reviewed-on: https://gerrit.libreoffice.org/26103
Tested-by: Jenkins <ci@libreoffice.org>
Reviewed-by: Caolán McNamara <caolanm@redhat.com>
Tested-by: Caolán McNamara <caolanm@redhat.com>
Diffstat (limited to 'drawinglayer')
-rw-r--r-- | drawinglayer/source/primitive2d/animatedprimitive2d.cxx | 9 | ||||
-rw-r--r-- | drawinglayer/source/primitive2d/graphicprimitivehelper2d.cxx | 516 |
2 files changed, 386 insertions, 139 deletions
diff --git a/drawinglayer/source/primitive2d/animatedprimitive2d.cxx b/drawinglayer/source/primitive2d/animatedprimitive2d.cxx index 5533b265426b..2fa2916f1498 100644 --- a/drawinglayer/source/primitive2d/animatedprimitive2d.cxx +++ b/drawinglayer/source/primitive2d/animatedprimitive2d.cxx @@ -31,6 +31,15 @@ namespace drawinglayer { namespace primitive2d { + void AnimatedSwitchPrimitive2D::setAnimationEntry(const animation::AnimationEntry& rNew) + { + // delete cloned animation description + delete mpAnimationEntry; + + // clone given animation description + mpAnimationEntry = rNew.clone(); + } + AnimatedSwitchPrimitive2D::AnimatedSwitchPrimitive2D( const animation::AnimationEntry& rAnimationEntry, const Primitive2DContainer& rChildren, diff --git a/drawinglayer/source/primitive2d/graphicprimitivehelper2d.cxx b/drawinglayer/source/primitive2d/graphicprimitivehelper2d.cxx index b51fc91f5b43..66a67d4c5bbf 100644 --- a/drawinglayer/source/primitive2d/graphicprimitivehelper2d.cxx +++ b/drawinglayer/source/primitive2d/graphicprimitivehelper2d.cxx @@ -25,6 +25,7 @@ #include <drawinglayer/primitive2d/transformprimitive2d.hxx> #include <drawinglayer/primitive2d/maskprimitive2d.hxx> #include <drawinglayer/primitive2d/modifiedcolorprimitive2d.hxx> +#include <drawinglayer/geometry/viewinformation2d.hxx> #include <basegfx/polygon/b2dpolygon.hxx> #include <basegfx/polygon/b2dpolygontools.hxx> #include <basegfx/numeric/ftools.hxx> @@ -37,152 +38,413 @@ #include <vcl/svapp.hxx> #include <vcl/metaact.hxx> -namespace +namespace drawinglayer { - struct animationStep + namespace primitive2d { - BitmapEx maBitmapEx; - sal_uInt32 mnTime; - }; + class AnimatedGraphicPrimitive2D : public AnimatedSwitchPrimitive2D + { + private: + /// the geometric definition + basegfx::B2DHomMatrix maTransform; - class animatedBitmapExPreparator - { - ::Animation maAnimation; - ::std::vector< animationStep > maSteps; + /** 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 cpoy for now + */ + const Graphic maGraphic; - sal_uInt32 generateStepTime(sal_uInt32 nIndex) const; + /// local animation processing data, excerpt from maGraphic + ::Animation maAnimation; - public: - explicit animatedBitmapExPreparator(const Graphic& rGraphic); + /// the on-demand created VirtualDevices for frame creation + ScopedVclPtrInstance< VirtualDevice > maVirtualDevice; + ScopedVclPtrInstance< VirtualDevice > maVirtualDeviceMask; - sal_uInt32 count() const { return maSteps.size(); } - sal_uInt32 loopCount() const { return (sal_uInt32)maAnimation.GetLoopCount(); } - sal_uInt32 stepTime(sal_uInt32 a) const { return maSteps[a].mnTime; } - const BitmapEx& stepBitmapEx(sal_uInt32 a) const { return maSteps[a].maBitmapEx; } - }; + // index of the next frame that would be regularly prepared + sal_uInt32 mnNextFrameToPrepare; - sal_uInt32 animatedBitmapExPreparator::generateStepTime(sal_uInt32 nIndex) const - { - const AnimationBitmap& rAnimBitmap = maAnimation.Get(sal_uInt16(nIndex)); - sal_uInt32 nWaitTime(rAnimBitmap.nWait * 10); + /// buffering of 1st frame (always active) + Primitive2DReference maBufferedFirstFrame; - // 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 == rAnimBitmap.nWait) - { - // ATM the huge value would block the timer, so - // use a long time to show first page (whole day) - nWaitTime = 100 * 60 * 60 * 24; - } + /// buffering of all frames + Primitive2DContainer maBufferedPrimitives; + bool mbBufferingAllowed; - // Bad trap: There are animated gifs with no set WaitTime (!). - // In that case use a default value. - if(0L == nWaitTime) - { - nWaitTime = 100L; - } + /// set if the animation is huge so that just always the next frame + /// is used instead of using timing + bool mbHugeSize; - return nWaitTime; - } + /// helper methods + bool isValidData() const + { + return (GraphicType::Bitmap == maGraphic.GetType() + && maGraphic.IsAnimated() + && maAnimation.Count()); + } - animatedBitmapExPreparator::animatedBitmapExPreparator(const Graphic& rGraphic) - : maAnimation(rGraphic.GetAnimation()) - { - OSL_ENSURE(GraphicType::Bitmap == rGraphic.GetType() && rGraphic.IsAnimated(), "animatedBitmapExPreparator: graphic is not animated (!)"); + void ensureVirtualDeviceSizeAndState() + { + if (isValidData()) + { + const Size aCurrent(maVirtualDevice->GetOutputSizePixel()); + const Size aTarget(maAnimation.GetDisplaySizePixel()); - // #128539# secure access to Animation, looks like there exist animated GIFs out there - // with a step count of zero - if(maAnimation.Count()) - { - ScopedVclPtrInstance< VirtualDevice > aVirtualDevice(*Application::GetDefaultDevice()); - ScopedVclPtrInstance< VirtualDevice > aVirtualDeviceMask(*Application::GetDefaultDevice(), - DeviceFormat::BITMASK); - - // Prepare VirtualDevices and their states - aVirtualDevice->EnableMapMode(false); - aVirtualDeviceMask->EnableMapMode(false); - aVirtualDevice->SetOutputSizePixel(maAnimation.GetDisplaySizePixel()); - aVirtualDeviceMask->SetOutputSizePixel(maAnimation.GetDisplaySizePixel()); - aVirtualDevice->Erase(); - aVirtualDeviceMask->Erase(); - - for(size_t a(0); a < maAnimation.Count(); a++) + if (aCurrent != aTarget) + { + maVirtualDevice->EnableMapMode(false); + maVirtualDeviceMask->EnableMapMode(false); + maVirtualDevice->SetOutputSizePixel(aTarget); + maVirtualDeviceMask->SetOutputSizePixel(aTarget); + } + + maVirtualDevice->Erase(); + maVirtualDeviceMask->Erase(); + } + } + + sal_uInt32 generateStepTime(sal_uInt32 nIndex) const { - animationStep aNextStep; - aNextStep.mnTime = generateStepTime(a); + const AnimationBitmap& rAnimBitmap = maAnimation.Get(sal_uInt16(nIndex)); + sal_uInt32 nWaitTime(rAnimBitmap.nWait * 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 == rAnimBitmap.nWait) + { + // ATM the huge value would block the timer, so + // use a long time to show first page (whole day) + nWaitTime = 100 * 60 * 60 * 24; + } - // prepare step - const AnimationBitmap& rAnimBitmap = maAnimation.Get(sal_uInt16(a)); + // Bad trap: There are animated gifs with no set WaitTime (!). + // In that case use a default value. + if (0L == nWaitTime) + { + nWaitTime = 100L; + } + + return nWaitTime; + } - switch(rAnimBitmap.eDisposal) + void createAndSetAnimationTiming() + { + if (isValidData()) { - case Disposal::Not: + animation::AnimationEntryLoop aAnimationLoop(maAnimation.GetLoopCount() ? maAnimation.GetLoopCount() : 0xffff); + const sal_uInt32 nCount(maAnimation.Count()); + + for (sal_uInt32 a(0); a < nCount; a++) { - aVirtualDevice->DrawBitmapEx(rAnimBitmap.aPosPix, rAnimBitmap.aBmpEx); - Bitmap aMask = rAnimBitmap.aBmpEx.GetMask(); + const sal_uInt32 aStepTime(generateStepTime(a)); + const animation::AnimationEntryFixed aTime((double)aStepTime, (double)a / (double)nCount); - if(aMask.IsEmpty()) - { - const Point aEmpty; - const Rectangle aRect(aEmpty, aVirtualDeviceMask->GetOutputSizePixel()); - const Wallpaper aWallpaper(COL_BLACK); - aVirtualDeviceMask->DrawWallpaper(aRect, aWallpaper); - } - else + 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())); +#if defined(MACOSX) || defined(IOS) + const AlphaMask aMaskBitmap(maVirtualDeviceMask->GetBitmap(Point(), maVirtualDeviceMask->GetOutputSizePixel())); +#else + const Bitmap aMaskBitmap(maVirtualDeviceMask->GetBitmap(Point(), maVirtualDeviceMask->GetOutputSizePixel())); +#endif + + return Primitive2DReference( + new BitmapPrimitive2D( + BitmapEx(aMainBitmap, aMaskBitmap), + getTransform())); + } + + void checkSafeToBuffer(sal_uInt32 nIndex) + { + if (mbBufferingAllowed) + { + // all frames buffered + if (mbBufferingAllowed && maBufferedPrimitives.size() && nIndex < maBufferedPrimitives.size()) + { + if (!maBufferedPrimitives[nIndex].is()) { - BitmapEx aExpandVisibilityMask = BitmapEx(aMask, aMask); - aVirtualDeviceMask->DrawBitmapEx(rAnimBitmap.aPosPix, aExpandVisibilityMask); - } + maBufferedPrimitives[nIndex] = createFromBuffer(); - break; + // check if buffering is complete + bool bBufferingComplete(true); + + for (sal_uInt32 a(0); bBufferingComplete && a < maBufferedPrimitives.size(); a++) + { + bBufferingComplete = maBufferedPrimitives[a].is(); + } + + if (bBufferingComplete) + { + maVirtualDevice.disposeAndClear(); + maVirtualDeviceMask.disposeAndClear(); + } + } + } + } + else + { + // always buffer first frame + if (0 == nIndex && !maBufferedFirstFrame.is()) + { + maBufferedFirstFrame = createFromBuffer(); } - case Disposal::Back: + } + } + + 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 traget frame. + if (isValidData()) + { + if (mnNextFrameToPrepare > nTarget) { - // #i70772# react on no mask, for primitives, too. - const Bitmap aMask(rAnimBitmap.aBmpEx.GetMask()); - const Bitmap aContent(rAnimBitmap.aBmpEx.GetBitmap()); + // we are ahead request, reset mechanism to start at frame zero + ensureVirtualDeviceSizeAndState(); + mnNextFrameToPrepare = 0; + } - aVirtualDeviceMask->Erase(); - aVirtualDevice->DrawBitmap(rAnimBitmap.aPosPix, aContent); + while (mnNextFrameToPrepare <= nTarget) + { + // prepare step + const AnimationBitmap& rAnimBitmap = maAnimation.Get(sal_uInt16(mnNextFrameToPrepare)); - if(aMask.IsEmpty()) + switch (rAnimBitmap.eDisposal) { - const Rectangle aRect(rAnimBitmap.aPosPix, aContent.GetSizePixel()); - aVirtualDeviceMask->SetFillColor(COL_BLACK); - aVirtualDeviceMask->SetLineColor(); - aVirtualDeviceMask->DrawRect(aRect); + case Disposal::Not: + { + maVirtualDevice->DrawBitmapEx(rAnimBitmap.aPosPix, rAnimBitmap.aBmpEx); + Bitmap aMask = rAnimBitmap.aBmpEx.GetMask(); + + if (aMask.IsEmpty()) + { + const Point aEmpty; + const Rectangle aRect(aEmpty, maVirtualDeviceMask->GetOutputSizePixel()); + const Wallpaper aWallpaper(COL_BLACK); + maVirtualDeviceMask->DrawWallpaper(aRect, aWallpaper); + } + else + { + BitmapEx aExpandVisibilityMask = BitmapEx(aMask, aMask); + maVirtualDeviceMask->DrawBitmapEx(rAnimBitmap.aPosPix, aExpandVisibilityMask); + } + + break; + } + case Disposal::Back: + { + // #i70772# react on no mask, for primitives, too. + const Bitmap aMask(rAnimBitmap.aBmpEx.GetMask()); + const Bitmap aContent(rAnimBitmap.aBmpEx.GetBitmap()); + + maVirtualDeviceMask->Erase(); + maVirtualDevice->DrawBitmap(rAnimBitmap.aPosPix, aContent); + + if (aMask.IsEmpty()) + { + const Rectangle aRect(rAnimBitmap.aPosPix, aContent.GetSizePixel()); + maVirtualDeviceMask->SetFillColor(COL_BLACK); + maVirtualDeviceMask->SetLineColor(); + maVirtualDeviceMask->DrawRect(aRect); + } + else + { + maVirtualDeviceMask->DrawBitmap(rAnimBitmap.aPosPix, aMask); + } + + break; + } + case Disposal::Previous: + { + maVirtualDevice->DrawBitmapEx(rAnimBitmap.aPosPix, rAnimBitmap.aBmpEx); + maVirtualDeviceMask->DrawBitmap(rAnimBitmap.aPosPix, rAnimBitmap.aBmpEx.GetMask()); + break; + } } - else + + // 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.size() && nIndex < maBufferedPrimitives.size()) + { + if (maBufferedPrimitives[nIndex].is()) { - aVirtualDeviceMask->DrawBitmap(rAnimBitmap.aPosPix, aMask); + return maBufferedPrimitives[nIndex]; } - - break; } - case Disposal::Previous : + } + else + { + // always buffer first frame, it's sometimes requested out-of-order + if (0 == nIndex && maBufferedFirstFrame.is()) { - aVirtualDevice->DrawBitmapEx(rAnimBitmap.aPosPix, rAnimBitmap.aBmpEx); - aVirtualDeviceMask->DrawBitmap(rAnimBitmap.aPosPix, rAnimBitmap.aBmpEx.GetMask()); - break; + return maBufferedFirstFrame; } } - // create BitmapEx - Bitmap aMainBitmap = aVirtualDevice->GetBitmap(Point(), aVirtualDevice->GetOutputSizePixel()); -#if defined(MACOSX) || defined(IOS) - AlphaMask aMaskBitmap( aVirtualDeviceMask->GetBitmap( Point(), aVirtualDeviceMask->GetOutputSizePixel())); -#else - Bitmap aMaskBitmap = aVirtualDeviceMask->GetBitmap( Point(), aVirtualDeviceMask->GetOutputSizePixel()); -#endif - aNextStep.maBitmapEx = BitmapEx(aMainBitmap, aMaskBitmap); + return Primitive2DReference(); + } + + public: + /// constructor + AnimatedGraphicPrimitive2D( + const Graphic& rGraphic, + const basegfx::B2DHomMatrix& rTransform); + + /// data read access + const Graphic& getGraphic() const { return maGraphic; } + const basegfx::B2DHomMatrix& getTransform() const { return maTransform; } + + /// compare operator + virtual bool operator==(const BasePrimitive2D& rPrimitive) const override; - // add to vector - maSteps.push_back(aNextStep); + /// override to deliver the correct expected frame dependent of timing + virtual Primitive2DContainer get2DDecomposition(const geometry::ViewInformation2D& rViewInformation) const override; + }; + + AnimatedGraphicPrimitive2D::AnimatedGraphicPrimitive2D( + const Graphic& rGraphic, + const basegfx::B2DHomMatrix& rTransform) + : AnimatedSwitchPrimitive2D( + animation::AnimationEntryList(), + Primitive2DContainer(), + false), + maTransform(rTransform), + maGraphic(rGraphic), + maAnimation(rGraphic.GetAnimation()), + maVirtualDevice(*Application::GetDefaultDevice()), + maVirtualDeviceMask(*Application::GetDefaultDevice(), DeviceFormat::BITMASK), + mnNextFrameToPrepare(SAL_MAX_UINT32), + maBufferedFirstFrame(), + maBufferedPrimitives(), + 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((sal_uInt64)maAnimation.Count() * aTarget.Width() * aTarget.Height()); + + if (nUsedSize < nAllowedSize) + { + mbBufferingAllowed = true; + } + + if (nUsedSize > nHugeSize) + { + mbHugeSize = true; + } + } + + // prepare buffer space + if (mbBufferingAllowed && isValidData()) + { + maBufferedPrimitives = Primitive2DContainer(maAnimation.Count()); } } - } -} // end of anonymous namespace + + 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<const AnimatedGraphicPrimitive2D*>(&rPrimitive); + + // use operator== of Graphic - if that is equal, the basic definition is equal + return (nullptr != pCompare + && getTransform() == pCompare->getTransform() + && getGraphic() == pCompare->getGraphic()); + } + + Primitive2DContainer AnimatedGraphicPrimitive2D::get2DDecomposition(const geometry::ViewInformation2D& rViewInformation) const + { + if (isValidData()) + { + Primitive2DContainer aRetval(1); + const double fState(getAnimationEntry().getStateAtTime(rViewInformation.getViewTime())); + const sal_uInt32 nLen(maAnimation.Count()); + sal_uInt32 nIndex(basegfx::fround(fState * (double)nLen)); + + // nIndex is the requested frame - it is in range [0..nLen[ + // create frame representation in VirtualDevices + if (nIndex >= nLen) + { + nIndex = nLen - 1L; + } + + // check buffering shortcuts, may already be created + aRetval[0] = tryTogetFromBuffer(nIndex); + + if (aRetval[0].is()) + { + return aRetval; + } + + // 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<AnimatedGraphicPrimitive2D*>(this)->createFrame(nIndex); + + // try to get from buffer again, may have been added from createFrame + aRetval[0] = tryTogetFromBuffer(nIndex); + + if (aRetval[0].is()) + { + return aRetval; + } + + // did not work (not buffered and not 1st frame), create from buffer + aRetval[0] = createFromBuffer(); + + return aRetval; + } + + return Primitive2DContainer(); + } + + } // end of namespace primitive2d +} // end of namespace drawinglayer namespace drawinglayer { @@ -200,35 +462,11 @@ namespace drawinglayer { if(rGraphic.IsAnimated()) { - // prepare animation data - animatedBitmapExPreparator aData(rGraphic); - - if(aData.count()) - { - // create sub-primitives for animated bitmap and the needed animation loop - animation::AnimationEntryLoop aAnimationLoop(aData.loopCount() ? aData.loopCount() : 0xffff); - Primitive2DContainer aBitmapPrimitives(aData.count()); - - for(sal_uInt32 a(0); a < aData.count(); a++) - { - animation::AnimationEntryFixed aTime((double)aData.stepTime(a), (double)a / (double)aData.count()); - aAnimationLoop.append(aTime); - aBitmapPrimitives[a] = new BitmapPrimitive2D( - aData.stepBitmapEx(a), - rTransform); - } - - // prepare animation list - animation::AnimationEntryList aAnimationList; - aAnimationList.append(aAnimationLoop); - - // create and add animated switch primitive - aRetval.resize(1); - aRetval[0] = new AnimatedSwitchPrimitive2D( - aAnimationList, - aBitmapPrimitives, - false); - } + // prepare specialized AnimatedGraphicPrimitive2D + aRetval.resize(1); + aRetval[0] = new AnimatedGraphicPrimitive2D( + rGraphic, + rTransform); } else if(rGraphic.getSvgData().get()) { |