/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* 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/.
*/
#include <sal/config.h>
#include <drawinglayer/processor2d/cairopixelprocessor2d.hxx>
#include <drawinglayer/processor2d/SDPRProcessor2dTools.hxx>
#include <sal/log.hxx>
#include <vcl/BitmapTools.hxx>
#include <vcl/BitmapWriteAccess.hxx>
#include <vcl/alpha.hxx>
#include <vcl/cairo.hxx>
#include <vcl/outdev.hxx>
#include <vcl/svapp.hxx>
#include <basegfx/polygon/b2dpolygontools.hxx>
#include <basegfx/polygon/b2dpolypolygontools.hxx>
#include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
#include <drawinglayer/primitive2d/PolyPolygonColorPrimitive2D.hxx>
#include <drawinglayer/primitive2d/PolygonHairlinePrimitive2D.hxx>
#include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
#include <drawinglayer/primitive2d/unifiedtransparenceprimitive2d.hxx>
#include <drawinglayer/primitive2d/backgroundcolorprimitive2d.hxx>
#include <drawinglayer/primitive2d/baseprimitive2d.hxx>
#include <drawinglayer/primitive2d/markerarrayprimitive2d.hxx>
#include <drawinglayer/primitive2d/maskprimitive2d.hxx>
#include <drawinglayer/primitive2d/modifiedcolorprimitive2d.hxx>
#include <drawinglayer/primitive2d/pointarrayprimitive2d.hxx>
#include <drawinglayer/primitive2d/PolygonStrokePrimitive2D.hxx>
#include <drawinglayer/primitive2d/Tools.hxx>
#include <drawinglayer/primitive2d/transformprimitive2d.hxx>
#include <drawinglayer/primitive2d/transparenceprimitive2d.hxx>
#include <drawinglayer/primitive2d/fillgraphicprimitive2d.hxx>
#include <drawinglayer/primitive2d/fillgradientprimitive2d.hxx>
#include <drawinglayer/primitive2d/invertprimitive2d.hxx>
#include <drawinglayer/primitive2d/PolyPolygonGradientPrimitive2D.hxx>
#include <drawinglayer/primitive2d/PolyPolygonRGBAPrimitive2D.hxx>
#include <drawinglayer/primitive2d/PolyPolygonAlphaGradientPrimitive2D.hxx>
#include <drawinglayer/primitive2d/BitmapAlphaPrimitive2D.hxx>
#include <drawinglayer/primitive2d/textprimitive2d.hxx>
#include <drawinglayer/primitive2d/textdecoratedprimitive2d.hxx>
#include <drawinglayer/primitive2d/shadowprimitive2d.hxx>
#include <drawinglayer/primitive2d/svggradientprimitive2d.hxx>
#include <drawinglayer/converters.hxx>
#include <drawinglayer/primitive2d/textlayoutdevice.hxx>
#include <basegfx/curve/b2dcubicbezier.hxx>
#include <basegfx/matrix/b2dhommatrixtools.hxx>
#include <basegfx/utils/systemdependentdata.hxx>
#include <basegfx/utils/bgradient.hxx>
#include <vcl/BitmapReadAccess.hxx>
#include <officecfg/Office/Common.hxx>
#include <vcl/vcllayout.hxx>
#include <unordered_map>
#include <dlfcn.h>
using namespace com::sun::star;
namespace
{
void impl_cairo_set_hairline(cairo_t* pRT,
const drawinglayer::geometry::ViewInformation2D& rViewInformation)
{
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 18, 0)
void* addr(dlsym(nullptr, "cairo_set_hairline"));
if (nullptr != addr)
{
cairo_set_hairline(pRT, true);
return;
}
#endif
// avoid cairo_device_to_user_distance, see note on that below
const double fPx(
(rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(1.0, 0.0))
.getLength());
cairo_set_line_width(pRT, fPx);
}
void addB2DPolygonToPathGeometry(cairo_t* pRT, const basegfx::B2DPolygon& rPolygon)
{
const sal_uInt32 nPointCount(rPolygon.count());
if (0 == nPointCount)
// no points, done
return;
// get basic infos
const bool bClosed(rPolygon.isClosed());
const sal_uInt32 nEdgeCount(bClosed ? nPointCount : nPointCount - 1);
// get 1st point and move to it
basegfx::B2DPoint aCurrent(rPolygon.getB2DPoint(0));
cairo_move_to(pRT, aCurrent.getX(), aCurrent.getY());
for (sal_uInt32 nIndex(0); nIndex < nEdgeCount; nIndex++)
{
// get index for and next point
const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount);
const basegfx::B2DPoint aNext(rPolygon.getB2DPoint(nNextIndex));
// get and check curve stuff
basegfx::B2DPoint aCP1(rPolygon.getNextControlPoint(nIndex));
basegfx::B2DPoint aCP2(rPolygon.getPrevControlPoint(nNextIndex));
const bool bCP1Equal(aCP1.equal(aCurrent));
const bool bCP2Equal(aCP2.equal(aNext));
if (!bCP1Equal || !bCP2Equal)
{
// tdf#99165, see other similar changes for more info
if (bCP1Equal)
aCP1 = aCurrent + ((aCP2 - aCurrent) * 0.0005);
if (bCP2Equal)
aCP2 = aNext + ((aCP1 - aNext) * 0.0005);
cairo_curve_to(pRT, aCP1.getX(), aCP1.getY(), aCP2.getX(), aCP2.getY(), aNext.getX(),
aNext.getY());
}
else
{
cairo_line_to(pRT, aNext.getX(), aNext.getY());
}
// prepare next step
aCurrent = aNext;
}
if (bClosed)
cairo_close_path(pRT);
}
// needed as helper, see below. It guarantees clean
// construction/cleanup using destructor
// NOTE: maybe mpSurface can be constructed even simpler,
// not sure about that. It is only used to construct
// and hold path data
struct CairoContextHolder
{
cairo_surface_t* mpSurface;
cairo_t* mpRenderContext;
CairoContextHolder()
: mpSurface(cairo_image_surface_create(CAIRO_FORMAT_A1, 1, 1))
, mpRenderContext(cairo_create(mpSurface))
{
}
~CairoContextHolder()
{
cairo_destroy(mpRenderContext);
cairo_surface_destroy(mpSurface);
}
cairo_t* getContext() const { return mpRenderContext; }
};
// global static helper instance
CairoContextHolder globalStaticCairoContext;
// it shows that re-using and buffering path geometry data using
// cairo is more complicated than initially thought: when adding
// a path to a cairo_t render context it already *uses* the set
// transformation, also usually consumes the path when painting.
// The (only available) method cairo_copy_path to preserve that
// data *also* transforms the path - if not already created in
// transformed form - using the current transformation set at the
// cairo context.
// This is not what we want to have a re-usable path that is
// buffered at the Poly(poly)gon: we explicitly want *exactly*
// the coordinates in the polygon preserved *at* the polygon to
// be able to re-use that data independent from any set
// transformation at any cairo context.
// Thus, create paths using a helper (CairoPathHelper) using a
// helper cairo context (CairoContextHolder) that never gets
// transformed. This removes the need to feed it the cairo context,
// but also does not immediately add the path data to the target
// context, that needs to be done using cairo_append_path at the
// target cairo context. That works since all geometry is designed
// to use exactly that coordinate system the polygon is already
// designed for anyways, and it transforms as needed inside the
// target cairo context as needed (if transform is set)
class CairoPathHelper
{
// the created CairoPath
cairo_path_t* mpCairoPath;
public:
CairoPathHelper(const basegfx::B2DPolygon& rPolygon)
: mpCairoPath(nullptr)
{
cairo_new_path(globalStaticCairoContext.getContext());
addB2DPolygonToPathGeometry(globalStaticCairoContext.getContext(), rPolygon);
mpCairoPath = cairo_copy_path(globalStaticCairoContext.getContext());
}
CairoPathHelper(const basegfx::B2DPolyPolygon& rPolyPolygon)
: mpCairoPath(nullptr)
{
cairo_new_path(globalStaticCairoContext.getContext());
for (const auto& rPolygon : rPolyPolygon)
addB2DPolygonToPathGeometry(globalStaticCairoContext.getContext(), rPolygon);
mpCairoPath = cairo_copy_path(globalStaticCairoContext.getContext());
}
~CairoPathHelper()
{
// need to cleanup instance
cairo_path_destroy(mpCairoPath);
}
// read access
cairo_path_t* getCairoPath() const { return mpCairoPath; }
sal_Int64 getEstimatedSize() const
{
if (nullptr == mpCairoPath)
return 0;
// per node:
// - num_data incarnations of
// - sizeof(cairo_path_data_t) which is a union of defines and point data
// thus may 2 x sizeof(double)
return mpCairoPath->num_data * sizeof(cairo_path_data_t);
}
};
class SystemDependentData_CairoPathGeometry : public basegfx::SystemDependentData
{
// the CairoPath holder
std::shared_ptr<CairoPathHelper> mpCairoPathHelper;
public:
SystemDependentData_CairoPathGeometry(const std::shared_ptr<CairoPathHelper>& pCairoPathHelper)
: basegfx::SystemDependentData(Application::GetSystemDependentDataManager(),
basegfx::SDD_Type::SDDType_CairoPathGeometry)
, mpCairoPathHelper(pCairoPathHelper)
{
}
// read access
const std::shared_ptr<CairoPathHelper>& getCairoPathHelper() const { return mpCairoPathHelper; }
virtual sal_Int64 estimateUsageInBytes() const override
{
return (nullptr != mpCairoPathHelper) ? mpCairoPathHelper->getEstimatedSize() : 0;
}
};
constexpr unsigned long nMinimalPointsPath(4);
constexpr unsigned long nMinimalPointsFill(12);
void checkAndDoPixelSnap(cairo_t* pRT,
const drawinglayer::geometry::ViewInformation2D& rViewInformation)
{
const bool bPixelSnap(rViewInformation.getPixelSnapHairline()
&& rViewInformation.getUseAntiAliasing());
if (!bPixelSnap)
// no pixel snap, done
return;
// with the comments above at CairoPathHelper we cannot do PixelSnap
// at path construction time, so it needs to be done *after* the path
// data is added to the cairo context. Advantage is that all general
// path data can be buffered, though, but needs view-dependent manipulation
// here after being added.
// For now, just snap all points - no real need to identify hor/ver lines
// when you think about it
// get helper path
cairo_path_t* path(cairo_copy_path(pRT));
if (0 == path->num_data)
{
// path is empty, done
cairo_path_destroy(path);
return;
}
auto doPixelSnap([&pRT](double& rX, double& rY) {
// transform to discrete pixels
cairo_user_to_device(pRT, &rX, &rY);
// round them, also add 0.5 which will be as transform in
// the paint method to move to 'inside' pixels when AA used.
// remember: this is only done when AA is active (see bPixelSnap
// above) and moves the hairline to full-pixel position
rX = trunc(rX) + 0.5;
rY = trunc(rY) + 0.5;
// transform back to former transformed state
cairo_device_to_user(pRT, &rX, &rY);
});
for (int a(0); a < path->num_data; a += path->data[a].header.length)
{
cairo_path_data_t* data(&path->data[a]);
switch (data->header.type)
{
case CAIRO_PATH_CURVE_TO:
{
// curve: snap all three point positions,
// thus use fallthrough below
doPixelSnap(data[2].point.x, data[2].point.y);
doPixelSnap(data[3].point.x, data[3].point.y);
[[fallthrough]]; // break;
}
case CAIRO_PATH_MOVE_TO:
case CAIRO_PATH_LINE_TO:
{
// path/move: snap first point position
doPixelSnap(data[1].point.x, data[1].point.y);
break;
}
case CAIRO_PATH_CLOSE_PATH:
{
break;
}
}
}
// set changed path back at cairo context
cairo_new_path(pRT);
cairo_append_path(pRT, path);
// destroy helper path
cairo_path_destroy(path);
}
void getOrCreatePathGeometry(cairo_t* pRT, const basegfx::B2DPolygon& rPolygon,
const drawinglayer::geometry::ViewInformation2D& rViewInformation,
bool bPixelSnap)
{
// try to access buffered data
std::shared_ptr<SystemDependentData_CairoPathGeometry> pSystemDependentData_CairoPathGeometry(
rPolygon.getSystemDependentData<SystemDependentData_CairoPathGeometry>(
basegfx::SDD_Type::SDDType_CairoPathGeometry));
if (pSystemDependentData_CairoPathGeometry)
{
// re-use data and do evtl. needed pixel snap after adding on cairo path data
cairo_append_path(
pRT, pSystemDependentData_CairoPathGeometry->getCairoPathHelper()->getCairoPath());
if (bPixelSnap)
checkAndDoPixelSnap(pRT, rViewInformation);
return;
}
// create new data and add path data to pRT and do evtl. needed pixel snap after adding on cairo path data
std::shared_ptr<CairoPathHelper> pCairoPathHelper(std::make_shared<CairoPathHelper>(rPolygon));
cairo_append_path(pRT, pCairoPathHelper->getCairoPath());
if (bPixelSnap)
checkAndDoPixelSnap(pRT, rViewInformation);
// add to buffering mechanism if not trivial
if (rPolygon.count() > nMinimalPointsPath)
rPolygon.addOrReplaceSystemDependentData<SystemDependentData_CairoPathGeometry>(
pCairoPathHelper);
}
void getOrCreateFillGeometry(cairo_t* pRT, const basegfx::B2DPolyPolygon& rPolyPolygon)
{
// try to access buffered data
std::shared_ptr<SystemDependentData_CairoPathGeometry> pSystemDependentData_CairoPathGeometry(
rPolyPolygon.getSystemDependentData<SystemDependentData_CairoPathGeometry>(
basegfx::SDD_Type::SDDType_CairoPathGeometry));
if (pSystemDependentData_CairoPathGeometry)
{
// re-use data
cairo_append_path(
pRT, pSystemDependentData_CairoPathGeometry->getCairoPathHelper()->getCairoPath());
return;
}
// create new data and add path data to pRT
std::shared_ptr<CairoPathHelper> pCairoPathHelper(
std::make_shared<CairoPathHelper>(rPolyPolygon));
cairo_append_path(pRT, pCairoPathHelper->getCairoPath());
// get all PointCount to detect non-trivial
sal_uInt32 nAllPointCount(0);
for (const auto& rPolygon : rPolyPolygon)
nAllPointCount += rPolygon.count();
// add to buffering mechanism when no PixelSnapHairline (see above) and not trivial
if (nAllPointCount > nMinimalPointsFill)
rPolyPolygon.addOrReplaceSystemDependentData<SystemDependentData_CairoPathGeometry>(
pCairoPathHelper);
}
// check for env var that decides for using downscale pattern
const char* pDisableDownScale(getenv("SAL_DISABLE_CAIRO_DOWNSCALE"));
const bool bDisableDownScale(nullptr != pDisableDownScale);
constexpr unsigned long nMinimalDiscreteSize(15);
constexpr unsigned long nHalfMDSize((nMinimalDiscreteSize + 1) / 2);
constexpr unsigned long
nMinimalDiscreteSquareSizeToBuffer(nMinimalDiscreteSize* nMinimalDiscreteSize);
class CairoSurfaceHelper
{
// the buffered CairoSurface (bitmap data)
cairo_surface_t* mpCairoSurface;
// evtl. MipMapped data (pre-scale to reduce data processing load)
mutable std::unordered_map<sal_uInt64, cairo_surface_t*> maDownscaled;
// create 32bit RGBA data for given BitmapEx
void createRGBA(const BitmapEx& rBitmapEx)
{
Bitmap aSrcAlpha(rBitmapEx.GetAlphaMask().GetBitmap());
BitmapScopedReadAccess pReadAccess(rBitmapEx.GetBitmap());
BitmapScopedReadAccess pAlphaReadAccess(aSrcAlpha);
const tools::Long nHeight(pReadAccess->Height());
const tools::Long nWidth(pReadAccess->Width());
mpCairoSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, nWidth, nHeight);
if (cairo_surface_status(mpCairoSurface) != CAIRO_STATUS_SUCCESS)
{
SAL_WARN("drawinglayer",
"cairo_image_surface_create failed for: " << nWidth << " x " << nHeight);
return;
}
const sal_uInt32 nStride(cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, nWidth));
unsigned char* surfaceData(cairo_image_surface_get_data(mpCairoSurface));
for (tools::Long y(0); y < nHeight; ++y)
{
unsigned char* pPixelData(surfaceData + (nStride * y));
for (tools::Long x(0); x < nWidth; ++x)
{
const BitmapColor aColor(pReadAccess->GetColor(y, x));
const BitmapColor aAlpha(pAlphaReadAccess->GetColor(y, x));
const sal_uInt16 nAlpha(aAlpha.GetRed());
pPixelData[SVP_CAIRO_RED] = vcl::bitmap::premultiply(aColor.GetRed(), nAlpha);
pPixelData[SVP_CAIRO_GREEN] = vcl::bitmap::premultiply(aColor.GetGreen(), nAlpha);
pPixelData[SVP_CAIRO_BLUE] = vcl::bitmap::premultiply(aColor.GetBlue(), nAlpha);
pPixelData[SVP_CAIRO_ALPHA] = nAlpha;
pPixelData += 4;
}
}
cairo_surface_mark_dirty(mpCairoSurface);
}
// create 32bit RGB data for given BitmapEx
void createRGB(const BitmapEx& rBitmapEx)
{
BitmapScopedReadAccess pReadAccess(rBitmapEx.GetBitmap());
const tools::Long nHeight(pReadAccess->Height());
const tools::Long nWidth(pReadAccess->Width());
mpCairoSurface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, nWidth, nHeight);
if (cairo_surface_status(mpCairoSurface) != CAIRO_STATUS_SUCCESS)
{
SAL_WARN("drawinglayer",
"cairo_image_surface_create failed for: " << nWidth << " x " << nHeight);
return;
}
sal_uInt32 nStride(cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, nWidth));
unsigned char* surfaceData(cairo_image_surface_get_data(mpCairoSurface));
for (tools::Long y(0); y < nHeight; ++y)
{
unsigned char* pPixelData(surfaceData + (nStride * y));
for (tools::Long x(0); x < nWidth; ++x)
{
const BitmapColor aColor(pReadAccess->GetColor(y, x));
pPixelData[SVP_CAIRO_RED] = aColor.GetRed();
pPixelData[SVP_CAIRO_GREEN] = aColor.GetGreen();
pPixelData[SVP_CAIRO_BLUE] = aColor.GetBlue();
pPixelData[SVP_CAIRO_ALPHA] = 255; // not really needed
pPixelData += 4;
}
}
cairo_surface_mark_dirty(mpCairoSurface);
}
// #define TEST_RGB16
#ifdef TEST_RGB16
// experimental: create 16bit RGB data for given BitmapEx
void createRGB16(const BitmapEx& rBitmapEx)
{
BitmapScopedReadAccess pReadAccess(rBitmapEx.GetBitmap());
const tools::Long nHeight(pReadAccess->Height());
const tools::Long nWidth(pReadAccess->Width());
mpCairoSurface = cairo_image_surface_create(CAIRO_FORMAT_RGB16_565, nWidth, nHeight);
if (cairo_surface_status(mpCairoSurface) != CAIRO_STATUS_SUCCESS)
{
SAL_WARN("drawinglayer",
"cairo_image_surface_create failed for: " << nWidth << " x " << nHeight);
return;
}
sal_uInt32 nStride(cairo_format_stride_for_width(CAIRO_FORMAT_RGB16_565, nWidth));
unsigned char* surfaceData(cairo_image_surface_get_data(mpCairoSurface));
for (tools::Long y(0); y < nHeight; ++y)
{
unsigned char* pPixelData(surfaceData + (nStride * y));
for (tools::Long x(0); x < nWidth; ++x)
{
const BitmapColor aColor(pReadAccess->GetColor(y, x));
const sal_uInt8 aLeft((aColor.GetBlue() >> 3) | ((aColor.GetGreen() << 3) & 0xe0));
const sal_uInt8 aRight((aColor.GetRed() & 0xf8) | (aColor.GetGreen() >> 5));
#ifdef OSL_BIGENDIAN
pPixelData[1] = aRight;
pPixelData[0] = aLeft;
#else
pPixelData[0] = aLeft;
pPixelData[1] = aRight;
#endif
pPixelData += 2;
}
}
cairo_surface_mark_dirty(mpCairoSurface);
}
#endif
public:
CairoSurfaceHelper(const BitmapEx& rBitmapEx)
: mpCairoSurface(nullptr)
, maDownscaled()
{
if (rBitmapEx.IsAlpha())
createRGBA(rBitmapEx);
else
#ifdef TEST_RGB16
createRGB16(rBitmapEx);
#else
createRGB(rBitmapEx);
#endif
}
~CairoSurfaceHelper()
{
// cleanup surface
cairo_surface_destroy(mpCairoSurface);
// cleanup MipMap surfaces
for (auto& candidate : maDownscaled)
cairo_surface_destroy(candidate.second);
}
cairo_surface_t* getCairoSurface(sal_uInt32 nTargetWidth = 0,
sal_uInt32 nTargetHeight = 0) const
{
// in simple cases just return the single created surface
if (bDisableDownScale || nullptr == mpCairoSurface || 0 == nTargetWidth
|| 0 == nTargetHeight)
return mpCairoSurface;
// get width/height of original surface
const sal_uInt32 nSourceWidth(cairo_image_surface_get_width(mpCairoSurface));
const sal_uInt32 nSourceHeight(cairo_image_surface_get_height(mpCairoSurface));
// zoomed in, need to stretch at paint, no pre-scale useful
if (nTargetWidth >= nSourceWidth || nTargetHeight >= nSourceHeight)
return mpCairoSurface;
// calculate downscale factor. Only use ONE factor to get the diagonal
// MipMap, NOT the full MipMap field in X/Y for uneven factors in both dimensions
sal_uInt32 nFactor(1);
sal_uInt32 nW((nSourceWidth + 1) / 2);
sal_uInt32 nH((nSourceHeight + 1) / 2);
while (nW > nTargetWidth && nW > nHalfMDSize && nH > nTargetHeight && nH > nHalfMDSize)
{
nW = (nW + 1) / 2;
nH = (nH + 1) / 2;
nFactor *= 2;
}
if (1 == nFactor)
{
// original size *is* best binary size, use it
return mpCairoSurface;
}
// go up one scale again
nW *= 2;
nH *= 2;
// bail out if the multiplication for the key would overflow
if (nW >= SAL_MAX_UINT32 || nH >= SAL_MAX_UINT32)
return mpCairoSurface;
// check if we have a downscaled version of required size
const sal_uInt64 key((nW * static_cast<sal_uInt64>(SAL_MAX_UINT32)) + nH);
auto isHit(maDownscaled.find(key));
// found -> return it
if (isHit != maDownscaled.end())
return isHit->second;
// create new surface in the targeted size
cairo_surface_t* pSurfaceTarget(cairo_surface_create_similar(
mpCairoSurface, cairo_surface_get_content(mpCairoSurface), nW, nH));
// made a version to scale self first with direct memory access.
// That worked well, but would've been hard to support
// CAIRO_FORMAT_A1 and similar (including bit shifting), so
// I decided to go with cairo itself - use CAIRO_FILTER_FAST or
// CAIRO_FILTER_GOOD though. Please modify as needed for
// performance/quality
cairo_t* cr = cairo_create(pSurfaceTarget);
const double fScaleX(static_cast<double>(nW) / static_cast<double>(nSourceWidth));
const double fScaleY(static_cast<double>(nH) / static_cast<double>(nSourceHeight));
cairo_scale(cr, fScaleX, fScaleY);
cairo_set_source_surface(cr, mpCairoSurface, 0.0, 0.0);
cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_GOOD);
cairo_paint(cr);
cairo_destroy(cr);
// NOTE: Took out, until now not really needed
// need to set device_scale for downscale surfaces to get
// them handled correctly
// cairo_surface_set_device_scale(pSurfaceTarget, fScaleX, fScaleY);
// add entry to cached entries
maDownscaled[key] = pSurfaceTarget;
return pSurfaceTarget;
}
bool isTrivial() const
{
if (nullptr == mpCairoSurface)
return true;
const sal_uInt32 nSourceWidth(cairo_image_surface_get_width(mpCairoSurface));
const sal_uInt32 nSourceHeight(cairo_image_surface_get_height(mpCairoSurface));
return nSourceWidth * nSourceHeight < nMinimalDiscreteSquareSizeToBuffer;
}
};
class SystemDependentData_CairoSurface : public basegfx::SystemDependentData
{
// the CairoSurface holder
std::shared_ptr<CairoSurfaceHelper> mpCairoSurfaceHelper;
// need to remember alpha source for combined BitmapEx to detect/
// react on that changing
std::shared_ptr<SalBitmap> maAssociatedAlpha;
public:
SystemDependentData_CairoSurface(const BitmapEx& rBitmapEx)
: basegfx::SystemDependentData(Application::GetSystemDependentDataManager(),
basegfx::SDD_Type::SDDType_CairoSurface)
, mpCairoSurfaceHelper(std::make_shared<CairoSurfaceHelper>(rBitmapEx))
, maAssociatedAlpha()
{
if (rBitmapEx.IsAlpha())
maAssociatedAlpha = rBitmapEx.GetAlphaMask().GetBitmap().ImplGetSalBitmap();
}
// read access
const std::shared_ptr<CairoSurfaceHelper>& getCairoSurfaceHelper() const
{
return mpCairoSurfaceHelper;
}
const std::shared_ptr<SalBitmap>& getAssociatedAlpha() const { return maAssociatedAlpha; }
virtual sal_Int64 estimateUsageInBytes() const override;
};
sal_Int64 SystemDependentData_CairoSurface::estimateUsageInBytes() const
{
sal_Int64 nRetval(0);
if (mpCairoSurfaceHelper)
{
cairo_surface_t* pSurface(mpCairoSurfaceHelper->getCairoSurface());
const tools::Long nStride(cairo_image_surface_get_stride(pSurface));
const tools::Long nHeight(cairo_image_surface_get_height(pSurface));
nRetval = nStride * nHeight;
// if we do downscale, size will grow by 1/4 + 1/16 + 1/32 + ...,
// rough estimation just multiplies by 1.25 .. 1.33, should be good enough
// for estimation of buffer survival time
if (!bDisableDownScale)
{
nRetval = (nRetval * 5) / 4;
}
}
return nRetval;
}
std::shared_ptr<CairoSurfaceHelper> getOrCreateCairoSurfaceHelper(const BitmapEx& rBitmapEx)
{
const basegfx::SystemDependentDataHolder* pHolder(
rBitmapEx.GetBitmap().accessSystemDependentDataHolder());
std::shared_ptr<SystemDependentData_CairoSurface> pSystemDependentData_CairoSurface;
if (nullptr != pHolder)
{
// try to access SystemDependentDataHolder and buffered data
pSystemDependentData_CairoSurface
= std::static_pointer_cast<SystemDependentData_CairoSurface>(
pHolder->getSystemDependentData(basegfx::SDD_Type::SDDType_CairoSurface));
// check data validity for associated Alpha
if (pSystemDependentData_CairoSurface && rBitmapEx.IsAlpha()
&& pSystemDependentData_CairoSurface->getAssociatedAlpha()
!= rBitmapEx.GetAlphaMask().GetBitmap().ImplGetSalBitmap())
{
// AssociatedAlpha did change, data invalid
pSystemDependentData_CairoSurface.reset();
}
}
if (!pSystemDependentData_CairoSurface)
{
// create new SystemDependentData_CairoSurface
pSystemDependentData_CairoSurface
= std::make_shared<SystemDependentData_CairoSurface>(rBitmapEx);
// only add if feasible
if (nullptr != pHolder
&& !pSystemDependentData_CairoSurface->getCairoSurfaceHelper()->isTrivial()
&& pSystemDependentData_CairoSurface->calculateCombinedHoldCyclesInSeconds() > 0)
{
basegfx::SystemDependentData_SharedPtr r2(pSystemDependentData_CairoSurface);
const_cast<basegfx::SystemDependentDataHolder*>(pHolder)
->addOrReplaceSystemDependentData(r2);
}
}
return pSystemDependentData_CairoSurface->getCairoSurfaceHelper();
}
// This bit-tweaking looping is unpleasant and unfortunate
void LuminanceToAlpha(cairo_surface_t* pMask)
{
cairo_surface_flush(pMask);
const sal_uInt32 nWidth(cairo_image_surface_get_width(pMask));
const sal_uInt32 nHeight(cairo_image_surface_get_height(pMask));
const sal_uInt32 nStride(cairo_image_surface_get_stride(pMask));
if (0 == nWidth || 0 == nHeight)
return;
unsigned char* mask_surface_data(cairo_image_surface_get_data(pMask));
// change to unsigned 16bit and shifting. This is not much
// faster on modern processors due to nowadays good double/
// float HW, but may also be used on smaller HW (ARM, ...).
// Since source is sal_uInt8 integer using double (see version
// before) is not required numerically either.
// scaling values are now put to a 256 entry lookup for R, G and B
// thus 768 bytes, so no multiplications have to happen. The values
// used to create these are (54+183+18 == 255):
// sal_uInt16 nR(0.2125 * 256.0); // -> 54.4
// sal_uInt16 nG(0.7154 * 256.0); // -> 183.1424
// sal_uInt16 nB(0.0721 * 256.0); // -> 18.4576
// and the short loop (for nR, nG and nB resp.) like:
// for(unsigned short a(0); a < 256; a++)
// std::cout << ((a * nR) / 255) << ", ";
static constexpr std::array<sal_uInt8, 256> nRArray
= { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4,
4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9,
9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13,
13, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18,
18, 18, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 23,
23, 23, 23, 23, 24, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 26, 27, 27, 27, 27,
27, 28, 28, 28, 28, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32,
32, 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37,
37, 37, 37, 37, 38, 38, 38, 38, 38, 39, 39, 39, 39, 40, 40, 40, 40, 40, 41, 41, 41, 41,
41, 42, 42, 42, 42, 42, 43, 43, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 46, 46,
46, 46, 47, 47, 47, 47, 47, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 50, 50, 50, 50, 51,
51, 51, 51, 51, 52, 52, 52, 52, 52, 53, 53, 53, 53, 54 };
static constexpr std::array<sal_uInt8, 256> nGArray
= { 0, 0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 7, 8, 9, 10, 10,
11, 12, 12, 13, 14, 15, 15, 16, 17, 17, 18, 19, 20, 20, 21, 22,
22, 23, 24, 25, 25, 26, 27, 27, 28, 29, 30, 30, 31, 32, 33, 33,
34, 35, 35, 36, 37, 38, 38, 39, 40, 40, 41, 42, 43, 43, 44, 45,
45, 46, 47, 48, 48, 49, 50, 50, 51, 52, 53, 53, 54, 55, 55, 56,
57, 58, 58, 59, 60, 61, 61, 62, 63, 63, 64, 65, 66, 66, 67, 68,
68, 69, 70, 71, 71, 72, 73, 73, 74, 75, 76, 76, 77, 78, 78, 79,
80, 81, 81, 82, 83, 83, 84, 85, 86, 86, 87, 88, 88, 89, 90, 91,
91, 92, 93, 94, 94, 95, 96, 96, 97, 98, 99, 99, 100, 101, 101, 102,
103, 104, 104, 105, 106, 106, 107, 108, 109, 109, 110, 111, 111, 112, 113, 114,
114, 115, 116, 116, 117, 118, 119, 119, 120, 121, 122, 122, 123, 124, 124, 125,
126, 127, 127, 128, 129, 129, 130, 131, 132, 132, 133, 134, 134, 135, 136, 137,
137, 138, 139, 139, 140, 141, 142, 142, 143, 144, 144, 145, 146, 147, 147, 148,
149, 149, 150, 151, 152, 152, 153, 154, 155, 155, 156, 157, 157, 158, 159, 160,
160, 161, 162, 162, 163, 164, 165, 165, 166, 167, 167, 168, 169, 170, 170, 171,
172, 172, 173, 174, 175, 175, 176, 177, 177, 178, 179, 180, 180, 181, 182, 183 };
static constexpr std::array<sal_uInt8, 256> nBArray
= { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18 };
for (sal_uInt32 y(0); y < nHeight; ++y)
{
unsigned char* pMaskPixelData = mask_surface_data + (nStride * y);
for (sal_uInt32 x(0); x < nWidth; ++x)
{
// do not forget that we have pre-multiplied alpha
sal_uInt8 nAlpha(pMaskPixelData[SVP_CAIRO_ALPHA]);
if (0 != nAlpha)
{
// get Luminance in range [0..255]
const sal_uInt8 nLum(nRArray[pMaskPixelData[SVP_CAIRO_RED]]
+ nGArray[pMaskPixelData[SVP_CAIRO_GREEN]]
+ nBArray[pMaskPixelData[SVP_CAIRO_BLUE]]);
if (255 != nAlpha)
// remove pre-multiplied alpha (use existing VCL tooling)
nAlpha = vcl::bitmap::unpremultiply(nLum, nAlpha);
else
// already what we need
nAlpha = nLum;
pMaskPixelData[SVP_CAIRO_ALPHA] = 255 - nAlpha;
}
pMaskPixelData += 4;
}
}
cairo_surface_mark_dirty(pMask);
}
basegfx::B2DRange getDiscreteViewRange(cairo_t* pRT)
{
double clip_x1, clip_x2, clip_y1, clip_y2;
cairo_save(pRT);
cairo_identity_matrix(pRT);
cairo_clip_extents(pRT, &clip_x1, &clip_y1, &clip_x2, &clip_y2);
cairo_restore(pRT);
return basegfx::B2DRange(basegfx::B2DPoint(clip_x1, clip_y1),
basegfx::B2DPoint(clip_x2, clip_y2));
}
}
namespace drawinglayer::processor2d
{
CairoPixelProcessor2D::CairoPixelProcessor2D(const geometry::ViewInformation2D& rViewInformation,
tools::Long nWidthPixel, tools::Long nHeightPixel,
bool bUseRGBA)
: BaseProcessor2D(rViewInformation)
, maBColorModifierStack()
, mpOwnedSurface(nullptr)
, mpRT(nullptr)
, mbRenderSimpleTextDirect(
officecfg::Office::Common::Drawinglayer::RenderSimpleTextDirect::get())
, mbRenderDecoratedTextDirect(
officecfg::Office::Common::Drawinglayer::RenderDecoratedTextDirect::get())
, mnClipRecursionCount(0)
{
if (nWidthPixel <= 0 || nHeightPixel <= 0)
// no size, invalid
return;
mpOwnedSurface = cairo_image_surface_create(bUseRGBA ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24,
nWidthPixel, nHeightPixel);
if (nullptr == mpOwnedSurface)
// error, invalid
return;
// create RenderTarget for full target
mpRT = cairo_create(mpOwnedSurface);
if (nullptr == mpRT)
// error, invalid
return;
// initialize some basic used values/settings
cairo_set_antialias(mpRT, rViewInformation.getUseAntiAliasing() ? CAIRO_ANTIALIAS_DEFAULT
: CAIRO_ANTIALIAS_NONE);
cairo_set_fill_rule(mpRT, CAIRO_FILL_RULE_EVEN_ODD);
cairo_set_operator(mpRT, CAIRO_OPERATOR_OVER);
}
CairoPixelProcessor2D::CairoPixelProcessor2D(const geometry::ViewInformation2D& rViewInformation,
cairo_surface_t* pTarget, tools::Long nOffsetPixelX,
tools::Long nOffsetPixelY, tools::Long nWidthPixel,
tools::Long nHeightPixel)
: BaseProcessor2D(rViewInformation)
, maBColorModifierStack()
, mpOwnedSurface(nullptr)
, mpRT(nullptr)
, mbRenderSimpleTextDirect(
officecfg::Office::Common::Drawinglayer::RenderSimpleTextDirect::get())
, mbRenderDecoratedTextDirect(
officecfg::Office::Common::Drawinglayer::RenderDecoratedTextDirect::get())
, mnClipRecursionCount(0)
{
if (pTarget)
{
bool bClipNeeded(false);
if (0 != nOffsetPixelX || 0 != nOffsetPixelY || 0 != nWidthPixel || 0 != nHeightPixel)
{
if (0 != nOffsetPixelX || 0 != nOffsetPixelY)
{
// if offset is used we need initial clip
bClipNeeded = true;
}
else
{
// no offset used, compare to real pixel size
const tools::Long nRealPixelWidth(cairo_image_surface_get_width(pTarget));
const tools::Long nRealPixelHeight(cairo_image_surface_get_height(pTarget));
if (nRealPixelWidth != nWidthPixel || nRealPixelHeight != nHeightPixel)
{
// if size differs we need initial clip
bClipNeeded = true;
}
}
}
if (bClipNeeded)
{
// optional: if the possibility to add an initial clip relative
// to the real pixel dimensions of the target surface is used,
// apply it here using that nice existing method of cairo
mpOwnedSurface = cairo_surface_create_for_rectangle(
pTarget, nOffsetPixelX, nOffsetPixelY, nWidthPixel, nHeightPixel);
if (nullptr != mpOwnedSurface)
mpRT = cairo_create(mpOwnedSurface);
}
else
{
// create RenderTarget for full target
mpRT = cairo_create(pTarget);
}
if (nullptr != mpRT)
{
// initialize some basic used values/settings
cairo_set_antialias(mpRT, rViewInformation.getUseAntiAliasing()
? CAIRO_ANTIALIAS_DEFAULT
: CAIRO_ANTIALIAS_NONE);
cairo_set_fill_rule(mpRT, CAIRO_FILL_RULE_EVEN_ODD);
cairo_set_operator(mpRT, CAIRO_OPERATOR_OVER);
}
}
}
CairoPixelProcessor2D::~CairoPixelProcessor2D()
{
if (nullptr != mpRT)
cairo_destroy(mpRT);
if (nullptr != mpOwnedSurface)
cairo_surface_destroy(mpOwnedSurface);
}
BitmapEx CairoPixelProcessor2D::extractBitmapEx() const
{
// default is empty BitmapEx
BitmapEx aRetval;
if (nullptr == mpRT)
// no RenderContext, not valid
return aRetval;
cairo_surface_t* pSource(cairo_get_target(mpRT));
if (nullptr == pSource)
// no surface, not valid
return aRetval;
// check pixel sizes
const sal_uInt32 nWidth(cairo_image_surface_get_width(pSource));
const sal_uInt32 nHeight(cairo_image_surface_get_height(pSource));
if (0 == nWidth || 0 == nHeight)
// no content, not valid
return aRetval;
// check format
const cairo_format_t aFormat(cairo_image_surface_get_format(pSource));
if (CAIRO_FORMAT_ARGB32 != aFormat && CAIRO_FORMAT_RGB24 != aFormat)
// we for now only support ARGB32 and RGB24, format not supported, not valid
return aRetval;
// ensure surface read access, wer need CAIRO_SURFACE_TYPE_IMAGE
cairo_surface_t* pReadSource(pSource);
if (CAIRO_SURFACE_TYPE_IMAGE != cairo_surface_get_type(pReadSource))
{
// create mapping for read access to source
pReadSource = cairo_surface_map_to_image(pReadSource, nullptr);
}
// prepare VCL/Bitmap stuff
const Size aBitmapSize(nWidth, nHeight);
Bitmap aBitmap(aBitmapSize, vcl::PixelFormat::N24_BPP);
BitmapWriteAccess aAccess(aBitmap);
// prepare VCL/AlphaMask stuff
const bool bHasAlpha(CAIRO_FORMAT_ARGB32 == aFormat);
std::optional<AlphaMask> aAlphaMask;
// NOTE: Tried to use std::optional for pAlphaWrite but
// BitmapWriteAccess does not have all needed operators
BitmapWriteAccess* pAlphaWrite(nullptr);
if (bHasAlpha)
{
aAlphaMask = AlphaMask(aBitmapSize);
pAlphaWrite = new BitmapWriteAccess(*aAlphaMask);
}
// prepare cairo stuff
const sal_uInt32 nStride(cairo_image_surface_get_stride(pReadSource));
unsigned char* pStartPixelData(cairo_image_surface_get_data(pReadSource));
// separate loops for bHasAlpha so that we have *no* branch in the
// loops itself
if (bHasAlpha)
{
for (sal_uInt32 y(0); y < nHeight; ++y)
{
// prepare scanline
unsigned char* pPixelData(pStartPixelData + (nStride * y));
Scanline pWriteRGB = aAccess.GetScanline(y);
Scanline pWriteA = pAlphaWrite->GetScanline(y);
for (sal_uInt32 x(0); x < nWidth; ++x)
{
// RGBA: Do not forget: it's pre-multiplied
sal_uInt8 nAlpha(pPixelData[SVP_CAIRO_ALPHA]);
aAccess.SetPixelOnData(
pWriteRGB, x,
BitmapColor(vcl::bitmap::unpremultiply(pPixelData[SVP_CAIRO_RED], nAlpha),
vcl::bitmap::unpremultiply(pPixelData[SVP_CAIRO_GREEN], nAlpha),
vcl::bitmap::unpremultiply(pPixelData[SVP_CAIRO_BLUE], nAlpha)));
pAlphaWrite->SetPixelOnData(pWriteA, x, BitmapColor(nAlpha));
pPixelData += 4;
}
}
}
else
{
for (sal_uInt32 y(0); y < nHeight; ++y)
{
// prepare scanline
unsigned char* pPixelData(pStartPixelData + (nStride * y));
Scanline pWriteRGB = aAccess.GetScanline(y);
for (sal_uInt32 x(0); x < nWidth; ++x)
{
aAccess.SetPixelOnData(pWriteRGB, x,
BitmapColor(pPixelData[SVP_CAIRO_RED],
pPixelData[SVP_CAIRO_GREEN],
pPixelData[SVP_CAIRO_BLUE]));
pPixelData += 4;
}
}
}
// cleanup optional BitmapWriteAccess pAlphaWrite
if (nullptr != pAlphaWrite)
delete pAlphaWrite;
if (bHasAlpha)
// construct and return BitmapEx
aRetval = BitmapEx(aBitmap, *aAlphaMask);
else
// reset BitmapEx to just Bitmap content
aRetval = aBitmap;
if (pReadSource != pSource)
{
// cleanup mapping for read/write access to source
cairo_surface_unmap_image(pSource, pReadSource);
}
return aRetval;
}
void CairoPixelProcessor2D::processBitmapPrimitive2D(
const primitive2d::BitmapPrimitive2D& rBitmapCandidate)
{
paintBitmapAlpha(rBitmapCandidate.getBitmap(), rBitmapCandidate.getTransform());
}
void CairoPixelProcessor2D::paintBitmapAlpha(const BitmapEx& rBitmapEx,
const basegfx::B2DHomMatrix& rTransform,
double fTransparency)
{
// transparency invalid or completely transparent, done
if (fTransparency < 0.0 || fTransparency >= 1.0)
{
return;
}
// check if graphic content is inside discrete local ViewPort
const basegfx::B2DRange& rDiscreteViewPort(getViewInformation2D().getDiscreteViewport());
const basegfx::B2DHomMatrix aLocalTransform(
getViewInformation2D().getObjectToViewTransformation() * rTransform);
if (!rDiscreteViewPort.isEmpty())
{
basegfx::B2DRange aUnitRange(0.0, 0.0, 1.0, 1.0);
aUnitRange.transform(aLocalTransform);
if (!aUnitRange.overlaps(rDiscreteViewPort))
{
// content is outside discrete local ViewPort
return;
}
}
BitmapEx aBitmapEx(rBitmapEx);
if (aBitmapEx.IsEmpty() || aBitmapEx.GetSizePixel().IsEmpty())
{
// no pixel data, done
return;
}
if (maBColorModifierStack.count())
{
// need to apply ColorModifier to Bitmap data
aBitmapEx = aBitmapEx.ModifyBitmapEx(maBColorModifierStack);
if (aBitmapEx.IsEmpty())
{
// color gets completely replaced, get it
const basegfx::BColor aModifiedColor(
maBColorModifierStack.getModifiedColor(basegfx::BColor()));
// use unit geometry as fallback object geometry. Do *not*
// transform, the below used method will use the already
// correctly initialized local ViewInformation
const basegfx::B2DPolygon& aPolygon(basegfx::utils::createUnitPolygon());
// draw directly, done
paintPolyPoylgonRGBA(basegfx::B2DPolyPolygon(aPolygon), aModifiedColor, fTransparency);
return;
}
}
// access or create cairo bitmap data
std::shared_ptr<CairoSurfaceHelper> aCairoSurfaceHelper(
getOrCreateCairoSurfaceHelper(aBitmapEx));
if (!aCairoSurfaceHelper)
{
SAL_WARN("drawinglayer", "SDPRCairo: No SurfaceHelper from BitmapEx (!)");
return;
}
// work with dimensions in discrete target pixels to use evtl. MipMap pre-scale
const tools::Long nDestWidth((aLocalTransform * basegfx::B2DVector(1.0, 0.0)).getLength());
const tools::Long nDestHeight((aLocalTransform * basegfx::B2DVector(0.0, 1.0)).getLength());
cairo_surface_t* pTarget(aCairoSurfaceHelper->getCairoSurface(nDestWidth, nDestHeight));
if (nullptr == pTarget)
{
SAL_WARN("drawinglayer", "SDPRCairo: No CairoSurface from BitmapEx SurfaceHelper (!)");
return;
}
cairo_save(mpRT);
// set linear transformation - no fAAOffset for bitmap data
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aLocalTransform.a(), aLocalTransform.b(), aLocalTransform.c(),
aLocalTransform.d(), aLocalTransform.e(), aLocalTransform.f());
cairo_set_matrix(mpRT, &aMatrix);
static bool bRenderTransformationBounds(false);
if (bRenderTransformationBounds)
{
cairo_set_source_rgba(mpRT, 1, 0, 0, 0.8);
impl_cairo_set_hairline(mpRT, getViewInformation2D());
cairo_rectangle(mpRT, 0, 0, 1, 1);
cairo_stroke(mpRT);
}
const sal_uInt32 nWidth(cairo_image_surface_get_width(pTarget));
const sal_uInt32 nHeight(cairo_image_surface_get_height(pTarget));
cairo_set_source_surface(mpRT, pTarget, 0, 0);
// get the pattern created by cairo_set_source_surface and
// it's transformation
cairo_pattern_t* sourcepattern = cairo_get_source(mpRT);
cairo_pattern_get_matrix(sourcepattern, &aMatrix);
// RGBA sources overlap the unit geometry range, slightly,
// to see that activate bRenderTransformationBounds and
// insert a ARGB image, zoom to the borders. Seems to be half
// a pixel. Very good to demonstrate: 8x1 pixel, some
// transparent.
// Also errors with images 1 pixel wide/high, e.g. insert
// RGBA 8x1, 1x8 to see (and deactivate fix below). It also
// depends on the used filter, see comment below at
// cairo_pattern_set_filter. Found also errors with more
// than one pixel, so cannot use as criteria.
// This effect is also visible in the left/right/bottom/top
// page shadows, these DO use 8x1/1x8 images which led me to
// that problem. I double-checked that these *are* correctly
// defined, that is not the problem.
// I see two solutions:
static bool bRenderMasked(true);
if (bRenderMasked)
{
// Consequence is that these need clipping. That again is
// simple (we are in unit coordinates). Only do for RGBA,
// for RGB this effect does not happen
if (CAIRO_FORMAT_ARGB32 == cairo_image_surface_get_format(pTarget))
{
cairo_rectangle(mpRT, 0, 0, 1, 1);
cairo_clip(mpRT);
}
cairo_matrix_scale(&aMatrix, nWidth, nHeight);
}
else
{
// Alternative: for RGBA, resize/scale it SLIGHTLY to force
// that half pixel overlap to be inside the unit range.
// That makes the error disappear, so no clip needed, but
// SLIGHTLY smaller.
if (CAIRO_FORMAT_ARGB32 == cairo_image_surface_get_format(pTarget))
{
cairo_matrix_init_scale(&aMatrix, nWidth + 1, nHeight + 1);
cairo_matrix_translate(&aMatrix, -0.5 / (nWidth + 1), -0.5 / (nHeight + 1));
}
else
{
cairo_matrix_scale(&aMatrix, nWidth, nHeight);
}
}
// The error/effect described above also is connected to the
// filter used, so I checked the filter modes available
// in Cairo:
//
// CAIRO_FILTER_FAST: okay, small errors, sometimes stretching some pixels
// CAIRO_FILTER_GOOD: stretching error
// CAIRO_FILTER_BEST: okay, small errors
// CAIRO_FILTER_NEAREST: similar to CAIRO_FILTER_FAST
// CAIRO_FILTER_BILINEAR: similar to CAIRO_FILTER_GOOD
// CAIRO_FILTER_GAUSSIAN: same as CAIRO_FILTER_GOOD/CAIRO_FILTER_BILINEAR, should
// not be used anyways (see docs)
//
// CAIRO_FILTER_GOOD seems to be the default anyways, but set it
// to be on the safe side
cairo_pattern_set_filter(sourcepattern, CAIRO_FILTER_GOOD);
cairo_pattern_set_matrix(sourcepattern, &aMatrix);
// paint bitmap data, evtl. with additional alpha channel
if (!basegfx::fTools::equalZero(fTransparency))
cairo_paint_with_alpha(mpRT, 1.0 - fTransparency);
else
cairo_paint(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processPointArrayPrimitive2D(
const primitive2d::PointArrayPrimitive2D& rPointArrayCandidate)
{
const std::vector<basegfx::B2DPoint>& rPositions(rPointArrayCandidate.getPositions());
if (rPositions.empty())
{
// no geometry, done
return;
}
cairo_save(mpRT);
// determine & set color
const basegfx::BColor aPointColor(
maBColorModifierStack.getModifiedColor(rPointArrayCandidate.getRGBColor()));
cairo_set_source_rgb(mpRT, aPointColor.getRed(), aPointColor.getGreen(), aPointColor.getBlue());
// To really paint a single pixel I found nothing better than
// switch off AA and draw a pixel-aligned rectangle
const cairo_antialias_t eOldAAMode(cairo_get_antialias(mpRT));
cairo_set_antialias(mpRT, CAIRO_ANTIALIAS_NONE);
for (auto const& pos : rPositions)
{
const basegfx::B2DPoint aDiscretePos(getViewInformation2D().getObjectToViewTransformation()
* pos);
const double fX(ceil(aDiscretePos.getX()));
const double fY(ceil(aDiscretePos.getY()));
cairo_rectangle(mpRT, fX, fY, 1, 1);
cairo_fill(mpRT);
}
cairo_set_antialias(mpRT, eOldAAMode);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processPolygonHairlinePrimitive2D(
const primitive2d::PolygonHairlinePrimitive2D& rPolygonHairlinePrimitive2D)
{
const basegfx::B2DPolygon& rPolygon(rPolygonHairlinePrimitive2D.getB2DPolygon());
if (!rPolygon.count())
{
// no geometry, done
return;
}
cairo_save(mpRT);
// set linear transformation
cairo_matrix_t aMatrix;
const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0);
const basegfx::B2DHomMatrix& rObjectToView(
getViewInformation2D().getObjectToViewTransformation());
cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(),
rObjectToView.d(), rObjectToView.e() + fAAOffset,
rObjectToView.f() + fAAOffset);
cairo_set_matrix(mpRT, &aMatrix);
// determine & set color
const basegfx::BColor aHairlineColor(
maBColorModifierStack.getModifiedColor(rPolygonHairlinePrimitive2D.getBColor()));
cairo_set_source_rgb(mpRT, aHairlineColor.getRed(), aHairlineColor.getGreen(),
aHairlineColor.getBlue());
// set LineWidth, use Cairo's special cairo_set_hairline
impl_cairo_set_hairline(mpRT, getViewInformation2D());
// get PathGeometry & paint it
cairo_new_path(mpRT);
getOrCreatePathGeometry(mpRT, rPolygon, getViewInformation2D(),
getViewInformation2D().getUseAntiAliasing());
cairo_stroke(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processPolyPolygonColorPrimitive2D(
const primitive2d::PolyPolygonColorPrimitive2D& rPolyPolygonColorPrimitive2D)
{
paintPolyPoylgonRGBA(rPolyPolygonColorPrimitive2D.getB2DPolyPolygon(),
rPolyPolygonColorPrimitive2D.getBColor());
}
void CairoPixelProcessor2D::paintPolyPoylgonRGBA(const basegfx::B2DPolyPolygon& rPolyPolygon,
const basegfx::BColor& rColor,
double fTransparency)
{
// transparency invalid or completely transparent, done
if (fTransparency < 0.0 || fTransparency >= 1.0)
{
return;
}
const sal_uInt32 nCount(rPolyPolygon.count());
if (!nCount)
{
// no geometry, done
return;
}
cairo_save(mpRT);
// set linear transformation
cairo_matrix_t aMatrix;
const basegfx::B2DHomMatrix& rObjectToView(
getViewInformation2D().getObjectToViewTransformation());
cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(),
rObjectToView.d(), rObjectToView.e(), rObjectToView.f());
cairo_set_matrix(mpRT, &aMatrix);
// determine & set color
const basegfx::BColor aFillColor(maBColorModifierStack.getModifiedColor(rColor));
if (!basegfx::fTools::equalZero(fTransparency))
cairo_set_source_rgba(mpRT, aFillColor.getRed(), aFillColor.getGreen(),
aFillColor.getBlue(), 1.0 - fTransparency);
else
cairo_set_source_rgb(mpRT, aFillColor.getRed(), aFillColor.getGreen(),
aFillColor.getBlue());
// get PathGeometry & paint it
cairo_new_path(mpRT);
getOrCreateFillGeometry(mpRT, rPolyPolygon);
cairo_fill(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processTransparencePrimitive2D(
const primitive2d::TransparencePrimitive2D& rTransCandidate)
{
if (rTransCandidate.getChildren().empty())
{
// no content, done
return;
}
if (rTransCandidate.getTransparence().empty())
{
// no mask (so nothing visible), done
return;
}
// calculate visible range, create only for that range
basegfx::B2DRange aDiscreteRange(
rTransCandidate.getChildren().getB2DRange(getViewInformation2D()));
aDiscreteRange.transform(getViewInformation2D().getObjectToViewTransformation());
basegfx::B2DRange aVisibleRange(aDiscreteRange);
aVisibleRange.intersect(getDiscreteViewRange(mpRT));
if (aVisibleRange.isEmpty())
{
// not visible, done
return;
}
cairo_save(mpRT);
// create embedding transformation for sub-surface
const basegfx::B2DHomMatrix aEmbedTransform(basegfx::utils::createTranslateB2DHomMatrix(
-aVisibleRange.getMinX(), -aVisibleRange.getMinY()));
geometry::ViewInformation2D aViewInformation2D(getViewInformation2D());
aViewInformation2D.setViewTransformation(aEmbedTransform
* getViewInformation2D().getViewTransformation());
// draw mask to temporary surface
cairo_surface_t* pTarget(cairo_get_target(mpRT));
const double fContainedWidth(ceil(aVisibleRange.getWidth()));
const double fContainedHeight(ceil(aVisibleRange.getHeight()));
cairo_surface_t* pMask(cairo_surface_create_similar_image(pTarget, CAIRO_FORMAT_ARGB32,
fContainedWidth, fContainedHeight));
CairoPixelProcessor2D aMaskRenderer(aViewInformation2D, pMask);
aMaskRenderer.process(rTransCandidate.getTransparence());
// convert mask to something cairo can use
LuminanceToAlpha(pMask);
// draw content to temporary surface
cairo_surface_t* pContent(cairo_surface_create_similar(
pTarget, cairo_surface_get_content(pTarget), fContainedWidth, fContainedHeight));
CairoPixelProcessor2D aContent(aViewInformation2D, pContent);
// important for content rendering: need to take over the ColorModifierStack
aContent.setBColorModifierStack(getBColorModifierStack());
aContent.process(rTransCandidate.getChildren());
// munge the temporary surfaces to our target surface
cairo_set_source_surface(mpRT, pContent, aVisibleRange.getMinX(), aVisibleRange.getMinY());
cairo_mask_surface(mpRT, pMask, aVisibleRange.getMinX(), aVisibleRange.getMinY());
// cleanup temporary surfaces
cairo_surface_destroy(pContent);
cairo_surface_destroy(pMask);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processInvertPrimitive2D(
const primitive2d::InvertPrimitive2D& rInvertCandidate)
{
if (rInvertCandidate.getChildren().empty())
{
// no content, done
return;
}
// calculate visible range, create only for that range
basegfx::B2DRange aDiscreteRange(
rInvertCandidate.getChildren().getB2DRange(getViewInformation2D()));
aDiscreteRange.transform(getViewInformation2D().getObjectToViewTransformation());
basegfx::B2DRange aVisibleRange(aDiscreteRange);
aVisibleRange.intersect(getDiscreteViewRange(mpRT));
if (aVisibleRange.isEmpty())
{
// not visible, done
return;
}
cairo_save(mpRT);
// create embedding transformation for sub-surface
const basegfx::B2DHomMatrix aEmbedTransform(basegfx::utils::createTranslateB2DHomMatrix(
-aVisibleRange.getMinX(), -aVisibleRange.getMinY()));
geometry::ViewInformation2D aViewInformation2D(getViewInformation2D());
aViewInformation2D.setViewTransformation(aEmbedTransform
* getViewInformation2D().getViewTransformation());
// draw sub-content to temporary surface
cairo_surface_t* pTarget(cairo_get_target(mpRT));
const double fContainedWidth(ceil(aVisibleRange.getWidth()));
const double fContainedHeight(ceil(aVisibleRange.getHeight()));
cairo_surface_t* pContent(cairo_surface_create_similar_image(
pTarget, CAIRO_FORMAT_ARGB32, fContainedWidth, fContainedHeight));
CairoPixelProcessor2D aContent(aViewInformation2D, pContent);
// take over evtl. used ColorModifierStack for content
aContent.setBColorModifierStack(getBColorModifierStack());
aContent.process(rInvertCandidate.getChildren());
cairo_surface_flush(pContent);
// decide if to use builtin or create XOR yourself
// NOTE: not using and doing self is closer to what the
// current default does, so keep it
static bool bUseBuiltinXOR(false);
if (bUseBuiltinXOR)
{
// draw XOR to target using Cairo Operator CAIRO_OPERATOR_XOR
cairo_set_source_surface(mpRT, pContent, aVisibleRange.getMinX(), aVisibleRange.getMinY());
cairo_rectangle(mpRT, aVisibleRange.getMinX(), aVisibleRange.getMinY(),
aVisibleRange.getWidth(), aVisibleRange.getHeight());
cairo_set_operator(mpRT, CAIRO_OPERATOR_XOR);
cairo_fill(mpRT);
}
else
{
// get read/write access to target - XOR unfortunately needs that
cairo_surface_t* pRenderTarget(pTarget);
if (CAIRO_SURFACE_TYPE_IMAGE != cairo_surface_get_type(pRenderTarget))
{
// create mapping for read/write access to pRenderTarget
pRenderTarget = cairo_surface_map_to_image(pRenderTarget, nullptr);
}
// iterate over pre-rendered pContent (call it Front)
const sal_uInt32 nFrontWidth(cairo_image_surface_get_width(pContent));
const sal_uInt32 nFrontHeight(cairo_image_surface_get_height(pContent));
const sal_uInt32 nFrontStride(cairo_image_surface_get_stride(pContent));
unsigned char* pFrontDataRoot(cairo_image_surface_get_data(pContent));
// in parallel, iterate over original data (call it Back)
const sal_uInt32 nBackOffX(floor(aVisibleRange.getMinX()));
const sal_uInt32 nBackOffY(floor(aVisibleRange.getMinY()));
const sal_uInt32 nBackStride(cairo_image_surface_get_stride(pRenderTarget));
unsigned char* pBackDataRoot(cairo_image_surface_get_data(pRenderTarget));
const bool bBackPreMultiply(CAIRO_FORMAT_ARGB32
== cairo_image_surface_get_format(pRenderTarget));
if (nullptr != pFrontDataRoot && nullptr != pBackDataRoot)
{
for (sal_uInt32 y(0); y < nFrontHeight; ++y)
{
// get mem locations
unsigned char* pFrontData(pFrontDataRoot + (nFrontStride * y));
unsigned char* pBackData(pBackDataRoot + (nBackStride * (y + nBackOffY))
+ (nBackOffX * 4));
// added advance mem to for-expression to be able to continue calls inside
for (sal_uInt32 x(0); x < nFrontWidth; ++x, pBackData += 4, pFrontData += 4)
{
// do not forget pre-multiply. Use 255 for non-premultiplied to
// not have to do if not needed
const sal_uInt8 nBackAlpha(bBackPreMultiply ? pBackData[SVP_CAIRO_ALPHA] : 255);
// change will only be visible in back/target when not fully transparent
if (0 == nBackAlpha)
continue;
// do not forget pre-multiply -> need to get both alphas. Use 255
// for non-premultiplied to not have to do if not needed
const sal_uInt8 nFrontAlpha(pFrontData[SVP_CAIRO_ALPHA]);
// only something to do if source is not fully transparent
if (0 == nFrontAlpha)
continue;
sal_uInt8 nFrontB(pFrontData[SVP_CAIRO_BLUE]);
sal_uInt8 nFrontG(pFrontData[SVP_CAIRO_GREEN]);
sal_uInt8 nFrontR(pFrontData[SVP_CAIRO_RED]);
if (255 != nFrontAlpha)
{
// get front color (Front is always CAIRO_FORMAT_ARGB32 and
// thus pre-multiplied)
nFrontB = vcl::bitmap::unpremultiply(nFrontB, nFrontAlpha);
nFrontG = vcl::bitmap::unpremultiply(nFrontG, nFrontAlpha);
nFrontR = vcl::bitmap::unpremultiply(nFrontR, nFrontAlpha);
}
sal_uInt8 nBackB(pBackData[SVP_CAIRO_BLUE]);
sal_uInt8 nBackG(pBackData[SVP_CAIRO_GREEN]);
sal_uInt8 nBackR(pBackData[SVP_CAIRO_RED]);
if (255 != nBackAlpha)
{
// get back color if bBackPreMultiply (aka 255)
nBackB = vcl::bitmap::unpremultiply(nBackB, nBackAlpha);
nBackG = vcl::bitmap::unpremultiply(nBackG, nBackAlpha);
nBackR = vcl::bitmap::unpremultiply(nBackR, nBackAlpha);
}
// create XOR r,g,b
const sal_uInt8 b(nFrontB ^ nBackB);
const sal_uInt8 g(nFrontG ^ nBackG);
const sal_uInt8 r(nFrontR ^ nBackR);
// write back directly to pBackData/target
if (255 == nBackAlpha)
{
pBackData[SVP_CAIRO_BLUE] = b;
pBackData[SVP_CAIRO_GREEN] = g;
pBackData[SVP_CAIRO_RED] = r;
}
else
{
// additionally premultiply if bBackPreMultiply (aka 255)
pBackData[SVP_CAIRO_BLUE] = vcl::bitmap::premultiply(b, nBackAlpha);
pBackData[SVP_CAIRO_GREEN] = vcl::bitmap::premultiply(g, nBackAlpha);
pBackData[SVP_CAIRO_RED] = vcl::bitmap::premultiply(r, nBackAlpha);
}
}
}
cairo_surface_mark_dirty(pRenderTarget);
}
if (pRenderTarget != pTarget)
{
// cleanup mapping for read/write access to target
cairo_surface_unmap_image(pTarget, pRenderTarget);
}
}
// cleanup temporary surface
cairo_surface_destroy(pContent);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processMaskPrimitive2D(
const primitive2d::MaskPrimitive2D& rMaskCandidate)
{
if (rMaskCandidate.getChildren().empty())
{
// no content, done
return;
}
const basegfx::B2DPolyPolygon& rMask(rMaskCandidate.getMask());
if (!rMask.count())
{
// no mask (so nothing inside), done
return;
}
// calculate visible range
basegfx::B2DRange aMaskRange(rMask.getB2DRange());
aMaskRange.transform(getViewInformation2D().getObjectToViewTransformation());
if (!getDiscreteViewRange(mpRT).overlaps(aMaskRange))
{
// not visible, done
return;
}
cairo_save(mpRT);
// set linear transformation for applying mask. use no fAAOffset for mask
cairo_matrix_t aMatrix;
const basegfx::B2DHomMatrix& rObjectToView(
getViewInformation2D().getObjectToViewTransformation());
cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(),
rObjectToView.d(), rObjectToView.e(), rObjectToView.f());
cairo_set_matrix(mpRT, &aMatrix);
// create path geometry and put mask as path
cairo_new_path(mpRT);
getOrCreateFillGeometry(mpRT, rMask);
// clip to this mask
cairo_clip(mpRT);
// reset transformation to not have it set when processing
// child content below (was only used to set clip path)
cairo_identity_matrix(mpRT);
// process sub-content (that shall be masked)
mnClipRecursionCount++;
process(rMaskCandidate.getChildren());
mnClipRecursionCount--;
cairo_restore(mpRT);
if (0 == mnClipRecursionCount)
{
// for *some* reason Cairo seems to have problems using cairo_clip
// recursively, in combination with cairo_save/cairo_restore. I think
// it *should* work as used here, see
// https://www.cairographics.org/manual/cairo-cairo-t.html#cairo-clip
// where this combination is explicitly mentioned/explained. It may
// just be a error in cairo, too (?).
// The error is that without that for some reason the last clip is not
// restored but *stays*, so e.g. when having a shape filled with
// 'tux.svg' and an ellipse overlapping in front, suddenly (but not
// always?) the ellipse gets 'clipped' against the shape filled with
// the tux graphic.
// What helps is to count the clip recursion for each incarnation of
// CairoPixelProcessor2D/cairo_t used and call/use cairo_reset_clip
// when last clip is left.
cairo_reset_clip(mpRT);
}
}
void CairoPixelProcessor2D::processModifiedColorPrimitive2D(
const primitive2d::ModifiedColorPrimitive2D& rModifiedCandidate)
{
// standard implementation
if (!rModifiedCandidate.getChildren().empty())
{
maBColorModifierStack.push(rModifiedCandidate.getColorModifier());
process(rModifiedCandidate.getChildren());
maBColorModifierStack.pop();
}
}
void CairoPixelProcessor2D::processTransformPrimitive2D(
const primitive2d::TransformPrimitive2D& rTransformCandidate)
{
// standard implementation
// remember current transformation and ViewInformation
const geometry::ViewInformation2D aLastViewInformation2D(getViewInformation2D());
// create new transformations for local ViewInformation2D
geometry::ViewInformation2D aViewInformation2D(getViewInformation2D());
aViewInformation2D.setObjectTransformation(getViewInformation2D().getObjectTransformation()
* rTransformCandidate.getTransformation());
updateViewInformation(aViewInformation2D);
// process content
process(rTransformCandidate.getChildren());
// restore transformations
updateViewInformation(aLastViewInformation2D);
}
void CairoPixelProcessor2D::processUnifiedTransparencePrimitive2D(
const primitive2d::UnifiedTransparencePrimitive2D& rTransCandidate)
{
if (rTransCandidate.getChildren().empty())
{
// no content, done
return;
}
if (0.0 == rTransCandidate.getTransparence())
{
// not transparent at all, use content
process(rTransCandidate.getChildren());
return;
}
if (rTransCandidate.getTransparence() < 0.0 || rTransCandidate.getTransparence() > 1.0)
{
// invalid transparence, done
return;
}
cairo_save(mpRT);
// calculate visible range, create only for that range
basegfx::B2DRange aDiscreteRange(
rTransCandidate.getChildren().getB2DRange(getViewInformation2D()));
aDiscreteRange.transform(getViewInformation2D().getObjectToViewTransformation());
basegfx::B2DRange aVisibleRange(aDiscreteRange);
aVisibleRange.intersect(getDiscreteViewRange(mpRT));
if (aVisibleRange.isEmpty())
{
// not visible, done
return;
}
// create embedding transformation for sub-surface
const basegfx::B2DHomMatrix aEmbedTransform(basegfx::utils::createTranslateB2DHomMatrix(
-aVisibleRange.getMinX(), -aVisibleRange.getMinY()));
geometry::ViewInformation2D aViewInformation2D(getViewInformation2D());
aViewInformation2D.setViewTransformation(aEmbedTransform
* getViewInformation2D().getViewTransformation());
// draw content to temporary surface
cairo_surface_t* pTarget(cairo_get_target(mpRT));
const double fContainedWidth(ceil(aVisibleRange.getWidth()));
const double fContainedHeight(ceil(aVisibleRange.getHeight()));
cairo_surface_t* pContent(cairo_surface_create_similar(
pTarget, cairo_surface_get_content(pTarget), fContainedWidth, fContainedHeight));
CairoPixelProcessor2D aContent(aViewInformation2D, pContent);
aContent.process(rTransCandidate.getChildren());
// paint temporary surface to target with fixed transparence
cairo_set_source_surface(mpRT, pContent, aVisibleRange.getMinX(), aVisibleRange.getMinY());
cairo_paint_with_alpha(mpRT, 1.0 - rTransCandidate.getTransparence());
// cleanup temporary surface
cairo_surface_destroy(pContent);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processMarkerArrayPrimitive2D(
const primitive2d::MarkerArrayPrimitive2D& rMarkerArrayCandidate)
{
const std::vector<basegfx::B2DPoint>& rPositions(rMarkerArrayCandidate.getPositions());
if (rPositions.empty())
{
// no geometry, done
return;
}
const BitmapEx& rMarker(rMarkerArrayCandidate.getMarker());
if (rMarker.IsEmpty())
{
// no marker defined, done
return;
}
// access or create cairo bitmap data
const BitmapEx& rBitmapEx(rMarkerArrayCandidate.getMarker());
std::shared_ptr<CairoSurfaceHelper> aCairoSurfaceHelper(
getOrCreateCairoSurfaceHelper(rBitmapEx));
if (!aCairoSurfaceHelper)
{
SAL_WARN("drawinglayer", "SDPRCairo: No SurfaceHelper from BitmapEx (!)");
return;
}
// do not use dimensions, these are usually small instances
cairo_surface_t* pTarget(aCairoSurfaceHelper->getCairoSurface());
if (nullptr == pTarget)
{
SAL_WARN("drawinglayer", "SDPRCairo: No CairoSurface from BitmapEx SurfaceHelper (!)");
return;
}
const sal_uInt32 nWidth(cairo_image_surface_get_width(pTarget));
const sal_uInt32 nHeight(cairo_image_surface_get_height(pTarget));
const tools::Long nMiX((nWidth / 2) + 1);
const tools::Long nMiY((nHeight / 2) + 1);
cairo_save(mpRT);
cairo_identity_matrix(mpRT);
const cairo_antialias_t eOldAAMode(cairo_get_antialias(mpRT));
cairo_set_antialias(mpRT, CAIRO_ANTIALIAS_NONE);
for (auto const& pos : rPositions)
{
const basegfx::B2DPoint aDiscretePos(getViewInformation2D().getObjectToViewTransformation()
* pos);
const double fX(ceil(aDiscretePos.getX()));
const double fY(ceil(aDiscretePos.getY()));
cairo_set_source_surface(mpRT, pTarget, fX - nMiX, fY - nMiY);
cairo_paint(mpRT);
}
cairo_set_antialias(mpRT, eOldAAMode);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processBackgroundColorPrimitive2D(
const primitive2d::BackgroundColorPrimitive2D& rBackgroundColorCandidate)
{
// check for allowed range [0.0 .. 1.0[
if (rBackgroundColorCandidate.getTransparency() < 0.0
|| rBackgroundColorCandidate.getTransparency() >= 1.0)
return;
cairo_save(mpRT);
const basegfx::BColor aFillColor(
maBColorModifierStack.getModifiedColor(rBackgroundColorCandidate.getBColor()));
cairo_set_source_rgba(mpRT, aFillColor.getRed(), aFillColor.getGreen(), aFillColor.getBlue(),
1.0 - rBackgroundColorCandidate.getTransparency());
// to also copy alpha part of color, see cairo docu. Will be reset by restore below
cairo_set_operator(mpRT, CAIRO_OPERATOR_SOURCE);
cairo_paint(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processPolygonStrokePrimitive2D(
const primitive2d::PolygonStrokePrimitive2D& rPolygonStrokeCandidate)
{
const basegfx::B2DPolygon& rPolygon(rPolygonStrokeCandidate.getB2DPolygon());
const attribute::LineAttribute& rLineAttribute(rPolygonStrokeCandidate.getLineAttribute());
if (!rPolygon.count() || rLineAttribute.getWidth() < 0.0)
{
// no geometry, done
return;
}
// get some values early that might be used for decisions
const bool bHairline(0.0 == rLineAttribute.getWidth());
const basegfx::B2DHomMatrix& rObjectToView(
getViewInformation2D().getObjectToViewTransformation());
const double fDiscreteLineWidth(
bHairline
? 1.0
: (rObjectToView * basegfx::B2DVector(rLineAttribute.getWidth(), 0.0)).getLength());
// Here for every combination which the system-specific implementation is not
// capable of visualizing, use the (for decomposable Primitives always possible)
// fallback to the decomposition.
if (basegfx::B2DLineJoin::NONE == rLineAttribute.getLineJoin() && fDiscreteLineWidth > 1.5)
{
// basegfx::B2DLineJoin::NONE is special for our office, no other GraphicSystem
// knows that (so far), so fallback to decomposition. This is only needed if
// LineJoin will be used, so also check for discrete LineWidth before falling back
process(rPolygonStrokeCandidate);
return;
}
// This is a method every system-specific implementation of a decomposable Primitive
// can use to allow simple optical control of paint implementation:
// Create a copy, e.g. change color to 'red' as here and paint before the system
// paints it using the decomposition. That way you can - if active - directly
// optically compare if the system-specific solution is geometrically identical to
// the decomposition (which defines our interpretation that we need to visualize).
// Look below in the impl for bRenderDecomposeForCompareInRed to see that in that case
// we create a half-transparent paint to better support visual control
static bool bRenderDecomposeForCompareInRed(false);
if (bRenderDecomposeForCompareInRed)
{
const attribute::LineAttribute aRed(
basegfx::BColor(1.0, 0.0, 0.0), rLineAttribute.getWidth(), rLineAttribute.getLineJoin(),
rLineAttribute.getLineCap(), rLineAttribute.getMiterMinimumAngle());
rtl::Reference<primitive2d::PolygonStrokePrimitive2D> xCopy(
new primitive2d::PolygonStrokePrimitive2D(
rPolygonStrokeCandidate.getB2DPolygon(), aRed,
rPolygonStrokeCandidate.getStrokeAttribute()));
process(*xCopy);
}
cairo_save(mpRT);
// set linear transformation
cairo_matrix_t aMatrix;
const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0);
cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(),
rObjectToView.d(), rObjectToView.e() + fAAOffset,
rObjectToView.f() + fAAOffset);
cairo_set_matrix(mpRT, &aMatrix);
// setup line attributes
cairo_line_join_t eCairoLineJoin = CAIRO_LINE_JOIN_MITER;
switch (rLineAttribute.getLineJoin())
{
case basegfx::B2DLineJoin::Bevel:
eCairoLineJoin = CAIRO_LINE_JOIN_BEVEL;
break;
case basegfx::B2DLineJoin::Round:
eCairoLineJoin = CAIRO_LINE_JOIN_ROUND;
break;
case basegfx::B2DLineJoin::NONE:
case basegfx::B2DLineJoin::Miter:
eCairoLineJoin = CAIRO_LINE_JOIN_MITER;
break;
}
cairo_set_line_join(mpRT, eCairoLineJoin);
// convert miter minimum angle to miter limit
double fMiterLimit
= 1.0 / sin(std::max(rLineAttribute.getMiterMinimumAngle(), 0.01 * M_PI) / 2.0);
cairo_set_miter_limit(mpRT, fMiterLimit);
// setup cap attribute
cairo_line_cap_t eCairoLineCap(CAIRO_LINE_CAP_BUTT);
switch (rLineAttribute.getLineCap())
{
default: // css::drawing::LineCap_BUTT:
{
eCairoLineCap = CAIRO_LINE_CAP_BUTT;
break;
}
case css::drawing::LineCap_ROUND:
{
eCairoLineCap = CAIRO_LINE_CAP_ROUND;
break;
}
case css::drawing::LineCap_SQUARE:
{
eCairoLineCap = CAIRO_LINE_CAP_SQUARE;
break;
}
}
cairo_set_line_cap(mpRT, eCairoLineCap);
// determine & set color
basegfx::BColor aLineColor(maBColorModifierStack.getModifiedColor(rLineAttribute.getColor()));
if (bRenderDecomposeForCompareInRed)
aLineColor.setRed(0.5);
cairo_set_source_rgb(mpRT, aLineColor.getRed(), aLineColor.getGreen(), aLineColor.getBlue());
// process/set LineWidth
const double fObjectLineWidth(
bHairline ? (getViewInformation2D().getInverseObjectToViewTransformation()
* basegfx::B2DVector(1.0, 0.0))
.getLength()
: rLineAttribute.getWidth());
cairo_set_line_width(mpRT, fObjectLineWidth);
// check stroke
const attribute::StrokeAttribute& rStrokeAttribute(
rPolygonStrokeCandidate.getStrokeAttribute());
const bool bDashUsed(!rStrokeAttribute.isDefault()
&& !rStrokeAttribute.getDotDashArray().empty()
&& 0.0 < rStrokeAttribute.getFullDotDashLen());
if (bDashUsed)
{
const std::vector<double>& rStroke = rStrokeAttribute.getDotDashArray();
cairo_set_dash(mpRT, rStroke.data(), rStroke.size(), 0.0);
}
// create path geometry and put mask as path
cairo_new_path(mpRT);
getOrCreatePathGeometry(mpRT, rPolygon, getViewInformation2D(),
bHairline && getViewInformation2D().getUseAntiAliasing());
// render
cairo_stroke(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processLineRectanglePrimitive2D(
const primitive2d::LineRectanglePrimitive2D& rLineRectanglePrimitive2D)
{
if (rLineRectanglePrimitive2D.getB2DRange().isEmpty())
{
// no geometry, done
return;
}
cairo_save(mpRT);
cairo_matrix_t aMatrix;
const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0);
const basegfx::B2DHomMatrix& rObjectToView(
getViewInformation2D().getObjectToViewTransformation());
cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(),
rObjectToView.d(), rObjectToView.e() + fAAOffset,
rObjectToView.f() + fAAOffset);
// set linear transformation
cairo_set_matrix(mpRT, &aMatrix);
const basegfx::BColor aHairlineColor(
maBColorModifierStack.getModifiedColor(rLineRectanglePrimitive2D.getBColor()));
cairo_set_source_rgb(mpRT, aHairlineColor.getRed(), aHairlineColor.getGreen(),
aHairlineColor.getBlue());
const double fDiscreteLineWidth((getViewInformation2D().getInverseObjectToViewTransformation()
* basegfx::B2DVector(1.0, 0.0))
.getLength());
cairo_set_line_width(mpRT, fDiscreteLineWidth);
const basegfx::B2DRange& rRange(rLineRectanglePrimitive2D.getB2DRange());
cairo_rectangle(mpRT, rRange.getMinX(), rRange.getMinY(), rRange.getWidth(),
rRange.getHeight());
cairo_stroke(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processFilledRectanglePrimitive2D(
const primitive2d::FilledRectanglePrimitive2D& rFilledRectanglePrimitive2D)
{
if (rFilledRectanglePrimitive2D.getB2DRange().isEmpty())
{
// no geometry, done
return;
}
cairo_save(mpRT);
cairo_matrix_t aMatrix;
const basegfx::B2DHomMatrix& rObjectToView(
getViewInformation2D().getObjectToViewTransformation());
cairo_matrix_init(&aMatrix, rObjectToView.a(), rObjectToView.b(), rObjectToView.c(),
rObjectToView.d(), rObjectToView.e(), rObjectToView.f());
// set linear transformation
cairo_set_matrix(mpRT, &aMatrix);
const basegfx::BColor aFillColor(
maBColorModifierStack.getModifiedColor(rFilledRectanglePrimitive2D.getBColor()));
cairo_set_source_rgb(mpRT, aFillColor.getRed(), aFillColor.getGreen(), aFillColor.getBlue());
const basegfx::B2DRange& rRange(rFilledRectanglePrimitive2D.getB2DRange());
cairo_rectangle(mpRT, rRange.getMinX(), rRange.getMinY(), rRange.getWidth(),
rRange.getHeight());
cairo_fill(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processSingleLinePrimitive2D(
const primitive2d::SingleLinePrimitive2D& rSingleLinePrimitive2D)
{
cairo_save(mpRT);
const basegfx::BColor aLineColor(
maBColorModifierStack.getModifiedColor(rSingleLinePrimitive2D.getBColor()));
cairo_set_source_rgb(mpRT, aLineColor.getRed(), aLineColor.getGreen(), aLineColor.getBlue());
const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0);
const basegfx::B2DHomMatrix& rObjectToView(
getViewInformation2D().getObjectToViewTransformation());
const basegfx::B2DPoint aStart(rObjectToView * rSingleLinePrimitive2D.getStart());
const basegfx::B2DPoint aEnd(rObjectToView * rSingleLinePrimitive2D.getEnd());
cairo_set_line_width(mpRT, 1.0f);
cairo_move_to(mpRT, aStart.getX() + fAAOffset, aStart.getY() + fAAOffset);
cairo_line_to(mpRT, aEnd.getX() + fAAOffset, aEnd.getY() + fAAOffset);
cairo_stroke(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processFillGraphicPrimitive2D(
const primitive2d::FillGraphicPrimitive2D& rFillGraphicPrimitive2D)
{
if (rFillGraphicPrimitive2D.getTransparency() < 0.0
|| rFillGraphicPrimitive2D.getTransparency() > 1.0)
{
// invalid transparence, done
return;
}
BitmapEx aPreparedBitmap;
basegfx::B2DRange aFillUnitRange(rFillGraphicPrimitive2D.getFillGraphic().getGraphicRange());
constexpr double fBigDiscreteArea(300.0 * 300.0);
// use tooling to do various checks and prepare tiled rendering, see
// description of method, parameters and return value there
if (!prepareBitmapForDirectRender(rFillGraphicPrimitive2D, getViewInformation2D(),
aPreparedBitmap, aFillUnitRange, fBigDiscreteArea))
{
// no output needed, done
return;
}
if (aPreparedBitmap.IsEmpty())
{
// output needed and Bitmap data empty, so no bitmap data based
// tiled rendering is suggested. Use fallback for paint (decomposition)
process(rFillGraphicPrimitive2D);
return;
}
// render tiled using the prepared Bitmap data
if (maBColorModifierStack.count())
{
// need to apply ColorModifier to Bitmap data
aPreparedBitmap = aPreparedBitmap.ModifyBitmapEx(maBColorModifierStack);
if (aPreparedBitmap.IsEmpty())
{
// color gets completely replaced, get it (any input works)
const basegfx::BColor aModifiedColor(
maBColorModifierStack.getModifiedColor(basegfx::BColor()));
// use unit geometry as fallback object geometry. Do *not*
// transform, the below used method will use the already
// correctly initialized local ViewInformation
basegfx::B2DPolygon aPolygon(basegfx::utils::createUnitPolygon());
// what we still need to apply is the object transform from the
// local primitive, that is not part of DisplayInfo yet
aPolygon.transform(rFillGraphicPrimitive2D.getTransformation());
// draw directly
paintPolyPoylgonRGBA(basegfx::B2DPolyPolygon(aPolygon), aModifiedColor,
rFillGraphicPrimitive2D.getTransparency());
return;
}
}
// access or create cairo bitmap data
std::shared_ptr<CairoSurfaceHelper> aCairoSurfaceHelper(
getOrCreateCairoSurfaceHelper(aPreparedBitmap));
if (!aCairoSurfaceHelper)
{
SAL_WARN("drawinglayer", "SDPRCairo: No SurfaceHelper from BitmapEx (!)");
return;
}
// work with dimensions in discrete target pixels to use evtl. MipMap pre-scale
const basegfx::B2DHomMatrix aLocalTransform(
getViewInformation2D().getObjectToViewTransformation()
* rFillGraphicPrimitive2D.getTransformation());
const tools::Long nDestWidth(
(aLocalTransform * basegfx::B2DVector(aFillUnitRange.getWidth(), 0.0)).getLength());
const tools::Long nDestHeight(
(aLocalTransform * basegfx::B2DVector(0.0, aFillUnitRange.getHeight())).getLength());
cairo_surface_t* pTarget(aCairoSurfaceHelper->getCairoSurface(nDestWidth, nDestHeight));
if (nullptr == pTarget)
{
SAL_WARN("drawinglayer", "SDPRCairo: No CairoSurface from BitmapEx SurfaceHelper (!)");
return;
}
cairo_save(mpRT);
// set linear transformation - no fAAOffset for bitmap data
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aLocalTransform.a(), aLocalTransform.b(), aLocalTransform.c(),
aLocalTransform.d(), aLocalTransform.e(), aLocalTransform.f());
cairo_set_matrix(mpRT, &aMatrix);
const sal_uInt32 nWidth(cairo_image_surface_get_width(pTarget));
const sal_uInt32 nHeight(cairo_image_surface_get_height(pTarget));
cairo_set_source_surface(mpRT, pTarget, 0, 0);
// get the pattern created by cairo_set_source_surface and
// it's transformation
cairo_pattern_t* sourcepattern = cairo_get_source(mpRT);
cairo_pattern_get_matrix(sourcepattern, &aMatrix);
// clip for RGBA (see other places)
if (CAIRO_FORMAT_ARGB32 == cairo_image_surface_get_format(pTarget))
{
cairo_rectangle(mpRT, 0, 0, 1, 1);
cairo_clip(mpRT);
}
// create transformation for source pattern (inverse, see
// cairo docu: uses user space to pattern space transformation)
cairo_matrix_init_scale(&aMatrix, nWidth / aFillUnitRange.getWidth(),
nHeight / aFillUnitRange.getHeight());
cairo_matrix_translate(&aMatrix, -aFillUnitRange.getMinX(), -aFillUnitRange.getMinY());
// set source pattern transform & activate pattern repeat
cairo_pattern_set_matrix(sourcepattern, &aMatrix);
cairo_pattern_set_extend(sourcepattern, CAIRO_EXTEND_REPEAT);
// CAIRO_FILTER_GOOD seems to be the default anyways, but set it
// to be on the safe side
cairo_pattern_set_filter(sourcepattern, CAIRO_FILTER_GOOD);
// paint
if (rFillGraphicPrimitive2D.hasTransparency())
cairo_paint_with_alpha(mpRT, 1.0 - rFillGraphicPrimitive2D.getTransparency());
else
cairo_paint(mpRT);
static bool bRenderTransformationBounds(false);
if (bRenderTransformationBounds)
{
cairo_set_source_rgba(mpRT, 0, 1, 0, 0.8);
impl_cairo_set_hairline(mpRT, getViewInformation2D());
// full object
cairo_rectangle(mpRT, 0, 0, 1, 1);
// outline of pattern root image
cairo_rectangle(mpRT, aFillUnitRange.getMinX(), aFillUnitRange.getMinY(),
aFillUnitRange.getWidth(), aFillUnitRange.getHeight());
cairo_stroke(mpRT);
}
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processFillGradientPrimitive2D_drawOutputRange(
const primitive2d::FillGradientPrimitive2D& rFillGradientPrimitive2D)
{
cairo_save(mpRT);
// fill simple rect with outer color
const basegfx::BColor aColor(
maBColorModifierStack.getModifiedColor(rFillGradientPrimitive2D.getOuterColor()));
if (rFillGradientPrimitive2D.hasAlphaGradient())
{
const attribute::FillGradientAttribute& rAlphaGradient(
rFillGradientPrimitive2D.getAlphaGradient());
double fLuminance(0.0);
if (!rAlphaGradient.getColorStops().empty())
{
if (css::awt::GradientStyle_AXIAL == rAlphaGradient.getStyle())
fLuminance = rAlphaGradient.getColorStops().back().getStopColor().luminance();
else
fLuminance = rAlphaGradient.getColorStops().front().getStopColor().luminance();
}
cairo_set_source_rgba(mpRT, aColor.getRed(), aColor.getGreen(), aColor.getBlue(),
1.0 - fLuminance);
}
else
{
cairo_set_source_rgb(mpRT, aColor.getRed(), aColor.getGreen(), aColor.getBlue());
}
const basegfx::B2DHomMatrix aTrans(getViewInformation2D().getObjectToViewTransformation());
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aTrans.a(), aTrans.b(), aTrans.c(), aTrans.d(), aTrans.e(),
aTrans.f());
cairo_set_matrix(mpRT, &aMatrix);
const basegfx::B2DRange& rRange(rFillGradientPrimitive2D.getOutputRange());
cairo_rectangle(mpRT, rRange.getMinX(), rRange.getMinY(), rRange.getWidth(),
rRange.getHeight());
cairo_fill(mpRT);
cairo_restore(mpRT);
}
bool CairoPixelProcessor2D::processFillGradientPrimitive2D_isCompletelyBordered(
const primitive2d::FillGradientPrimitive2D& rFillGradientPrimitive2D)
{
const attribute::FillGradientAttribute& rFillGradient(
rFillGradientPrimitive2D.getFillGradient());
const double fBorder(rFillGradient.getBorder());
// check if completely 'bordered out'. This can be the case for all
// types of gradients
if (basegfx::fTools::less(fBorder, 1.0) && fBorder >= 0.0)
{
// no, we have visible content besides border
return false;
}
// draw all-covering polygon using getOuterColor and getOutputRange
processFillGradientPrimitive2D_drawOutputRange(rFillGradientPrimitive2D);
return true;
}
void CairoPixelProcessor2D::processFillGradientPrimitive2D_linear_axial(
const primitive2d::FillGradientPrimitive2D& rFillGradientPrimitive2D)
{
const attribute::FillGradientAttribute& rFillGradient(
rFillGradientPrimitive2D.getFillGradient());
assert(!rFillGradientPrimitive2D.hasAlphaGradient()
|| rFillGradient.sameDefinitionThanAlpha(rFillGradientPrimitive2D.getAlphaGradient()));
assert(
(css::awt::GradientStyle_LINEAR == rFillGradientPrimitive2D.getFillGradient().getStyle()
|| css::awt::GradientStyle_AXIAL == rFillGradientPrimitive2D.getFillGradient().getStyle())
&& "SDPRCairo: Helper allows only SPECIFIED types (!)");
cairo_save(mpRT);
// need to do 'antique' stuff adaptions for rotate/transitionStart in object coordinates
// (DefinitionRange) to have the right 'bending' on rotation
basegfx::B2DRange aAdaptedRange(rFillGradientPrimitive2D.getDefinitionRange());
const double fAngle(basegfx::normalizeToRange((2 * M_PI) - rFillGradient.getAngle(), 2 * M_PI));
const bool bAngle(!basegfx::fTools::equalZero(fAngle));
const basegfx::B2DPoint aCenter(aAdaptedRange.getCenter());
// pack rotation and offset into a transformation covering that part
basegfx::B2DHomMatrix aRotation(basegfx::utils::createRotateAroundPoint(aCenter, fAngle));
// create local transform to work in object coordinates based on OutputRange,
// combine with rotation - that way we can then just draw into AdaptedRange
basegfx::B2DHomMatrix aLocalTransform(getViewInformation2D().getObjectToViewTransformation()
* aRotation);
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aLocalTransform.a(), aLocalTransform.b(), aLocalTransform.c(),
aLocalTransform.d(), aLocalTransform.e(), aLocalTransform.f());
cairo_set_matrix(mpRT, &aMatrix);
if (bAngle)
{
// expand Range by rotating
aAdaptedRange.transform(aRotation);
}
// create linear pattern in unit coordinates in y-direction
cairo_pattern_t* pPattern(
cairo_pattern_create_linear(aAdaptedRange.getCenterX(), aAdaptedRange.getMinY(),
aAdaptedRange.getCenterX(), aAdaptedRange.getMaxY()));
// get color stops (make copy, might have to be changed)
basegfx::BColorStops aBColorStops(rFillGradient.getColorStops());
basegfx::BColorStops aBColorStopsAlpha;
const bool bHasAlpha(rFillGradientPrimitive2D.hasAlphaGradient());
if (bHasAlpha)
aBColorStopsAlpha = rFillGradientPrimitive2D.getAlphaGradient().getColorStops();
const bool bAxial(css::awt::GradientStyle_AXIAL == rFillGradient.getStyle());
// get and apply border - create soace at start in gradient
const double fBorder(std::max(std::min(rFillGradient.getBorder(), 1.0), 0.0));
if (!basegfx::fTools::equalZero(fBorder))
{
if (bAxial)
{
aBColorStops.reverseColorStops();
if (bHasAlpha)
aBColorStopsAlpha.reverseColorStops();
}
aBColorStops.createSpaceAtStart(fBorder);
if (bHasAlpha)
aBColorStopsAlpha.createSpaceAtStart(fBorder);
if (bAxial)
{
aBColorStops.reverseColorStops();
if (bHasAlpha)
aBColorStopsAlpha.reverseColorStops();
}
}
if (bAxial)
{
// expand with mirrored ColorStops to create axial
aBColorStops.doApplyAxial();
if (bHasAlpha)
aBColorStopsAlpha.doApplyAxial();
}
// Apply steps if used to 'emulate' LO's 'discrete step' feature
if (rFillGradient.getSteps())
{
aBColorStops.doApplySteps(rFillGradient.getSteps());
if (bHasAlpha)
aBColorStopsAlpha.doApplySteps(rFillGradient.getSteps());
}
// add color stops
for (size_t a(0); a < aBColorStops.size(); a++)
{
const basegfx::BColorStop& rStop(aBColorStops[a]);
const basegfx::BColor aColor(maBColorModifierStack.getModifiedColor(rStop.getStopColor()));
if (bHasAlpha)
{
const basegfx::BColor aAlpha(aBColorStopsAlpha[a].getStopColor());
cairo_pattern_add_color_stop_rgba(pPattern, rStop.getStopOffset(), aColor.getRed(),
aColor.getGreen(), aColor.getBlue(),
1.0 - aAlpha.luminance());
}
else
{
if (rFillGradientPrimitive2D.hasTransparency())
{
cairo_pattern_add_color_stop_rgba(pPattern, rStop.getStopOffset(), aColor.getRed(),
aColor.getGreen(), aColor.getBlue(),
1.0 - rFillGradientPrimitive2D.getTransparency());
}
else
{
cairo_pattern_add_color_stop_rgb(pPattern, rStop.getStopOffset(), aColor.getRed(),
aColor.getGreen(), aColor.getBlue());
}
}
}
// draw OutRange
basegfx::B2DRange aOutRange(rFillGradientPrimitive2D.getOutputRange());
if (bAngle)
{
// expand backwards to cover all area needed for OutputRange
aRotation.invert();
aOutRange.transform(aRotation);
}
cairo_rectangle(mpRT, aOutRange.getMinX(), aOutRange.getMinY(), aOutRange.getWidth(),
aOutRange.getHeight());
cairo_set_source(mpRT, pPattern);
cairo_fill(mpRT);
// cleanup
cairo_pattern_destroy(pPattern);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processFillGradientPrimitive2D_square_rect(
const primitive2d::FillGradientPrimitive2D& rFillGradientPrimitive2D)
{
if (rFillGradientPrimitive2D.hasAlphaGradient() || rFillGradientPrimitive2D.hasTransparency())
{
// Do not use direct alpha for this: It paints using four trapez that
// do not add up at edges due to being painted AntiAliased; that means
// common pixels do not add up, but blend by transparency, so leaving
// visual traces -> process recursively
process(rFillGradientPrimitive2D);
return;
}
assert(
(css::awt::GradientStyle_SQUARE == rFillGradientPrimitive2D.getFillGradient().getStyle()
|| css::awt::GradientStyle_RECT == rFillGradientPrimitive2D.getFillGradient().getStyle())
&& "SDPRCairo: Helper allows only SPECIFIED types (!)");
cairo_save(mpRT);
// draw all-covering polygon using getOuterColor and getOutputRange,
// the partial paints below will not fill areas outside automatically
// as happens in the other gradient paints
processFillGradientPrimitive2D_drawOutputRange(rFillGradientPrimitive2D);
// get DefinitionRange and adapt if needed
basegfx::B2DRange aAdaptedRange(rFillGradientPrimitive2D.getDefinitionRange());
const bool bSquare(css::awt::GradientStyle_SQUARE
== rFillGradientPrimitive2D.getFillGradient().getStyle());
const basegfx::B2DPoint aCenter(aAdaptedRange.getCenter());
bool bLandscape(false);
double fSmallRadius(1.0);
// get rotation and offset values
const attribute::FillGradientAttribute& rFillGradient(
rFillGradientPrimitive2D.getFillGradient());
const double fAngle(basegfx::normalizeToRange((2 * M_PI) - rFillGradient.getAngle(), 2 * M_PI));
const bool bAngle(!basegfx::fTools::equalZero(fAngle));
const double fOffxsetX(std::max(std::min(rFillGradient.getOffsetX(), 1.0), 0.0));
const double fOffxsetY(std::max(std::min(rFillGradient.getOffsetY(), 1.0), 0.0));
if (bSquare)
{
// expand to make width == height
const basegfx::B2DRange& rDefRange(rFillGradientPrimitive2D.getDefinitionRange());
if (rDefRange.getWidth() > rDefRange.getHeight())
{
// landscape -> square
const double fRadius(0.5 * rDefRange.getWidth());
aAdaptedRange.expand(basegfx::B2DPoint(rDefRange.getMinX(), aCenter.getY() - fRadius));
aAdaptedRange.expand(basegfx::B2DPoint(rDefRange.getMaxX(), aCenter.getY() + fRadius));
}
else
{
// portrait -> square
const double fRadius(0.5 * rDefRange.getHeight());
aAdaptedRange.expand(basegfx::B2DPoint(aCenter.getX() - fRadius, rDefRange.getMinY()));
aAdaptedRange.expand(basegfx::B2DPoint(aCenter.getX() + fRadius, rDefRange.getMaxY()));
}
bLandscape = true;
fSmallRadius = 0.5 * aAdaptedRange.getWidth();
}
else
{
if (bAngle)
{
// expand range using applied rotation
aAdaptedRange.transform(basegfx::utils::createRotateAroundPoint(aCenter, fAngle));
}
// set local params as needed for non-square
bLandscape = aAdaptedRange.getWidth() > aAdaptedRange.getHeight();
fSmallRadius = 0.5 * (bLandscape ? aAdaptedRange.getHeight() : aAdaptedRange.getWidth());
}
// pack rotation and offset into a combined transformation that covers that parts
basegfx::B2DHomMatrix aRotAndTranslate;
aRotAndTranslate.translate(-aCenter.getX(), -aCenter.getY());
if (bAngle)
aRotAndTranslate.rotate(fAngle);
aRotAndTranslate.translate(aAdaptedRange.getMinX() + (fOffxsetX * aAdaptedRange.getWidth()),
aAdaptedRange.getMinY() + (fOffxsetY * aAdaptedRange.getHeight()));
// create local transform to work in object coordinates based on OutputRange,
// combine with rotation and offset - that way we can then just draw into
// AdaptedRange
basegfx::B2DHomMatrix aLocalTransform(getViewInformation2D().getObjectToViewTransformation()
* aRotAndTranslate);
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aLocalTransform.a(), aLocalTransform.b(), aLocalTransform.c(),
aLocalTransform.d(), aLocalTransform.e(), aLocalTransform.f());
cairo_set_matrix(mpRT, &aMatrix);
// get color stops (make copy, might have to be changed)
basegfx::BColorStops aBColorStops(rFillGradient.getColorStops());
// apply BColorModifierStack early - the BColorStops are used multiple
// times below, so do this only once
if (0 != maBColorModifierStack.count())
{
aBColorStops.tryToApplyBColorModifierStack(maBColorModifierStack);
}
// get and apply border - create soace at start in gradient
const double fBorder(std::max(std::min(rFillGradient.getBorder(), 1.0), 0.0));
if (!basegfx::fTools::equalZero(fBorder))
{
aBColorStops.createSpaceAtStart(fBorder);
}
// Apply steps if used to 'emulate' LO's 'discrete step' feature
if (rFillGradient.getSteps())
{
aBColorStops.doApplySteps(rFillGradient.getSteps());
}
// get half single pixel size to fill touching 'gaps'
// NOTE: I formally used cairo_device_to_user_distance, but that
// can indeed create negative sizes if the transformation e.g.
// contains rotation(s). could use fabs(), but just rely on
// linear algebra and use the (always positive) length of a vector
const double fHalfPx((getViewInformation2D().getInverseObjectToViewTransformation()
* basegfx::B2DVector(1.0, 0.0))
.getLength());
// draw top part trapez/triangle
{
cairo_move_to(mpRT, aAdaptedRange.getMinX(), aAdaptedRange.getMinY());
cairo_line_to(mpRT, aAdaptedRange.getMaxX(), aAdaptedRange.getMinY());
cairo_line_to(mpRT, aAdaptedRange.getMaxX(), aAdaptedRange.getMinY() + fHalfPx);
if (!bSquare && bLandscape)
{
cairo_line_to(mpRT, aAdaptedRange.getMaxX() - fSmallRadius, aCenter.getY() + fHalfPx);
cairo_line_to(mpRT, aAdaptedRange.getMinX() + fSmallRadius, aCenter.getY() + fHalfPx);
}
else
{
cairo_line_to(mpRT, aCenter.getX(), aAdaptedRange.getMinY() + fSmallRadius + fHalfPx);
}
cairo_line_to(mpRT, aAdaptedRange.getMinX(), aAdaptedRange.getMinY() + fHalfPx);
cairo_close_path(mpRT);
// create linear pattern in needed coordinates directly
// NOTE: I *tried* to create in unit coordinates and adapt modifying and re-using
// cairo_pattern_set_matrix - that *seems* to work but sometimes runs into
// numerical problems -> probably cairo implementation. So stay safe and do
// it the easy way, for the cost of re-creating gradient definitions (still cheap)
cairo_pattern_t* pPattern(cairo_pattern_create_linear(
aCenter.getX(), aAdaptedRange.getMinY(), aCenter.getX(),
aAdaptedRange.getMinY()
+ (bLandscape ? aAdaptedRange.getHeight() * 0.5 : fSmallRadius)));
for (const auto& aStop : aBColorStops)
{
const basegfx::BColor& rColor(aStop.getStopColor());
cairo_pattern_add_color_stop_rgb(pPattern, aStop.getStopOffset(), rColor.getRed(),
rColor.getGreen(), rColor.getBlue());
}
cairo_set_source(mpRT, pPattern);
cairo_fill(mpRT);
cairo_pattern_destroy(pPattern);
}
{
// draw right part trapez/triangle
cairo_move_to(mpRT, aAdaptedRange.getMaxX(), aAdaptedRange.getMinY());
cairo_line_to(mpRT, aAdaptedRange.getMaxX(), aAdaptedRange.getMaxY());
if (bSquare || bLandscape)
{
cairo_line_to(mpRT, aAdaptedRange.getMaxX() - fSmallRadius - fHalfPx, aCenter.getY());
}
else
{
cairo_line_to(mpRT, aCenter.getX() - fHalfPx, aAdaptedRange.getMaxY() - fSmallRadius);
cairo_line_to(mpRT, aCenter.getX() - fHalfPx, aAdaptedRange.getMinY() + fSmallRadius);
}
cairo_close_path(mpRT);
// create linear pattern in needed coordinates directly
cairo_pattern_t* pPattern(cairo_pattern_create_linear(
aAdaptedRange.getMaxX(), aCenter.getY(),
aAdaptedRange.getMaxX() - (bLandscape ? fSmallRadius : aAdaptedRange.getWidth() * 0.5),
aCenter.getY()));
for (const auto& aStop : aBColorStops)
{
const basegfx::BColor& rColor(aStop.getStopColor());
cairo_pattern_add_color_stop_rgb(pPattern, aStop.getStopOffset(), rColor.getRed(),
rColor.getGreen(), rColor.getBlue());
}
cairo_set_source(mpRT, pPattern);
cairo_fill(mpRT);
cairo_pattern_destroy(pPattern);
}
{
// draw bottom part trapez/triangle
cairo_move_to(mpRT, aAdaptedRange.getMaxX(), aAdaptedRange.getMaxY());
cairo_line_to(mpRT, aAdaptedRange.getMinX(), aAdaptedRange.getMaxY());
cairo_line_to(mpRT, aAdaptedRange.getMinX(), aAdaptedRange.getMaxY() - fHalfPx);
if (!bSquare && bLandscape)
{
cairo_line_to(mpRT, aAdaptedRange.getMinX() + fSmallRadius, aCenter.getY() - fHalfPx);
cairo_line_to(mpRT, aAdaptedRange.getMaxX() - fSmallRadius, aCenter.getY() - fHalfPx);
}
else
{
cairo_line_to(mpRT, aCenter.getX(), aAdaptedRange.getMaxY() - fSmallRadius - fHalfPx);
}
cairo_line_to(mpRT, aAdaptedRange.getMaxX(), aAdaptedRange.getMaxY() - fHalfPx);
cairo_close_path(mpRT);
// create linear pattern in needed coordinates directly
cairo_pattern_t* pPattern(cairo_pattern_create_linear(
aCenter.getX(), aAdaptedRange.getMaxY(), aCenter.getX(),
aAdaptedRange.getMaxY()
- (bLandscape ? aAdaptedRange.getHeight() * 0.5 : fSmallRadius)));
for (const auto& aStop : aBColorStops)
{
const basegfx::BColor& rColor(aStop.getStopColor());
cairo_pattern_add_color_stop_rgb(pPattern, aStop.getStopOffset(), rColor.getRed(),
rColor.getGreen(), rColor.getBlue());
}
cairo_set_source(mpRT, pPattern);
cairo_fill(mpRT);
cairo_pattern_destroy(pPattern);
}
{
// draw left part trapez/triangle
cairo_move_to(mpRT, aAdaptedRange.getMinX(), aAdaptedRange.getMaxY());
cairo_line_to(mpRT, aAdaptedRange.getMinX(), aAdaptedRange.getMinY());
if (bSquare || bLandscape)
{
cairo_line_to(mpRT, aAdaptedRange.getMinX() + fSmallRadius + fHalfPx, aCenter.getY());
}
else
{
cairo_line_to(mpRT, aCenter.getX() + fHalfPx, aAdaptedRange.getMinY() + fSmallRadius);
cairo_line_to(mpRT, aCenter.getX() + fHalfPx, aAdaptedRange.getMaxY() - fSmallRadius);
}
cairo_close_path(mpRT);
// create linear pattern in needed coordinates directly
cairo_pattern_t* pPattern(cairo_pattern_create_linear(
aAdaptedRange.getMinX(), aCenter.getY(),
aAdaptedRange.getMinX() + (bLandscape ? fSmallRadius : aAdaptedRange.getWidth() * 0.5),
aCenter.getY()));
for (const auto& aStop : aBColorStops)
{
const basegfx::BColor& rColor(aStop.getStopColor());
cairo_pattern_add_color_stop_rgb(pPattern, aStop.getStopOffset(), rColor.getRed(),
rColor.getGreen(), rColor.getBlue());
}
cairo_set_source(mpRT, pPattern);
cairo_fill(mpRT);
cairo_pattern_destroy(pPattern);
}
// cleanup
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processFillGradientPrimitive2D_radial_elliptical(
const primitive2d::FillGradientPrimitive2D& rFillGradientPrimitive2D)
{
const attribute::FillGradientAttribute& rFillGradient(
rFillGradientPrimitive2D.getFillGradient());
assert(!rFillGradientPrimitive2D.hasAlphaGradient()
|| rFillGradient.sameDefinitionThanAlpha(rFillGradientPrimitive2D.getAlphaGradient()));
assert((css::awt::GradientStyle_RADIAL == rFillGradientPrimitive2D.getFillGradient().getStyle()
|| css::awt::GradientStyle_ELLIPTICAL
== rFillGradientPrimitive2D.getFillGradient().getStyle())
&& "SDPRCairo: Helper allows only SPECIFIED types (!)");
cairo_save(mpRT);
// need to do 'antique' stuff adaptions for rotate/transitionStart in object coordinates
// (DefinitionRange) to have the right 'bending' on rotation
const basegfx::B2DRange rDefRange(rFillGradientPrimitive2D.getDefinitionRange());
const basegfx::B2DPoint aCenter(rDefRange.getCenter());
double fRadius(1.0);
double fRatioElliptical(1.0);
const bool bRadial(css::awt::GradientStyle_RADIAL == rFillGradient.getStyle());
// use what is done in initEllipticalGradientInfo method to get as close as
// possible to former stuff, expand AdaptedRange as needed
if (bRadial)
{
const double fHalfOriginalDiag(std::hypot(rDefRange.getWidth(), rDefRange.getHeight())
* 0.5);
fRadius = fHalfOriginalDiag;
}
else
{
double fTargetSizeX(M_SQRT2 * rDefRange.getWidth());
double fTargetSizeY(M_SQRT2 * rDefRange.getHeight());
fRatioElliptical = fTargetSizeX / fTargetSizeY;
fRadius = std::max(fTargetSizeX, fTargetSizeY) * 0.5;
}
// get rotation and offset values
const double fAngle(basegfx::normalizeToRange((2 * M_PI) - rFillGradient.getAngle(), 2 * M_PI));
const bool bAngle(!basegfx::fTools::equalZero(fAngle));
const double fOffxsetX(std::max(std::min(rFillGradient.getOffsetX(), 1.0), 0.0));
const double fOffxsetY(std::max(std::min(rFillGradient.getOffsetY(), 1.0), 0.0));
// pack rotation and offset into a combined transformation covering that parts
basegfx::B2DHomMatrix aRotAndTranslate;
aRotAndTranslate.translate(-aCenter.getX(), -aCenter.getY());
if (bAngle)
aRotAndTranslate.rotate(fAngle);
aRotAndTranslate.translate(rDefRange.getMinX() + (fOffxsetX * rDefRange.getWidth()),
rDefRange.getMinY() + (fOffxsetY * rDefRange.getHeight()));
// create local transform to work in object coordinates based on OutputRange,
// combine with rotation and offset - that way we can then just draw into
// AdaptedRange
basegfx::B2DHomMatrix aLocalTransform(getViewInformation2D().getObjectToViewTransformation()
* aRotAndTranslate);
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aLocalTransform.a(), aLocalTransform.b(), aLocalTransform.c(),
aLocalTransform.d(), aLocalTransform.e(), aLocalTransform.f());
cairo_set_matrix(mpRT, &aMatrix);
// create linear pattern in unit coordinates in y-direction
cairo_pattern_t* pPattern(cairo_pattern_create_radial(aCenter.getX(), aCenter.getY(), fRadius,
aCenter.getX(), aCenter.getY(), 0.0));
// get color stops (make copy, might have to be changed)
basegfx::BColorStops aBColorStops(rFillGradient.getColorStops());
basegfx::BColorStops aBColorStopsAlpha;
const bool bHasAlpha(rFillGradientPrimitive2D.hasAlphaGradient());
if (bHasAlpha)
aBColorStopsAlpha = rFillGradientPrimitive2D.getAlphaGradient().getColorStops();
// get and apply border - create soace at start in gradient
const double fBorder(std::max(std::min(rFillGradient.getBorder(), 1.0), 0.0));
if (!basegfx::fTools::equalZero(fBorder))
{
aBColorStops.createSpaceAtStart(fBorder);
if (bHasAlpha)
aBColorStopsAlpha.createSpaceAtStart(fBorder);
}
// Apply steps if used to 'emulate' LO's 'discrete step' feature
if (rFillGradient.getSteps())
{
aBColorStops.doApplySteps(rFillGradient.getSteps());
if (bHasAlpha)
aBColorStopsAlpha.doApplySteps(rFillGradient.getSteps());
}
// add color stops
for (size_t a(0); a < aBColorStops.size(); a++)
{
const basegfx::BColorStop& rStop(aBColorStops[a]);
const basegfx::BColor aColor(maBColorModifierStack.getModifiedColor(rStop.getStopColor()));
if (bHasAlpha)
{
const basegfx::BColor aAlpha(aBColorStopsAlpha[a].getStopColor());
cairo_pattern_add_color_stop_rgba(pPattern, rStop.getStopOffset(), aColor.getRed(),
aColor.getGreen(), aColor.getBlue(),
1.0 - aAlpha.luminance());
}
else
{
if (rFillGradientPrimitive2D.hasTransparency())
{
cairo_pattern_add_color_stop_rgba(pPattern, rStop.getStopOffset(), aColor.getRed(),
aColor.getGreen(), aColor.getBlue(),
1.0 - rFillGradientPrimitive2D.getTransparency());
}
else
{
cairo_pattern_add_color_stop_rgb(pPattern, rStop.getStopOffset(), aColor.getRed(),
aColor.getGreen(), aColor.getBlue());
}
}
}
cairo_set_source(mpRT, pPattern);
if (!bRadial) // css::awt::GradientStyle_ELLIPTICAL
{
// set cairo matrix at cairo_pattern_t to get needed ratio scale done.
// this is necessary since cairo_pattern_create_radial does *not*
// support ellipse resp. radial gradient with non-equidistant
// ratio directly
// this uses the transformation 'from user space to pattern space' as
// cairo docu states. That is the inverse of the intuitive thought
// model: describe from coordinates in texture, so use B2DHomMatrix
// and invert at the end to have better control about what has to happen
basegfx::B2DHomMatrix aTrans;
// move center to origin to prepare scale/rotate
aTrans.translate(-aCenter.getX(), -aCenter.getY());
// get scale factor and apply as needed
if (fRatioElliptical > 1.0)
aTrans.scale(1.0, 1.0 / fRatioElliptical);
else
aTrans.scale(fRatioElliptical, 1.0);
// move transformed stuff back to center
aTrans.translate(aCenter.getX(), aCenter.getY());
// invert and set at cairo_pattern_t
aTrans.invert();
cairo_matrix_init(&aMatrix, aTrans.a(), aTrans.b(), aTrans.c(), aTrans.d(), aTrans.e(),
aTrans.f());
cairo_pattern_set_matrix(pPattern, &aMatrix);
}
// draw OutRange. Due to rot and translate being part of the
// set transform in cairo we need to back-transform (and expand
// as needed) the OutputRange to paint at the right place and
// get all OutputRange covered
basegfx::B2DRange aOutRange(rFillGradientPrimitive2D.getOutputRange());
aRotAndTranslate.invert();
aOutRange.transform(aRotAndTranslate);
cairo_rectangle(mpRT, aOutRange.getMinX(), aOutRange.getMinY(), aOutRange.getWidth(),
aOutRange.getHeight());
cairo_fill(mpRT);
// cleanup
cairo_pattern_destroy(pPattern);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processFillGradientPrimitive2D_fallback_decompose(
const primitive2d::FillGradientPrimitive2D& rFillGradientPrimitive2D)
{
if (rFillGradientPrimitive2D.hasAlphaGradient())
{
// process recursively to eliminate alpha, cannot be used in decompose fallback
process(rFillGradientPrimitive2D);
return;
}
// this helper draws the given gradient using the decompose fallback,
// maybe needed in some cases an can/will be handy
cairo_save(mpRT);
// draw all-covering initial BG polygon 1st using getOuterColor and getOutputRange
processFillGradientPrimitive2D_drawOutputRange(rFillGradientPrimitive2D);
// bet basic form in unit coordinates
CairoPathHelper aForm(rFillGradientPrimitive2D.getUnitPolygon());
// paint solid fill steps by providing callback as lambda
auto aCallback([this, &aForm](const basegfx::B2DHomMatrix& rMatrix,
const basegfx::BColor& rColor) {
const basegfx::B2DHomMatrix aTrans(getViewInformation2D().getObjectToViewTransformation()
* rMatrix);
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aTrans.a(), aTrans.b(), aTrans.c(), aTrans.d(), aTrans.e(),
aTrans.f());
cairo_set_matrix(mpRT, &aMatrix);
const basegfx::BColor aColor(maBColorModifierStack.getModifiedColor(rColor));
cairo_set_source_rgb(mpRT, aColor.getRed(), aColor.getGreen(), aColor.getBlue());
cairo_append_path(mpRT, aForm.getCairoPath());
cairo_fill(mpRT);
});
// call value generator to trigger callbacks
rFillGradientPrimitive2D.generateMatricesAndColors(aCallback);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processFillGradientPrimitive2D(
const primitive2d::FillGradientPrimitive2D& rFillGradientPrimitive2D)
{
if (rFillGradientPrimitive2D.getDefinitionRange().isEmpty())
{
// no definition area, done
return;
}
if (rFillGradientPrimitive2D.getOutputRange().isEmpty())
{
// no output area, done
return;
}
const attribute::FillGradientAttribute& rFillGradient(
rFillGradientPrimitive2D.getFillGradient());
if (rFillGradient.isDefault())
{
// no gradient definition, done
return;
}
// check if completely 'bordered out'
if (processFillGradientPrimitive2D_isCompletelyBordered(rFillGradientPrimitive2D))
{
// yes, done, was processed as single filled rectangle (using getOuterColor())
return;
}
// evtl. prefer fallback: cairo does *not* render hard color transitions
// in gradients anti-aliased which is most visible in 'step'ed gradients,
// but may also happen in normal ones -> may need to be checked in
// basegfx::BColorStops (as tooling, like isSymmetrical() or similar).
// due to the nature of 'step'ing this also means a low number of
// filled polygons to be drawn (no 'smooth' parts to be replicated),
// so this is no runtime burner by definition.
// Making this configurable using static bool, may be moved to settings
// somewhere later. Do not forget to deactivate when working on 'step'ping
// stuff in the other helpers (!)
static bool bPreferAntiAliasedHardColorTransitions(true);
if (bPreferAntiAliasedHardColorTransitions && rFillGradient.getSteps())
{
processFillGradientPrimitive2D_fallback_decompose(rFillGradientPrimitive2D);
return;
}
switch (rFillGradient.getStyle())
{
case css::awt::GradientStyle_LINEAR:
case css::awt::GradientStyle_AXIAL:
{
// use specialized renderer for this cases - linear, axial
processFillGradientPrimitive2D_linear_axial(rFillGradientPrimitive2D);
return;
}
case css::awt::GradientStyle_RADIAL:
case css::awt::GradientStyle_ELLIPTICAL:
{
// use specialized renderer for this cases - radial, elliptical
// NOTE for css::awt::GradientStyle_ELLIPTICAL:
// The first time ever I will accept slight deviations for the
// elliptical case here due to it's old chaotic move-two-pixels inside
// rendering method that cannot be patched into a lineartransformation
// and is hard/difficult to support in more modern systems. Differences
// are small and mostly would be visible *if* in steps-mode what is
// also rare. IF that should make problems reactivation of that case
// for the default case below is possible. main reason is that speed
// for direct rendering in cairo is much better.
processFillGradientPrimitive2D_radial_elliptical(rFillGradientPrimitive2D);
return;
}
case css::awt::GradientStyle_SQUARE:
case css::awt::GradientStyle_RECT:
{
// use specialized renderer for this cases - square, rect
// NOTE: *NO* support for FillGradientAlpha here. it is anyways
// hard to map these to direct rendering, but to do so the four
// trapezoids/sides are 'stitched' together, so painting RGBA
// directly will make the overlaps look bad and like errors.
// Anyways, these gradient types are only our internal heritage
// and rendering them directly is already much faster, will be okay.
processFillGradientPrimitive2D_square_rect(rFillGradientPrimitive2D);
return;
}
default:
{
// NOTE: All cases are covered above, but keep this as fallback,
// so it is possible anytime to exclude one of the cases above again
// and go back to decomposed version - just in case...
processFillGradientPrimitive2D_fallback_decompose(rFillGradientPrimitive2D);
break;
}
}
}
void CairoPixelProcessor2D::processPolyPolygonRGBAPrimitive2D(
const primitive2d::PolyPolygonRGBAPrimitive2D& rPolyPolygonRGBAPrimitive2D)
{
if (!rPolyPolygonRGBAPrimitive2D.hasTransparency())
{
// do what CairoPixelProcessor2D::processPolyPolygonColorPrimitive2D does
paintPolyPoylgonRGBA(rPolyPolygonRGBAPrimitive2D.getB2DPolyPolygon(),
rPolyPolygonRGBAPrimitive2D.getBColor());
return;
}
// draw with alpha directly
paintPolyPoylgonRGBA(rPolyPolygonRGBAPrimitive2D.getB2DPolyPolygon(),
rPolyPolygonRGBAPrimitive2D.getBColor(),
rPolyPolygonRGBAPrimitive2D.getTransparency());
}
void CairoPixelProcessor2D::processPolyPolygonAlphaGradientPrimitive2D(
const primitive2d::PolyPolygonAlphaGradientPrimitive2D& rPolyPolygonAlphaGradientPrimitive2D)
{
const basegfx::B2DPolyPolygon& rPolyPolygon(
rPolyPolygonAlphaGradientPrimitive2D.getB2DPolyPolygon());
if (0 == rPolyPolygon.count())
{
// no geometry, done
return;
}
const basegfx::BColor& rColor(rPolyPolygonAlphaGradientPrimitive2D.getBColor());
const attribute::FillGradientAttribute& rAlphaGradient(
rPolyPolygonAlphaGradientPrimitive2D.getAlphaGradient());
if (rAlphaGradient.isDefault())
{
// default is a single ColorStop at 0.0 with black (0, 0, 0). The
// luminance is then 0.0, too -> not transparent at all
paintPolyPoylgonRGBA(rPolyPolygon, rColor);
return;
}
basegfx::BColor aSingleColor;
const basegfx::BColorStops& rAlphaStops(rAlphaGradient.getColorStops());
if (rAlphaStops.isSingleColor(aSingleColor))
{
// draw with alpha directly
paintPolyPoylgonRGBA(rPolyPolygon, rColor, aSingleColor.luminance());
return;
}
const css::awt::GradientStyle aStyle(rAlphaGradient.getStyle());
if (css::awt::GradientStyle_SQUARE == aStyle || css::awt::GradientStyle_RECT == aStyle)
{
// direct paint cannot be used for these styles since they get 'stitched'
// by multiple parts, so *need* single alpha for multiple pieces, go
// with decompose/recursion
process(rPolyPolygonAlphaGradientPrimitive2D);
return;
}
// render as FillGradientPrimitive2D. The idea is to create BColorStops
// with the same number of entries, but all the same color, using the
// polygon's target fill color, so we can directly paint gradients as
// RGBA in Cairo
basegfx::BColorStops aColorStops;
// create ColorStops at same stops but single color
aColorStops.reserve(rAlphaStops.size());
for (const auto& entry : rAlphaStops)
aColorStops.emplace_back(entry.getStopOffset(), rColor);
// create FillGradient using that single-color ColorStops
const attribute::FillGradientAttribute aFillGradient(
rAlphaGradient.getStyle(), rAlphaGradient.getBorder(), rAlphaGradient.getOffsetX(),
rAlphaGradient.getOffsetY(), rAlphaGradient.getAngle(), aColorStops,
rAlphaGradient.getSteps());
// create temporary FillGradientPrimitive2D, but do not forget
// to embed to MaskPrimitive2D to get the PolyPolygon form
const basegfx::B2DRange aRange(basegfx::utils::getRange(rPolyPolygon));
const primitive2d::Primitive2DContainer aContainerMaskedFillGradient{
rtl::Reference<primitive2d::MaskPrimitive2D>(new primitive2d::MaskPrimitive2D(
rPolyPolygon,
primitive2d::Primitive2DContainer{ rtl::Reference<primitive2d::FillGradientPrimitive2D>(
new primitive2d::FillGradientPrimitive2D(aRange, // OutputRange
aRange, // DefinitionRange
aFillGradient, &rAlphaGradient)) }))
};
// render this. Use container to not trigger decompose for temporary content
process(aContainerMaskedFillGradient);
}
void CairoPixelProcessor2D::processBitmapAlphaPrimitive2D(
const primitive2d::BitmapAlphaPrimitive2D& rBitmapAlphaPrimitive2D)
{
if (!rBitmapAlphaPrimitive2D.hasTransparency())
{
// do what CairoPixelProcessor2D::processPolyPolygonColorPrimitive2D does
paintBitmapAlpha(rBitmapAlphaPrimitive2D.getBitmap(),
rBitmapAlphaPrimitive2D.getTransform());
return;
}
// draw with alpha directly
paintBitmapAlpha(rBitmapAlphaPrimitive2D.getBitmap(), rBitmapAlphaPrimitive2D.getTransform(),
rBitmapAlphaPrimitive2D.getTransparency());
}
void CairoPixelProcessor2D::processTextSimplePortionPrimitive2D(
const primitive2d::TextSimplePortionPrimitive2D& rCandidate)
{
if (SAL_LIKELY(mbRenderSimpleTextDirect))
{
renderTextSimpleOrDecoratedPortionPrimitive2D(rCandidate, nullptr);
}
else
{
process(rCandidate);
}
}
void CairoPixelProcessor2D::processTextDecoratedPortionPrimitive2D(
const primitive2d::TextDecoratedPortionPrimitive2D& rCandidate)
{
if (SAL_LIKELY(mbRenderDecoratedTextDirect))
{
if (!rCandidate.getOrCreateBrokenUpText().empty())
{
// if BrokenUpText/WordLineMode is used, go into recursion
// with single snippets
process(rCandidate.getOrCreateBrokenUpText());
return;
}
renderTextSimpleOrDecoratedPortionPrimitive2D(rCandidate, &rCandidate);
}
else
{
process(rCandidate);
}
}
void CairoPixelProcessor2D::renderTextBackground(
const primitive2d::TextSimplePortionPrimitive2D& rTextCandidate, double fAscent,
double fDescent, const basegfx::B2DHomMatrix& rTransform, double fTextWidth)
{
cairo_save(mpRT);
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, rTransform.a(), rTransform.b(), rTransform.c(), rTransform.d(),
rTransform.e(), rTransform.f());
cairo_set_matrix(mpRT, &aMatrix);
const basegfx::BColor aFillColor(
maBColorModifierStack.getModifiedColor(rTextCandidate.getTextFillColor().getBColor()));
cairo_set_source_rgb(mpRT, aFillColor.getRed(), aFillColor.getGreen(), aFillColor.getBlue());
cairo_rectangle(mpRT, 0.0, -fAscent, fTextWidth, fAscent + fDescent);
cairo_fill(mpRT);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::renderSalLayout(const std::unique_ptr<SalLayout>& rSalLayout,
const basegfx::BColor& rTextColor,
const basegfx::B2DHomMatrix& rTransform,
bool bAntiAliase) const
{
cairo_save(mpRT);
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, rTransform.a(), rTransform.b(), rTransform.c(), rTransform.d(),
rTransform.e(), rTransform.f());
cairo_set_matrix(mpRT, &aMatrix);
rSalLayout->drawSalLayout(mpRT, rTextColor, bAntiAliase);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::renderTextDecorationWithOptionalTransformAndColor(
const primitive2d::TextDecoratedPortionPrimitive2D& rDecoratedCandidate,
const basegfx::utils::B2DHomMatrixBufferedOnDemandDecompose& rDecTrans,
const basegfx::B2DHomMatrix* pOptionalObjectTransform, const basegfx::BColor* pReplacementColor)
{
// get decorations from Primitive (using original TextTransform),
// guaranteed the same visualization as a decomposition would create
const primitive2d::Primitive2DContainer& rDecorationGeometryContent(
rDecoratedCandidate.getOrCreateDecorationGeometryContent(
rDecTrans, rDecoratedCandidate.getText(), rDecoratedCandidate.getTextPosition(),
rDecoratedCandidate.getTextLength(), rDecoratedCandidate.getDXArray()));
if (rDecorationGeometryContent.empty())
{
// no decoration, done
return;
}
// modify ColorStack as needed - if needed
if (nullptr != pReplacementColor)
maBColorModifierStack.push(
std::make_shared<basegfx::BColorModifier_replace>(*pReplacementColor));
// modify transformation as needed - if needed
const geometry::ViewInformation2D aLastViewInformation2D(getViewInformation2D());
if (nullptr != pOptionalObjectTransform)
{
geometry::ViewInformation2D aViewInformation2D(getViewInformation2D());
aViewInformation2D.setObjectTransformation(*pOptionalObjectTransform);
updateViewInformation(aViewInformation2D);
}
// render primitives
process(rDecorationGeometryContent);
// restore mods
if (nullptr != pOptionalObjectTransform)
updateViewInformation(aLastViewInformation2D);
if (nullptr != pReplacementColor)
maBColorModifierStack.pop();
}
void CairoPixelProcessor2D::renderTextSimpleOrDecoratedPortionPrimitive2D(
const primitive2d::TextSimplePortionPrimitive2D& rTextCandidate,
const primitive2d::TextDecoratedPortionPrimitive2D* pDecoratedCandidate)
{
primitive2d::TextLayouterDevice aTextLayouter;
rTextCandidate.createTextLayouter(aTextLayouter);
std::unique_ptr<SalLayout> pSalLayout(rTextCandidate.createSalLayout(aTextLayouter));
if (!pSalLayout)
{
// got no layout, error. use decompose as fallback
process(rTextCandidate);
return;
}
// prepare local transformations
basegfx::utils::B2DHomMatrixBufferedOnDemandDecompose aDecTrans(
rTextCandidate.getTextTransform());
const basegfx::B2DHomMatrix aObjTransformWithoutScale(
basegfx::utils::createShearXRotateTranslateB2DHomMatrix(
aDecTrans.getShearX(), aDecTrans.getRotate(), aDecTrans.getTranslate()));
const basegfx::B2DHomMatrix aFullTextTransform(
getViewInformation2D().getObjectToViewTransformation() * aObjTransformWithoutScale);
if (!rTextCandidate.getTextFillColor().IsTransparent())
{
// render TextBackground first -> casts no shadow itself, so do independent of
// text shadow being activated
double fAscent(aTextLayouter.getFontAscent());
double fDescent(aTextLayouter.getFontDescent());
if (nullptr != pDecoratedCandidate
&& primitive2d::TEXT_FONT_EMPHASIS_MARK_NONE
!= pDecoratedCandidate->getTextEmphasisMark())
{
if (pDecoratedCandidate->getEmphasisMarkAbove())
fAscent += aTextLayouter.getTextHeight() * (250.0 / 1000.0);
if (pDecoratedCandidate->getEmphasisMarkBelow())
fDescent += aTextLayouter.getTextHeight() * (250.0 / 1000.0);
}
renderTextBackground(rTextCandidate, fAscent, fDescent, aFullTextTransform,
pSalLayout->GetTextWidth());
}
if (rTextCandidate.hasShadow())
{
// Text shadow is constant, relative to font size, *not* rotated with
// text (always from top-left!)
static const double fFactor(1.0 / 24.0);
const double fTextShadowOffset(aDecTrans.getScale().getY() * fFactor);
// see ::ImplDrawSpecialText -> no longer simple fixed color
const basegfx::BColor aBlack(0.0, 0.0, 0.0);
basegfx::BColor aShadowColor(aBlack);
if (aBlack == rTextCandidate.getFontColor()
|| rTextCandidate.getFontColor().luminance() < (8.0 / 255.0))
aShadowColor = COL_LIGHTGRAY.getBColor();
aShadowColor = maBColorModifierStack.getModifiedColor(aShadowColor);
// create shadow offset
const basegfx::B2DHomMatrix aShadowTransform(
basegfx::utils::createTranslateB2DHomMatrix(fTextShadowOffset, fTextShadowOffset));
const basegfx::B2DHomMatrix aShadowFullTextTransform(
// right to left: 1st the ObjTrans, then the shadow offset, last ObjToView. That way
// the shadow is always from top-left, independent of text rotation. Independent from
// thinking about if that is wanted (shadow direction *could* rotate with the text)
// this is what the office currently does -> do *not* change visualization (!)
getViewInformation2D().getObjectToViewTransformation() * aShadowTransform
* aObjTransformWithoutScale);
// render text as shadow
renderSalLayout(pSalLayout, aShadowColor, aShadowFullTextTransform,
getViewInformation2D().getUseAntiAliasing());
if (rTextCandidate.hasTextDecoration())
{
const basegfx::B2DHomMatrix aTransform(getViewInformation2D().getObjectTransformation()
* aShadowTransform);
renderTextDecorationWithOptionalTransformAndColor(*pDecoratedCandidate, aDecTrans,
&aTransform, &aShadowColor);
}
}
// get TextColor early, may have to be modified
basegfx::BColor aTextColor(rTextCandidate.getFontColor());
if (rTextCandidate.hasOutline())
{
// render as outline
aTextColor = maBColorModifierStack.getModifiedColor(aTextColor);
basegfx::B2DHomMatrix aInvViewTransform;
// discrete offsets defined here to easily allow to change them,
// e.g. if more 'fat' outline is wanted, it may be increased to 1.5
constexpr double fZero(0.0);
constexpr double fPlus(1.0);
constexpr double fMinus(-1.0);
static constexpr std::array<std::pair<double, double>, 8> offsets{
std::pair<double, double>{ fMinus, fMinus }, std::pair<double, double>{ fZero, fMinus },
std::pair<double, double>{ fPlus, fMinus }, std::pair<double, double>{ fMinus, fZero },
std::pair<double, double>{ fPlus, fZero }, std::pair<double, double>{ fMinus, fPlus },
std::pair<double, double>{ fZero, fPlus }, std::pair<double, double>{ fPlus, fPlus }
};
if (rTextCandidate.hasTextDecoration())
{
// to use discrete offset (pixels) we will need the back-transform from
// discrete view coordinates to 'world' coordinates (logic view coordinates),
// this is the inverse ViewTransformation.
// NOTE: Alternatively we could calculate the lengths for fPlus/fMinus in
// logic view coordinates, but would need to create another B2DHomMatrix and
// to do it correct would need to handle two vectors holding the directions,
// else - if ever someone will rotate/shear that transformation - it would
// break
aInvViewTransform = getViewInformation2D().getViewTransformation();
aInvViewTransform.invert();
}
for (const auto& offset : offsets)
{
const basegfx::B2DHomMatrix aDiscreteOffset(
basegfx::utils::createTranslateB2DHomMatrix(offset.first, offset.second));
renderSalLayout(pSalLayout, aTextColor, aDiscreteOffset * aFullTextTransform,
getViewInformation2D().getUseAntiAliasing());
if (rTextCandidate.hasTextDecoration())
{
const basegfx::B2DHomMatrix aTransform(
aInvViewTransform * aDiscreteOffset
* getViewInformation2D().getObjectToViewTransformation());
renderTextDecorationWithOptionalTransformAndColor(*pDecoratedCandidate, aDecTrans,
&aTransform);
}
}
// at (center, center) paint in COL_WHITE
aTextColor = maBColorModifierStack.getModifiedColor(COL_WHITE.getBColor());
renderSalLayout(pSalLayout, aTextColor, aFullTextTransform,
getViewInformation2D().getUseAntiAliasing());
if (rTextCandidate.hasTextDecoration())
{
renderTextDecorationWithOptionalTransformAndColor(*pDecoratedCandidate, aDecTrans,
nullptr, &aTextColor);
}
// paint is complete, Outline and TextRelief cannot be combined, return
return;
}
if (rTextCandidate.hasTextRelief())
{
// manipulate TextColor for final text paint below (see ::ImplDrawSpecialText)
if (aTextColor == COL_BLACK.getBColor())
aTextColor = COL_WHITE.getBColor();
// relief offset defined here to easily allow to change them
// see ::ImplDrawSpecialText and the comment @ 'nOff += mnDPIX/300'
const bool bEmboss(primitive2d::TEXT_RELIEF_EMBOSSED
== pDecoratedCandidate->getTextRelief());
constexpr double fReliefOffset(1.1);
const double fOffset(bEmboss ? fReliefOffset : -fReliefOffset);
const basegfx::B2DHomMatrix aDiscreteOffset(
basegfx::utils::createTranslateB2DHomMatrix(fOffset, fOffset));
// see aReliefColor in ::ImplDrawSpecialText
basegfx::BColor aReliefColor(COL_LIGHTGRAY.getBColor());
if (COL_WHITE.getBColor() == aTextColor)
aReliefColor = COL_BLACK.getBColor();
aReliefColor = maBColorModifierStack.getModifiedColor(aReliefColor);
// render relief text with offset
renderSalLayout(pSalLayout, aReliefColor, aDiscreteOffset * aFullTextTransform,
getViewInformation2D().getUseAntiAliasing());
if (rTextCandidate.hasTextDecoration())
{
basegfx::B2DHomMatrix aInvViewTransform(getViewInformation2D().getViewTransformation());
aInvViewTransform.invert();
const basegfx::B2DHomMatrix aTransform(
aInvViewTransform * aDiscreteOffset
* getViewInformation2D().getObjectToViewTransformation());
renderTextDecorationWithOptionalTransformAndColor(*pDecoratedCandidate, aDecTrans,
&aTransform, &aReliefColor);
}
}
// render text
aTextColor = maBColorModifierStack.getModifiedColor(aTextColor);
renderSalLayout(pSalLayout, aTextColor, aFullTextTransform,
getViewInformation2D().getUseAntiAliasing());
if (rTextCandidate.hasTextDecoration())
{
// render using same geometry/primitives that a decompose would
// create -> safe to get the same visualization for both
renderTextDecorationWithOptionalTransformAndColor(*pDecoratedCandidate, aDecTrans);
}
}
bool CairoPixelProcessor2D::handleSvgGradientHelper(
const primitive2d::SvgGradientHelper& rCandidate)
{
// check PolyPolygon to be filled
const basegfx::B2DPolyPolygon& rPolyPolygon(rCandidate.getPolyPolygon());
if (!rPolyPolygon.count())
{
// no PolyPolygon, done
return true;
}
// calculate visible range
basegfx::B2DRange aPolyPolygonRange(rPolyPolygon.getB2DRange());
aPolyPolygonRange.transform(getViewInformation2D().getObjectToViewTransformation());
if (!getDiscreteViewRange(mpRT).overlaps(aPolyPolygonRange))
{
// not visible, done
return true;
}
if (!rCandidate.getCreatesContent())
{
// creates no content, done
return true;
}
if (rCandidate.getSingleEntry())
{
// only one color entry, fill with last existing color, done
primitive2d::SvgGradientEntryVector::const_reference aEntry(
rCandidate.getGradientEntries().back());
paintPolyPoylgonRGBA(rCandidate.getPolyPolygon(), aEntry.getColor(),
1.0 - aEntry.getOpacity());
return true;
}
return false;
}
void CairoPixelProcessor2D::processSvgLinearGradientPrimitive2D(
const primitive2d::SvgLinearGradientPrimitive2D& rCandidate)
{
// check for simple cases, returns if all necessary is already done
if (handleSvgGradientHelper(rCandidate))
{
// simple case, handled, done
return;
}
cairo_save(mpRT);
// set ObjectToView as regular transformation at CairoContext
const basegfx::B2DHomMatrix aTrans(getViewInformation2D().getObjectToViewTransformation());
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aTrans.a(), aTrans.b(), aTrans.c(), aTrans.d(), aTrans.e(),
aTrans.f());
cairo_set_matrix(mpRT, &aMatrix);
// create pattern using unit coordinates. Unit coordinates here means that
// the transformation provided by the primitive maps the linear gradient
// to (0,0) -> (1,0) at the unified object coordinates, along the unified
// X-Axis
cairo_pattern_t* pPattern(cairo_pattern_create_linear(0, 0, 1, 0));
// get pre-defined UnitGradientToObject transformation from primitive
// and invert to get ObjectToUnitGradient transform
basegfx::B2DHomMatrix aObjectToUnitGradient(
rCandidate.createUnitGradientToObjectTransformation());
aObjectToUnitGradient.invert();
// set ObjectToUnitGradient as transformation at gradient - patterns
// need the inverted transformation, see cairo documentation
cairo_matrix_init(&aMatrix, aObjectToUnitGradient.a(), aObjectToUnitGradient.b(),
aObjectToUnitGradient.c(), aObjectToUnitGradient.d(),
aObjectToUnitGradient.e(), aObjectToUnitGradient.f());
cairo_pattern_set_matrix(pPattern, &aMatrix);
// add color stops
const primitive2d::SvgGradientEntryVector& rGradientEntries(rCandidate.getGradientEntries());
for (const auto& entry : rGradientEntries)
{
const basegfx::BColor aColor(maBColorModifierStack.getModifiedColor(entry.getColor()));
cairo_pattern_add_color_stop_rgba(pPattern, entry.getOffset(), aColor.getRed(),
aColor.getGreen(), aColor.getBlue(), entry.getOpacity());
}
// set SpreadMethod. Note that we have no SpreadMethod::None because the
// source is SVG and SVG does also not have that (checked that)
switch (rCandidate.getSpreadMethod())
{
case primitive2d::SpreadMethod::Pad:
cairo_pattern_set_extend(pPattern, CAIRO_EXTEND_PAD);
break;
case primitive2d::SpreadMethod::Reflect:
cairo_pattern_set_extend(pPattern, CAIRO_EXTEND_REFLECT);
break;
case primitive2d::SpreadMethod::Repeat:
cairo_pattern_set_extend(pPattern, CAIRO_EXTEND_REPEAT);
break;
}
// get PathGeometry & paint it filed with gradient
cairo_new_path(mpRT);
getOrCreateFillGeometry(mpRT, rCandidate.getPolyPolygon());
cairo_set_source(mpRT, pPattern);
cairo_fill(mpRT);
// cleanup
cairo_pattern_destroy(pPattern);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processSvgRadialGradientPrimitive2D(
const primitive2d::SvgRadialGradientPrimitive2D& rCandidate)
{
// check for simple cases, returns if all necessary is already done
if (handleSvgGradientHelper(rCandidate))
{
// simple case, handled, done
return;
}
cairo_save(mpRT);
// set ObjectToView as regular transformation at CairoContext
const basegfx::B2DHomMatrix aTrans(getViewInformation2D().getObjectToViewTransformation());
cairo_matrix_t aMatrix;
cairo_matrix_init(&aMatrix, aTrans.a(), aTrans.b(), aTrans.c(), aTrans.d(), aTrans.e(),
aTrans.f());
cairo_set_matrix(mpRT, &aMatrix);
// get pre-defined UnitGradientToObject transformation from primitive
// and invert to get ObjectToUnitGradient transform
basegfx::B2DHomMatrix aObjectToUnitGradient(
rCandidate.createUnitGradientToObjectTransformation());
aObjectToUnitGradient.invert();
// prepare empty FocalVector
basegfx::B2DVector aFocalVector(0.0, 0.0);
if (rCandidate.isFocalSet())
{
// FocalPoint is used, create ObjectTransform based on polygon range
const basegfx::B2DRange aPolyRange(rCandidate.getPolyPolygon().getB2DRange());
const double fPolyWidth(aPolyRange.getWidth());
const double fPolyHeight(aPolyRange.getHeight());
const basegfx::B2DHomMatrix aObjectTransform(
basegfx::utils::createScaleTranslateB2DHomMatrix(
fPolyWidth, fPolyHeight, aPolyRange.getMinX(), aPolyRange.getMinY()));
// get vector, then transform to object coordinates, then to
// UnitGradient coordinates to be in the needed coordinate system
aFocalVector = basegfx::B2DVector(rCandidate.getStart() - rCandidate.getFocal());
aFocalVector *= aObjectTransform;
aFocalVector *= aObjectToUnitGradient;
}
// create pattern using unit coordinates. Unit coordinates here means that
// the transformation provided by the primitive maps the radial gradient
// to (0,0) as center, 1.0 as radius - which is the unit circle. The
// FocalPoint (if used) has to be relative to that, so - since unified
// center is at (0, 0), handling as vector is sufficient
cairo_pattern_t* pPattern(
cairo_pattern_create_radial(0, 0, 0, aFocalVector.getX(), aFocalVector.getY(), 1));
// set ObjectToUnitGradient as transformation at gradient - patterns
// need the inverted transformation, see cairo documentation
cairo_matrix_init(&aMatrix, aObjectToUnitGradient.a(), aObjectToUnitGradient.b(),
aObjectToUnitGradient.c(), aObjectToUnitGradient.d(),
aObjectToUnitGradient.e(), aObjectToUnitGradient.f());
cairo_pattern_set_matrix(pPattern, &aMatrix);
// add color stops
const primitive2d::SvgGradientEntryVector& rGradientEntries(rCandidate.getGradientEntries());
for (const auto& entry : rGradientEntries)
{
const basegfx::BColor aColor(maBColorModifierStack.getModifiedColor(entry.getColor()));
cairo_pattern_add_color_stop_rgba(pPattern, entry.getOffset(), aColor.getRed(),
aColor.getGreen(), aColor.getBlue(), entry.getOpacity());
}
// set SpreadMethod
switch (rCandidate.getSpreadMethod())
{
case primitive2d::SpreadMethod::Pad:
cairo_pattern_set_extend(pPattern, CAIRO_EXTEND_PAD);
break;
case primitive2d::SpreadMethod::Reflect:
cairo_pattern_set_extend(pPattern, CAIRO_EXTEND_REFLECT);
break;
case primitive2d::SpreadMethod::Repeat:
cairo_pattern_set_extend(pPattern, CAIRO_EXTEND_REPEAT);
break;
}
// get PathGeometry & paint it filed with gradient
cairo_new_path(mpRT);
getOrCreateFillGeometry(mpRT, rCandidate.getPolyPolygon());
cairo_set_source(mpRT, pPattern);
cairo_fill(mpRT);
// cleanup
cairo_pattern_destroy(pPattern);
cairo_restore(mpRT);
}
void CairoPixelProcessor2D::processBasePrimitive2D(const primitive2d::BasePrimitive2D& rCandidate)
{
const cairo_status_t aStart(cairo_status(mpRT));
switch (rCandidate.getPrimitive2DID())
{
// geometry that *has* to be processed
case PRIMITIVE2D_ID_BITMAPPRIMITIVE2D:
{
processBitmapPrimitive2D(
static_cast<const primitive2d::BitmapPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_POINTARRAYPRIMITIVE2D:
{
processPointArrayPrimitive2D(
static_cast<const primitive2d::PointArrayPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_POLYGONHAIRLINEPRIMITIVE2D:
{
processPolygonHairlinePrimitive2D(
static_cast<const primitive2d::PolygonHairlinePrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_POLYPOLYGONCOLORPRIMITIVE2D:
{
processPolyPolygonColorPrimitive2D(
static_cast<const primitive2d::PolyPolygonColorPrimitive2D&>(rCandidate));
break;
}
// embedding/groups that *have* to be processed
case PRIMITIVE2D_ID_TRANSPARENCEPRIMITIVE2D:
{
processTransparencePrimitive2D(
static_cast<const primitive2d::TransparencePrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_INVERTPRIMITIVE2D:
{
processInvertPrimitive2D(
static_cast<const primitive2d::InvertPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_MASKPRIMITIVE2D:
{
processMaskPrimitive2D(static_cast<const primitive2d::MaskPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_MODIFIEDCOLORPRIMITIVE2D:
{
processModifiedColorPrimitive2D(
static_cast<const primitive2d::ModifiedColorPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_TRANSFORMPRIMITIVE2D:
{
processTransformPrimitive2D(
static_cast<const primitive2d::TransformPrimitive2D&>(rCandidate));
break;
}
// geometry that *may* be processed due to being able to do it better
// then using the decomposition
case PRIMITIVE2D_ID_UNIFIEDTRANSPARENCEPRIMITIVE2D:
{
processUnifiedTransparencePrimitive2D(
static_cast<const primitive2d::UnifiedTransparencePrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_MARKERARRAYPRIMITIVE2D:
{
processMarkerArrayPrimitive2D(
static_cast<const primitive2d::MarkerArrayPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_BACKGROUNDCOLORPRIMITIVE2D:
{
processBackgroundColorPrimitive2D(
static_cast<const primitive2d::BackgroundColorPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_POLYGONSTROKEPRIMITIVE2D:
{
processPolygonStrokePrimitive2D(
static_cast<const primitive2d::PolygonStrokePrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_LINERECTANGLEPRIMITIVE2D:
{
processLineRectanglePrimitive2D(
static_cast<const primitive2d::LineRectanglePrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_FILLEDRECTANGLEPRIMITIVE2D:
{
processFilledRectanglePrimitive2D(
static_cast<const primitive2d::FilledRectanglePrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_SINGLELINEPRIMITIVE2D:
{
processSingleLinePrimitive2D(
static_cast<const primitive2d::SingleLinePrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_FILLGRAPHICPRIMITIVE2D:
{
processFillGraphicPrimitive2D(
static_cast<const primitive2d::FillGraphicPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_FILLGRADIENTPRIMITIVE2D:
{
processFillGradientPrimitive2D(
static_cast<const primitive2d::FillGradientPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_POLYPOLYGONRGBAPRIMITIVE2D:
{
processPolyPolygonRGBAPrimitive2D(
static_cast<const primitive2d::PolyPolygonRGBAPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_BITMAPALPHAPRIMITIVE2D:
{
processBitmapAlphaPrimitive2D(
static_cast<const primitive2d::BitmapAlphaPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_POLYPOLYGONALPHAGRADIENTPRIMITIVE2D:
{
processPolyPolygonAlphaGradientPrimitive2D(
static_cast<const primitive2d::PolyPolygonAlphaGradientPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_TEXTSIMPLEPORTIONPRIMITIVE2D:
{
processTextSimplePortionPrimitive2D(
static_cast<const primitive2d::TextSimplePortionPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_TEXTDECORATEDPORTIONPRIMITIVE2D:
{
processTextDecoratedPortionPrimitive2D(
static_cast<const primitive2d::TextDecoratedPortionPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_SVGLINEARGRADIENTPRIMITIVE2D:
{
processSvgLinearGradientPrimitive2D(
static_cast<const primitive2d::SvgLinearGradientPrimitive2D&>(rCandidate));
break;
}
case PRIMITIVE2D_ID_SVGRADIALGRADIENTPRIMITIVE2D:
{
processSvgRadialGradientPrimitive2D(
static_cast<const primitive2d::SvgRadialGradientPrimitive2D&>(rCandidate));
break;
}
// continue with decompose
default:
{
SAL_INFO("drawinglayer", "default case for " << drawinglayer::primitive2d::idToString(
rCandidate.getPrimitive2DID()));
// process recursively
process(rCandidate);
break;
}
}
const cairo_status_t aEnd(cairo_status(mpRT));
if (aStart != aEnd)
{
SAL_WARN("drawinglayer", "CairoSDPR: Cairo status problem (!)");
}
}
} // end of namespace
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
↑ V516 Consider inspecting an odd expression. Non-null function pointer is compared to null: 'nullptr != pDisableDownScale'.
↑ V595 The 'pAlphaWrite' pointer was utilized before it was verified against nullptr. Check lines: 1056, 1081.
↑ V1007 The value from the potentially uninitialized optional 'aAlphaMask' is used. Probably it is a mistake.