forked from amazingfate/loongoffice
The task is fixed with the previous commit for this tdf number, but convinced me to do also some more safe stuff in the CairoSDPR implementation. The associated OutputDevice *is* now handed over to CairoSDPR and some stuff being done in the helper to create the SDPR is now done directly in the CairoSDPR constructors. Also the FormControl rendering is now closer to what the VCLPrimitiveRenderer does, to be on the safe side. It is still just the FormControl rendering that indirectly uses the associated OutputDevice, but more convenient. The CairoSDPR now also (as VCLRenderer) resets the MapMode in the associated OutputDevice for the time using it - just in case there might be other *indirect* usages like the FormControl one. Change-Id: I5029788655ff81bf360d98312d417b7886208e1f Reviewed-on: https://gerrit.libreoffice.org/c/core/+/183204 Reviewed-by: Armin Le Grand <Armin.Le.Grand@me.com> Tested-by: Jenkins (cherry picked from commit 7a1de78ec2e58d2cceded3bf03c0c3c3cccc675a) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/186371 Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
4218 lines
168 KiB
C++
4218 lines
168 KiB
C++
/* -*- 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/sysdata.hxx>
|
|
#include <vcl/svapp.hxx>
|
|
#include <comphelper/lok.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/primitive2d/controlprimitive2d.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 <vcl/vcllayout.hxx>
|
|
#include <officecfg/Office/Common.hxx>
|
|
#include <com/sun/star/awt/XView.hpp>
|
|
#include <com/sun/star/awt/XControl.hpp>
|
|
#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,
|
|
bool bCairoCoordinateLimitWorkaroundActive)
|
|
{
|
|
#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
|
|
if (bCairoCoordinateLimitWorkaroundActive)
|
|
{
|
|
// we have to render in view coordiantes, set line width to 1.0
|
|
cairo_set_line_width(pRT, 1.0);
|
|
}
|
|
else
|
|
{
|
|
// 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));
|
|
}
|
|
|
|
bool checkCoordinateLimitWorkaroundNeededForUsedCairo()
|
|
{
|
|
// setup surface and render context
|
|
cairo_surface_t* pSurface(cairo_image_surface_create(CAIRO_FORMAT_RGB24, 8, 8));
|
|
if (!pSurface)
|
|
// got no surface -> be pessimistic
|
|
return true;
|
|
|
|
cairo_t* pRender(cairo_create(pSurface));
|
|
if (!pRender)
|
|
{
|
|
// got no render -> be pessimistic
|
|
cairo_surface_destroy(pSurface);
|
|
return true;
|
|
}
|
|
|
|
// set basic values
|
|
cairo_set_antialias(pRender, CAIRO_ANTIALIAS_NONE);
|
|
cairo_set_fill_rule(pRender, CAIRO_FILL_RULE_EVEN_ODD);
|
|
cairo_set_operator(pRender, CAIRO_OPERATOR_OVER);
|
|
cairo_set_source_rgb(pRender, 1.0, 0.0, 0.0);
|
|
|
|
// create a to-be rendered area centered at the fNumCairoMax
|
|
// spot and 8x8 discrete units in size
|
|
constexpr double fNumCairoMax(1 << 23);
|
|
const basegfx::B2DPoint aCenter(fNumCairoMax, fNumCairoMax);
|
|
const basegfx::B2DPoint aOffset(4, 4);
|
|
const basegfx::B2DRange aObject(aCenter - aOffset, aCenter + aOffset);
|
|
|
|
// create transformation to render that to an aerea with
|
|
// range(0, 0, 8, 8) and set as transformation
|
|
const basegfx::B2DHomMatrix aObjectToView(basegfx::utils::createSourceRangeTargetRangeTransform(
|
|
aObject, basegfx::B2DRange(0, 0, 8, 8)));
|
|
cairo_matrix_t aMatrix;
|
|
cairo_matrix_init(&aMatrix, aObjectToView.a(), aObjectToView.b(), aObjectToView.c(),
|
|
aObjectToView.d(), aObjectToView.e(), aObjectToView.f());
|
|
cairo_set_matrix(pRender, &aMatrix);
|
|
|
|
// get/create the path for an object exactly filling that area
|
|
cairo_new_path(pRender);
|
|
basegfx::B2DPolyPolygon aObjectPolygon(basegfx::utils::createPolygonFromRect(aObject));
|
|
CairoPathHelper aPathHelper(aObjectPolygon);
|
|
cairo_append_path(pRender, aPathHelper.getCairoPath());
|
|
|
|
// render it and flush since we want to immediately inspect result
|
|
cairo_fill(pRender);
|
|
cairo_surface_flush(pSurface);
|
|
|
|
// get access to pixel data
|
|
const sal_uInt32 nStride(cairo_image_surface_get_stride(pSurface));
|
|
sal_uInt8* pStartPixelData(cairo_image_surface_get_data(pSurface));
|
|
|
|
// extract red value for pixels at (1,1) and (7,7)
|
|
sal_uInt8 aRedAt_1_1((pStartPixelData + (nStride * 1) + 1)[SVP_CAIRO_RED]);
|
|
sal_uInt8 aRedAt_6_6((pStartPixelData + (nStride * 6) + 6)[SVP_CAIRO_RED]);
|
|
|
|
// cleanup
|
|
cairo_destroy(pRender);
|
|
cairo_surface_destroy(pSurface);
|
|
|
|
// if cairo works or has no 24.8 internal format all pixels
|
|
// have to be red (255), thus workaround is needed if !=
|
|
return aRedAt_1_1 != aRedAt_6_6;
|
|
}
|
|
}
|
|
|
|
namespace drawinglayer::processor2d
|
|
{
|
|
CairoPixelProcessor2D::CairoPixelProcessor2D(const geometry::ViewInformation2D& rViewInformation,
|
|
cairo_surface_t* pTarget)
|
|
: BaseProcessor2D(rViewInformation)
|
|
, mpTargetOutputDevice(nullptr)
|
|
, maBColorModifierStack()
|
|
, mpOwnedSurface(nullptr)
|
|
, mpRT(nullptr)
|
|
, mbRenderSimpleTextDirect(
|
|
officecfg::Office::Common::Drawinglayer::RenderSimpleTextDirect::get())
|
|
, mbRenderDecoratedTextDirect(
|
|
officecfg::Office::Common::Drawinglayer::RenderDecoratedTextDirect::get())
|
|
, mnClipRecursionCount(0)
|
|
, mbCairoCoordinateLimitWorkaroundActive(false)
|
|
{
|
|
// no target, nothing to initialize
|
|
if (nullptr == pTarget)
|
|
return;
|
|
|
|
// create RenderTarget for full target
|
|
mpRT = cairo_create(pTarget);
|
|
|
|
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);
|
|
|
|
// evaluate if CairoCoordinateLimitWorkaround is needed
|
|
evaluateCairoCoordinateLimitWorkaround();
|
|
}
|
|
|
|
CairoPixelProcessor2D::CairoPixelProcessor2D(const geometry::ViewInformation2D& rViewInformation,
|
|
tools::Long nWidthPixel, tools::Long nHeightPixel,
|
|
bool bUseRGBA)
|
|
: BaseProcessor2D(rViewInformation)
|
|
, mpTargetOutputDevice(nullptr)
|
|
, maBColorModifierStack()
|
|
, mpOwnedSurface(nullptr)
|
|
, mpRT(nullptr)
|
|
, mbRenderSimpleTextDirect(
|
|
officecfg::Office::Common::Drawinglayer::RenderSimpleTextDirect::get())
|
|
, mbRenderDecoratedTextDirect(
|
|
officecfg::Office::Common::Drawinglayer::RenderDecoratedTextDirect::get())
|
|
, mnClipRecursionCount(0)
|
|
, mbCairoCoordinateLimitWorkaroundActive(false)
|
|
{
|
|
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);
|
|
|
|
// evaluate if CairoCoordinateLimitWorkaround is needed
|
|
evaluateCairoCoordinateLimitWorkaround();
|
|
}
|
|
|
|
CairoPixelProcessor2D::CairoPixelProcessor2D(OutputDevice& rOutputDevice,
|
|
const geometry::ViewInformation2D& rViewInformation)
|
|
: BaseProcessor2D(rViewInformation)
|
|
, mpTargetOutputDevice(&rOutputDevice)
|
|
, maBColorModifierStack()
|
|
, mpOwnedSurface(nullptr)
|
|
, mpRT(nullptr)
|
|
, mbRenderSimpleTextDirect(
|
|
officecfg::Office::Common::Drawinglayer::RenderSimpleTextDirect::get())
|
|
, mbRenderDecoratedTextDirect(
|
|
officecfg::Office::Common::Drawinglayer::RenderDecoratedTextDirect::get())
|
|
, mnClipRecursionCount(0)
|
|
, mbCairoCoordinateLimitWorkaroundActive(false)
|
|
{
|
|
SystemGraphicsData aData(mpTargetOutputDevice->GetSystemGfxData());
|
|
cairo_surface_t* pTarget(static_cast<cairo_surface_t*>(aData.pSurface));
|
|
|
|
// no target, nothing to initialize
|
|
if (nullptr == pTarget)
|
|
return;
|
|
|
|
// get evtl. offsets if OutputDevice is e.g. a OUTDEV_WINDOW
|
|
// to evaluate if initial clip is needed
|
|
const tools::Long nOffsetPixelX(mpTargetOutputDevice->GetOutOffXPixel());
|
|
const tools::Long nOffsetPixelY(mpTargetOutputDevice->GetOutOffYPixel());
|
|
const tools::Long nWidthPixel(mpTargetOutputDevice->GetOutputWidthPixel());
|
|
const tools::Long nHeightPixel(mpTargetOutputDevice->GetOutputHeightPixel());
|
|
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)
|
|
{
|
|
// Make use of the possibility to add an initial clip relative
|
|
// to the 'real' pixel dimensions of the target surface. This is e.g.
|
|
// needed here due to the existence of 'virtual' target surfaces that
|
|
// internally use an offset and limited pixel size, mainly used for
|
|
// UI elements.
|
|
// let the CairoPixelProcessor2D do this, it has internal,
|
|
// system-specific possibilities to do that in an elegant and
|
|
// efficient way (using cairo_surface_create_for_rectangle).
|
|
mpOwnedSurface = cairo_surface_create_for_rectangle(pTarget, nOffsetPixelX, nOffsetPixelY,
|
|
nWidthPixel, nHeightPixel);
|
|
|
|
if (nullptr == mpOwnedSurface)
|
|
// error, invalid
|
|
return;
|
|
|
|
mpRT = cairo_create(mpOwnedSurface);
|
|
}
|
|
else
|
|
{
|
|
// create RenderTarget for full target
|
|
mpRT = cairo_create(pTarget);
|
|
}
|
|
|
|
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);
|
|
|
|
// prepare output directly to pixels
|
|
mpTargetOutputDevice->Push(vcl::PushFlags::MAPMODE);
|
|
mpTargetOutputDevice->SetMapMode();
|
|
|
|
// evaluate if CairoCoordinateLimitWorkaround is needed
|
|
evaluateCairoCoordinateLimitWorkaround();
|
|
}
|
|
|
|
CairoPixelProcessor2D::~CairoPixelProcessor2D()
|
|
{
|
|
if (nullptr != mpTargetOutputDevice) // restore MapMode
|
|
mpTargetOutputDevice->Pop();
|
|
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(),
|
|
isCairoCoordinateLimitWorkaroundActive());
|
|
cairo_rectangle(mpRT, 0, 0, 1, 1);
|
|
cairo_stroke(mpRT);
|
|
}
|
|
|
|
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.
|
|
// Decided now to use clipping always. That again is
|
|
// simple (we are in unit coordinates)
|
|
cairo_rectangle(mpRT, 0, 0, 1, 1);
|
|
cairo_clip(mpRT);
|
|
cairo_matrix_scale(&aMatrix, cairo_image_surface_get_width(pTarget),
|
|
cairo_image_surface_get_height(pTarget));
|
|
|
|
// The alternative wpuld be: 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. Keeping this code if someone might have
|
|
// to finetune this later for reference.
|
|
//
|
|
// cairo_matrix_init_scale(&aMatrix, nWidth + 1, nHeight + 1);
|
|
// cairo_matrix_translate(&aMatrix, -0.5 / (nWidth + 1), -0.5 / (nHeight + 1));
|
|
|
|
// 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);
|
|
|
|
// also set extend to CAIRO_EXTEND_PAD, else the outside of the
|
|
// bitmap is guessed as COL_BLACK and the filtering would blend
|
|
// against COL_BLACK what might give strange gray lines at borders
|
|
// of white-on-white bitmaps (used e.g. when painting controls).
|
|
// NOTE: CAIRO_EXTEND_REPEAT also works with clipping and might be
|
|
// broader supported by CVairo implementations
|
|
cairo_pattern_set_extend(sourcepattern, CAIRO_EXTEND_PAD);
|
|
|
|
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);
|
|
|
|
// 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(), isCairoCoordinateLimitWorkaroundActive());
|
|
|
|
if (isCairoCoordinateLimitWorkaroundActive())
|
|
{
|
|
// need to fallback to paint in view coordinates, unfortunately
|
|
// need to transform self (cairo will do it wrong in this coordinate
|
|
// space), so no need to try to buffer
|
|
cairo_new_path(mpRT);
|
|
basegfx::B2DPolygon aAdaptedPolygon(rPolygon);
|
|
const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0);
|
|
aAdaptedPolygon.transform(basegfx::utils::createTranslateB2DHomMatrix(fAAOffset, fAAOffset)
|
|
* getViewInformation2D().getObjectToViewTransformation());
|
|
cairo_identity_matrix(mpRT);
|
|
addB2DPolygonToPathGeometry(mpRT, aAdaptedPolygon);
|
|
cairo_stroke(mpRT);
|
|
}
|
|
else
|
|
{
|
|
// set linear transformation. use own, prepared, re-usable
|
|
// ObjectToViewTransformation and PolyPoylgon data and let
|
|
// cairo do the transformations
|
|
cairo_matrix_t aMatrix;
|
|
const basegfx::B2DHomMatrix& rObjectToView(
|
|
getViewInformation2D().getObjectToViewTransformation());
|
|
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);
|
|
|
|
// 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);
|
|
|
|
// 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());
|
|
|
|
if (isCairoCoordinateLimitWorkaroundActive())
|
|
{
|
|
// need to fallback to paint in view coordinates, unfortunately
|
|
// need to transform self (cairo will do it wrong in this coordinate
|
|
// space), so no need to try to buffer
|
|
cairo_new_path(mpRT);
|
|
basegfx::B2DPolyPolygon aAdaptedPolyPolygon(rPolyPolygon);
|
|
aAdaptedPolyPolygon.transform(getViewInformation2D().getObjectToViewTransformation());
|
|
cairo_identity_matrix(mpRT);
|
|
for (const auto& rPolygon : aAdaptedPolyPolygon)
|
|
addB2DPolygonToPathGeometry(mpRT, rPolygon);
|
|
cairo_fill(mpRT);
|
|
}
|
|
else
|
|
{
|
|
// set linear transformation. use own, prepared, re-usable
|
|
// ObjectToViewTransformation and PolyPoylgon data and let
|
|
// cairo do the transformations
|
|
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);
|
|
|
|
// 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);
|
|
|
|
if (isCairoCoordinateLimitWorkaroundActive())
|
|
{
|
|
// need to fallback to paint in view coordinates, unfortunately
|
|
// need to transform self (cairo will do it wrong in this coordinate
|
|
// space), so no need to try to buffer
|
|
cairo_new_path(mpRT);
|
|
basegfx::B2DPolyPolygon aAdaptedPolyPolygon(rMask);
|
|
aAdaptedPolyPolygon.transform(getViewInformation2D().getObjectToViewTransformation());
|
|
for (const auto& rPolygon : aAdaptedPolyPolygon)
|
|
addB2DPolygonToPathGeometry(mpRT, rPolygon);
|
|
|
|
// clip to this mask
|
|
cairo_clip(mpRT);
|
|
}
|
|
else
|
|
{
|
|
// 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);
|
|
|
|
// take over evtl. used ColorModifierStack for content
|
|
aContent.setBColorModifierStack(getBColorModifierStack());
|
|
|
|
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);
|
|
|
|
// 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());
|
|
|
|
// check stroke
|
|
const attribute::StrokeAttribute& rStrokeAttribute(
|
|
rPolygonStrokeCandidate.getStrokeAttribute());
|
|
const bool bDashUsed(!rStrokeAttribute.isDefault()
|
|
&& !rStrokeAttribute.getDotDashArray().empty()
|
|
&& 0.0 < rStrokeAttribute.getFullDotDashLen());
|
|
if (isCairoCoordinateLimitWorkaroundActive())
|
|
{
|
|
// need to fallback to paint in view coordinates, unfortunately
|
|
// need to transform self (cairo will do it wrong in this coordinate
|
|
// space), so no need to try to buffer
|
|
cairo_new_path(mpRT);
|
|
basegfx::B2DPolygon aAdaptedPolygon(rPolygon);
|
|
const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0);
|
|
aAdaptedPolygon.transform(basegfx::utils::createTranslateB2DHomMatrix(fAAOffset, fAAOffset)
|
|
* getViewInformation2D().getObjectToViewTransformation());
|
|
cairo_identity_matrix(mpRT);
|
|
addB2DPolygonToPathGeometry(mpRT, aAdaptedPolygon);
|
|
|
|
// process/set LineWidth
|
|
const double fObjectLineWidth(bHairline
|
|
? 1.0
|
|
: (getViewInformation2D().getObjectToViewTransformation()
|
|
* basegfx::B2DVector(rLineAttribute.getWidth(), 0.0))
|
|
.getLength());
|
|
cairo_set_line_width(mpRT, fObjectLineWidth);
|
|
|
|
if (bDashUsed)
|
|
{
|
|
std::vector<double> aStroke(rStrokeAttribute.getDotDashArray());
|
|
for (auto& rCandidate : aStroke)
|
|
rCandidate = (getViewInformation2D().getObjectToViewTransformation()
|
|
* basegfx::B2DVector(rCandidate, 0.0))
|
|
.getLength();
|
|
cairo_set_dash(mpRT, aStroke.data(), aStroke.size(), 0.0);
|
|
}
|
|
|
|
cairo_stroke(mpRT);
|
|
}
|
|
else
|
|
{
|
|
// 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);
|
|
|
|
// create path geometry and put mask as path
|
|
cairo_new_path(mpRT);
|
|
getOrCreatePathGeometry(mpRT, rPolygon, getViewInformation2D(),
|
|
bHairline && getViewInformation2D().getUseAntiAliasing());
|
|
|
|
// process/set LineWidth
|
|
const double fObjectLineWidth(
|
|
bHairline ? (getViewInformation2D().getInverseObjectToViewTransformation()
|
|
* basegfx::B2DVector(1.0, 0.0))
|
|
.getLength()
|
|
: rLineAttribute.getWidth());
|
|
cairo_set_line_width(mpRT, fObjectLineWidth);
|
|
|
|
if (bDashUsed)
|
|
{
|
|
const std::vector<double>& rStroke = rStrokeAttribute.getDotDashArray();
|
|
cairo_set_dash(mpRT, rStroke.data(), rStroke.size(), 0.0);
|
|
}
|
|
|
|
// 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);
|
|
|
|
// work in view coordinates
|
|
const double fAAOffset(getViewInformation2D().getUseAntiAliasing() ? 0.5 : 0.0);
|
|
basegfx::B2DRange aRange(rLineRectanglePrimitive2D.getB2DRange());
|
|
aRange.transform(getViewInformation2D().getObjectToViewTransformation());
|
|
cairo_identity_matrix(mpRT);
|
|
|
|
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);
|
|
|
|
cairo_rectangle(mpRT, aRange.getMinX() + fAAOffset, aRange.getMinY() + fAAOffset,
|
|
aRange.getWidth(), aRange.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);
|
|
|
|
// work in view coordinates
|
|
basegfx::B2DRange aRange(rFilledRectanglePrimitive2D.getB2DRange());
|
|
aRange.transform(getViewInformation2D().getObjectToViewTransformation());
|
|
cairo_identity_matrix(mpRT);
|
|
|
|
const basegfx::BColor aFillColor(
|
|
maBColorModifierStack.getModifiedColor(rFilledRectanglePrimitive2D.getBColor()));
|
|
cairo_set_source_rgb(mpRT, aFillColor.getRed(), aFillColor.getGreen(), aFillColor.getBlue());
|
|
|
|
cairo_rectangle(mpRT, aRange.getMinX(), aRange.getMinY(), aRange.getWidth(),
|
|
aRange.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_identity_matrix(mpRT);
|
|
|
|
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(),
|
|
isCairoCoordinateLimitWorkaroundActive());
|
|
// 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::processControlPrimitive2D(
|
|
const primitive2d::ControlPrimitive2D& rControlPrimitive)
|
|
{
|
|
// find out if the control is already visualized as a VCL-ChildWindow
|
|
bool bControlIsVisibleAsChildWindow(rControlPrimitive.isVisibleAsChildWindow());
|
|
|
|
// tdf#131281 FormControl rendering for Tiled Rendering
|
|
if (bControlIsVisibleAsChildWindow && comphelper::LibreOfficeKit::isActive())
|
|
{
|
|
// Do force paint when we are in Tiled Renderer and FormControl is 'visible'
|
|
bControlIsVisibleAsChildWindow = false;
|
|
}
|
|
|
|
if (bControlIsVisibleAsChildWindow)
|
|
{
|
|
// f the control is already visualized as a VCL-ChildWindow it
|
|
// does not need to be painted at all
|
|
return;
|
|
}
|
|
|
|
bool bDone(false);
|
|
|
|
try
|
|
{
|
|
if (nullptr != mpTargetOutputDevice)
|
|
{
|
|
const uno::Reference<awt::XGraphics> xTargetGraphics(
|
|
mpTargetOutputDevice->CreateUnoGraphics());
|
|
|
|
if (xTargetGraphics.is())
|
|
{
|
|
// Needs to be drawn. Link new graphics and view
|
|
const uno::Reference<awt::XControl>& rXControl(rControlPrimitive.getXControl());
|
|
uno::Reference<awt::XView> xControlView(rXControl, uno::UNO_QUERY_THROW);
|
|
const uno::Reference<awt::XGraphics> xOriginalGraphics(xControlView->getGraphics());
|
|
xControlView->setGraphics(xTargetGraphics);
|
|
|
|
// get position
|
|
const basegfx::B2DHomMatrix aObjectToPixel(
|
|
getViewInformation2D().getObjectToViewTransformation()
|
|
* rControlPrimitive.getTransform());
|
|
const basegfx::B2DPoint aTopLeftPixel(aObjectToPixel * basegfx::B2DPoint(0.0, 0.0));
|
|
|
|
xControlView->draw(basegfx::fround(aTopLeftPixel.getX()),
|
|
basegfx::fround(aTopLeftPixel.getY()));
|
|
|
|
// restore original graphics
|
|
xControlView->setGraphics(xOriginalGraphics);
|
|
bDone = true;
|
|
}
|
|
}
|
|
}
|
|
catch (const uno::Exception&)
|
|
{
|
|
// #i116763# removing since there is a good alternative when the xControlView
|
|
// is not found and it is allowed to happen
|
|
// DBG_UNHANDLED_EXCEPTION();
|
|
}
|
|
|
|
if (!bDone)
|
|
{
|
|
// process recursively and use the decomposition as Bitmap
|
|
process(rControlPrimitive);
|
|
}
|
|
}
|
|
|
|
void CairoPixelProcessor2D::evaluateCairoCoordinateLimitWorkaround()
|
|
{
|
|
static bool bAlreadyCheckedIfNeeded(false);
|
|
static bool bIsNeeded(false);
|
|
|
|
if (!bAlreadyCheckedIfNeeded)
|
|
{
|
|
// check once for office runtime: is workarund needed?
|
|
bAlreadyCheckedIfNeeded = true;
|
|
bIsNeeded = checkCoordinateLimitWorkaroundNeededForUsedCairo();
|
|
}
|
|
|
|
if (!bIsNeeded)
|
|
{
|
|
// we have a working cairo, so workarund is not needed
|
|
// and mbCairoCoordinateLimitWorkaroundActive can stay false
|
|
return;
|
|
}
|
|
|
|
// get discrete size (pixels)
|
|
basegfx::B2DRange aLogicViewRange(getDiscreteViewRange(mpRT));
|
|
|
|
// transform to world coordinates -> logic view range
|
|
basegfx::B2DHomMatrix aInvViewTrans(getViewInformation2D().getViewTransformation());
|
|
aInvViewTrans.invert();
|
|
aLogicViewRange.transform(aInvViewTrans);
|
|
|
|
// create 1<<23 CairoCoordinate limit from 24.8 internal format
|
|
// and a range fitting to it (just once, this is static)
|
|
constexpr double fNumCairoMax(1 << 23);
|
|
static const basegfx::B2DRange aNumericalCairoLimit(-fNumCairoMax, -fNumCairoMax,
|
|
fNumCairoMax - 1.0, fNumCairoMax - 1.0);
|
|
|
|
if (!aLogicViewRange.isEmpty() && !aNumericalCairoLimit.isInside(aLogicViewRange))
|
|
{
|
|
// aLogicViewRange is not completely inside region covered by
|
|
// 24.8 cairo format, thus workaround is needed, set flag
|
|
mbCairoCoordinateLimitWorkaroundActive = true;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
case PRIMITIVE2D_ID_CONTROLPRIMITIVE2D:
|
|
{
|
|
processControlPrimitive2D(
|
|
static_cast<const primitive2d::ControlPrimitive2D&>(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: */
|