office-gobmx/drawinglayer/source/primitive2d/graphicprimitivehelper2d.cxx
Noel Grandin b153d147c9 remove support for BITMASK in vcl backends
Rather use a proper alpha channel if we need transparency.

This is another small step towards merged alpha in our vcl
layer.

I suspect the intent in a lot of this code was to save memory.
Which have been a thing way back then, but these days our
backends mostly end up doing a copy-and-convert to a real
alpha channel anyway, so the existing code is actually
now a pessimisation.

Change-Id: I4a2bcbb2f76b841f05bc00580f364492829c69de
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/114808
Tested-by: Jenkins
Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
2021-04-29 08:40:56 +02:00

699 lines
28 KiB
C++

/* -*- 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 <sal/config.h>
#include <algorithm>
#include <primitive2d/graphicprimitivehelper2d.hxx>
#include <drawinglayer/animation/animationtiming.hxx>
#include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
#include <drawinglayer/primitive2d/animatedprimitive2d.hxx>
#include <drawinglayer/primitive2d/metafileprimitive2d.hxx>
#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>
// helper class for animated graphics
#include <vcl/animate/Animation.hxx>
#include <vcl/graph.hxx>
#include <vcl/virdev.hxx>
#include <vcl/svapp.hxx>
#include <toolkit/helper/vclunohelper.hxx>
#include <vcl/skia/SkiaHelper.hxx>
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
Primitive2DContainer 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);
}
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 AnimationBitmap& rAnimationBitmap = maAnimation.Get(sal_uInt16(nIndex));
sal_uInt32 nWaitTime(rAnimationBitmap.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 == rAnimationBitmap.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<double>(aStepTime), static_cast<double>(a) / static_cast<double>(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
{
const Bitmap aMaskBitmap(maVirtualDeviceMask->GetBitmap(Point(), maVirtualDeviceMask->GetOutputSizePixel()));
bitmap = BitmapEx(aMainBitmap, aMaskBitmap);
}
return Primitive2DReference(
new BitmapPrimitive2D(
VCLUnoHelper::CreateVCLXBitmap(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 AnimationBitmap& rAnimationBitmap = maAnimation.Get(sal_uInt16(mnNextFrameToPrepare));
switch (rAnimationBitmap.meDisposal)
{
case Disposal::Not:
{
maVirtualDevice->DrawBitmapEx(rAnimationBitmap.maPositionPixel, rAnimationBitmap.maBitmapEx);
Bitmap aAlphaMask = rAnimationBitmap.maBitmapEx.GetAlpha();
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, aAlphaMask);
maVirtualDeviceMask->DrawBitmapEx(rAnimationBitmap.maPositionPixel, aExpandVisibilityMask);
}
break;
}
case Disposal::Back:
{
// #i70772# react on no mask, for primitives, too.
const Bitmap & rMask(rAnimationBitmap.maBitmapEx.GetAlpha());
const Bitmap & rContent(rAnimationBitmap.maBitmapEx.GetBitmap());
maVirtualDeviceMask->Erase();
maVirtualDevice->DrawBitmap(rAnimationBitmap.maPositionPixel, rContent);
if (rMask.IsEmpty())
{
const ::tools::Rectangle aRect(rAnimationBitmap.maPositionPixel, rContent.GetSizePixel());
maVirtualDeviceMask->SetFillColor(COL_BLACK);
maVirtualDeviceMask->SetLineColor();
maVirtualDeviceMask->DrawRect(aRect);
}
else
{
BitmapEx aExpandVisibilityMask(rMask, rMask);
maVirtualDeviceMask->DrawBitmapEx(rAnimationBitmap.maPositionPixel, aExpandVisibilityMask);
}
break;
}
case Disposal::Previous:
{
maVirtualDevice->DrawBitmapEx(rAnimationBitmap.maPositionPixel, rAnimationBitmap.maBitmapEx);
BitmapEx aExpandVisibilityMask(rAnimationBitmap.maBitmapEx.GetAlpha(), rAnimationBitmap.maBitmapEx.GetAlpha());
maVirtualDeviceMask->DrawBitmapEx(rAnimationBitmap.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,
const basegfx::B2DHomMatrix& rTransform);
/// 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,
const basegfx::B2DHomMatrix& rTransform)
: AnimatedSwitchPrimitive2D(
animation::AnimationEntryList(),
Primitive2DContainer(),
false),
maTransform(rTransform),
maGraphic(rGraphic),
maAnimation(rGraphic.GetAnimation()),
maVirtualDevice(*Application::GetDefaultDevice()),
maVirtualDeviceMask(*Application::GetDefaultDevice()),
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(static_cast<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());
}
}
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()
&& 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<double>(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.append(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<AnimatedGraphicPrimitive2D*>(this)->createFrame(nIndex);
// try to get from buffer again, may have been added from createFrame
aRetval = tryTogetFromBuffer(nIndex);
if (aRetval.is())
{
rVisitor.append(aRetval);
return;
}
// did not work (not buffered and not 1st frame), create from buffer
aRetval = createFromBuffer();
rVisitor.append(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,
rGraphic.getVectorGraphicData()->getPrimitive2DSequence());
}
}
else
{
aRetval.resize(1);
aRetval[0] = new BitmapPrimitive2D(
VCLUnoHelper::CreateVCLXBitmap(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);
Primitive2DReference mask = new MaskPrimitive2D(
basegfx::B2DPolyPolygon(aMaskPolygon),
aRetval);
aRetval[0] = mask;
}
break;
}
default:
{
// nothing to create
break;
}
}
rContainer.insert(rContainer.end(), aRetval.begin(), aRetval.end());
}
Primitive2DContainer create2DColorModifierEmbeddingsAsNeeded(
const 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 = 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(
aRetval,
std::make_shared<basegfx::BColorModifier_gray>()));
aRetval = Primitive2DContainer { aPrimitiveGrey };
break;
}
case GraphicDrawMode::Mono:
{
// convert to mono (black/white with threshold 0.5)
const Primitive2DReference aPrimitiveBlackAndWhite(
new ModifiedColorPrimitive2D(
aRetval,
std::make_shared<basegfx::BColorModifier_black_and_white>(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(
aRetval,
std::make_shared<basegfx::BColorModifier_RGBLuminanceContrast>(
fRed,
fGreen,
fBlue,
fLuminance,
fContrast)));
aRetval = Primitive2DContainer { aPrimitiveRGBLuminannceContrast };
}
// gamma (boolean)
if(!basegfx::fTools::equal(fGamma, 1.0))
{
const Primitive2DReference aPrimitiveGamma(
new ModifiedColorPrimitive2D(
aRetval,
std::make_shared<basegfx::BColorModifier_gamma>(
fGamma)));
aRetval = Primitive2DContainer { aPrimitiveGamma };
}
// invert (boolean)
if(bInvert)
{
const Primitive2DReference aPrimitiveInvert(
new ModifiedColorPrimitive2D(
aRetval,
std::make_shared<basegfx::BColorModifier_invert>()));
aRetval = Primitive2DContainer { aPrimitiveInvert };
}
return aRetval;
}
} // end of namespace
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */