diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
commit | 940b4d1848e8c70ab7642901a68594e8016caffc (patch) | |
tree | eb72f344ee6c3d9b80a7ecc079ea79e9fba8676d /vcl/skia/gdiimpl.cxx | |
parent | Initial commit. (diff) | |
download | libreoffice-upstream.tar.xz libreoffice-upstream.zip |
Adding upstream version 1:7.0.4.upstream/1%7.0.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | vcl/skia/gdiimpl.cxx | 1829 |
1 files changed, 1829 insertions, 0 deletions
diff --git a/vcl/skia/gdiimpl.cxx b/vcl/skia/gdiimpl.cxx new file mode 100644 index 000000000..bd497d7f8 --- /dev/null +++ b/vcl/skia/gdiimpl.cxx @@ -0,0 +1,1829 @@ +/* -*- 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/skia/SkiaHelper.hxx> +#include <skia/utils.hxx> +#include <skia/zone.hxx> + +#include <SkCanvas.h> +#include <SkPath.h> +#include <SkRegion.h> +#include <SkDashPathEffect.h> +#include <GrBackendSurface.h> +#include <SkTextBlob.h> +#include <SkRSXform.h> + +#include <numeric> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> +#include <o3tl/sorted_vector.hxx> + +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, + bool* hasOnlyOrthogonal = nullptr) +{ + const sal_uInt32 nPointCount(rPolygon.count()); + + if (nPointCount <= 1) + return; + + const bool bClosePath(rPolygon.isClosed()); + const bool bHasCurves(rPolygon.areControlPointsUsed()); + + bool bFirst = true; + + 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; + 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)) + { + 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) + { + rPath.close(); + } +} + +void addPolyPolygonToPath(const basegfx::B2DPolyPolygon& rPolyPolygon, SkPath& rPath, + bool* hasOnlyOrthogonal = nullptr) +{ + const sal_uInt32 nPolygonCount(rPolyPolygon.count()); + + if (nPolygonCount == 0) + return; + + 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 +} + +SkColor toSkColor(Color color) +{ + return SkColorSetARGB(255 - color.GetTransparency(), color.GetRed(), color.GetGreen(), + color.GetBlue()); +} + +SkColor toSkColorWithTransparency(Color aColor, double fTransparency) +{ + return SkColorSetA(toSkColor(aColor), 255 * (1.0 - fTransparency)); +} + +Color fromSkColor(SkColor color) +{ + return Color(255 - SkColorGetA(color), SkColorGetR(color), SkColorGetG(color), + SkColorGetB(color)); +} + +// 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; +} + +} // 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); } + const char* get_debug_name(SkiaSalGraphicsImpl* pGraphics) + { + // 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 + const char* get_debug_name(SkiaSalGraphicsImpl*) { return "skia idle"; } +#endif + + virtual void Invoke() override + { + mpGraphics->performFlush(); + Stop(); + SetPriority(TaskPriority::HIGHEST); + } +}; + +SkiaSalGraphicsImpl::SkiaSalGraphicsImpl(SalGraphics& rParent, SalGeometryProvider* pProvider) + : mParent(rParent) + , mProvider(pProvider) + , mIsGPU(false) + , mLineColor(SALCOLOR_NONE) + , mFillColor(SALCOLOR_NONE) + , mXorMode(false) + , mFlush(new SkiaFlushIdle(this)) +{ +} + +SkiaSalGraphicsImpl::~SkiaSalGraphicsImpl() +{ + assert(!mSurface); + assert(!mWindowContext); +} + +void SkiaSalGraphicsImpl::Init() {} + +void SkiaSalGraphicsImpl::createSurface() +{ + SkiaZone zone; + if (isOffscreen()) + createOffscreenSurface(); + else + createWindowSurface(); + mSurface->getCanvas()->save(); // see SetClipRegion() + mClipRegion = vcl::Region(tools::Rectangle(0, 0, GetWidth(), GetHeight())); + + // 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); + assert(!mWindowContext); + createWindowContext(forceRaster); + if (mWindowContext) + mSurface = mWindowContext->getBackbufferSurface(); + if (!mSurface) + { + switch (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::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 SkiaHelper::RenderRaster: + abort(); // This should not really happen, do not even try to cope with it. + } + } + mIsGPU = mSurface->getCanvas()->getGrContext() != nullptr; +#ifdef DBG_UTIL + SkiaHelper::prefillSurface(mSurface); +#endif +} + +void SkiaSalGraphicsImpl::createOffscreenSurface() +{ + SkiaZone zone; + assert(isOffscreen()); + assert(!mSurface); + assert(!mWindowContext); + // When created (especially on Windows), Init() gets called with size (0,0), which is invalid size + // for Skia. May happen also in rare cases such as shutting down (tdf#131939). + int width = std::max(1, GetWidth()); + int height = std::max(1, GetHeight()); + switch (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::RenderVulkan: + { + if (SkiaHelper::getSharedGrContext()) + { + mSurface = SkiaHelper::createSkSurface(width, height); + if (mSurface) + { + mIsGPU = mSurface->getCanvas()->getGrContext() != nullptr; + return; + } + } + break; + } + default: + break; + } + // Create raster surface as a fallback. + mSurface = SkiaHelper::createSkSurface(width, height); + assert(mSurface); + assert(!mSurface->getCanvas()->getGrContext()); // is not GPU-backed + mIsGPU = false; +} + +void SkiaSalGraphicsImpl::destroySurface() +{ + SkiaZone zone; + if (mSurface) + { + // check setClipRegion() invariant + assert(mSurface->getCanvas()->getSaveCount() == 2); + // if this fails, something forgot to use SkAutoCanvasRestore + assert(mSurface->getCanvas()->getTotalMatrix().isIdentity()); + } + // If we use e.g. Vulkan, we must destroy the surface before the context, + // otherwise destroying the surface will reference the context. This is + // handled by calling destroySurface() before destroying the context. + // However we also need to flush the surface before destroying it, + // otherwise when destroying the context later there still could be queued + // commands referring to the surface data. This is probably a Skia bug, + // but work around it here. + if (mSurface) + mSurface->flushAndSubmit(); + mSurface.reset(); + mWindowContext.reset(); + mIsGPU = false; +} + +void SkiaSalGraphicsImpl::DeInit() { destroySurface(); } + +void SkiaSalGraphicsImpl::preDraw() +{ + assert(comphelper::SolarMutex::get()->IsCurrentThread()); + SkiaZone::enter(); // matched in postDraw() + checkSurface(); + checkPendingDrawing(); +} + +void SkiaSalGraphicsImpl::postDraw() +{ + scheduleFlush(); + SkiaZone::leave(); // matched in preDraw() + // If there's a problem with the GPU context, abort. + if (GrContext* context = mSurface->getCanvas()->getGrContext()) + { + // Running out of memory on the GPU technically could be possibly recoverable, + // but we don't know the exact status of the surface (and what has or has not been drawn to it), + // so in practice this is unrecoverable without possible data loss. + if (context->oomed()) + { + 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 (GetWidth() != mSurface->width() || GetHeight() != mSurface->height()) + { + if (avoidRecreateByResize()) + return; + + if (!GetWidth() || !GetHeight()) + { + SAL_WARN("vcl.skia", "recreate(" << this << "): can't create empty surface " + << Size(GetWidth(), GetHeight()) + << " => keeping old one!"); + return; + } + + { + 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 = SkiaHelper::makeCheckedImageSnapshot(mSurface); + } + + destroySurface(); + createSurface(); + + if (snapshot) + { + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is + mSurface->getCanvas()->drawImage(snapshot, 0, 0, &paint); + } + SAL_INFO("vcl.skia.trace", "recreate(" << this << "): old " << oldSize << " new " + << Size(mSurface->width(), mSurface->height()) + << " requested " + << Size(GetWidth(), GetHeight())); + } + } +} + +void SkiaSalGraphicsImpl::flushDrawing() +{ + if (!mSurface) + return; + checkPendingDrawing(); + if (mXorMode) + applyXor(); + mSurface->flushAndSubmit(); +} + +bool SkiaSalGraphicsImpl::setClipRegion(const vcl::Region& region) +{ + if (mClipRegion == region) + return true; + SkiaZone zone; + checkPendingDrawing(); + checkSurface(); + mClipRegion = region; + SAL_INFO("vcl.skia.trace", "setclipregion(" << this << "): " << region); + SkCanvas* canvas = mSurface->getCanvas(); + // 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. + assert(canvas->getSaveCount() == 2); // = there is just one save() + canvas->restore(); + canvas->save(); + setCanvasClipRegion(canvas, region); + return true; +} + +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); + 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; } + +long SkiaSalGraphicsImpl::GetGraphicsWidth() const { return GetWidth(); } + +void SkiaSalGraphicsImpl::SetLineColor() +{ + checkPendingDrawing(); + mLineColor = SALCOLOR_NONE; +} + +void SkiaSalGraphicsImpl::SetLineColor(Color nColor) +{ + checkPendingDrawing(); + mLineColor = nColor; +} + +void SkiaSalGraphicsImpl::SetFillColor() +{ + checkPendingDrawing(); + mFillColor = SALCOLOR_NONE; +} + +void SkiaSalGraphicsImpl::SetFillColor(Color nColor) +{ + checkPendingDrawing(); + mFillColor = nColor; +} + +void SkiaSalGraphicsImpl::SetXORMode(bool set, bool) +{ + if (mXorMode == set) + return; + checkPendingDrawing(); + SAL_INFO("vcl.skia.trace", "setxormode(" << this << "): " << set); + if (set) + mXorRegion.setEmpty(); + else + applyXor(); + mXorMode = set; +} + +SkCanvas* SkiaSalGraphicsImpl::getXorCanvas() +{ + SkiaZone zone; + assert(mXorMode); + // Skia does not implement xor drawing, so we need to handle it manually by redirecting + // to a temporary SkBitmap and then doing the xor operation on the data ourselves. + // There's no point in using SkSurface for GPU, we'd immediately need to get the pixels back. + if (!mXorCanvas) + { + // Use unpremultiplied alpha (see xor applying in applyXor()). + if (!mXorBitmap.tryAllocPixels(mSurface->imageInfo().makeAlphaType(kUnpremul_SkAlphaType))) + abort(); + mXorBitmap.eraseARGB(0, 0, 0, 0); + mXorCanvas = std::make_unique<SkCanvas>(mXorBitmap); + setCanvasClipRegion(mXorCanvas.get(), mClipRegion); + } + return mXorCanvas.get(); +} + +void SkiaSalGraphicsImpl::applyXor() +{ + // Apply the result from the temporary bitmap manually. This is indeed + // slow, but it doesn't seem to be needed often and is optimized + // in each operation by extending mXorRegion with the area that should be + // updated. + assert(mXorMode); + if (!mSurface || !mXorCanvas + || !mXorRegion.op(SkIRect::MakeXYWH(0, 0, mSurface->width(), mSurface->height()), + SkRegion::kIntersect_Op)) + { + mXorRegion.setEmpty(); + return; + } + SAL_INFO("vcl.skia.trace", "applyxor(" << this << "): " << mXorRegion); + // Copy the surface contents to another pixmap. + SkBitmap surfaceBitmap; + // Use unpremultiplied alpha format, so that we do not have to do the conversions to get + // the RGB and back (Skia will do it when converting, but it'll be presumably faster at it). + if (!surfaceBitmap.tryAllocPixels(mSurface->imageInfo().makeAlphaType(kUnpremul_SkAlphaType))) + abort(); + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is + SkCanvas canvas(surfaceBitmap); + canvas.drawImageRect(SkiaHelper::makeCheckedImageSnapshot(mSurface), mXorRegion.getBounds(), + SkRect::Make(mXorRegion.getBounds()), &paint); + // xor to surfaceBitmap + assert(surfaceBitmap.info().alphaType() == kUnpremul_SkAlphaType); + assert(mXorBitmap.info().alphaType() == kUnpremul_SkAlphaType); + assert(surfaceBitmap.bytesPerPixel() == 4); + assert(mXorBitmap.bytesPerPixel() == 4); + for (SkRegion::Iterator it(mXorRegion); !it.done(); it.next()) + { + for (int y = it.rect().top(); y < it.rect().bottom(); ++y) + { + uint8_t* data = static_cast<uint8_t*>(surfaceBitmap.getAddr(it.rect().x(), y)); + const uint8_t* xordata = static_cast<uint8_t*>(mXorBitmap.getAddr(it.rect().x(), y)); + for (int x = 0; x < it.rect().width(); ++x) + { + *data++ ^= *xordata++; + *data++ ^= *xordata++; + *data++ ^= *xordata++; + // alpha is not xor-ed + data++; + xordata++; + } + } + } + surfaceBitmap.notifyPixelsChanged(); + mSurface->getCanvas()->drawBitmapRect(surfaceBitmap, mXorRegion.getBounds(), + SkRect::Make(mXorRegion.getBounds()), &paint); + mXorCanvas.reset(); + mXorBitmap.reset(); + mXorRegion.setEmpty(); +} + +void SkiaSalGraphicsImpl::SetROPLineColor(SalROPColor nROPColor) +{ + checkPendingDrawing(); + switch (nROPColor) + { + case SalROPColor::N0: + mLineColor = Color(0, 0, 0); + break; + case SalROPColor::N1: + mLineColor = Color(0xff, 0xff, 0xff); + break; + case SalROPColor::Invert: + mLineColor = Color(0xff, 0xff, 0xff); + break; + } +} + +void SkiaSalGraphicsImpl::SetROPFillColor(SalROPColor nROPColor) +{ + checkPendingDrawing(); + switch (nROPColor) + { + case SalROPColor::N0: + mFillColor = Color(0, 0, 0); + break; + case SalROPColor::N1: + mFillColor = Color(0xff, 0xff, 0xff); + break; + case SalROPColor::Invert: + mFillColor = Color(0xff, 0xff, 0xff); + break; + } +} + +void SkiaSalGraphicsImpl::drawPixel(long nX, long nY) { drawPixel(nX, nY, mLineColor); } + +void SkiaSalGraphicsImpl::drawPixel(long nX, long nY, Color nColor) +{ + if (nColor == SALCOLOR_NONE) + return; + preDraw(); + SAL_INFO("vcl.skia.trace", "drawpixel(" << this << "): " << Point(nX, nY) << ":" << nColor); + addXorRegion(SkRect::MakeXYWH(nX, nY, 1, 1)); + SkPaint paint; + paint.setColor(toSkColor(nColor)); + // Apparently drawPixel() is actually expected to set the pixel and not draw it. + paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha + getDrawCanvas()->drawPoint(toSkX(nX), toSkY(nY), paint); + postDraw(); +} + +void SkiaSalGraphicsImpl::drawLine(long nX1, long nY1, long nX2, long nY2) +{ + if (mLineColor == SALCOLOR_NONE) + return; + preDraw(); + SAL_INFO("vcl.skia.trace", "drawline(" << this << "): " << Point(nX1, nY1) << "->" + << Point(nX2, nY2) << ":" << mLineColor); + addXorRegion(SkRect::MakeLTRB(nX1, nY1, nX2, nY2).makeSorted()); + SkPaint paint; + paint.setColor(toSkColor(mLineColor)); + paint.setAntiAlias(mParent.getAntiAliasB2DDraw()); + getDrawCanvas()->drawLine(toSkX(nX1), toSkY(nY1), toSkX(nX2), toSkY(nY2), paint); + postDraw(); +} + +void SkiaSalGraphicsImpl::privateDrawAlphaRect(long nX, long nY, long nWidth, long nHeight, + double fTransparency, bool blockAA) +{ + preDraw(); + SAL_INFO("vcl.skia.trace", + "privatedrawrect(" << this << "): " << SkIRect::MakeXYWH(nX, nY, nWidth, nHeight) + << ":" << mLineColor << ":" << mFillColor << ":" << fTransparency); + addXorRegion(SkRect::MakeXYWH(nX, nY, nWidth, nHeight)); + SkCanvas* canvas = getDrawCanvas(); + SkPaint paint; + paint.setAntiAlias(!blockAA && mParent.getAntiAliasB2DDraw()); + if (mFillColor != SALCOLOR_NONE) + { + paint.setColor(toSkColorWithTransparency(mFillColor, fTransparency)); + paint.setStyle(SkPaint::kFill_Style); + // 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 (mLineColor == SALCOLOR_NONE && SkSize::Make(nWidth, nHeight).isEmpty()) + paint.setStyle(SkPaint::kStroke_Style); + canvas->drawIRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), paint); + } + if (mLineColor != SALCOLOR_NONE) + { + paint.setColor(toSkColorWithTransparency(mLineColor, fTransparency)); + paint.setStyle(SkPaint::kStroke_Style); + // 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->drawIRect( + SkIRect::MakeXYWH(nX, nY, std::max(1L, nWidth - 1), std::max(1L, nHeight - 1)), paint); + } + postDraw(); +} + +void SkiaSalGraphicsImpl::drawRect(long nX, long nY, long nWidth, long nHeight) +{ + privateDrawAlphaRect(nX, nY, nWidth, nHeight, 0.0, true); +} + +void SkiaSalGraphicsImpl::drawPolyLine(sal_uInt32 nPoints, const SalPoint* pPtAry) +{ + basegfx::B2DPolygon aPolygon; + aPolygon.append(basegfx::B2DPoint(pPtAry->mnX, pPtAry->mnY), nPoints); + for (sal_uInt32 i = 1; i < nPoints; ++i) + aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].mnX, pPtAry[i].mnY)); + 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 SalPoint* pPtAry) +{ + basegfx::B2DPolygon aPolygon; + aPolygon.append(basegfx::B2DPoint(pPtAry->mnX, pPtAry->mnY), nPoints); + for (sal_uInt32 i = 1; i < nPoints; ++i) + aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].mnX, pPtAry[i].mnY)); + + drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aPolygon), 0.0); +} + +void SkiaSalGraphicsImpl::drawPolyPolygon(sal_uInt32 nPoly, const sal_uInt32* pPoints, + PCONSTSALPOINT* pPtAry) +{ + basegfx::B2DPolyPolygon aPolyPolygon; + for (sal_uInt32 nPolygon = 0; nPolygon < nPoly; ++nPolygon) + { + sal_uInt32 nPoints = pPoints[nPolygon]; + if (nPoints) + { + PCONSTSALPOINT pSalPoints = pPtAry[nPolygon]; + basegfx::B2DPolygon aPolygon; + aPolygon.append(basegfx::B2DPoint(pSalPoints->mnX, pSalPoints->mnY), nPoints); + for (sal_uInt32 i = 1; i < nPoints; ++i) + aPolygon.setB2DPoint(i, basegfx::B2DPoint(pSalPoints[i].mnX, pSalPoints[i].mnY)); + + aPolyPolygon.append(aPolygon); + } + } + + drawPolyPolygon(basegfx::B2DHomMatrix(), aPolyPolygon, 0.0); +} + +bool SkiaSalGraphicsImpl::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolyPolygon& rPolyPolygon, + double fTransparency) +{ + const bool bHasFill(mFillColor != SALCOLOR_NONE); + const bool bHasLine(mLineColor != SALCOLOR_NONE); + + if (rPolyPolygon.count() == 0 || !(bHasFill || bHasLine) || fTransparency < 0.0 + || fTransparency >= 1.0) + return true; + + basegfx::B2DPolyPolygon aPolyPolygon(rPolyPolygon); + aPolyPolygon.transform(rObjectToDevice); + + SAL_INFO("vcl.skia.trace", "drawpolypolygon(" << this << "): " << aPolyPolygon << ":" + << mLineColor << ":" << mFillColor); + + if (delayDrawPolyPolygon(aPolyPolygon, fTransparency)) + { + scheduleFlush(); + return true; + } + + performDrawPolyPolygon(aPolyPolygon, fTransparency, mParent.getAntiAliasB2DDraw()); + return true; +} + +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); + addXorRegion(polygonPath.getBounds()); + + SkPaint aPaint; + aPaint.setAntiAlias(useAA); + + // 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 + const SkScalar posFix = useAA ? toSkXYFix : 0; + polygonPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr); + } + if (mFillColor != SALCOLOR_NONE) + { + aPaint.setColor(toSkColorWithTransparency(mFillColor, fTransparency)); + aPaint.setStyle(SkPaint::kFill_Style); + // 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 (mLineColor == SALCOLOR_NONE && polygonPath.getBounds().isEmpty()) + aPaint.setStyle(SkPaint::kStroke_Style); + getDrawCanvas()->drawPath(polygonPath, aPaint); + } + if (mLineColor != SALCOLOR_NONE) + { + aPaint.setColor(toSkColorWithTransparency(mLineColor, fTransparency)); + aPaint.setStyle(SkPaint::kStroke_Style); + getDrawCanvas()->drawPath(polygonPath, aPaint); + } + postDraw(); +#if defined LINUX + // WORKAROUND: The logo in the about dialog has drawing errors. This seems to happen + // only on Linux (not Windows on the same machine), with both AMDGPU and Mesa, + // and only when antialiasing is enabled. Flushing seems to avoid the problem. + if (useAA && SkiaHelper::getVendor() == DriverBlocklist::VendorAMD) + mSurface->flushAndSubmit(); +#endif +} + +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.getAntiAliasB2DDraw()) + return false; + // Only filled polygons without an outline are problematic. + if (mFillColor == SALCOLOR_NONE || mLineColor != SALCOLOR_NONE) + 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.size() != 0 + && (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; +} + +void SkiaSalGraphicsImpl::checkPendingDrawing() +{ + if (mLastPolyPolygonInfo.polygons.size() != 0) + { // 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 + // TODO: tdf#136222 shows that basegfx::utils::mergeToSinglePolyPolygon() is unreliable + // in corner cases, possibly either a bug or rounding errors somewhere. + 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 + || mLineColor == SALCOLOR_NONE) + { + return true; + } + + preDraw(); + SAL_INFO("vcl.skia.trace", "drawpolyline(" << this << "): " << rPolyLine << ":" << mLineColor); + + // tdf#124848 get correct LineWidth in discrete coordinates, + if (fLineWidth == 0) // hairline + fLineWidth = 1.0; + else // Adjust line width for object-to-device scale. + fLineWidth = (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength(); + + // Transform to DeviceCoordinates, get DeviceLineWidth, execute PixelSnapHairline + basegfx::B2DPolyPolygon aPolyPolygonLine; + aPolyPolygonLine.append(rPolyLine); + aPolyPolygonLine.transform(rObjectToDevice); + if (bPixelSnapHairline) + { + aPolyPolygonLine = basegfx::utils::snapPointsOfHorizontalOrVerticalEdges(aPolyPolygonLine); + } + + // Setup Line Join + SkPaint::Join eSkLineJoin = SkPaint::kMiter_Join; + switch (eLineJoin) + { + case basegfx::B2DLineJoin::Bevel: + eSkLineJoin = SkPaint::kBevel_Join; + break; + case basegfx::B2DLineJoin::Round: + eSkLineJoin = SkPaint::kRound_Join; + break; + case basegfx::B2DLineJoin::NONE: + case basegfx::B2DLineJoin::Miter: + eSkLineJoin = SkPaint::kMiter_Join; + break; + } + + // convert miter minimum angle to miter limit + double fMiterLimit = 1.0 / std::sin(fMiterMinimumAngle / 2.0); + + // Setup Line Cap + SkPaint::Cap eSkLineCap(SkPaint::kButt_Cap); + + switch (eLineCap) + { + case css::drawing::LineCap_ROUND: + eSkLineCap = SkPaint::kRound_Cap; + break; + case css::drawing::LineCap_SQUARE: + eSkLineCap = SkPaint::kSquare_Cap; + break; + default: // css::drawing::LineCap_BUTT: + eSkLineCap = SkPaint::kButt_Cap; + break; + } + + SkPaint aPaint; + aPaint.setStyle(SkPaint::kStroke_Style); + aPaint.setStrokeCap(eSkLineCap); + aPaint.setStrokeJoin(eSkLineJoin); + aPaint.setColor(toSkColorWithTransparency(mLineColor, fTransparency)); + aPaint.setStrokeMiter(fMiterLimit); + aPaint.setStrokeWidth(fLineWidth); + aPaint.setAntiAlias(mParent.getAntiAliasB2DDraw()); + // See the tdf#134346 comment above. + const SkScalar posFix = mParent.getAntiAliasB2DDraw() ? 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.setFillType(SkPathFillType::kEvenOdd); + for (sal_uInt32 a(0); a < aPolyPolygonLine.count(); a++) + addPolygonToPath(aPolyPolygonLine.getB2DPolygon(a), aPath); + aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr); + addXorRegion(aPath.getBounds()); + getDrawCanvas()->drawPath(aPath, aPaint); + } + else + { + for (sal_uInt32 i = 0; i < aPolyPolygonLine.count(); ++i) + { + const basegfx::B2DPolygon& rPolygon = aPolyPolygonLine.getB2DPolygon(i); + sal_uInt32 nPoints = rPolygon.count(); + bool bClosed = rPolygon.isClosed(); + for (sal_uInt32 j = 0; j < (bClosed ? nPoints : nPoints - 1); ++j) + { + sal_uInt32 index1 = (j + 0) % nPoints; + sal_uInt32 index2 = (j + 1) % nPoints; + SkPath aPath; + aPath.moveTo(rPolygon.getB2DPoint(index1).getX(), + rPolygon.getB2DPoint(index1).getY()); + aPath.lineTo(rPolygon.getB2DPoint(index2).getX(), + rPolygon.getB2DPoint(index2).getY()); + + aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr); + addXorRegion(aPath.getBounds()); + getDrawCanvas()->drawPath(aPath, aPaint); + } + } + } + + postDraw(); + + return true; +} + +bool SkiaSalGraphicsImpl::drawPolyLineBezier(sal_uInt32, const SalPoint*, const PolyFlags*) +{ + // TODO? + return false; +} + +bool SkiaSalGraphicsImpl::drawPolygonBezier(sal_uInt32, const SalPoint*, const PolyFlags*) +{ + // TODO? + return false; +} + +bool SkiaSalGraphicsImpl::drawPolyPolygonBezier(sal_uInt32, const sal_uInt32*, + const SalPoint* const*, const PolyFlags* const*) +{ + // TODO? + return false; +} + +static void copyArea(SkCanvas* canvas, sk_sp<SkSurface> surface, long nDestX, long nDestY, + long nSrcX, long nSrcY, long nSrcWidth, long nSrcHeight, bool srcIsRaster, + bool destIsRaster) +{ + // Using SkSurface::draw() should be more efficient than SkSurface::makeImageSnapshot(), + // because it may detect copying to itself and avoid some needless copies. + // But it has problems with drawing to itself + // (https://groups.google.com/forum/#!topic/skia-discuss/6yiuw24jv0I) and also + // raster surfaces do not avoid a copy of the source + // (https://groups.google.com/forum/#!topic/skia-discuss/S3FMpCi82k0). + // Finally, there's not much point if one of them is raster and the other is not (chrome/m86 even crashes). + if (canvas == surface->getCanvas() || srcIsRaster || (srcIsRaster != destIsRaster)) + { + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + canvas->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(surface), + SkIRect::MakeXYWH(nSrcX, nSrcY, nSrcWidth, nSrcHeight), + SkRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight), &paint); + return; + } + // SkCanvas::draw() cannot do a subrectangle, so clip. + canvas->save(); + canvas->clipRect(SkRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight)); + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + surface->draw(canvas, nDestX - nSrcX, nDestY - nSrcY, &paint); + canvas->restore(); +} + +void SkiaSalGraphicsImpl::copyArea(long nDestX, long nDestY, long nSrcX, long nSrcY, long nSrcWidth, + 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)); + assert(!mXorMode); + ::copyArea(getDrawCanvas(), mSurface, nDestX, nDestY, nSrcX, nSrcY, nSrcWidth, nSrcHeight, + !isGPU(), !isGPU()); + 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); + } + if (rPosAry.mnSrcWidth == rPosAry.mnDestWidth && rPosAry.mnSrcHeight == rPosAry.mnDestHeight) + { + 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() << " copy area: " << rPosAry); + ::copyArea(getDrawCanvas(), src->mSurface, rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnSrcX, + rPosAry.mnSrcY, rPosAry.mnDestWidth, rPosAry.mnDestHeight, !src->isGPU(), + !isGPU()); + } + else + { + SAL_INFO("vcl.skia.trace", "copybits(" << this << "): (" << src << "): " << rPosAry); + // Do not use makeImageSnapshot(rect), as that one may make a needless data copy. + sk_sp<SkImage> image = SkiaHelper::makeCheckedImageSnapshot(src->mSurface); + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + if (rPosAry.mnSrcWidth != rPosAry.mnDestWidth + || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) + paint.setFilterQuality(kHigh_SkFilterQuality); + getDrawCanvas()->drawImageRect(image, + SkIRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, + rPosAry.mnSrcWidth, rPosAry.mnSrcHeight), + SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, + rPosAry.mnDestWidth, rPosAry.mnDestHeight), + &paint); + } + assert(!mXorMode); + postDraw(); +} + +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 which + // is actually one-minus-alpha (opacity). Therefore white=0xff=transparent, + // black=0x00=opaque. So the result is transparent only if both the inputs + // are transparent. Since for blending operations white=1.0 and black=0.0, + // kMultiply should handle exactly that (transparent*transparent=transparent, + // opaque*transparent=opaque). And guessing from the "floor" in TYPE_BLEND in opengl's + // combinedTextureFragmentShader.glsl, the layer is not even alpha values but + // simply yes-or-no mask. + // See also blendAlphaBitmap(). + drawImage(rPosAry, rSkiaBitmap.GetSkImage(), SkBlendMode::kMultiply); + return true; +} + +bool SkiaSalGraphicsImpl::blendAlphaBitmap(const SalTwoRect& rPosAry, + const SalBitmap& rSourceBitmap, + const SalBitmap& rMaskBitmap, + const SalBitmap& rAlphaBitmap) +{ + 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); + + // 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(). + + // 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::kDstOut, rSkiaMaskBitmap.GetAlphaSkImage()->makeShader(), + rSkiaAlphaBitmap.GetAlphaSkImage()->makeShader()); + // And now draw the bitmap with "1 - x", where x is the "( 1 - alpha ) * mask". + sk_sp<SkShader> shader = SkShaders::Blend(SkBlendMode::kSrcOut, shaderAlpha, + rSkiaSourceBitmap.GetSkImage()->makeShader()); + 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); + + drawImage(rPosAry, rSkiaSourceBitmap.GetSkImage()); +} + +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); + drawShader(rPosAry, + SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. + SkShaders::Color(toSkColor(nMaskColor)), + skiaBitmap.GetAlphaSkImage()->makeShader())); +} + +std::shared_ptr<SalBitmap> SkiaSalGraphicsImpl::getBitmap(long nX, long nY, long nWidth, + 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 = SkiaHelper::makeCheckedImageSnapshot( + mSurface, SkIRect::MakeXYWH(nX, nY, nWidth, nHeight)); + return std::make_shared<SkiaSalBitmap>(image); +} + +Color SkiaSalGraphicsImpl::getPixel(long nX, 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(GetWidth(), GetHeight())) + abort(); + if (!mSurface->readPixels(bitmap, 0, 0)) + abort(); + return fromSkColor(bitmap.getColor(nX, nY)); +} + +void SkiaSalGraphicsImpl::invert(basegfx::B2DPolygon const& rPoly, SalInvert eFlags) +{ + preDraw(); + SAL_INFO("vcl.skia.trace", "invert(" << this << "): " << rPoly << ":" << int(eFlags)); + assert(!mXorMode); + // Intel Vulkan drivers (up to current 0.401.3889) have a problem + // with SkBlendMode::kDifference(?) and surfaces wider than 1024 pixels, resulting + // in drawing errors. Work that around by fetching the relevant part of the surface + // and drawing using CPU. + bool intelHack + = (isGPU() && SkiaHelper::getVendor() == DriverBlocklist::VendorIntel && !mXorMode); + // TrackFrame just inverts a dashed path around the polygon + if (eFlags == SalInvert::TrackFrame) + { + SkPath aPath; + addPolygonToPath(rPoly, aPath); + aPath.setFillType(SkPathFillType::kEvenOdd); + // 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. + SkAutoCanvasRestore autoRestore(getDrawCanvas(), true); + getDrawCanvas()->clipRect(aPath.getBounds(), SkClipOp::kIntersect, false); + SkPaint aPaint; + aPaint.setStrokeWidth(2); + float intervals[] = { 4.0f, 4.0f }; + aPaint.setStyle(SkPaint::kStroke_Style); + aPaint.setPathEffect(SkDashPathEffect::Make(intervals, SK_ARRAY_COUNT(intervals), 0)); + aPaint.setColor(SkColorSetARGB(255, 255, 255, 255)); + aPaint.setBlendMode(SkBlendMode::kDifference); + if (!intelHack) + getDrawCanvas()->drawPath(aPath, aPaint); + else + { + SkRect area; + aPath.getBounds().roundOut(&area); + SkRect size = SkRect::MakeWH(area.width(), area.height()); + sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(area.width(), area.height()); + SkPaint copy; + copy.setBlendMode(SkBlendMode::kSrc); + flushDrawing(); + surface->getCanvas()->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(mSurface), + area, size, ©); + aPath.offset(-area.x(), -area.y()); + surface->getCanvas()->drawPath(aPath, aPaint); + getDrawCanvas()->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(surface), size, + area, ©); + } + } + else + { + SkPath aPath; + addPolygonToPath(rPoly, aPath); + aPath.setFillType(SkPathFillType::kEvenOdd); + SkPaint aPaint; + aPaint.setColor(SkColorSetARGB(255, 255, 255, 255)); + aPaint.setStyle(SkPaint::kFill_Style); + aPaint.setBlendMode(SkBlendMode::kDifference); + + // N50 inverts in checker pattern + if (eFlags == SalInvert::N50) + { + // This creates 2x2 checker pattern bitmap + // TODO Use SkiaHelper::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)); + } + if (!intelHack) + getDrawCanvas()->drawPath(aPath, aPaint); + else + { + SkRect area; + aPath.getBounds().roundOut(&area); + SkRect size = SkRect::MakeWH(area.width(), area.height()); + sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(area.width(), area.height()); + SkPaint copy; + copy.setBlendMode(SkBlendMode::kSrc); + flushDrawing(); + surface->getCanvas()->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(mSurface), + area, size, ©); + aPath.offset(-area.x(), -area.y()); + surface->getCanvas()->drawPath(aPath, aPaint); + getDrawCanvas()->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(surface), size, + area, ©); + } + } + postDraw(); +} + +void SkiaSalGraphicsImpl::invert(long nX, long nY, long nWidth, 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 SalPoint* pPointArray, SalInvert eFlags) +{ + basegfx::B2DPolygon aPolygon; + aPolygon.append(basegfx::B2DPoint(pPointArray[0].mnX, pPointArray[0].mnY), nPoints); + for (sal_uInt32 i = 1; i < nPoints; ++i) + { + aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPointArray[i].mnX, pPointArray[i].mnY)); + } + aPolygon.setClosed(true); + + invert(aPolygon, eFlags); +} + +bool SkiaSalGraphicsImpl::drawEPS(long, long, long, long, void*, sal_uInt32) +{ + // TODO? + 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. +sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitmap, + const SkiaSalBitmap* alphaBitmap, + const Size targetSize) +{ + sk_sp<SkImage> image; + // GPU-accelerated drawing with SkShader should be fast enough to not need caching. + if (isGPU()) + return image; + if (targetSize.IsEmpty()) + return image; + // Probably not much point in caching of just doing a copy. + if (alphaBitmap == nullptr && targetSize == bitmap.GetSize()) + return image; + // Image too small to be worth caching. + if (bitmap.GetSize().Width() < 100 && bitmap.GetSize().Height() < 100 + && targetSize.Width() < 100 && targetSize.Height() < 100) + return image; + // In some cases (tdf#134237) the draw 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. + if (targetSize.Width() > GetWidth() || targetSize.Height() > GetHeight()) + { + // 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 = 1.0 * targetSize.Width() / bitmap.GetSize().Width() + * targetSize.Height() / bitmap.GetSize().Height(); + const double oversizeRatio + = 1.0 * targetSize.Width() / GetWidth() * targetSize.Height() / GetHeight(); + const double ratio = upscaleRatio * oversizeRatio; + if (ratio > 10) + { + SAL_INFO("vcl.skia.trace", "mergecachebitmaps(" + << this << "): not caching upscaling, ratio:" << ratio + << ", " << bitmap.GetSize() << "->" << targetSize + << " in " << Size(GetWidth(), GetHeight())); + return image; + } + } + OString key; + OStringBuffer keyBuf; + keyBuf.append(targetSize.Width()) + .append("x") + .append(targetSize.Height()) + .append("_0x") + .append(reinterpret_cast<sal_IntPtr>(&bitmap), 16) + .append("_0x") + .append(reinterpret_cast<sal_IntPtr>(alphaBitmap), 16) + .append("_") + .append(static_cast<sal_Int64>(bitmap.GetSkImage()->uniqueID())); + if (alphaBitmap) + keyBuf.append("_").append( + static_cast<sal_Int64>(alphaBitmap->GetAlphaSkImage()->uniqueID())); + key = keyBuf.makeStringAndClear(); + image = SkiaHelper::findCachedImage(key); + if (image) + { + assert(image->width() == targetSize.Width() && image->height() == targetSize.Height()); + return image; + } + // Combine bitmap + alpha bitmap into one temporary bitmap with alpha. + // If scaling is needed, first apply the alpha, then scale, otherwise the scaling might affect the alpha values. + if (alphaBitmap && targetSize != bitmap.GetSize()) + { + sk_sp<SkSurface> mergedSurface = SkiaHelper::createSkSurface(bitmap.GetSize()); + if (!mergedSurface) + return nullptr; + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + mergedSurface->getCanvas()->drawImage(bitmap.GetSkImage(), 0, 0, &paint); + paint.setBlendMode(SkBlendMode::kDstOut); // VCL alpha is one-minus-alpha + mergedSurface->getCanvas()->drawImage(alphaBitmap->GetAlphaSkImage(), 0, 0, &paint); + sk_sp<SkSurface> scaledSurface = SkiaHelper::createSkSurface(targetSize); + if (!scaledSurface) + return nullptr; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + paint.setFilterQuality(kHigh_SkFilterQuality); + scaledSurface->getCanvas()->drawImageRect( + mergedSurface->makeImageSnapshot(), + SkRect::MakeXYWH(0, 0, bitmap.GetSize().Width(), bitmap.GetSize().Height()), + SkRect::MakeXYWH(0, 0, targetSize.Width(), targetSize.Height()), &paint); + image = scaledSurface->makeImageSnapshot(); + } + else // No alpha or no scaling, scale directly. + { + sk_sp<SkSurface> tmpSurface = SkiaHelper::createSkSurface(targetSize); + if (!tmpSurface) + return nullptr; + SkCanvas* canvas = tmpSurface->getCanvas(); + SkAutoCanvasRestore autoRestore(canvas, true); + SkPaint paint; + if (targetSize != bitmap.GetSize()) + { + SkMatrix matrix; + matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / bitmap.GetSize().Width()); + matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / bitmap.GetSize().Height()); + canvas->concat(matrix); + paint.setFilterQuality(kHigh_SkFilterQuality); + } + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + canvas->drawImage(bitmap.GetSkImage(), 0, 0, &paint); + if (alphaBitmap != nullptr) + { + paint.setBlendMode(SkBlendMode::kDstOut); // VCL alpha is one-minus-alpha + canvas->drawImage(alphaBitmap->GetAlphaSkImage(), 0, 0, &paint); + } + image = SkiaHelper::makeCheckedImageSnapshot(tmpSurface); + } + SkiaHelper::addCachedImage(key, image); + return image; +} + +bool SkiaSalGraphicsImpl::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSourceBitmap, + const SalBitmap& rAlphaBitmap) +{ + assert(dynamic_cast<const SkiaSalBitmap*>(&rSourceBitmap)); + assert(dynamic_cast<const SkiaSalBitmap*>(&rAlphaBitmap)); + // In raster mode use mergeCacheBitmaps(), which will cache the result, avoiding repeated + // alpha blending or scaling. In GPU mode it is simpler to just use SkShader. + 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(static_cast<const SkiaSalBitmap&>(rSourceBitmap), + static_cast<const SkiaSalBitmap*>(&rAlphaBitmap), imageSize); + if (image) + drawImage(imagePosAry, image); + else + drawShader( + rPosAry, + SkShaders::Blend( + SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. + static_cast<const SkiaSalBitmap&>(rSourceBitmap).GetSkImage()->makeShader(), + static_cast<const SkiaSalBitmap*>(&rAlphaBitmap)->GetAlphaSkImage()->makeShader())); + return true; +} + +void SkiaSalGraphicsImpl::drawImage(const SalTwoRect& rPosAry, const sk_sp<SkImage>& aImage, + SkBlendMode eBlendMode) +{ + SkRect aSourceRect + = SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); + SkRect aDestinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, + rPosAry.mnDestWidth, rPosAry.mnDestHeight); + + SkPaint aPaint; + aPaint.setBlendMode(eBlendMode); + if (rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) + aPaint.setFilterQuality(kHigh_SkFilterQuality); + + preDraw(); + SAL_INFO("vcl.skia.trace", + "drawimage(" << this << "): " << rPosAry << ":" << SkBlendMode_Name(eBlendMode)); + addXorRegion(aDestinationRect); + getDrawCanvas()->drawImageRect(aImage, aSourceRect, aDestinationRect, &aPaint); + 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) +{ + preDraw(); + SAL_INFO("vcl.skia.trace", "drawshader(" << this << "): " << rPosAry); + SkRect destinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, + rPosAry.mnDestHeight); + addXorRegion(destinationRect); + SkPaint paint; + paint.setShader(shader); + if (rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) + paint.setFilterQuality(kHigh_SkFilterQuality); + 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); + assert(matrix.mapXY(rPosAry.mnSrcX, rPosAry.mnSrcY) + == SkPoint::Make(rPosAry.mnDestX, rPosAry.mnDestY)); + assert(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::drawTransformedBitmap(const basegfx::B2DPoint& rNull, + const basegfx::B2DPoint& rX, + const basegfx::B2DPoint& rY, + const SalBitmap& rSourceBitmap, + const SalBitmap* pAlphaBitmap) +{ + 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); + + // 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); + + addXorRegion(SkRect::MakeWH(GetWidth(), GetHeight())); // can't tell, use whole area + // In raster mode scaling and alpha blending is still somewhat expensive if done repeatedly, + // so use mergeCacheBitmaps(), which will cache the result if useful. + // It is better to use SkShader if in GPU mode, if the operation is simple or if the temporary + // image would be very large. + sk_sp<SkImage> imageToDraw = mergeCacheBitmaps( + rSkiaBitmap, pSkiaAlphaBitmap, Size(round(aXRel.getLength()), round(aYRel.getLength()))); + 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()) / imageToDraw->width()); + matrix.set(SkMatrix::kMScaleY, round(aYRel.getY()) / imageToDraw->height()); + matrix.set(SkMatrix::kMSkewY, aXRel.getY() / imageToDraw->width()); + matrix.set(SkMatrix::kMSkewX, aYRel.getX() / imageToDraw->height()); + matrix.set(SkMatrix::kMTransX, rNull.getX()); + matrix.set(SkMatrix::kMTransY, rNull.getY()); + SkCanvas* canvas = getDrawCanvas(); + SkAutoCanvasRestore autoRestore(canvas, true); + canvas->concat(matrix); + SkPaint paint; + paint.setFilterQuality(kHigh_SkFilterQuality); + canvas->drawImage(imageToDraw, 0, 0, &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); + SkPaint paint; + paint.setFilterQuality(kHigh_SkFilterQuality); + if (pSkiaAlphaBitmap) + { + paint.setShader(SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. + rSkiaBitmap.GetSkImage()->makeShader(), + pSkiaAlphaBitmap->GetAlphaSkImage()->makeShader())); + canvas->drawRect(SkRect::MakeWH(aSize.Width(), aSize.Height()), paint); + } + else + { + canvas->drawImage(rSkiaBitmap.GetSkImage(), 0, 0, &paint); + } + } + postDraw(); + return true; +} + +bool SkiaSalGraphicsImpl::drawAlphaRect(long nX, long nY, long nWidth, long nHeight, + sal_uInt8 nTransparency) +{ + privateDrawAlphaRect(nX, nY, nWidth, nHeight, nTransparency / 100.0); + return true; +} + +bool SkiaSalGraphicsImpl::drawGradient(const tools::PolyPolygon&, const Gradient&) +{ + // TODO? + return false; +} + +static double toRadian(int degree10th) { return (3600 - degree10th) * M_PI / 1800.0; } +static double toCos(int degree10th) { return SkScalarCos(toRadian(degree10th)); } +static double toSin(int degree10th) { return SkScalarSin(toRadian(degree10th)); } + +void SkiaSalGraphicsImpl::drawGenericLayout(const GenericSalLayout& layout, Color textColor, + const SkFont& font, GlyphOrientation glyphOrientation) +{ + SkiaZone zone; + std::vector<SkGlyphID> glyphIds; + std::vector<SkRSXform> glyphForms; + glyphIds.reserve(256); + glyphForms.reserve(256); + Point aPos; + const GlyphItem* pGlyph; + int nStart = 0; + while (layout.GetNextGlyph(&pGlyph, aPos, nStart)) + { + glyphIds.push_back(pGlyph->glyphId()); + int angle = 0; // 10th of degree + if (glyphOrientation == GlyphOrientation::Apply) + { + angle = layout.GetOrientation(); + if (pGlyph->IsVertical()) + angle += 900; // 90 degree + } + SkRSXform form = SkRSXform::Make(toCos(angle), toSin(angle), aPos.X(), aPos.Y()); + glyphForms.emplace_back(std::move(form)); + } + if (glyphIds.empty()) + return; + sk_sp<SkTextBlob> textBlob + = SkTextBlob::MakeFromRSXform(glyphIds.data(), glyphIds.size() * sizeof(SkGlyphID), + glyphForms.data(), font, SkTextEncoding::kGlyphID); + preDraw(); + SAL_INFO("vcl.skia.trace", + "drawtextblob(" << this << "): " << textBlob->bounds() << ":" << textColor); + addXorRegion(textBlob->bounds()); + SkPaint paint; + paint.setColor(toSkColor(textColor)); + getDrawCanvas()->drawTextBlob(textBlob, 0, 0, paint); + postDraw(); +} + +bool SkiaSalGraphicsImpl::supportsOperation(OutDevSupportType eType) const +{ + switch (eType) + { + case OutDevSupportType::B2DDraw: + case OutDevSupportType::TransparentRect: + return true; + default: + return false; + } +} + +#ifdef DBG_UTIL +void SkiaSalGraphicsImpl::dump(const char* file) const +{ + assert(mSurface.get()); + SkiaHelper::dump(mSurface, file); +} +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |