ef2c71d3a7
Setting the stroke width and cap has a noticeable performance penalty
when running on GTK3. Since tdf#148569 only appears to occur on macOS
Retina displays, revert commit a4488013ee
for non-macOS platforms.
Change-Id: I2435434c6f66b84483046ea558bac3d47b8803ce
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/169475
Tested-by: Jenkins
Reviewed-by: Patrick Luby <guibomacdev@gmail.com>
2286 lines
95 KiB
C++
2286 lines
95 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
|
/*
|
|
* This file is part of the LibreOffice project.
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*
|
|
* This file incorporates work covered by the following license notice:
|
|
*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed
|
|
* with this work for additional information regarding copyright
|
|
* ownership. The ASF licenses this file to you under the Apache
|
|
* License, Version 2.0 (the "License"); you may not use this file
|
|
* except in compliance with the License. You may obtain a copy of
|
|
* the License at http://www.apache.org/licenses/LICENSE-2.0 .
|
|
*/
|
|
|
|
#include <skia/gdiimpl.hxx>
|
|
|
|
#include <salgdi.hxx>
|
|
#include <skia/salbmp.hxx>
|
|
#include <vcl/idle.hxx>
|
|
#include <vcl/svapp.hxx>
|
|
#include <vcl/lazydelete.hxx>
|
|
#include <vcl/gradient.hxx>
|
|
#include <vcl/skia/SkiaHelper.hxx>
|
|
#include <skia/utils.hxx>
|
|
#include <skia/zone.hxx>
|
|
#include <tools/debug.hxx>
|
|
|
|
#include <SkBitmap.h>
|
|
#include <SkCanvas.h>
|
|
#include <SkGradientShader.h>
|
|
#include <SkPath.h>
|
|
#include <SkRegion.h>
|
|
#include <SkPathEffect.h>
|
|
#include <SkDashPathEffect.h>
|
|
#include <GrBackendSurface.h>
|
|
#include <SkTextBlob.h>
|
|
#include <SkRSXform.h>
|
|
|
|
#include <numeric>
|
|
#include <sstream>
|
|
|
|
#include <basegfx/polygon/b2dpolygontools.hxx>
|
|
#include <basegfx/polygon/b2dpolypolygontools.hxx>
|
|
#include <basegfx/polygon/b2dpolypolygoncutter.hxx>
|
|
#include <o3tl/sorted_vector.hxx>
|
|
#include <rtl/math.hxx>
|
|
|
|
using namespace SkiaHelper;
|
|
|
|
namespace
|
|
{
|
|
// Create Skia Path from B2DPolygon
|
|
// Note that polygons generally have the complication that when used
|
|
// for area (fill) operations they usually miss the right-most and
|
|
// bottom-most line of pixels of the bounding rectangle (see
|
|
// https://lists.freedesktop.org/archives/libreoffice/2019-November/083709.html).
|
|
// So be careful with rectangle->polygon conversions (generally avoid them).
|
|
void addPolygonToPath(const basegfx::B2DPolygon& rPolygon, SkPath& rPath, sal_uInt32 nFirstIndex,
|
|
sal_uInt32 nLastIndex, const sal_uInt32 nPointCount, const bool bClosePath,
|
|
const bool bHasCurves, bool* hasOnlyOrthogonal = nullptr)
|
|
{
|
|
assert(nFirstIndex < nPointCount || (nFirstIndex == 0 && nPointCount == 0));
|
|
assert(nLastIndex <= nPointCount);
|
|
|
|
if (nPointCount <= 1)
|
|
return;
|
|
|
|
bool bFirst = true;
|
|
sal_uInt32 nPreviousIndex = nFirstIndex == 0 ? nPointCount - 1 : nFirstIndex - 1;
|
|
basegfx::B2DPoint aPreviousPoint = rPolygon.getB2DPoint(nPreviousIndex);
|
|
|
|
for (sal_uInt32 nIndex = nFirstIndex; nIndex <= nLastIndex; nIndex++)
|
|
{
|
|
if (nIndex == nPointCount && !bClosePath)
|
|
continue;
|
|
|
|
// Make sure we loop the last point to first point
|
|
sal_uInt32 nCurrentIndex = nIndex % nPointCount;
|
|
basegfx::B2DPoint aCurrentPoint = rPolygon.getB2DPoint(nCurrentIndex);
|
|
|
|
if (bFirst)
|
|
{
|
|
rPath.moveTo(aCurrentPoint.getX(), aCurrentPoint.getY());
|
|
bFirst = false;
|
|
}
|
|
else if (!bHasCurves)
|
|
{
|
|
rPath.lineTo(aCurrentPoint.getX(), aCurrentPoint.getY());
|
|
// If asked for, check whether the polygon has a line that is not
|
|
// strictly horizontal or vertical.
|
|
if (hasOnlyOrthogonal != nullptr && aCurrentPoint.getX() != aPreviousPoint.getX()
|
|
&& aCurrentPoint.getY() != aPreviousPoint.getY())
|
|
*hasOnlyOrthogonal = false;
|
|
}
|
|
else
|
|
{
|
|
basegfx::B2DPoint aPreviousControlPoint = rPolygon.getNextControlPoint(nPreviousIndex);
|
|
basegfx::B2DPoint aCurrentControlPoint = rPolygon.getPrevControlPoint(nCurrentIndex);
|
|
|
|
if (aPreviousControlPoint.equal(aPreviousPoint)
|
|
&& aCurrentControlPoint.equal(aCurrentPoint))
|
|
{
|
|
rPath.lineTo(aCurrentPoint.getX(), aCurrentPoint.getY()); // a straight line
|
|
if (hasOnlyOrthogonal != nullptr && aCurrentPoint.getX() != aPreviousPoint.getX()
|
|
&& aCurrentPoint.getY() != aPreviousPoint.getY())
|
|
*hasOnlyOrthogonal = false;
|
|
}
|
|
else
|
|
{
|
|
if (aPreviousControlPoint.equal(aPreviousPoint))
|
|
{
|
|
aPreviousControlPoint
|
|
= aPreviousPoint + ((aPreviousControlPoint - aCurrentPoint) * 0.0005);
|
|
}
|
|
if (aCurrentControlPoint.equal(aCurrentPoint))
|
|
{
|
|
aCurrentControlPoint
|
|
= aCurrentPoint + ((aCurrentControlPoint - aPreviousPoint) * 0.0005);
|
|
}
|
|
rPath.cubicTo(aPreviousControlPoint.getX(), aPreviousControlPoint.getY(),
|
|
aCurrentControlPoint.getX(), aCurrentControlPoint.getY(),
|
|
aCurrentPoint.getX(), aCurrentPoint.getY());
|
|
if (hasOnlyOrthogonal != nullptr)
|
|
*hasOnlyOrthogonal = false;
|
|
}
|
|
}
|
|
aPreviousPoint = aCurrentPoint;
|
|
nPreviousIndex = nCurrentIndex;
|
|
}
|
|
if (bClosePath && nFirstIndex == 0 && nLastIndex == nPointCount)
|
|
{
|
|
rPath.close();
|
|
}
|
|
}
|
|
|
|
void addPolygonToPath(const basegfx::B2DPolygon& rPolygon, SkPath& rPath,
|
|
bool* hasOnlyOrthogonal = nullptr)
|
|
{
|
|
addPolygonToPath(rPolygon, rPath, 0, rPolygon.count(), rPolygon.count(), rPolygon.isClosed(),
|
|
rPolygon.areControlPointsUsed(), hasOnlyOrthogonal);
|
|
}
|
|
|
|
void addPolyPolygonToPath(const basegfx::B2DPolyPolygon& rPolyPolygon, SkPath& rPath,
|
|
bool* hasOnlyOrthogonal = nullptr)
|
|
{
|
|
const sal_uInt32 nPolygonCount(rPolyPolygon.count());
|
|
|
|
if (nPolygonCount == 0)
|
|
return;
|
|
|
|
sal_uInt32 nPointCount = 0;
|
|
for (const auto& rPolygon : rPolyPolygon)
|
|
nPointCount += rPolygon.count() * 3; // because cubicTo is 3 elements
|
|
rPath.incReserve(nPointCount);
|
|
|
|
for (const auto& rPolygon : rPolyPolygon)
|
|
{
|
|
addPolygonToPath(rPolygon, rPath, hasOnlyOrthogonal);
|
|
}
|
|
}
|
|
|
|
// Check if the given polygon contains a straight line. If not, it consists
|
|
// solely of curves.
|
|
bool polygonContainsLine(const basegfx::B2DPolyPolygon& rPolyPolygon)
|
|
{
|
|
if (!rPolyPolygon.areControlPointsUsed())
|
|
return true; // no curves at all
|
|
for (const auto& rPolygon : rPolyPolygon)
|
|
{
|
|
const sal_uInt32 nPointCount(rPolygon.count());
|
|
bool bFirst = true;
|
|
|
|
const bool bClosePath(rPolygon.isClosed());
|
|
|
|
sal_uInt32 nCurrentIndex = 0;
|
|
sal_uInt32 nPreviousIndex = nPointCount - 1;
|
|
|
|
basegfx::B2DPoint aCurrentPoint;
|
|
basegfx::B2DPoint aPreviousPoint;
|
|
|
|
for (sal_uInt32 nIndex = 0; nIndex <= nPointCount; nIndex++)
|
|
{
|
|
if (nIndex == nPointCount && !bClosePath)
|
|
continue;
|
|
|
|
// Make sure we loop the last point to first point
|
|
nCurrentIndex = nIndex % nPointCount;
|
|
if (bFirst)
|
|
bFirst = false;
|
|
else
|
|
{
|
|
basegfx::B2DPoint aPreviousControlPoint
|
|
= rPolygon.getNextControlPoint(nPreviousIndex);
|
|
basegfx::B2DPoint aCurrentControlPoint
|
|
= rPolygon.getPrevControlPoint(nCurrentIndex);
|
|
|
|
if (aPreviousControlPoint.equal(aPreviousPoint)
|
|
&& aCurrentControlPoint.equal(aCurrentPoint))
|
|
{
|
|
return true; // found a straight line
|
|
}
|
|
}
|
|
aPreviousPoint = aCurrentPoint;
|
|
nPreviousIndex = nCurrentIndex;
|
|
}
|
|
}
|
|
return false; // no straight line found
|
|
}
|
|
|
|
// returns true if the source or destination rectangles are invalid
|
|
bool checkInvalidSourceOrDestination(SalTwoRect const& rPosAry)
|
|
{
|
|
return rPosAry.mnSrcWidth <= 0 || rPosAry.mnSrcHeight <= 0 || rPosAry.mnDestWidth <= 0
|
|
|| rPosAry.mnDestHeight <= 0;
|
|
}
|
|
|
|
std::string dumpOptionalColor(const std::optional<Color>& c)
|
|
{
|
|
std::ostringstream oss;
|
|
if (c)
|
|
oss << *c;
|
|
else
|
|
oss << "no color";
|
|
|
|
return std::move(oss).str(); // optimized in C++20
|
|
}
|
|
|
|
} // end anonymous namespace
|
|
|
|
// Class that triggers flushing the backing buffer when idle.
|
|
class SkiaFlushIdle : public Idle
|
|
{
|
|
SkiaSalGraphicsImpl* mpGraphics;
|
|
#ifndef NDEBUG
|
|
char* debugname;
|
|
#endif
|
|
|
|
public:
|
|
explicit SkiaFlushIdle(SkiaSalGraphicsImpl* pGraphics)
|
|
: Idle(get_debug_name(pGraphics))
|
|
, mpGraphics(pGraphics)
|
|
{
|
|
// We don't want to be swapping before we've painted.
|
|
SetPriority(TaskPriority::POST_PAINT);
|
|
}
|
|
#ifndef NDEBUG
|
|
virtual ~SkiaFlushIdle() { free(debugname); }
|
|
#endif
|
|
const char* get_debug_name(SkiaSalGraphicsImpl* pGraphics)
|
|
{
|
|
#ifndef NDEBUG
|
|
// Idle keeps just a pointer, so we need to store the string
|
|
debugname = strdup(
|
|
OString("skia idle 0x" + OString::number(reinterpret_cast<sal_uIntPtr>(pGraphics), 16))
|
|
.getStr());
|
|
return debugname;
|
|
#else
|
|
(void)pGraphics;
|
|
return "skia idle";
|
|
#endif
|
|
}
|
|
|
|
virtual void Invoke() override
|
|
{
|
|
mpGraphics->performFlush();
|
|
Stop();
|
|
// tdf#157312 Don't change priority
|
|
// Instances of this class are constructed with
|
|
// TaskPriority::POST_PAINT, but then it was set to
|
|
// TaskPriority::HIGHEST when reused. Flushing
|
|
// seems to be expensive (at least with Skia/Metal) so keep the
|
|
// existing priority when reused.
|
|
SetPriority(TaskPriority::POST_PAINT);
|
|
}
|
|
};
|
|
|
|
SkiaSalGraphicsImpl::SkiaSalGraphicsImpl(SalGraphics& rParent, SalGeometryProvider* pProvider)
|
|
: mParent(rParent)
|
|
, mProvider(pProvider)
|
|
, mIsGPU(false)
|
|
, moLineColor(std::nullopt)
|
|
, moFillColor(std::nullopt)
|
|
, mXorMode(XorMode::None)
|
|
, mFlush(new SkiaFlushIdle(this))
|
|
, mScaling(1)
|
|
, mInWindowBackingPropertiesChanged(false)
|
|
{
|
|
}
|
|
|
|
SkiaSalGraphicsImpl::~SkiaSalGraphicsImpl()
|
|
{
|
|
assert(!mSurface);
|
|
assert(!mWindowContext);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::Init() {}
|
|
|
|
void SkiaSalGraphicsImpl::createSurface()
|
|
{
|
|
SkiaZone zone;
|
|
if (isOffscreen())
|
|
createOffscreenSurface();
|
|
else
|
|
createWindowSurface();
|
|
mClipRegion = vcl::Region(tools::Rectangle(0, 0, GetWidth(), GetHeight()));
|
|
mDirtyRect = SkIRect::MakeWH(GetWidth(), GetHeight());
|
|
setCanvasScalingAndClipping();
|
|
|
|
// We don't want to be swapping before we've painted.
|
|
mFlush->Stop();
|
|
mFlush->SetPriority(TaskPriority::POST_PAINT);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::createWindowSurface(bool forceRaster)
|
|
{
|
|
SkiaZone zone;
|
|
assert(!isOffscreen());
|
|
assert(!mSurface);
|
|
createWindowSurfaceInternal(forceRaster);
|
|
if (!mSurface)
|
|
{
|
|
switch (forceRaster ? RenderRaster : renderMethodToUse())
|
|
{
|
|
case RenderVulkan:
|
|
SAL_WARN("vcl.skia",
|
|
"cannot create Vulkan GPU window surface, falling back to Raster");
|
|
destroySurface(); // destroys also WindowContext
|
|
return createWindowSurface(true); // try again
|
|
case RenderMetal:
|
|
SAL_WARN("vcl.skia",
|
|
"cannot create Metal GPU window surface, falling back to Raster");
|
|
destroySurface(); // destroys also WindowContext
|
|
return createWindowSurface(true); // try again
|
|
case RenderRaster:
|
|
abort(); // This should not really happen, do not even try to cope with it.
|
|
}
|
|
}
|
|
mIsGPU = mSurface->getCanvas()->recordingContext() != nullptr;
|
|
#ifdef DBG_UTIL
|
|
prefillSurface(mSurface);
|
|
#endif
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::isOffscreen() const
|
|
{
|
|
if (mProvider == nullptr || mProvider->IsOffScreen())
|
|
return true;
|
|
// HACK: Sometimes (tdf#131939, tdf#138022, tdf#140288) VCL passes us a zero-sized window,
|
|
// and zero size is invalid for Skia, so force offscreen surface, where we handle this.
|
|
if (GetWidth() <= 0 || GetHeight() <= 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::createOffscreenSurface()
|
|
{
|
|
SkiaZone zone;
|
|
assert(isOffscreen());
|
|
assert(!mSurface);
|
|
// HACK: See isOffscreen().
|
|
int width = std::max(1, GetWidth());
|
|
int height = std::max(1, GetHeight());
|
|
// We need to use window scaling even for offscreen surfaces, because the common usage is rendering something
|
|
// into an offscreen surface and then copy it to a window, so without scaling here the result would be originally
|
|
// drawn without scaling and only upscaled when drawing to a window.
|
|
mScaling = getWindowScaling();
|
|
mSurface = createSkSurface(width * mScaling, height * mScaling);
|
|
assert(mSurface);
|
|
mIsGPU = mSurface->getCanvas()->recordingContext() != nullptr;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::destroySurface()
|
|
{
|
|
SkiaZone zone;
|
|
if (mSurface)
|
|
{
|
|
// check setClipRegion() invariant
|
|
assert(mSurface->getCanvas()->getSaveCount() == 3);
|
|
// if this fails, something forgot to use SkAutoCanvasRestore
|
|
assert(mSurface->getCanvas()->getTotalMatrix() == SkMatrix::Scale(mScaling, mScaling));
|
|
}
|
|
mSurface.reset();
|
|
mWindowContext.reset();
|
|
mIsGPU = false;
|
|
mScaling = 1;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::performFlush()
|
|
{
|
|
SkiaZone zone;
|
|
flushDrawing();
|
|
if (mSurface)
|
|
{
|
|
// Related: tdf#152703 Eliminate flickering during live resizing of a window
|
|
// When in live resize, the SkiaSalGraphicsImpl class does not detect that
|
|
// the window size has changed until after the flush has been called so
|
|
// call checkSurface() to recreate the SkSurface if needed before flushing.
|
|
checkSurface();
|
|
if (mDirtyRect.intersect(SkIRect::MakeWH(GetWidth(), GetHeight())))
|
|
flushSurfaceToWindowContext();
|
|
mDirtyRect.setEmpty();
|
|
}
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::flushSurfaceToWindowContext()
|
|
{
|
|
sk_sp<SkSurface> screenSurface = mWindowContext->getBackbufferSurface();
|
|
if (screenSurface != mSurface)
|
|
{
|
|
// GPU-based window contexts require calling getBackbufferSurface()
|
|
// for every swapBuffers(), for this reason mSurface is an offscreen surface
|
|
// where we keep the contents (LO does not do full redraws).
|
|
// So here blit the surface to the window context surface and then swap it.
|
|
|
|
// Raster should always draw directly to backbuffer to save copying
|
|
// except for small sizes - see renderMethodToUseForSize
|
|
assert(isGPU() || (mSurface->width() <= 32 && mSurface->height() <= 32));
|
|
SkPaint paint;
|
|
paint.setBlendMode(SkBlendMode::kSrc); // copy as is
|
|
// We ignore mDirtyRect here, and mSurface already is in screenSurface coordinates,
|
|
// so no transformation needed.
|
|
screenSurface->getCanvas()->drawImage(makeCheckedImageSnapshot(mSurface), 0, 0,
|
|
SkSamplingOptions(), &paint);
|
|
screenSurface->flushAndSubmit(); // Otherwise the window is not drawn sometimes.
|
|
mWindowContext->swapBuffers(nullptr); // Must swap the entire surface.
|
|
}
|
|
else
|
|
{
|
|
// For raster mode use directly the backbuffer surface, it's just a bitmap
|
|
// surface anyway, and for those there's no real requirement to call
|
|
// getBackbufferSurface() repeatedly. Using our own surface would duplicate
|
|
// memory and cost time copying pixels around.
|
|
assert(!isGPU());
|
|
SkIRect dirtyRect = mDirtyRect;
|
|
if (mScaling != 1) // Adjust to mSurface coordinates if needed.
|
|
dirtyRect = scaleRect(dirtyRect, mScaling);
|
|
mWindowContext->swapBuffers(&dirtyRect);
|
|
}
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::DeInit() { destroySurface(); }
|
|
|
|
void SkiaSalGraphicsImpl::preDraw()
|
|
{
|
|
DBG_TESTSOLARMUTEX();
|
|
SkiaZone::enter(); // matched in postDraw()
|
|
checkSurface();
|
|
checkPendingDrawing();
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::postDraw()
|
|
{
|
|
scheduleFlush();
|
|
// Skia (at least when using Vulkan) queues drawing commands and executes them only later.
|
|
// But tdf#136369 leads to creating and queueing many tiny bitmaps, which makes
|
|
// Skia slow, and may make it even run out of memory. So force a flush if such
|
|
// a problematic operation has been performed too many times without a flush.
|
|
// Note that the counter is a static variable, as all drawing shares the same Skia drawing
|
|
// context (and so the flush here will also flush all drawing).
|
|
static int maxOperationsToFlush = 1000;
|
|
if (pendingOperationsToFlush > maxOperationsToFlush)
|
|
{
|
|
mSurface->flushAndSubmit();
|
|
pendingOperationsToFlush = 0;
|
|
}
|
|
SkiaZone::leave(); // matched in preDraw()
|
|
// If there's a problem with the GPU context, abort.
|
|
if (GrDirectContext* context = GrAsDirectContext(mSurface->getCanvas()->recordingContext()))
|
|
{
|
|
// We don't know the exact status of the surface (and what has or has not been drawn to it).
|
|
// But let's pretend it was drawn OK, and reduce the flush limit, to try to avoid possible
|
|
// small HW memory limitation
|
|
if (context->oomed())
|
|
{
|
|
if (maxOperationsToFlush > 10)
|
|
{
|
|
maxOperationsToFlush /= 2;
|
|
}
|
|
else
|
|
{
|
|
SAL_WARN("vcl.skia", "GPU context has run out of memory, aborting.");
|
|
abort();
|
|
}
|
|
}
|
|
// Unrecoverable problem.
|
|
if (context->abandoned())
|
|
{
|
|
SAL_WARN("vcl.skia", "GPU context has been abandoned, aborting.");
|
|
abort();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::scheduleFlush()
|
|
{
|
|
if (!isOffscreen())
|
|
{
|
|
if (!Application::IsInExecute())
|
|
performFlush(); // otherwise nothing would trigger idle rendering
|
|
else if (!mFlush->IsActive())
|
|
mFlush->Start();
|
|
}
|
|
}
|
|
|
|
// VCL can sometimes resize us without telling us, update the surface if needed.
|
|
// Also create the surface on demand if it has not been created yet (it is a waste
|
|
// to create it in Init() if it gets recreated later anyway).
|
|
void SkiaSalGraphicsImpl::checkSurface()
|
|
{
|
|
if (!mSurface)
|
|
{
|
|
createSurface();
|
|
SAL_INFO("vcl.skia.trace",
|
|
"create(" << this << "): " << Size(mSurface->width(), mSurface->height()));
|
|
}
|
|
else if (mInWindowBackingPropertiesChanged || GetWidth() * mScaling != mSurface->width()
|
|
|| GetHeight() * mScaling != mSurface->height())
|
|
{
|
|
if (!avoidRecreateByResize())
|
|
{
|
|
Size oldSize(mSurface->width(), mSurface->height());
|
|
// Recreating a surface means that the old SkSurface contents will be lost.
|
|
// But if a window has been resized the windowing system may send repaint events
|
|
// only for changed parts and VCL would not repaint the whole area, assuming
|
|
// that some parts have not changed (this is what seems to cause tdf#131952).
|
|
// So carry over the old contents for windows, even though generally everything
|
|
// will be usually repainted anyway.
|
|
sk_sp<SkImage> snapshot;
|
|
if (!isOffscreen())
|
|
{
|
|
flushDrawing();
|
|
snapshot = makeCheckedImageSnapshot(mSurface);
|
|
}
|
|
|
|
destroySurface();
|
|
createSurface();
|
|
|
|
if (snapshot)
|
|
{
|
|
SkPaint paint;
|
|
paint.setBlendMode(SkBlendMode::kSrc); // copy as is
|
|
// Scaling by current mScaling is active, undo that. We assume that the scaling
|
|
// does not change.
|
|
resetCanvasScalingAndClipping();
|
|
mSurface->getCanvas()->drawImage(snapshot, 0, 0, SkSamplingOptions(), &paint);
|
|
setCanvasScalingAndClipping();
|
|
}
|
|
SAL_INFO("vcl.skia.trace", "recreate(" << this << "): old " << oldSize << " new "
|
|
<< Size(mSurface->width(), mSurface->height())
|
|
<< " requested "
|
|
<< Size(GetWidth(), GetHeight()));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::avoidRecreateByResize() const
|
|
{
|
|
// Keep the old surface if VCL sends us a broken size (see isOffscreen()).
|
|
if (GetWidth() == 0 || GetHeight() == 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::flushDrawing()
|
|
{
|
|
if (!mSurface)
|
|
return;
|
|
checkPendingDrawing();
|
|
++pendingOperationsToFlush;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::setCanvasScalingAndClipping()
|
|
{
|
|
SkCanvas* canvas = mSurface->getCanvas();
|
|
assert(canvas->getSaveCount() == 1);
|
|
// If HiDPI scaling is active, simply set a scaling matrix for the canvas. This means
|
|
// that all painting can use VCL coordinates and they'll be automatically translated to mSurface
|
|
// scaled coordinates. If that is not wanted, the scale() state needs to be temporarily unset.
|
|
// State such as mDirtyRect is not scaled, the scaling matrix applies to clipping too,
|
|
// and the rest needs to be handled explicitly.
|
|
// When reading mSurface contents there's no automatic scaling and it needs to be handled explicitly.
|
|
canvas->save(); // keep the original state without any scaling
|
|
canvas->scale(mScaling, mScaling);
|
|
|
|
// SkCanvas::clipRegion() can only further reduce the clip region,
|
|
// but we need to set the given region, which may extend it.
|
|
// So handle that by always having the full clip region saved on the stack
|
|
// and always go back to that. SkCanvas::restore() only affects the clip
|
|
// and the matrix.
|
|
canvas->save(); // keep scaled state without clipping
|
|
setCanvasClipRegion(canvas, mClipRegion);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::resetCanvasScalingAndClipping()
|
|
{
|
|
SkCanvas* canvas = mSurface->getCanvas();
|
|
assert(canvas->getSaveCount() == 3);
|
|
canvas->restore(); // undo clipping
|
|
canvas->restore(); // undo scaling
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::setClipRegion(const vcl::Region& region)
|
|
{
|
|
if (mClipRegion == region)
|
|
return;
|
|
SkiaZone zone;
|
|
checkPendingDrawing();
|
|
checkSurface();
|
|
mClipRegion = region;
|
|
SAL_INFO("vcl.skia.trace", "setclipregion(" << this << "): " << region);
|
|
SkCanvas* canvas = mSurface->getCanvas();
|
|
assert(canvas->getSaveCount() == 3);
|
|
canvas->restore(); // undo previous clip state, see setCanvasScalingAndClipping()
|
|
canvas->save();
|
|
setCanvasClipRegion(canvas, region);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::setCanvasClipRegion(SkCanvas* canvas, const vcl::Region& region)
|
|
{
|
|
SkiaZone zone;
|
|
SkPath path;
|
|
// Always use region rectangles, regardless of what the region uses internally.
|
|
// That's what other VCL backends do, and trying to use addPolyPolygonToPath()
|
|
// in case a polygon is used leads to off-by-one errors such as tdf#133208.
|
|
RectangleVector rectangles;
|
|
region.GetRegionRectangles(rectangles);
|
|
path.incReserve(rectangles.size() + 1);
|
|
for (const tools::Rectangle& rectangle : rectangles)
|
|
path.addRect(SkRect::MakeXYWH(rectangle.getX(), rectangle.getY(), rectangle.GetWidth(),
|
|
rectangle.GetHeight()));
|
|
path.setFillType(SkPathFillType::kEvenOdd);
|
|
canvas->clipPath(path);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::ResetClipRegion()
|
|
{
|
|
setClipRegion(vcl::Region(tools::Rectangle(0, 0, GetWidth(), GetHeight())));
|
|
}
|
|
|
|
const vcl::Region& SkiaSalGraphicsImpl::getClipRegion() const { return mClipRegion; }
|
|
|
|
sal_uInt16 SkiaSalGraphicsImpl::GetBitCount() const { return 32; }
|
|
|
|
tools::Long SkiaSalGraphicsImpl::GetGraphicsWidth() const { return GetWidth(); }
|
|
|
|
void SkiaSalGraphicsImpl::SetLineColor()
|
|
{
|
|
checkPendingDrawing();
|
|
moLineColor = std::nullopt;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::SetLineColor(Color nColor)
|
|
{
|
|
checkPendingDrawing();
|
|
moLineColor = nColor;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::SetFillColor()
|
|
{
|
|
checkPendingDrawing();
|
|
moFillColor = std::nullopt;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::SetFillColor(Color nColor)
|
|
{
|
|
checkPendingDrawing();
|
|
moFillColor = nColor;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::SetXORMode(bool set, bool invert)
|
|
{
|
|
XorMode newMode = set ? (invert ? XorMode::Invert : XorMode::Xor) : XorMode::None;
|
|
if (newMode == mXorMode)
|
|
return;
|
|
checkPendingDrawing();
|
|
SAL_INFO("vcl.skia.trace", "setxormode(" << this << "): " << set << "/" << invert);
|
|
mXorMode = newMode;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::SetROPLineColor(SalROPColor nROPColor)
|
|
{
|
|
checkPendingDrawing();
|
|
switch (nROPColor)
|
|
{
|
|
case SalROPColor::N0:
|
|
moLineColor = Color(0, 0, 0);
|
|
break;
|
|
case SalROPColor::N1:
|
|
moLineColor = Color(0xff, 0xff, 0xff);
|
|
break;
|
|
case SalROPColor::Invert:
|
|
moLineColor = Color(0xff, 0xff, 0xff);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::SetROPFillColor(SalROPColor nROPColor)
|
|
{
|
|
checkPendingDrawing();
|
|
switch (nROPColor)
|
|
{
|
|
case SalROPColor::N0:
|
|
moFillColor = Color(0, 0, 0);
|
|
break;
|
|
case SalROPColor::N1:
|
|
moFillColor = Color(0xff, 0xff, 0xff);
|
|
break;
|
|
case SalROPColor::Invert:
|
|
moFillColor = Color(0xff, 0xff, 0xff);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY)
|
|
{
|
|
drawPixel(nX, nY, *moLineColor);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY, Color nColor)
|
|
{
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "drawpixel(" << this << "): " << Point(nX, nY) << ":" << nColor);
|
|
addUpdateRegion(SkRect::MakeXYWH(nX, nY, 1, 1));
|
|
SkPaint paint = makePixelPaint(nColor);
|
|
// Apparently drawPixel() is actually expected to set the pixel and not draw it.
|
|
paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
|
|
|
|
#ifdef MACOSX
|
|
// tdf#148569 set extra drawing constraints when scaling
|
|
// Previously, setting stroke width and cap was only done when running
|
|
// unit tests. But the same drawing constraints are necessary when running
|
|
// with a Retina display on macOS.
|
|
if (mScaling != 1)
|
|
#else
|
|
// Related tdf#148569: do not apply macOS fix to non-macOS platforms
|
|
// Setting the stroke width and cap has a noticeable performance penalty
|
|
// when running on GTK3. Since tdf#148569 only appears to occur on macOS
|
|
// Retina displays, revert commit a4488013ee6c87a97501b620dbbf56622fb70246
|
|
// for non-macOS platforms.
|
|
if (mScaling != 1 && isUnitTestRunning())
|
|
#endif
|
|
{
|
|
// On HiDPI displays, draw a square on the entire non-hidpi "pixel" when running unittests,
|
|
// since tests often require precise pixel drawing.
|
|
paint.setStrokeWidth(1); // this will be scaled by mScaling
|
|
paint.setStrokeCap(SkPaint::kSquare_Cap);
|
|
}
|
|
getDrawCanvas()->drawPoint(toSkX(nX), toSkY(nY), paint);
|
|
postDraw();
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2,
|
|
tools::Long nY2)
|
|
{
|
|
if (!moLineColor)
|
|
return;
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "drawline(" << this << "): " << Point(nX1, nY1) << "->"
|
|
<< Point(nX2, nY2) << ":" << *moLineColor);
|
|
addUpdateRegion(SkRect::MakeLTRB(nX1, nY1, nX2, nY2).makeSorted());
|
|
SkPaint paint = makeLinePaint();
|
|
paint.setAntiAlias(mParent.getAntiAlias());
|
|
if (mScaling != 1 && isUnitTestRunning())
|
|
{
|
|
// On HiDPI displays, do not draw hairlines, draw 1-pixel wide lines in order to avoid
|
|
// smoothing that would confuse unittests.
|
|
paint.setStrokeWidth(1); // this will be scaled by mScaling
|
|
paint.setStrokeCap(SkPaint::kSquare_Cap);
|
|
}
|
|
getDrawCanvas()->drawLine(toSkX(nX1), toSkY(nY1), toSkX(nX2), toSkY(nY2), paint);
|
|
postDraw();
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::privateDrawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth,
|
|
tools::Long nHeight, double fTransparency,
|
|
bool blockAA)
|
|
{
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "privatedrawrect("
|
|
<< this << "): " << SkIRect::MakeXYWH(nX, nY, nWidth, nHeight)
|
|
<< ":" << dumpOptionalColor(moLineColor) << ":"
|
|
<< dumpOptionalColor(moFillColor) << ":" << fTransparency);
|
|
addUpdateRegion(SkRect::MakeXYWH(nX, nY, nWidth, nHeight));
|
|
SkCanvas* canvas = getDrawCanvas();
|
|
if (moFillColor)
|
|
{
|
|
SkPaint paint = makeFillPaint(fTransparency);
|
|
paint.setAntiAlias(!blockAA && mParent.getAntiAlias());
|
|
// HACK: If the polygon is just a line, it still should be drawn. But when filling
|
|
// Skia doesn't draw empty polygons, so in that case ensure the line is drawn.
|
|
if (!moLineColor && SkSize::Make(nWidth, nHeight).isEmpty())
|
|
paint.setStyle(SkPaint::kStroke_Style);
|
|
canvas->drawIRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), paint);
|
|
}
|
|
if (moLineColor && moLineColor != moFillColor) // otherwise handled by fill
|
|
{
|
|
SkPaint paint = makeLinePaint(fTransparency);
|
|
paint.setAntiAlias(!blockAA && mParent.getAntiAlias());
|
|
if (mScaling != 1 && isUnitTestRunning())
|
|
{
|
|
// On HiDPI displays, do not draw just a hairline but instead a full-width "pixel" when running unittests,
|
|
// since tests often require precise pixel drawing.
|
|
paint.setStrokeWidth(1); // this will be scaled by mScaling
|
|
paint.setStrokeCap(SkPaint::kSquare_Cap);
|
|
}
|
|
// The obnoxious "-1 DrawRect()" hack that I don't understand the purpose of (and I'm not sure
|
|
// if anybody does), but without it some cases do not work. The max() is needed because Skia
|
|
// will not draw anything if width or height is 0.
|
|
canvas->drawRect(SkRect::MakeXYWH(toSkX(nX), toSkY(nY),
|
|
std::max(tools::Long(1), nWidth - 1),
|
|
std::max(tools::Long(1), nHeight - 1)),
|
|
paint);
|
|
}
|
|
postDraw();
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawRect(tools::Long nX, tools::Long nY, tools::Long nWidth,
|
|
tools::Long nHeight)
|
|
{
|
|
privateDrawAlphaRect(nX, nY, nWidth, nHeight, 0.0, true);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawPolyLine(sal_uInt32 nPoints, const Point* pPtAry)
|
|
{
|
|
basegfx::B2DPolygon aPolygon;
|
|
aPolygon.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints);
|
|
for (sal_uInt32 i = 1; i < nPoints; ++i)
|
|
aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY()));
|
|
aPolygon.setClosed(false);
|
|
|
|
drawPolyLine(basegfx::B2DHomMatrix(), aPolygon, 0.0, 1.0, nullptr, basegfx::B2DLineJoin::Miter,
|
|
css::drawing::LineCap_BUTT, basegfx::deg2rad(15.0) /*default*/, false);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawPolygon(sal_uInt32 nPoints, const Point* pPtAry)
|
|
{
|
|
basegfx::B2DPolygon aPolygon;
|
|
aPolygon.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints);
|
|
for (sal_uInt32 i = 1; i < nPoints; ++i)
|
|
aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY()));
|
|
|
|
drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aPolygon), 0.0);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawPolyPolygon(sal_uInt32 nPoly, const sal_uInt32* pPoints,
|
|
const Point** pPtAry)
|
|
{
|
|
basegfx::B2DPolyPolygon aPolyPolygon;
|
|
for (sal_uInt32 nPolygon = 0; nPolygon < nPoly; ++nPolygon)
|
|
{
|
|
sal_uInt32 nPoints = pPoints[nPolygon];
|
|
if (nPoints)
|
|
{
|
|
const Point* pSubPoints = pPtAry[nPolygon];
|
|
basegfx::B2DPolygon aPolygon;
|
|
aPolygon.append(basegfx::B2DPoint(pSubPoints->getX(), pSubPoints->getY()), nPoints);
|
|
for (sal_uInt32 i = 1; i < nPoints; ++i)
|
|
aPolygon.setB2DPoint(i,
|
|
basegfx::B2DPoint(pSubPoints[i].getX(), pSubPoints[i].getY()));
|
|
|
|
aPolyPolygon.append(aPolygon);
|
|
}
|
|
}
|
|
|
|
drawPolyPolygon(basegfx::B2DHomMatrix(), aPolyPolygon, 0.0);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice,
|
|
const basegfx::B2DPolyPolygon& rPolyPolygon,
|
|
double fTransparency)
|
|
{
|
|
const bool bHasFill(moFillColor.has_value());
|
|
const bool bHasLine(moLineColor.has_value());
|
|
|
|
if (rPolyPolygon.count() == 0 || !(bHasFill || bHasLine) || fTransparency < 0.0
|
|
|| fTransparency >= 1.0)
|
|
return;
|
|
|
|
basegfx::B2DPolyPolygon aPolyPolygon(rPolyPolygon);
|
|
aPolyPolygon.transform(rObjectToDevice);
|
|
|
|
SAL_INFO("vcl.skia.trace", "drawpolypolygon(" << this << "): " << aPolyPolygon << ":"
|
|
<< dumpOptionalColor(moLineColor) << ":"
|
|
<< dumpOptionalColor(moFillColor));
|
|
|
|
if (delayDrawPolyPolygon(aPolyPolygon, fTransparency))
|
|
{
|
|
scheduleFlush();
|
|
return;
|
|
}
|
|
|
|
performDrawPolyPolygon(aPolyPolygon, fTransparency, mParent.getAntiAlias());
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::performDrawPolyPolygon(const basegfx::B2DPolyPolygon& aPolyPolygon,
|
|
double fTransparency, bool useAA)
|
|
{
|
|
preDraw();
|
|
|
|
SkPath polygonPath;
|
|
bool hasOnlyOrthogonal = true;
|
|
addPolyPolygonToPath(aPolyPolygon, polygonPath, &hasOnlyOrthogonal);
|
|
polygonPath.setFillType(SkPathFillType::kEvenOdd);
|
|
addUpdateRegion(polygonPath.getBounds());
|
|
|
|
// For lines we use toSkX()/toSkY() in order to pass centers of pixels to Skia,
|
|
// as that leads to better results with floating-point coordinates
|
|
// (e.g. https://bugs.chromium.org/p/skia/issues/detail?id=9611).
|
|
// But that means that we generally need to use it also for areas, so that they
|
|
// line up properly if used together (tdf#134346).
|
|
// On the other hand, with AA enabled and rectangular areas, this leads to fuzzy
|
|
// edges (tdf#137329). But since rectangular areas line up perfectly to pixels
|
|
// everywhere, it shouldn't be necessary to do this for them.
|
|
// So if AA is enabled, avoid this fixup for rectangular areas.
|
|
if (!useAA || !hasOnlyOrthogonal)
|
|
{
|
|
// We normally use pixel at their center positions, but slightly off (see toSkX/Y()).
|
|
// With AA lines that "slightly off" causes tiny changes of color, making some tests
|
|
// fail. Since moving AA-ed line slightly to a side doesn't cause any real visual
|
|
// difference, just place exactly at the center. tdf#134346
|
|
// When running on macOS with a Retina display, one BackendTest unit
|
|
// test will fail if the position is adjusted.
|
|
if (!isUnitTestRunning() || getWindowScaling() == 1)
|
|
{
|
|
const SkScalar posFix = useAA ? toSkXYFix : 0;
|
|
polygonPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr);
|
|
}
|
|
}
|
|
if (moFillColor)
|
|
{
|
|
SkPaint aPaint = makeFillPaint(fTransparency);
|
|
aPaint.setAntiAlias(useAA);
|
|
// HACK: If the polygon is just a line, it still should be drawn. But when filling
|
|
// Skia doesn't draw empty polygons, so in that case ensure the line is drawn.
|
|
if (!moLineColor && polygonPath.getBounds().isEmpty())
|
|
aPaint.setStyle(SkPaint::kStroke_Style);
|
|
getDrawCanvas()->drawPath(polygonPath, aPaint);
|
|
}
|
|
if (moLineColor && moLineColor != moFillColor) // otherwise handled by fill
|
|
{
|
|
SkPaint aPaint = makeLinePaint(fTransparency);
|
|
aPaint.setAntiAlias(useAA);
|
|
getDrawCanvas()->drawPath(polygonPath, aPaint);
|
|
}
|
|
postDraw();
|
|
}
|
|
|
|
namespace
|
|
{
|
|
struct LessThan
|
|
{
|
|
bool operator()(const basegfx::B2DPoint& point1, const basegfx::B2DPoint& point2) const
|
|
{
|
|
if (basegfx::fTools::equal(point1.getX(), point2.getX()))
|
|
return basegfx::fTools::less(point1.getY(), point2.getY());
|
|
return basegfx::fTools::less(point1.getX(), point2.getX());
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
bool SkiaSalGraphicsImpl::delayDrawPolyPolygon(const basegfx::B2DPolyPolygon& aPolyPolygon,
|
|
double fTransparency)
|
|
{
|
|
// There is some code that needlessly subdivides areas into adjacent rectangles,
|
|
// but Skia doesn't line them up perfectly if AA is enabled (e.g. Cairo, Qt5 do,
|
|
// but Skia devs claim it's working as intended
|
|
// https://groups.google.com/d/msg/skia-discuss/NlKpD2X_5uc/Vuwd-kyYBwAJ).
|
|
// An example is tdf#133016, which triggers SvgStyleAttributes::add_stroke()
|
|
// implementing a line stroke as a bunch of polygons instead of just one, and
|
|
// SvgLinearAtomPrimitive2D::create2DDecomposition() creates a gradient
|
|
// as a series of polygons of gradually changing color. Those places should be
|
|
// changed, but try to merge those split polygons back into the original one,
|
|
// where the needlessly created edges causing problems will not exist.
|
|
// This means drawing of such polygons needs to be delayed, so that they can
|
|
// be possibly merged with the next one.
|
|
// Merge only polygons of the same properties (color, etc.), so the gradient problem
|
|
// actually isn't handled here.
|
|
|
|
// Only AA polygons need merging, because they do not line up well because of the AA of the edges.
|
|
if (!mParent.getAntiAlias())
|
|
return false;
|
|
// Only filled polygons without an outline are problematic.
|
|
if (!moFillColor || moLineColor)
|
|
return false;
|
|
// Merge only simple polygons, real polypolygons most likely aren't needlessly split,
|
|
// so they do not need joining.
|
|
if (aPolyPolygon.count() != 1)
|
|
return false;
|
|
// If the polygon is not closed, it doesn't mark an area to be filled.
|
|
if (!aPolyPolygon.isClosed())
|
|
return false;
|
|
// If a polygon does not contain a straight line, i.e. it's all curves, then do not merge.
|
|
// First of all that's even more expensive, and second it's very unlikely that it's a polygon
|
|
// split into more polygons.
|
|
if (!polygonContainsLine(aPolyPolygon))
|
|
return false;
|
|
|
|
if (!mLastPolyPolygonInfo.polygons.empty()
|
|
&& (mLastPolyPolygonInfo.transparency != fTransparency
|
|
|| !mLastPolyPolygonInfo.bounds.overlaps(aPolyPolygon.getB2DRange())))
|
|
{
|
|
checkPendingDrawing(); // Cannot be parts of the same larger polygon, draw the last and reset.
|
|
}
|
|
if (!mLastPolyPolygonInfo.polygons.empty())
|
|
{
|
|
assert(aPolyPolygon.count() == 1);
|
|
assert(mLastPolyPolygonInfo.polygons.back().count() == 1);
|
|
// Check if the new and the previous polygon share at least one point. If not, then they
|
|
// cannot be adjacent polygons, so there's no point in trying to merge them.
|
|
bool sharePoint = false;
|
|
const basegfx::B2DPolygon& poly1 = aPolyPolygon.getB2DPolygon(0);
|
|
const basegfx::B2DPolygon& poly2 = mLastPolyPolygonInfo.polygons.back().getB2DPolygon(0);
|
|
o3tl::sorted_vector<basegfx::B2DPoint, LessThan> poly1Points; // for O(n log n)
|
|
poly1Points.reserve(poly1.count());
|
|
for (sal_uInt32 i = 0; i < poly1.count(); ++i)
|
|
poly1Points.insert(poly1.getB2DPoint(i));
|
|
for (sal_uInt32 i = 0; i < poly2.count(); ++i)
|
|
if (poly1Points.find(poly2.getB2DPoint(i)) != poly1Points.end())
|
|
{
|
|
sharePoint = true;
|
|
break;
|
|
}
|
|
if (!sharePoint)
|
|
checkPendingDrawing(); // Draw the previous one and reset.
|
|
}
|
|
// Collect the polygons that can be possibly merged. Do the merging only once at the end,
|
|
// because it's not a cheap operation.
|
|
mLastPolyPolygonInfo.polygons.push_back(aPolyPolygon);
|
|
mLastPolyPolygonInfo.bounds.expand(aPolyPolygon.getB2DRange());
|
|
mLastPolyPolygonInfo.transparency = fTransparency;
|
|
return true;
|
|
}
|
|
|
|
// Tdf#140848 - basegfx::utils::mergeToSinglePolyPolygon() seems to have rounding
|
|
// errors that sometimes cause it to merge incorrectly.
|
|
static void roundPolygonPoints(basegfx::B2DPolyPolygon& polyPolygon)
|
|
{
|
|
for (basegfx::B2DPolygon& polygon : polyPolygon)
|
|
{
|
|
polygon.makeUnique();
|
|
for (sal_uInt32 i = 0; i < polygon.count(); ++i)
|
|
polygon.setB2DPoint(i, basegfx::B2DPoint(basegfx::fround(polygon.getB2DPoint(i))));
|
|
// Control points are saved as vectors relative to points, so hopefully
|
|
// there's no need to round those.
|
|
}
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::checkPendingDrawing()
|
|
{
|
|
if (!mLastPolyPolygonInfo.polygons.empty())
|
|
{ // Flush any pending polygon drawing.
|
|
basegfx::B2DPolyPolygonVector polygons;
|
|
std::swap(polygons, mLastPolyPolygonInfo.polygons);
|
|
double transparency = mLastPolyPolygonInfo.transparency;
|
|
mLastPolyPolygonInfo.bounds.reset();
|
|
if (polygons.size() == 1)
|
|
performDrawPolyPolygon(polygons.front(), transparency, true);
|
|
else
|
|
{
|
|
for (basegfx::B2DPolyPolygon& p : polygons)
|
|
roundPolygonPoints(p);
|
|
performDrawPolyPolygon(basegfx::utils::mergeToSinglePolyPolygon(polygons), transparency,
|
|
true);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice,
|
|
const basegfx::B2DPolygon& rPolyLine, double fTransparency,
|
|
double fLineWidth, const std::vector<double>* pStroke,
|
|
basegfx::B2DLineJoin eLineJoin,
|
|
css::drawing::LineCap eLineCap, double fMiterMinimumAngle,
|
|
bool bPixelSnapHairline)
|
|
{
|
|
if (!rPolyLine.count() || fTransparency < 0.0 || fTransparency > 1.0 || !moLineColor)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace",
|
|
"drawpolyline(" << this << "): " << rPolyLine << ":" << *moLineColor);
|
|
|
|
// Adjust line width for object-to-device scale.
|
|
fLineWidth = (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength();
|
|
// On HiDPI displays, do not draw hairlines, draw 1-pixel wide lines in order to avoid
|
|
// smoothing that would confuse unittests.
|
|
if (fLineWidth == 0 && mScaling != 1 && isUnitTestRunning())
|
|
fLineWidth = 1; // this will be scaled by mScaling
|
|
|
|
// Transform to DeviceCoordinates, get DeviceLineWidth, execute PixelSnapHairline
|
|
basegfx::B2DPolygon aPolyLine(rPolyLine);
|
|
aPolyLine.transform(rObjectToDevice);
|
|
if (bPixelSnapHairline)
|
|
{
|
|
aPolyLine = basegfx::utils::snapPointsOfHorizontalOrVerticalEdges(aPolyLine);
|
|
}
|
|
|
|
SkPaint aPaint = makeLinePaint(fTransparency);
|
|
|
|
switch (eLineJoin)
|
|
{
|
|
case basegfx::B2DLineJoin::Bevel:
|
|
aPaint.setStrokeJoin(SkPaint::kBevel_Join);
|
|
break;
|
|
case basegfx::B2DLineJoin::Round:
|
|
aPaint.setStrokeJoin(SkPaint::kRound_Join);
|
|
break;
|
|
case basegfx::B2DLineJoin::NONE:
|
|
break;
|
|
case basegfx::B2DLineJoin::Miter:
|
|
aPaint.setStrokeJoin(SkPaint::kMiter_Join);
|
|
// convert miter minimum angle to miter limit
|
|
aPaint.setStrokeMiter(1.0 / std::sin(fMiterMinimumAngle / 2.0));
|
|
break;
|
|
}
|
|
|
|
switch (eLineCap)
|
|
{
|
|
case css::drawing::LineCap_ROUND:
|
|
aPaint.setStrokeCap(SkPaint::kRound_Cap);
|
|
break;
|
|
case css::drawing::LineCap_SQUARE:
|
|
aPaint.setStrokeCap(SkPaint::kSquare_Cap);
|
|
break;
|
|
default: // css::drawing::LineCap_BUTT:
|
|
aPaint.setStrokeCap(SkPaint::kButt_Cap);
|
|
break;
|
|
}
|
|
|
|
aPaint.setStrokeWidth(fLineWidth);
|
|
aPaint.setAntiAlias(mParent.getAntiAlias());
|
|
// See the tdf#134346 comment above.
|
|
const SkScalar posFix = mParent.getAntiAlias() ? toSkXYFix : 0;
|
|
|
|
if (pStroke && std::accumulate(pStroke->begin(), pStroke->end(), 0.0) != 0)
|
|
{
|
|
std::vector<SkScalar> intervals;
|
|
// Transform size by the matrix.
|
|
for (double stroke : *pStroke)
|
|
intervals.push_back((rObjectToDevice * basegfx::B2DVector(stroke, 0)).getLength());
|
|
aPaint.setPathEffect(SkDashPathEffect::Make(intervals.data(), intervals.size(), 0));
|
|
}
|
|
|
|
// Skia does not support basegfx::B2DLineJoin::NONE, so in that case batch only if lines
|
|
// are not wider than a pixel.
|
|
if (eLineJoin != basegfx::B2DLineJoin::NONE || fLineWidth <= 1.0)
|
|
{
|
|
SkPath aPath;
|
|
aPath.incReserve(aPolyLine.count() * 3); // because cubicTo is 3 elements
|
|
addPolygonToPath(aPolyLine, aPath);
|
|
aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr);
|
|
addUpdateRegion(aPath.getBounds());
|
|
getDrawCanvas()->drawPath(aPath, aPaint);
|
|
}
|
|
else
|
|
{
|
|
sal_uInt32 nPoints = aPolyLine.count();
|
|
bool bClosed = aPolyLine.isClosed();
|
|
bool bHasCurves = aPolyLine.areControlPointsUsed();
|
|
for (sal_uInt32 j = 0; j < nPoints; ++j)
|
|
{
|
|
SkPath aPath;
|
|
aPath.incReserve(2 * 3); // because cubicTo is 3 elements
|
|
addPolygonToPath(aPolyLine, aPath, j, j + 1, nPoints, bClosed, bHasCurves);
|
|
aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr);
|
|
addUpdateRegion(aPath.getBounds());
|
|
getDrawCanvas()->drawPath(aPath, aPaint);
|
|
}
|
|
}
|
|
|
|
postDraw();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawPolyLineBezier(sal_uInt32, const Point*, const PolyFlags*)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawPolygonBezier(sal_uInt32, const Point*, const PolyFlags*)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawPolyPolygonBezier(sal_uInt32, const sal_uInt32*, const Point* const*,
|
|
const PolyFlags* const*)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::copyArea(tools::Long nDestX, tools::Long nDestY, tools::Long nSrcX,
|
|
tools::Long nSrcY, tools::Long nSrcWidth, tools::Long nSrcHeight,
|
|
bool /*bWindowInvalidate*/)
|
|
{
|
|
if (nDestX == nSrcX && nDestY == nSrcY)
|
|
return;
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "copyarea("
|
|
<< this << "): " << Point(nSrcX, nSrcY) << "->"
|
|
<< SkIRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight));
|
|
// Using SkSurface::draw() should be more efficient, but it's too buggy.
|
|
SalTwoRect rPosAry(nSrcX, nSrcY, nSrcWidth, nSrcHeight, nDestX, nDestY, nSrcWidth, nSrcHeight);
|
|
privateCopyBits(rPosAry, this);
|
|
postDraw();
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::copyBits(const SalTwoRect& rPosAry, SalGraphics* pSrcGraphics)
|
|
{
|
|
preDraw();
|
|
SkiaSalGraphicsImpl* src;
|
|
if (pSrcGraphics)
|
|
{
|
|
assert(dynamic_cast<SkiaSalGraphicsImpl*>(pSrcGraphics->GetImpl()));
|
|
src = static_cast<SkiaSalGraphicsImpl*>(pSrcGraphics->GetImpl());
|
|
src->checkSurface();
|
|
src->flushDrawing();
|
|
}
|
|
else
|
|
{
|
|
src = this;
|
|
assert(mXorMode == XorMode::None);
|
|
}
|
|
auto srcDebug = [&]() -> std::string {
|
|
if (src == this)
|
|
return "(self)";
|
|
else
|
|
{
|
|
std::ostringstream stream;
|
|
stream << "(" << src << ")";
|
|
return stream.str();
|
|
}
|
|
};
|
|
SAL_INFO("vcl.skia.trace", "copybits(" << this << "): " << srcDebug() << ": " << rPosAry);
|
|
privateCopyBits(rPosAry, src);
|
|
postDraw();
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::privateCopyBits(const SalTwoRect& rPosAry, SkiaSalGraphicsImpl* src)
|
|
{
|
|
assert(mXorMode == XorMode::None);
|
|
addUpdateRegion(SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth,
|
|
rPosAry.mnDestHeight));
|
|
SkPaint paint;
|
|
paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha
|
|
SkIRect srcRect = SkIRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth,
|
|
rPosAry.mnSrcHeight);
|
|
SkRect destRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth,
|
|
rPosAry.mnDestHeight);
|
|
|
|
if (!SkIRect::Intersects(srcRect, SkIRect::MakeWH(src->GetWidth(), src->GetHeight()))
|
|
|| !SkRect::Intersects(destRect, SkRect::MakeWH(GetWidth(), GetHeight())))
|
|
return;
|
|
|
|
if (src == this)
|
|
{
|
|
// Copy-to-self means that we'd take a snapshot, which would refcount the data,
|
|
// and then drawing would result in copy in write, copying the entire surface.
|
|
// Try to copy less by making a snapshot of only what is needed.
|
|
// A complication here is that drawImageRect() can handle coordinates outside
|
|
// of surface fine, but makeImageSnapshot() will crop to the surface area,
|
|
// so do that manually here in order to adjust also destination rectangle.
|
|
if (srcRect.x() < 0 || srcRect.y() < 0)
|
|
{
|
|
destRect.fLeft += -srcRect.x();
|
|
destRect.fTop += -srcRect.y();
|
|
srcRect.adjust(-srcRect.x(), -srcRect.y(), 0, 0);
|
|
}
|
|
// Note that right() and bottom() are not inclusive (are outside of the rect).
|
|
if (srcRect.right() - 1 > GetWidth() || srcRect.bottom() - 1 > GetHeight())
|
|
{
|
|
destRect.fRight += GetWidth() - srcRect.right();
|
|
destRect.fBottom += GetHeight() - srcRect.bottom();
|
|
srcRect.adjust(0, 0, GetWidth() - srcRect.right(), GetHeight() - srcRect.bottom());
|
|
}
|
|
// Scaling for source coordinates must be done manually.
|
|
if (src->mScaling != 1)
|
|
srcRect = scaleRect(srcRect, src->mScaling);
|
|
sk_sp<SkImage> image = makeCheckedImageSnapshot(src->mSurface, srcRect);
|
|
srcRect.offset(-srcRect.x(), -srcRect.y());
|
|
getDrawCanvas()->drawImageRect(image, SkRect::Make(srcRect), destRect,
|
|
makeSamplingOptions(rPosAry, mScaling, src->mScaling),
|
|
&paint, SkCanvas::kFast_SrcRectConstraint);
|
|
}
|
|
else
|
|
{
|
|
// Scaling for source coordinates must be done manually.
|
|
if (src->mScaling != 1)
|
|
srcRect = scaleRect(srcRect, src->mScaling);
|
|
// Do not use makeImageSnapshot(rect), as that one may make a needless data copy.
|
|
getDrawCanvas()->drawImageRect(makeCheckedImageSnapshot(src->mSurface),
|
|
SkRect::Make(srcRect), destRect,
|
|
makeSamplingOptions(rPosAry, mScaling, src->mScaling),
|
|
&paint, SkCanvas::kFast_SrcRectConstraint);
|
|
}
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::blendBitmap(const SalTwoRect& rPosAry, const SalBitmap& rBitmap)
|
|
{
|
|
if (checkInvalidSourceOrDestination(rPosAry))
|
|
return false;
|
|
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rBitmap));
|
|
const SkiaSalBitmap& rSkiaBitmap = static_cast<const SkiaSalBitmap&>(rBitmap);
|
|
// This is used by VirtualDevice in the alpha mode for the "alpha" layer
|
|
// So the result is transparent only if both the inputs
|
|
// are transparent. Which seems to be what SkBlendMode::kModulate does,
|
|
// so use that.
|
|
// See also blendAlphaBitmap().
|
|
if (rSkiaBitmap.IsFullyOpaqueAsAlpha())
|
|
{
|
|
// Optimization. If the bitmap means fully opaque, it's all one's. In CPU
|
|
// mode it should be faster to just copy instead of SkBlendMode::kMultiply.
|
|
drawBitmap(rPosAry, rSkiaBitmap);
|
|
}
|
|
else
|
|
drawBitmap(rPosAry, rSkiaBitmap, SkBlendMode::kModulate);
|
|
return true;
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::blendAlphaBitmap(const SalTwoRect& rPosAry,
|
|
const SalBitmap& rSourceBitmap,
|
|
const SalBitmap& rMaskBitmap,
|
|
const SalBitmap& rAlphaBitmap)
|
|
{
|
|
// tdf#156361 use slow blending path if alpha mask blending is disabled
|
|
// SkiaSalGraphicsImpl::blendBitmap() fails unexpectedly in the following
|
|
// cases so return false and use the non-Skia alpha mask blending code:
|
|
// - Unexpected white areas when running a slideshow or printing:
|
|
// https://bugs.documentfoundation.org/attachment.cgi?id=188447
|
|
// - Unexpected scaling of bitmap and/or alpha mask when exporting to PDF:
|
|
// https://bugs.documentfoundation.org/attachment.cgi?id=188498
|
|
if (!SkiaHelper::isAlphaMaskBlendingEnabled())
|
|
return false;
|
|
|
|
if (checkInvalidSourceOrDestination(rPosAry))
|
|
return false;
|
|
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rSourceBitmap));
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rMaskBitmap));
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rAlphaBitmap));
|
|
const SkiaSalBitmap& rSkiaSourceBitmap = static_cast<const SkiaSalBitmap&>(rSourceBitmap);
|
|
const SkiaSalBitmap& rSkiaMaskBitmap = static_cast<const SkiaSalBitmap&>(rMaskBitmap);
|
|
const SkiaSalBitmap& rSkiaAlphaBitmap = static_cast<const SkiaSalBitmap&>(rAlphaBitmap);
|
|
|
|
if (rSkiaMaskBitmap.IsFullyOpaqueAsAlpha())
|
|
{
|
|
// Optimization. If the mask of the bitmap to be blended means it's actually opaque,
|
|
// just draw the bitmap directly (that's what the math below will result in).
|
|
drawBitmap(rPosAry, rSkiaSourceBitmap);
|
|
return true;
|
|
}
|
|
// This was originally implemented for the OpenGL drawing method and it is poorly documented.
|
|
// The source and mask bitmaps are the usual data and alpha bitmaps, and 'alpha'
|
|
// is the "alpha" layer of the VirtualDevice (the alpha in VirtualDevice is also stored
|
|
// as a separate bitmap). Now if I understand it correctly these two alpha masks first need
|
|
// to be combined into the actual alpha mask to be used. The formula for TYPE_BLEND
|
|
// in opengl's combinedTextureFragmentShader.glsl is
|
|
// "result_alpha = 1.0 - (1.0 - floor(alpha)) * mask".
|
|
// See also blendBitmap().
|
|
|
|
SkSamplingOptions samplingOptions = makeSamplingOptions(rPosAry, mScaling);
|
|
// First do the "( 1 - alpha ) * mask"
|
|
// (no idea how to do "floor", but hopefully not needed in practice).
|
|
sk_sp<SkShader> shaderAlpha
|
|
= SkShaders::Blend(SkBlendMode::kDstIn, rSkiaMaskBitmap.GetAlphaSkShader(samplingOptions),
|
|
rSkiaAlphaBitmap.GetAlphaSkShader(samplingOptions));
|
|
// And now draw the bitmap with "1 - x", where x is the "( 1 - alpha ) * mask".
|
|
sk_sp<SkShader> shader = SkShaders::Blend(SkBlendMode::kSrcIn, shaderAlpha,
|
|
rSkiaSourceBitmap.GetSkShader(samplingOptions));
|
|
drawShader(rPosAry, shader);
|
|
return true;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap)
|
|
{
|
|
if (checkInvalidSourceOrDestination(rPosAry))
|
|
return;
|
|
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rSalBitmap));
|
|
const SkiaSalBitmap& rSkiaSourceBitmap = static_cast<const SkiaSalBitmap&>(rSalBitmap);
|
|
|
|
drawBitmap(rPosAry, rSkiaSourceBitmap);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap,
|
|
const SalBitmap& rMaskBitmap)
|
|
{
|
|
drawAlphaBitmap(rPosAry, rSalBitmap, rMaskBitmap);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawMask(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap,
|
|
Color nMaskColor)
|
|
{
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rSalBitmap));
|
|
const SkiaSalBitmap& skiaBitmap = static_cast<const SkiaSalBitmap&>(rSalBitmap);
|
|
// SkBlendMode::kDstOut must be used instead of SkBlendMode::kDstIn because
|
|
// the alpha channel of what is drawn appears to get inverted at some point
|
|
// after it is drawn
|
|
drawShader(
|
|
rPosAry,
|
|
SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is alpha.
|
|
SkShaders::Color(toSkColor(nMaskColor)),
|
|
skiaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry, mScaling))));
|
|
}
|
|
|
|
std::shared_ptr<SalBitmap> SkiaSalGraphicsImpl::getBitmap(tools::Long nX, tools::Long nY,
|
|
tools::Long nWidth, tools::Long nHeight)
|
|
{
|
|
SkiaZone zone;
|
|
checkSurface();
|
|
SAL_INFO("vcl.skia.trace",
|
|
"getbitmap(" << this << "): " << SkIRect::MakeXYWH(nX, nY, nWidth, nHeight));
|
|
flushDrawing();
|
|
// TODO makeImageSnapshot(rect) may copy the data, which may be a waste if this is used
|
|
// e.g. for VirtualDevice's lame alpha blending, in which case the image will eventually end up
|
|
// in blendAlphaBitmap(), where we could simply use the proper rect of the image.
|
|
sk_sp<SkImage> image = makeCheckedImageSnapshot(
|
|
mSurface, scaleRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), mScaling));
|
|
std::shared_ptr<SkiaSalBitmap> bitmap = std::make_shared<SkiaSalBitmap>(image);
|
|
// If the surface is scaled for HiDPI, the bitmap needs to be scaled down, otherwise
|
|
// it would have incorrect size from the API point of view. The DirectImage::Yes handling
|
|
// in mergeCacheBitmaps() should access the original unscaled bitmap data to avoid
|
|
// pointless scaling back and forth.
|
|
if (mScaling != 1)
|
|
{
|
|
if (!isUnitTestRunning())
|
|
bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, goodScalingQuality());
|
|
else
|
|
{
|
|
// Some tests require exact pixel values and would be confused by smooth-scaling.
|
|
// And some draw something smooth and not smooth-scaling there would break the checks.
|
|
// When running on macOS with a Retina display, several BackendTest unit tests
|
|
// also need a lower quality scaling level.
|
|
if (getWindowScaling() != 1
|
|
|| isUnitTestRunning("BackendTest__testDrawHaflEllipseAAWithPolyLineB2D_")
|
|
|| isUnitTestRunning("BackendTest__testDrawRectAAWithLine_")
|
|
|| isUnitTestRunning("GraphicsRenderTest__testDrawRectAAWithLine"))
|
|
{
|
|
bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, goodScalingQuality());
|
|
}
|
|
else
|
|
bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, BmpScaleFlag::NearestNeighbor);
|
|
}
|
|
}
|
|
return bitmap;
|
|
}
|
|
|
|
Color SkiaSalGraphicsImpl::getPixel(tools::Long nX, tools::Long nY)
|
|
{
|
|
SkiaZone zone;
|
|
checkSurface();
|
|
SAL_INFO("vcl.skia.trace", "getpixel(" << this << "): " << Point(nX, nY));
|
|
flushDrawing();
|
|
// This is presumably slow, but getPixel() should be generally used only by unit tests.
|
|
SkBitmap bitmap;
|
|
if (!bitmap.tryAllocN32Pixels(mSurface->width(), mSurface->height()))
|
|
abort();
|
|
if (!mSurface->readPixels(bitmap, 0, 0))
|
|
abort();
|
|
return fromSkColor(bitmap.getColor(nX * mScaling, nY * mScaling));
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::invert(basegfx::B2DPolygon const& rPoly, SalInvert eFlags)
|
|
{
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "invert(" << this << "): " << rPoly << ":" << int(eFlags));
|
|
assert(mXorMode == XorMode::None);
|
|
SkPath aPath;
|
|
aPath.incReserve(rPoly.count());
|
|
addPolygonToPath(rPoly, aPath);
|
|
aPath.setFillType(SkPathFillType::kEvenOdd);
|
|
addUpdateRegion(aPath.getBounds());
|
|
{
|
|
SkAutoCanvasRestore autoRestore(getDrawCanvas(), true);
|
|
SkPaint aPaint;
|
|
// There's no blend mode for inverting as such, but kExclusion is 's + d - 2*s*d',
|
|
// so with d = 1.0 (all channels) it becomes effectively '1 - s', i.e. inverted color.
|
|
aPaint.setBlendMode(SkBlendMode::kExclusion);
|
|
aPaint.setColor(SkColorSetARGB(255, 255, 255, 255));
|
|
// TrackFrame just inverts a dashed path around the polygon
|
|
if (eFlags == SalInvert::TrackFrame)
|
|
{
|
|
// TrackFrame is not supposed to paint outside of the polygon (usually rectangle),
|
|
// but wider stroke width usually results in that, so ensure the requirement
|
|
// by clipping.
|
|
getDrawCanvas()->clipRect(aPath.getBounds(), SkClipOp::kIntersect, false);
|
|
aPaint.setStrokeWidth(2);
|
|
constexpr float intervals[] = { 4.0f, 4.0f };
|
|
aPaint.setStyle(SkPaint::kStroke_Style);
|
|
aPaint.setPathEffect(SkDashPathEffect::Make(intervals, std::size(intervals), 0));
|
|
}
|
|
else
|
|
{
|
|
aPaint.setStyle(SkPaint::kFill_Style);
|
|
|
|
// N50 inverts in checker pattern
|
|
if (eFlags == SalInvert::N50)
|
|
{
|
|
// This creates 2x2 checker pattern bitmap
|
|
// TODO Use createSkSurface() and cache the image
|
|
SkBitmap aBitmap;
|
|
aBitmap.allocN32Pixels(2, 2);
|
|
const SkPMColor white = SkPreMultiplyARGB(0xFF, 0xFF, 0xFF, 0xFF);
|
|
const SkPMColor black = SkPreMultiplyARGB(0xFF, 0x00, 0x00, 0x00);
|
|
SkPMColor* scanline;
|
|
scanline = aBitmap.getAddr32(0, 0);
|
|
*scanline++ = white;
|
|
*scanline++ = black;
|
|
scanline = aBitmap.getAddr32(0, 1);
|
|
*scanline++ = black;
|
|
*scanline++ = white;
|
|
aBitmap.setImmutable();
|
|
// The bitmap is repeated in both directions the checker pattern is as big
|
|
// as the polygon (usually rectangle)
|
|
aPaint.setShader(aBitmap.makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat,
|
|
SkSamplingOptions()));
|
|
}
|
|
|
|
#ifdef SK_METAL
|
|
// tdf#153306 prevent subpixel shifting of X coordinate
|
|
// HACK: for some unknown reason, if the X coordinate of the
|
|
// path's bounds is more than 1024, SkBlendMode::kExclusion will
|
|
// shift by about a half a pixel to the right with Skia/Metal on
|
|
// a Retina display. Weirdly, if the same polygon is repeatedly
|
|
// drawn, the total shift is cumulative so if the drawn polygon
|
|
// is more than a few pixels wide, the blinking cursor in Writer
|
|
// will exhibit this bug but only for one thin vertical slice at
|
|
// a time. Apparently, shifting drawing a very tiny amount to
|
|
// the left seems to be enough to quell this runaway cumulative
|
|
// X coordinate shift.
|
|
if (isGPU())
|
|
{
|
|
SkMatrix aMatrix;
|
|
aMatrix.set(SkMatrix::kMTransX, -0.001);
|
|
getDrawCanvas()->concat(aMatrix);
|
|
}
|
|
#endif
|
|
}
|
|
getDrawCanvas()->drawPath(aPath, aPaint);
|
|
}
|
|
postDraw();
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::invert(tools::Long nX, tools::Long nY, tools::Long nWidth,
|
|
tools::Long nHeight, SalInvert eFlags)
|
|
{
|
|
basegfx::B2DRectangle aRectangle(nX, nY, nX + nWidth, nY + nHeight);
|
|
auto aRect = basegfx::utils::createPolygonFromRect(aRectangle);
|
|
invert(aRect, eFlags);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::invert(sal_uInt32 nPoints, const Point* pPointArray, SalInvert eFlags)
|
|
{
|
|
basegfx::B2DPolygon aPolygon;
|
|
aPolygon.append(basegfx::B2DPoint(pPointArray[0].getX(), pPointArray[0].getY()), nPoints);
|
|
for (sal_uInt32 i = 1; i < nPoints; ++i)
|
|
{
|
|
aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPointArray[i].getX(), pPointArray[i].getY()));
|
|
}
|
|
aPolygon.setClosed(true);
|
|
|
|
invert(aPolygon, eFlags);
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawEPS(tools::Long, tools::Long, tools::Long, tools::Long, void*,
|
|
sal_uInt32)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Create SkImage from a bitmap and possibly an alpha mask (the usual VCL one-minus-alpha),
|
|
// with the given target size. Result will be possibly cached, unless disabled.
|
|
// Especially in raster mode scaling and alpha blending may be expensive if done repeatedly.
|
|
sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitmap,
|
|
const SkiaSalBitmap* alphaBitmap,
|
|
const Size& targetSize)
|
|
{
|
|
if (alphaBitmap)
|
|
assert(bitmap.GetSize() == alphaBitmap->GetSize());
|
|
|
|
if (targetSize.IsEmpty())
|
|
return {};
|
|
if (alphaBitmap && alphaBitmap->IsFullyOpaqueAsAlpha())
|
|
alphaBitmap = nullptr; // the alpha can be ignored
|
|
if (bitmap.PreferSkShader() && (!alphaBitmap || alphaBitmap->PreferSkShader()))
|
|
return {};
|
|
|
|
// If the bitmap has SkImage that matches the required size, try to use it, even
|
|
// if it doesn't match bitmap.GetSize(). This can happen with delayed scaling.
|
|
// This will catch cases such as some code pre-scaling the bitmap, which would make GetSkImage()
|
|
// scale, changing GetImageKey() in the process so we'd have to re-cache, and then we'd need
|
|
// to scale again in this function.
|
|
bool bitmapReady = false;
|
|
bool alphaBitmapReady = false;
|
|
if (const sk_sp<SkImage>& image = bitmap.GetSkImage(DirectImage::Yes))
|
|
{
|
|
assert(!bitmap.PreferSkShader());
|
|
if (imageSize(image) == targetSize)
|
|
bitmapReady = true;
|
|
}
|
|
// If the image usable and there's no alpha, then it matches exactly what's wanted.
|
|
if (bitmapReady && !alphaBitmap)
|
|
return bitmap.GetSkImage(DirectImage::Yes);
|
|
if (alphaBitmap)
|
|
{
|
|
if (!alphaBitmap->GetAlphaSkImage(DirectImage::Yes)
|
|
&& alphaBitmap->GetSkImage(DirectImage::Yes)
|
|
&& imageSize(alphaBitmap->GetSkImage(DirectImage::Yes)) == targetSize)
|
|
{
|
|
// There's a usable non-alpha image, try to convert it to alpha.
|
|
assert(!alphaBitmap->PreferSkShader());
|
|
const_cast<SkiaSalBitmap*>(alphaBitmap)->TryDirectConvertToAlphaNoScaling();
|
|
}
|
|
if (const sk_sp<SkImage>& image = alphaBitmap->GetAlphaSkImage(DirectImage::Yes))
|
|
{
|
|
assert(!alphaBitmap->PreferSkShader());
|
|
if (imageSize(image) == targetSize)
|
|
alphaBitmapReady = true;
|
|
}
|
|
}
|
|
|
|
if (bitmapReady && (!alphaBitmap || alphaBitmapReady))
|
|
{
|
|
// Try to find a cached image based on the already existing images.
|
|
OString key = makeCachedImageKey(bitmap, alphaBitmap, targetSize, DirectImage::Yes,
|
|
DirectImage::Yes);
|
|
if (sk_sp<SkImage> image = findCachedImage(key))
|
|
{
|
|
assert(imageSize(image) == targetSize);
|
|
return image;
|
|
}
|
|
}
|
|
|
|
// Probably not much point in caching of just doing a copy.
|
|
if (alphaBitmap == nullptr && targetSize == bitmap.GetSize())
|
|
return {};
|
|
// Image too small to be worth caching if not scaling.
|
|
if (targetSize == bitmap.GetSize() && targetSize.Width() < 100 && targetSize.Height() < 100)
|
|
return {};
|
|
// GPU-accelerated drawing with SkShader should be fast enough to not need caching.
|
|
if (isGPU())
|
|
{
|
|
// tdf#140925: But if this is such an extensive downscaling that caching the result
|
|
// would noticeably reduce amount of data processed by the GPU on repeated usage, do it.
|
|
int reduceRatio = bitmap.GetSize().Width() * bitmap.GetSize().Height() / targetSize.Width()
|
|
/ targetSize.Height();
|
|
if (reduceRatio < 10)
|
|
return {};
|
|
}
|
|
// Do not cache the result if it would take most of the cache and thus get evicted soon.
|
|
if (targetSize.Width() * targetSize.Height() * 4 > maxImageCacheSize() * 0.7)
|
|
return {};
|
|
|
|
// Use ready direct image if they are both available, now even the size doesn't matter
|
|
// (we'll scale as necessary and it's better to scale from the original). Require only
|
|
// that they are the same size, or that one prefers a shader or doesn't exist
|
|
// (i.e. avoid two images of different size).
|
|
bitmapReady = bitmap.GetSkImage(DirectImage::Yes) != nullptr;
|
|
alphaBitmapReady = alphaBitmap && alphaBitmap->GetAlphaSkImage(DirectImage::Yes) != nullptr;
|
|
if (bitmapReady && alphaBitmap && !alphaBitmapReady && !alphaBitmap->PreferSkShader())
|
|
bitmapReady = false;
|
|
if (alphaBitmapReady && !bitmapReady && bitmap.PreferSkShader())
|
|
alphaBitmapReady = false;
|
|
|
|
DirectImage bitmapType = bitmapReady ? DirectImage::Yes : DirectImage::No;
|
|
DirectImage alphaBitmapType = alphaBitmapReady ? DirectImage::Yes : DirectImage::No;
|
|
|
|
// Try to find a cached result, this time after possible delayed scaling.
|
|
OString key = makeCachedImageKey(bitmap, alphaBitmap, targetSize, bitmapType, alphaBitmapType);
|
|
if (sk_sp<SkImage> image = findCachedImage(key))
|
|
{
|
|
assert(imageSize(image) == targetSize);
|
|
return image;
|
|
}
|
|
|
|
// In some cases (tdf#134237) the target size may be very large. In that case it's
|
|
// better to rely on Skia to clip and draw only the necessary, rather than prepare
|
|
// a very large image only to not use most of it. Do this only after checking whether
|
|
// the image is already cached, since it might have been already cached in a previous
|
|
// call that had the draw area large enough to be seen as worth caching.
|
|
const Size drawAreaSize = mClipRegion.GetBoundRect().GetSize() * mScaling;
|
|
if (targetSize.Width() > drawAreaSize.Width() || targetSize.Height() > drawAreaSize.Height())
|
|
{
|
|
// This is a bit tricky. The condition above just checks that at least a part of the resulting
|
|
// image will not be used (it's larger then our drawing area). But this may often happen
|
|
// when just scrolling a document with a large image, where the caching may very well be worth it.
|
|
// Since the problem is mainly the cost of upscaling and then the size of the resulting bitmap,
|
|
// compute a ratio of how much this is going to be scaled up, how much this is larger than
|
|
// the drawing area, and then refuse to cache if it's too much.
|
|
const double upscaleRatio
|
|
= std::max(1.0, 1.0 * targetSize.Width() / bitmap.GetSize().Width()
|
|
* targetSize.Height() / bitmap.GetSize().Height());
|
|
const double oversizeRatio = 1.0 * targetSize.Width() / drawAreaSize.Width()
|
|
* targetSize.Height() / drawAreaSize.Height();
|
|
const double ratio = upscaleRatio * oversizeRatio;
|
|
if (ratio > 4)
|
|
{
|
|
SAL_INFO("vcl.skia.trace", "mergecachebitmaps("
|
|
<< this << "): not caching, ratio:" << ratio << ", "
|
|
<< bitmap.GetSize() << "->" << targetSize << " in "
|
|
<< drawAreaSize);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
Size sourceSize;
|
|
if (bitmapReady)
|
|
sourceSize = imageSize(bitmap.GetSkImage(DirectImage::Yes));
|
|
else if (alphaBitmapReady)
|
|
sourceSize = imageSize(alphaBitmap->GetAlphaSkImage(DirectImage::Yes));
|
|
else
|
|
sourceSize = bitmap.GetSize();
|
|
|
|
// Generate a new result and cache it.
|
|
sk_sp<SkSurface> tmpSurface
|
|
= createSkSurface(targetSize, alphaBitmap ? kPremul_SkAlphaType : bitmap.alphaType());
|
|
if (!tmpSurface)
|
|
return nullptr;
|
|
SkCanvas* canvas = tmpSurface->getCanvas();
|
|
{
|
|
SkAutoCanvasRestore autoRestore(canvas, true);
|
|
SkPaint paint;
|
|
SkSamplingOptions samplingOptions;
|
|
if (targetSize != sourceSize)
|
|
{
|
|
SkMatrix matrix;
|
|
matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / sourceSize.Width());
|
|
matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / sourceSize.Height());
|
|
canvas->concat(matrix);
|
|
if (!isUnitTestRunning()) // unittests want exact pixel values
|
|
samplingOptions = makeSamplingOptions(matrix, 1);
|
|
}
|
|
if (alphaBitmap != nullptr)
|
|
{
|
|
canvas->clear(SK_ColorTRANSPARENT);
|
|
paint.setShader(SkShaders::Blend(
|
|
SkBlendMode::kDstIn, bitmap.GetSkShader(samplingOptions, bitmapType),
|
|
alphaBitmap->GetAlphaSkShader(samplingOptions, alphaBitmapType)));
|
|
canvas->drawPaint(paint);
|
|
}
|
|
else if (bitmap.PreferSkShader())
|
|
{
|
|
paint.setShader(bitmap.GetSkShader(samplingOptions, bitmapType));
|
|
canvas->drawPaint(paint);
|
|
}
|
|
else
|
|
canvas->drawImage(bitmap.GetSkImage(bitmapType), 0, 0, samplingOptions, &paint);
|
|
if (isGPU())
|
|
SAL_INFO("vcl.skia.trace", "mergecachebitmaps(" << this << "): caching GPU downscaling:"
|
|
<< bitmap.GetSize() << "->"
|
|
<< targetSize);
|
|
}
|
|
sk_sp<SkImage> image = makeCheckedImageSnapshot(tmpSurface);
|
|
addCachedImage(key, image);
|
|
return image;
|
|
}
|
|
|
|
OString SkiaSalGraphicsImpl::makeCachedImageKey(const SkiaSalBitmap& bitmap,
|
|
const SkiaSalBitmap* alphaBitmap,
|
|
const Size& targetSize, DirectImage bitmapType,
|
|
DirectImage alphaBitmapType)
|
|
{
|
|
OString key = OString::number(targetSize.Width()) + "x" + OString::number(targetSize.Height())
|
|
+ "_" + bitmap.GetImageKey(bitmapType);
|
|
if (alphaBitmap)
|
|
key += "_" + alphaBitmap->GetAlphaImageKey(alphaBitmapType);
|
|
return key;
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSourceBitmap,
|
|
const SalBitmap& rAlphaBitmap)
|
|
{
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rSourceBitmap));
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rAlphaBitmap));
|
|
const SkiaSalBitmap& rSkiaSourceBitmap = static_cast<const SkiaSalBitmap&>(rSourceBitmap);
|
|
const SkiaSalBitmap& rSkiaAlphaBitmap = static_cast<const SkiaSalBitmap&>(rAlphaBitmap);
|
|
// Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated
|
|
// alpha blending or scaling.
|
|
SalTwoRect imagePosAry(rPosAry);
|
|
Size imageSize = rSourceBitmap.GetSize();
|
|
// If the bitmap will be scaled, prefer to do it in mergeCacheBitmaps(), if possible.
|
|
if ((rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight)
|
|
&& rPosAry.mnSrcX == 0 && rPosAry.mnSrcY == 0
|
|
&& rPosAry.mnSrcWidth == rSourceBitmap.GetSize().Width()
|
|
&& rPosAry.mnSrcHeight == rSourceBitmap.GetSize().Height())
|
|
{
|
|
imagePosAry.mnSrcWidth = imagePosAry.mnDestWidth;
|
|
imagePosAry.mnSrcHeight = imagePosAry.mnDestHeight;
|
|
imageSize = Size(imagePosAry.mnSrcWidth, imagePosAry.mnSrcHeight);
|
|
}
|
|
sk_sp<SkImage> image
|
|
= mergeCacheBitmaps(rSkiaSourceBitmap, &rSkiaAlphaBitmap, imageSize * mScaling);
|
|
if (image)
|
|
drawImage(imagePosAry, image, mScaling);
|
|
else if (rSkiaAlphaBitmap.IsFullyOpaqueAsAlpha()
|
|
&& !rSkiaSourceBitmap.PreferSkShader()) // alpha can be ignored
|
|
drawBitmap(rPosAry, rSkiaSourceBitmap);
|
|
else
|
|
drawShader(rPosAry,
|
|
SkShaders::Blend(
|
|
SkBlendMode::kDstIn,
|
|
rSkiaSourceBitmap.GetSkShader(makeSamplingOptions(rPosAry, mScaling)),
|
|
rSkiaAlphaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry, mScaling))));
|
|
return true;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SkiaSalBitmap& bitmap,
|
|
SkBlendMode blendMode)
|
|
{
|
|
// Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated
|
|
// scaling.
|
|
SalTwoRect imagePosAry(rPosAry);
|
|
Size imageSize = bitmap.GetSize();
|
|
// If the bitmap will be scaled, prefer to do it in mergeCacheBitmaps(), if possible.
|
|
if ((rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight)
|
|
&& rPosAry.mnSrcX == 0 && rPosAry.mnSrcY == 0
|
|
&& rPosAry.mnSrcWidth == bitmap.GetSize().Width()
|
|
&& rPosAry.mnSrcHeight == bitmap.GetSize().Height())
|
|
{
|
|
imagePosAry.mnSrcWidth = imagePosAry.mnDestWidth;
|
|
imagePosAry.mnSrcHeight = imagePosAry.mnDestHeight;
|
|
imageSize = Size(imagePosAry.mnSrcWidth, imagePosAry.mnSrcHeight);
|
|
}
|
|
sk_sp<SkImage> image = mergeCacheBitmaps(bitmap, nullptr, imageSize * mScaling);
|
|
if (image)
|
|
drawImage(imagePosAry, image, mScaling, blendMode);
|
|
else if (bitmap.PreferSkShader())
|
|
drawShader(rPosAry, bitmap.GetSkShader(makeSamplingOptions(rPosAry, mScaling)), blendMode);
|
|
else
|
|
drawImage(rPosAry, bitmap.GetSkImage(), 1, blendMode);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::drawImage(const SalTwoRect& rPosAry, const sk_sp<SkImage>& aImage,
|
|
int srcScaling, SkBlendMode eBlendMode)
|
|
{
|
|
SkRect aSourceRect
|
|
= SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight);
|
|
if (srcScaling != 1)
|
|
aSourceRect = scaleRect(aSourceRect, srcScaling);
|
|
SkRect aDestinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY,
|
|
rPosAry.mnDestWidth, rPosAry.mnDestHeight);
|
|
|
|
SkPaint aPaint = makeBitmapPaint();
|
|
aPaint.setBlendMode(eBlendMode);
|
|
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace",
|
|
"drawimage(" << this << "): " << rPosAry << ":" << SkBlendMode_Name(eBlendMode));
|
|
addUpdateRegion(aDestinationRect);
|
|
getDrawCanvas()->drawImageRect(aImage, aSourceRect, aDestinationRect,
|
|
makeSamplingOptions(rPosAry, mScaling, srcScaling), &aPaint,
|
|
SkCanvas::kFast_SrcRectConstraint);
|
|
++pendingOperationsToFlush; // tdf#136369
|
|
postDraw();
|
|
}
|
|
|
|
// SkShader can be used to merge multiple bitmaps with appropriate blend modes (e.g. when
|
|
// merging a bitmap with its alpha mask).
|
|
void SkiaSalGraphicsImpl::drawShader(const SalTwoRect& rPosAry, const sk_sp<SkShader>& shader,
|
|
SkBlendMode blendMode)
|
|
{
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "drawshader(" << this << "): " << rPosAry);
|
|
SkRect destinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth,
|
|
rPosAry.mnDestHeight);
|
|
addUpdateRegion(destinationRect);
|
|
SkPaint paint = makeBitmapPaint();
|
|
paint.setBlendMode(blendMode);
|
|
paint.setShader(shader);
|
|
SkCanvas* canvas = getDrawCanvas();
|
|
// Scaling needs to be done explicitly using a matrix.
|
|
{
|
|
SkAutoCanvasRestore autoRestore(canvas, true);
|
|
SkMatrix matrix = SkMatrix::Translate(rPosAry.mnDestX, rPosAry.mnDestY)
|
|
* SkMatrix::Scale(1.0 * rPosAry.mnDestWidth / rPosAry.mnSrcWidth,
|
|
1.0 * rPosAry.mnDestHeight / rPosAry.mnSrcHeight)
|
|
* SkMatrix::Translate(-rPosAry.mnSrcX, -rPosAry.mnSrcY);
|
|
#ifndef NDEBUG
|
|
// Handle floating point imprecisions, round p1 to 2 decimal places.
|
|
auto compareRounded = [](const SkPoint& p1, const SkPoint& p2) {
|
|
return rtl::math::round(p1.x(), 2) == p2.x() && rtl::math::round(p1.y(), 2) == p2.y();
|
|
};
|
|
#endif
|
|
assert(compareRounded(matrix.mapXY(rPosAry.mnSrcX, rPosAry.mnSrcY),
|
|
SkPoint::Make(rPosAry.mnDestX, rPosAry.mnDestY)));
|
|
assert(compareRounded(
|
|
matrix.mapXY(rPosAry.mnSrcX + rPosAry.mnSrcWidth, rPosAry.mnSrcY + rPosAry.mnSrcHeight),
|
|
SkPoint::Make(rPosAry.mnDestX + rPosAry.mnDestWidth,
|
|
rPosAry.mnDestY + rPosAry.mnDestHeight)));
|
|
canvas->concat(matrix);
|
|
SkRect sourceRect = SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth,
|
|
rPosAry.mnSrcHeight);
|
|
canvas->drawRect(sourceRect, paint);
|
|
}
|
|
postDraw();
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::hasFastDrawTransformedBitmap() const
|
|
{
|
|
// Return true even in raster mode, even that way Skia is faster than e.g. GraphicObject
|
|
// trying to handle stuff manually.
|
|
return true;
|
|
}
|
|
|
|
// Whether applying matrix needs image smoothing for the transformation.
|
|
static bool matrixNeedsHighQuality(const SkMatrix& matrix)
|
|
{
|
|
if (matrix.isIdentity())
|
|
return false;
|
|
if (matrix.isScaleTranslate())
|
|
{
|
|
if (abs(matrix.getScaleX()) == 1 && abs(matrix.getScaleY()) == 1)
|
|
return false; // Only at most flipping and keeping the size.
|
|
return true;
|
|
}
|
|
assert(!matrix.hasPerspective()); // we do not use this
|
|
if (matrix.getScaleX() == 0 && matrix.getScaleY() == 0)
|
|
{
|
|
// Rotating 90 or 270 degrees while keeping the size.
|
|
if ((matrix.getSkewX() == 1 && matrix.getSkewY() == -1)
|
|
|| (matrix.getSkewX() == -1 && matrix.getSkewY() == 1))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
namespace SkiaTests
|
|
{
|
|
bool matrixNeedsHighQuality(const SkMatrix& matrix) { return ::matrixNeedsHighQuality(matrix); }
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawTransformedBitmap(const basegfx::B2DPoint& rNull,
|
|
const basegfx::B2DPoint& rX,
|
|
const basegfx::B2DPoint& rY,
|
|
const SalBitmap& rSourceBitmap,
|
|
const SalBitmap* pAlphaBitmap, double fAlpha)
|
|
{
|
|
assert(dynamic_cast<const SkiaSalBitmap*>(&rSourceBitmap));
|
|
assert(!pAlphaBitmap || dynamic_cast<const SkiaSalBitmap*>(pAlphaBitmap));
|
|
|
|
const SkiaSalBitmap& rSkiaBitmap = static_cast<const SkiaSalBitmap&>(rSourceBitmap);
|
|
const SkiaSalBitmap* pSkiaAlphaBitmap = static_cast<const SkiaSalBitmap*>(pAlphaBitmap);
|
|
|
|
if (pSkiaAlphaBitmap && pSkiaAlphaBitmap->IsFullyOpaqueAsAlpha())
|
|
pSkiaAlphaBitmap = nullptr; // the alpha can be ignored
|
|
|
|
// Setup the image transformation,
|
|
// using the rNull, rX, rY points as destinations for the (0,0), (Width,0), (0,Height) source points.
|
|
const basegfx::B2DVector aXRel = rX - rNull;
|
|
const basegfx::B2DVector aYRel = rY - rNull;
|
|
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "drawtransformedbitmap(" << this << "): " << rSourceBitmap.GetSize()
|
|
<< " " << rNull << ":" << rX << ":" << rY);
|
|
|
|
addUpdateRegion(SkRect::MakeWH(GetWidth(), GetHeight())); // can't tell, use whole area
|
|
// Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated
|
|
// alpha blending or scaling.
|
|
// The extra fAlpha blending is not cached, with the assumption that it usually gradually changes
|
|
// for each invocation.
|
|
// Pass size * mScaling to mergeCacheBitmaps() so that it prepares the size that will be needed
|
|
// after the mScaling-scaling matrix, but otherwise calculate everything else using the VCL coordinates.
|
|
Size imageSize(round(aXRel.getLength()), round(aYRel.getLength()));
|
|
sk_sp<SkImage> imageToDraw
|
|
= mergeCacheBitmaps(rSkiaBitmap, pSkiaAlphaBitmap, imageSize * mScaling);
|
|
if (imageToDraw)
|
|
{
|
|
SkMatrix matrix;
|
|
// Round sizes for scaling, so that sub-pixel differences don't
|
|
// trigger unnecessary scaling. Image has already been scaled
|
|
// by mergeCacheBitmaps() and we shouldn't scale here again
|
|
// unless the drawing is also skewed.
|
|
matrix.set(SkMatrix::kMScaleX, round(aXRel.getX()) / imageSize.Width());
|
|
matrix.set(SkMatrix::kMScaleY, round(aYRel.getY()) / imageSize.Height());
|
|
matrix.set(SkMatrix::kMSkewY, aXRel.getY() / imageSize.Width());
|
|
matrix.set(SkMatrix::kMSkewX, aYRel.getX() / imageSize.Height());
|
|
matrix.set(SkMatrix::kMTransX, rNull.getX());
|
|
matrix.set(SkMatrix::kMTransY, rNull.getY());
|
|
SkCanvas* canvas = getDrawCanvas();
|
|
SkAutoCanvasRestore autoRestore(canvas, true);
|
|
canvas->concat(matrix);
|
|
SkSamplingOptions samplingOptions;
|
|
// If the matrix changes geometry, we need to smooth-scale. If there's mScaling,
|
|
// that's already been handled by mergeCacheBitmaps().
|
|
if (matrixNeedsHighQuality(matrix))
|
|
samplingOptions = makeSamplingOptions(matrix, 1);
|
|
if (fAlpha == 1.0)
|
|
{
|
|
// Specify sizes to scale the image size back if needed (because of mScaling).
|
|
SkRect dstRect = SkRect::MakeWH(imageSize.Width(), imageSize.Height());
|
|
SkRect srcRect = SkRect::MakeWH(imageToDraw->width(), imageToDraw->height());
|
|
SkPaint paint = makeBitmapPaint();
|
|
canvas->drawImageRect(imageToDraw, srcRect, dstRect, samplingOptions, &paint,
|
|
SkCanvas::kFast_SrcRectConstraint);
|
|
}
|
|
else
|
|
{
|
|
SkPaint paint = makeBitmapPaint();
|
|
// Scale the image size back if needed.
|
|
SkMatrix scale = SkMatrix::Scale(1.0 / mScaling, 1.0 / mScaling);
|
|
paint.setShader(SkShaders::Blend(
|
|
SkBlendMode::kDstIn, imageToDraw->makeShader(samplingOptions, &scale),
|
|
SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0))));
|
|
canvas->drawRect(SkRect::MakeWH(imageSize.Width(), imageSize.Height()), paint);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SkMatrix matrix;
|
|
const Size aSize = rSourceBitmap.GetSize();
|
|
matrix.set(SkMatrix::kMScaleX, aXRel.getX() / aSize.Width());
|
|
matrix.set(SkMatrix::kMScaleY, aYRel.getY() / aSize.Height());
|
|
matrix.set(SkMatrix::kMSkewY, aXRel.getY() / aSize.Width());
|
|
matrix.set(SkMatrix::kMSkewX, aYRel.getX() / aSize.Height());
|
|
matrix.set(SkMatrix::kMTransX, rNull.getX());
|
|
matrix.set(SkMatrix::kMTransY, rNull.getY());
|
|
SkCanvas* canvas = getDrawCanvas();
|
|
SkAutoCanvasRestore autoRestore(canvas, true);
|
|
canvas->concat(matrix);
|
|
SkSamplingOptions samplingOptions;
|
|
if (matrixNeedsHighQuality(matrix) || (mScaling != 1 && !isUnitTestRunning()))
|
|
samplingOptions = makeSamplingOptions(matrix, mScaling);
|
|
if (pSkiaAlphaBitmap)
|
|
{
|
|
SkPaint paint = makeBitmapPaint();
|
|
paint.setShader(SkShaders::Blend(SkBlendMode::kDstIn,
|
|
rSkiaBitmap.GetSkShader(samplingOptions),
|
|
pSkiaAlphaBitmap->GetAlphaSkShader(samplingOptions)));
|
|
if (fAlpha != 1.0)
|
|
paint.setShader(
|
|
SkShaders::Blend(SkBlendMode::kDstIn, paint.refShader(),
|
|
SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0))));
|
|
canvas->drawRect(SkRect::MakeWH(aSize.Width(), aSize.Height()), paint);
|
|
}
|
|
else if (rSkiaBitmap.PreferSkShader() || fAlpha != 1.0)
|
|
{
|
|
SkPaint paint = makeBitmapPaint();
|
|
paint.setShader(rSkiaBitmap.GetSkShader(samplingOptions));
|
|
if (fAlpha != 1.0)
|
|
paint.setShader(
|
|
SkShaders::Blend(SkBlendMode::kDstIn, paint.refShader(),
|
|
SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0))));
|
|
canvas->drawRect(SkRect::MakeWH(aSize.Width(), aSize.Height()), paint);
|
|
}
|
|
else
|
|
{
|
|
SkPaint paint = makeBitmapPaint();
|
|
canvas->drawImage(rSkiaBitmap.GetSkImage(), 0, 0, samplingOptions, &paint);
|
|
}
|
|
}
|
|
postDraw();
|
|
return true;
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth,
|
|
tools::Long nHeight, sal_uInt8 nTransparency)
|
|
{
|
|
privateDrawAlphaRect(nX, nY, nWidth, nHeight, nTransparency / 100.0);
|
|
return true;
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::drawGradient(const tools::PolyPolygon& rPolyPolygon,
|
|
const Gradient& rGradient)
|
|
{
|
|
if (rGradient.GetStyle() != css::awt::GradientStyle_LINEAR
|
|
&& rGradient.GetStyle() != css::awt::GradientStyle_AXIAL
|
|
&& rGradient.GetStyle() != css::awt::GradientStyle_RADIAL)
|
|
return false; // unsupported
|
|
if (rGradient.GetSteps() != 0)
|
|
return false; // We can't tell Skia how many colors to use in the gradient.
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace", "drawgradient(" << this << "): " << rPolyPolygon.getB2DPolyPolygon()
|
|
<< ":" << static_cast<int>(rGradient.GetStyle()));
|
|
tools::Rectangle boundRect(rPolyPolygon.GetBoundRect());
|
|
if (boundRect.IsEmpty())
|
|
return true;
|
|
SkPath path;
|
|
if (rPolyPolygon.IsRect())
|
|
{
|
|
// Rect->Polygon conversion loses the right and bottom edge, fix that.
|
|
path.addRect(SkRect::MakeXYWH(boundRect.getX(), boundRect.getY(), boundRect.GetWidth(),
|
|
boundRect.GetHeight()));
|
|
boundRect.AdjustRight(1);
|
|
boundRect.AdjustBottom(1);
|
|
}
|
|
else
|
|
addPolyPolygonToPath(rPolyPolygon.getB2DPolyPolygon(), path);
|
|
path.setFillType(SkPathFillType::kEvenOdd);
|
|
addUpdateRegion(path.getBounds());
|
|
|
|
Gradient aGradient(rGradient);
|
|
tools::Rectangle aBoundRect;
|
|
Point aCenter;
|
|
aGradient.SetAngle(aGradient.GetAngle() + 2700_deg10);
|
|
aGradient.GetBoundRect(boundRect, aBoundRect, aCenter);
|
|
|
|
SkColor startColor
|
|
= toSkColorWithIntensity(rGradient.GetStartColor(), rGradient.GetStartIntensity());
|
|
SkColor endColor = toSkColorWithIntensity(rGradient.GetEndColor(), rGradient.GetEndIntensity());
|
|
|
|
sk_sp<SkShader> shader;
|
|
if (rGradient.GetStyle() == css::awt::GradientStyle_LINEAR)
|
|
{
|
|
tools::Polygon aPoly(aBoundRect);
|
|
aPoly.Rotate(aCenter, aGradient.GetAngle() % 3600_deg10);
|
|
SkPoint points[2] = { SkPoint::Make(toSkX(aPoly[0].X()), toSkY(aPoly[0].Y())),
|
|
SkPoint::Make(toSkX(aPoly[1].X()), toSkY(aPoly[1].Y())) };
|
|
SkColor colors[2] = { startColor, endColor };
|
|
SkScalar pos[2] = { SkDoubleToScalar(aGradient.GetBorder() / 100.0), 1.0 };
|
|
shader = SkGradientShader::MakeLinear(points, colors, pos, 2, SkTileMode::kClamp);
|
|
}
|
|
else if (rGradient.GetStyle() == css::awt::GradientStyle_AXIAL)
|
|
{
|
|
tools::Polygon aPoly(aBoundRect);
|
|
aPoly.Rotate(aCenter, aGradient.GetAngle() % 3600_deg10);
|
|
SkPoint points[2] = { SkPoint::Make(toSkX(aPoly[0].X()), toSkY(aPoly[0].Y())),
|
|
SkPoint::Make(toSkX(aPoly[1].X()), toSkY(aPoly[1].Y())) };
|
|
SkColor colors[3] = { endColor, startColor, endColor };
|
|
SkScalar border = SkDoubleToScalar(aGradient.GetBorder() / 100.0);
|
|
SkScalar pos[3] = { std::min<SkScalar>(border * 0.5f, 0.5f), 0.5f,
|
|
std::max<SkScalar>(1 - border * 0.5f, 0.5f) };
|
|
shader = SkGradientShader::MakeLinear(points, colors, pos, 3, SkTileMode::kClamp);
|
|
}
|
|
else
|
|
{
|
|
// Move the center by (-1,-1) (the default VCL algorithm is a bit off-center that way,
|
|
// Skia is the opposite way).
|
|
SkPoint center = SkPoint::Make(toSkX(aCenter.X()) - 1, toSkY(aCenter.Y()) - 1);
|
|
SkScalar radius = std::max(aBoundRect.GetWidth() / 2.0, aBoundRect.GetHeight() / 2.0);
|
|
SkColor colors[2] = { endColor, startColor };
|
|
SkScalar pos[2] = { SkDoubleToScalar(aGradient.GetBorder() / 100.0), 1.0 };
|
|
shader = SkGradientShader::MakeRadial(center, radius, colors, pos, 2, SkTileMode::kClamp);
|
|
}
|
|
|
|
SkPaint paint = makeGradientPaint();
|
|
paint.setAntiAlias(mParent.getAntiAlias());
|
|
paint.setShader(shader);
|
|
getDrawCanvas()->drawPath(path, paint);
|
|
postDraw();
|
|
return true;
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::implDrawGradient(const basegfx::B2DPolyPolygon& rPolyPolygon,
|
|
const SalGradient& rGradient)
|
|
{
|
|
preDraw();
|
|
SAL_INFO("vcl.skia.trace",
|
|
"impldrawgradient(" << this << "): " << rPolyPolygon << ":" << rGradient.maPoint1
|
|
<< "->" << rGradient.maPoint2 << ":" << rGradient.maStops.size());
|
|
|
|
SkPath path;
|
|
addPolyPolygonToPath(rPolyPolygon, path);
|
|
path.setFillType(SkPathFillType::kEvenOdd);
|
|
addUpdateRegion(path.getBounds());
|
|
|
|
SkPoint points[2]
|
|
= { SkPoint::Make(toSkX(rGradient.maPoint1.getX()), toSkY(rGradient.maPoint1.getY())),
|
|
SkPoint::Make(toSkX(rGradient.maPoint2.getX()), toSkY(rGradient.maPoint2.getY())) };
|
|
std::vector<SkColor> colors;
|
|
std::vector<SkScalar> pos;
|
|
for (const SalGradientStop& stop : rGradient.maStops)
|
|
{
|
|
colors.emplace_back(toSkColor(stop.maColor));
|
|
pos.emplace_back(stop.mfOffset);
|
|
}
|
|
sk_sp<SkShader> shader = SkGradientShader::MakeLinear(points, colors.data(), pos.data(),
|
|
colors.size(), SkTileMode::kDecal);
|
|
SkPaint paint = makeGradientPaint();
|
|
paint.setAntiAlias(mParent.getAntiAlias());
|
|
paint.setShader(shader);
|
|
getDrawCanvas()->drawPath(path, paint);
|
|
postDraw();
|
|
return true;
|
|
}
|
|
|
|
static double toRadian(Degree10 degree10th) { return toRadians(3600_deg10 - degree10th); }
|
|
static auto toCos(Degree10 degree10th) { return SkScalarCos(toRadian(degree10th)); }
|
|
static auto toSin(Degree10 degree10th) { return SkScalarSin(toRadian(degree10th)); }
|
|
|
|
void SkiaSalGraphicsImpl::drawGenericLayout(const GenericSalLayout& layout, Color textColor,
|
|
const SkFont& font, const SkFont& verticalFont)
|
|
{
|
|
SkiaZone zone;
|
|
std::vector<SkGlyphID> glyphIds;
|
|
std::vector<SkRSXform> glyphForms;
|
|
std::vector<bool> verticals;
|
|
glyphIds.reserve(256);
|
|
glyphForms.reserve(256);
|
|
verticals.reserve(256);
|
|
basegfx::B2DPoint aPos;
|
|
const GlyphItem* pGlyph;
|
|
int nStart = 0;
|
|
auto cos = toCos(layout.GetOrientation());
|
|
auto sin = toSin(layout.GetOrientation());
|
|
while (layout.GetNextGlyph(&pGlyph, aPos, nStart))
|
|
{
|
|
glyphIds.push_back(pGlyph->glyphId());
|
|
verticals.emplace_back(pGlyph->IsVertical());
|
|
auto cos1 = pGlyph->IsVertical() ? sin : cos; // cos (x - 90) = sin (x)
|
|
auto sin1 = pGlyph->IsVertical() ? -cos : sin; // sin (x - 90) = -cos (x)
|
|
SkRSXform form = SkRSXform::Make(cos1, sin1, aPos.getX(), aPos.getY());
|
|
glyphForms.emplace_back(std::move(form));
|
|
}
|
|
if (glyphIds.empty())
|
|
return;
|
|
|
|
preDraw();
|
|
auto getBoundRect = [&layout]() {
|
|
basegfx::B2DRectangle rect;
|
|
layout.GetBoundRect(rect);
|
|
return rect;
|
|
};
|
|
SAL_INFO("vcl.skia.trace", "drawtextblob(" << this << "): " << getBoundRect() << ", "
|
|
<< glyphIds.size() << " glyphs, " << textColor);
|
|
|
|
// Vertical glyphs need a different font, so split drawing into runs that each
|
|
// draw only consecutive horizontal or vertical glyphs.
|
|
std::vector<bool>::const_iterator pos = verticals.cbegin();
|
|
std::vector<bool>::const_iterator end = verticals.cend();
|
|
while (pos != end)
|
|
{
|
|
bool verticalRun = *pos;
|
|
std::vector<bool>::const_iterator rangeEnd = std::find(pos + 1, end, !verticalRun);
|
|
size_t index = pos - verticals.cbegin();
|
|
size_t count = rangeEnd - pos;
|
|
sk_sp<SkTextBlob> textBlob = SkTextBlob::MakeFromRSXform(
|
|
glyphIds.data() + index, count * sizeof(SkGlyphID), glyphForms.data() + index,
|
|
verticalRun ? verticalFont : font, SkTextEncoding::kGlyphID);
|
|
addUpdateRegion(textBlob->bounds());
|
|
SkPaint paint = makeTextPaint(textColor);
|
|
getDrawCanvas()->drawTextBlob(textBlob, 0, 0, paint);
|
|
pos = rangeEnd;
|
|
}
|
|
postDraw();
|
|
}
|
|
|
|
bool SkiaSalGraphicsImpl::supportsOperation(OutDevSupportType eType) const
|
|
{
|
|
switch (eType)
|
|
{
|
|
case OutDevSupportType::TransparentRect:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static int getScaling()
|
|
{
|
|
// It makes sense to support the debugging flag on all platforms
|
|
// for unittests purpose, even if the actual windows cannot do it.
|
|
if (const char* env = getenv("SAL_FORCE_HIDPI_SCALING"))
|
|
return atoi(env);
|
|
return 1;
|
|
}
|
|
|
|
int SkiaSalGraphicsImpl::getWindowScaling() const
|
|
{
|
|
static const int scaling = getScaling();
|
|
return scaling;
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::dump(const char* file) const
|
|
{
|
|
assert(mSurface.get());
|
|
SkiaHelper::dump(mSurface, file);
|
|
}
|
|
|
|
void SkiaSalGraphicsImpl::windowBackingPropertiesChanged()
|
|
{
|
|
if (mInWindowBackingPropertiesChanged || !isGPU())
|
|
return;
|
|
|
|
mInWindowBackingPropertiesChanged = true;
|
|
performFlush();
|
|
mInWindowBackingPropertiesChanged = false;
|
|
}
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|