Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/LibreOffice/vcl/skia/   (Office von Apache Version 25.8.3.2©)  Datei vom 5.10.2025 mit Größe 96 kB image not shown  

Quelle  gdiimpl.cxx   Sprache: 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 <tools/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 <ganesh/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&&nbsp;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)
    {
#ifdef MACOSX
        // tdf#165277 Skia needs to flush immediately before POST_PAINT
        // tasks on macOS
        SetPriority(TaskPriority::SKIA_FLUSH);
#else
        // We don't want to be swapping before we've painted.
        SetPriority(TaskPriority::POST_PAINT);
#endif
    }
#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();
#ifdef MACOSX
        // tdf#157312 and tdf#163945 Lower Skia flush timer priority on macOS
        // On macOS, flushing with Skia/Metal is noticeably slower than
        // with Skia/Raster. So lower the flush timer priority to
        // TaskPriority::SKIA_FLUSH so that the flush timer runs less
        // frequently but each pass copies a more up-to-date offscreen
        // surface.
        // tdf#165277 Skia needs to flush immediately before POST_PAINT
        // tasks on macOS
        SetPriority(TaskPriority::SKIA_FLUSH);
#else
        SetPriority(TaskPriority::HIGHEST);
#endif
    }
};

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::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();
#ifdef MACOSX
    // tdf#165277 Skia needs to flush immediately before POST_PAINT
    // tasks on macOS
    mFlush->SetPriority(TaskPriority::SKIA_FLUSH);
#else
    mFlush->SetPriority(TaskPriority::POST_PAINT);
#endif
}

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);
        // Otherwise the window is not drawn sometimes.
        if (auto dContext = GrAsDirectContext(screenSurface->getCanvas()->recordingContext()))
            dContext->flushAndSubmit();
        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)
    {
        if (auto dContext = GrAsDirectContext(mSurface->getCanvas()->recordingContext()))
            dContext->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&&nbsp;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:
        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:
        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());
#ifdef MACOSX
        // tdf#162646 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 and antialiasing is disabled.
        if (mScaling != 1 && (isUnitTestRunning() || !paint.isAntiAlias()))
#else
        if (mScaling != 1 && isUnitTestRunning())
#endif
        {
            // 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)
    {
#ifdef MACOSX
        // tdf#162646 don't move orthogonal polypolygons when scaling
        // Previously, polypolygons would be moved slightly but this causes
        // misdrawing of orthogonal polypolygons (i.e. polypolygons with only
        // vertical and horizontal lines) when using a Retina display on
        // macOS and antialiasing is disabled.
        if ((!isUnitTestRunning() && (useAA || !hasOnlyOrthogonal)) || getWindowScaling() == 1)
#else
        // 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)
#endif
        {
            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();
#ifdef MACOSX
    // tdf#162646 suppressing drawing hairlines when scaling
    // Previously, drawing of hairlines (i.e. zero line width) was only
    // suppressed when running unit tests. But drawing hairlines causes
    // unexpected shifting of the lines when using a Retina display on
    // macOS and antialiasing is disabled.
    if (fLineWidth == 0 && mScaling != 1 && (isUnitTestRunning() || !mParent.getAntiAlias()))
#else
    // 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())
#endif
        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, const 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&&nbsp;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&&nbsp;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&&nbsp;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);
            static 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;
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=93 H=95 G=93

¤ Dauer der Verarbeitung: 0.18 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.