/* -*- 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace SkiaHelper; namespace { // Create Skia Path from B2DPolygon // Note that polygons generally have the complication that when used // for area (fill) operations they usually miss the right-most and // bottom-most line of pixels of the bounding rectangle (see // https://lists.freedesktop.org/archives/libreoffice/2019-November/083709.html). // So be careful with rectangle->polygon conversions (generally avoid them). void addPolygonToPath(const basegfx::B2DPolygon& rPolygon, SkPath& rPath, sal_uInt32 nFirstIndex, sal_uInt32 nLastIndex, const sal_uInt32 nPointCount, const bool bClosePath, const bool bHasCurves, bool* hasOnlyOrthogonal = nullptr) { assert(nFirstIndex < nPointCount || (nFirstIndex == 0 && nPointCount == 0)); assert(nLastIndex <= nPointCount); if (nPointCount <= 1) return; bool bFirst = true; sal_uInt32 nPreviousIndex = nFirstIndex == 0 ? nPointCount - 1 : nFirstIndex - 1; basegfx::B2DPoint aPreviousPoint = rPolygon.getB2DPoint(nPreviousIndex); for (sal_uInt32 nIndex = nFirstIndex; nIndex <= nLastIndex; nIndex++) { if (nIndex == nPointCount && !bClosePath) continue; // Make sure we loop the last point to first point sal_uInt32 nCurrentIndex = nIndex % nPointCount; basegfx::B2DPoint aCurrentPoint = rPolygon.getB2DPoint(nCurrentIndex); if (bFirst) { rPath.moveTo(aCurrentPoint.getX(), aCurrentPoint.getY()); bFirst = false; } else if (!bHasCurves) { rPath.lineTo(aCurrentPoint.getX(), aCurrentPoint.getY()); // If asked for, check whether the polygon has a line that is not // strictly horizontal or vertical. if (hasOnlyOrthogonal != nullptr && aCurrentPoint.getX() != aPreviousPoint.getX() && aCurrentPoint.getY() != aPreviousPoint.getY()) *hasOnlyOrthogonal = false; } else { basegfx::B2DPoint aPreviousControlPoint = rPolygon.getNextControlPoint(nPreviousIndex); basegfx::B2DPoint aCurrentControlPoint = rPolygon.getPrevControlPoint(nCurrentIndex); if (aPreviousControlPoint.equal(aPreviousPoint) && aCurrentControlPoint.equal(aCurrentPoint)) { rPath.lineTo(aCurrentPoint.getX(), aCurrentPoint.getY()); // a straight line if (hasOnlyOrthogonal != nullptr && aCurrentPoint.getX() != aPreviousPoint.getX() && aCurrentPoint.getY() != aPreviousPoint.getY()) *hasOnlyOrthogonal = false; } else { if (aPreviousControlPoint.equal(aPreviousPoint)) { aPreviousControlPoint = aPreviousPoint + ((aPreviousControlPoint - aCurrentPoint) * 0.0005); } if (aCurrentControlPoint.equal(aCurrentPoint)) { aCurrentControlPoint = aCurrentPoint + ((aCurrentControlPoint - aPreviousPoint) * 0.0005); } rPath.cubicTo(aPreviousControlPoint.getX(), aPreviousControlPoint.getY(), aCurrentControlPoint.getX(), aCurrentControlPoint.getY(), aCurrentPoint.getX(), aCurrentPoint.getY()); if (hasOnlyOrthogonal != nullptr) *hasOnlyOrthogonal = false; } } aPreviousPoint = aCurrentPoint; nPreviousIndex = nCurrentIndex; } if (bClosePath && nFirstIndex == 0 && nLastIndex == nPointCount) { rPath.close(); } } void addPolygonToPath(const basegfx::B2DPolygon& rPolygon, SkPath& rPath, bool* hasOnlyOrthogonal = nullptr) { addPolygonToPath(rPolygon, rPath, 0, rPolygon.count(), rPolygon.count(), rPolygon.isClosed(), rPolygon.areControlPointsUsed(), hasOnlyOrthogonal); } void addPolyPolygonToPath(const basegfx::B2DPolyPolygon& rPolyPolygon, SkPath& rPath, bool* hasOnlyOrthogonal = nullptr) { const sal_uInt32 nPolygonCount(rPolyPolygon.count()); if (nPolygonCount == 0) return; sal_uInt32 nPointCount = 0; for (const auto& rPolygon : rPolyPolygon) nPointCount += rPolygon.count() * 3; // because cubicTo is 3 elements rPath.incReserve(nPointCount); for (const auto& rPolygon : rPolyPolygon) { addPolygonToPath(rPolygon, rPath, hasOnlyOrthogonal); } } // Check if the given polygon contains a straight line. If not, it consists // solely of curves. bool polygonContainsLine(const basegfx::B2DPolyPolygon& rPolyPolygon) { if (!rPolyPolygon.areControlPointsUsed()) return true; // no curves at all for (const auto& rPolygon : rPolyPolygon) { const sal_uInt32 nPointCount(rPolygon.count()); bool bFirst = true; const bool bClosePath(rPolygon.isClosed()); sal_uInt32 nCurrentIndex = 0; sal_uInt32 nPreviousIndex = nPointCount - 1; basegfx::B2DPoint aCurrentPoint; basegfx::B2DPoint aPreviousPoint; for (sal_uInt32 nIndex = 0; nIndex <= nPointCount; nIndex++) { if (nIndex == nPointCount && !bClosePath) continue; // Make sure we loop the last point to first point nCurrentIndex = nIndex % nPointCount; if (bFirst) bFirst = false; else { basegfx::B2DPoint aPreviousControlPoint = rPolygon.getNextControlPoint(nPreviousIndex); basegfx::B2DPoint aCurrentControlPoint = rPolygon.getPrevControlPoint(nCurrentIndex); if (aPreviousControlPoint.equal(aPreviousPoint) && aCurrentControlPoint.equal(aCurrentPoint)) { return true; // found a straight line } } aPreviousPoint = aCurrentPoint; nPreviousIndex = nCurrentIndex; } } return false; // no straight line found } // returns true if the source or destination rectangles are invalid bool checkInvalidSourceOrDestination(SalTwoRect const& rPosAry) { return rPosAry.mnSrcWidth <= 0 || rPosAry.mnSrcHeight <= 0 || rPosAry.mnDestWidth <= 0 || rPosAry.mnDestHeight <= 0; } std::string dumpOptionalColor(const std::optional& c) { std::ostringstream oss; if (c) oss << *c; else oss << "no color"; return std::move(oss).str(); // optimized in C++20 } } // end anonymous namespace // Class that triggers flushing the backing buffer when idle. class SkiaFlushIdle : public Idle { SkiaSalGraphicsImpl* mpGraphics; #ifndef NDEBUG char* debugname; #endif public: explicit SkiaFlushIdle(SkiaSalGraphicsImpl* pGraphics) : Idle(get_debug_name(pGraphics)) , mpGraphics(pGraphics) { // We don't want to be swapping before we've painted. SetPriority(TaskPriority::POST_PAINT); } #ifndef NDEBUG virtual ~SkiaFlushIdle() { free(debugname); } #endif const char* get_debug_name(SkiaSalGraphicsImpl* pGraphics) { #ifndef NDEBUG // Idle keeps just a pointer, so we need to store the string debugname = strdup( OString("skia idle 0x" + OString::number(reinterpret_cast(pGraphics), 16)) .getStr()); return debugname; #else (void)pGraphics; return "skia idle"; #endif } virtual void Invoke() override { mpGraphics->performFlush(); Stop(); // tdf#157312 Don't change priority // Instances of this class are constructed with // TaskPriority::POST_PAINT, but then it was set to // TaskPriority::HIGHEST when reused. Flushing // seems to be expensive (at least with Skia/Metal) so keep the // existing priority when reused. SetPriority(TaskPriority::POST_PAINT); } }; SkiaSalGraphicsImpl::SkiaSalGraphicsImpl(SalGraphics& rParent, SalGeometryProvider* pProvider) : mParent(rParent) , mProvider(pProvider) , mIsGPU(false) , moLineColor(std::nullopt) , moFillColor(std::nullopt) , mXorMode(XorMode::None) , mFlush(new SkiaFlushIdle(this)) , mScaling(1) , mInWindowBackingPropertiesChanged(false) { } SkiaSalGraphicsImpl::~SkiaSalGraphicsImpl() { assert(!mSurface); assert(!mWindowContext); } void SkiaSalGraphicsImpl::Init() {} void SkiaSalGraphicsImpl::createSurface() { SkiaZone zone; if (isOffscreen()) createOffscreenSurface(); else createWindowSurface(); mClipRegion = vcl::Region(tools::Rectangle(0, 0, GetWidth(), GetHeight())); mDirtyRect = SkIRect::MakeWH(GetWidth(), GetHeight()); setCanvasScalingAndClipping(); // We don't want to be swapping before we've painted. mFlush->Stop(); mFlush->SetPriority(TaskPriority::POST_PAINT); } void SkiaSalGraphicsImpl::createWindowSurface(bool forceRaster) { SkiaZone zone; assert(!isOffscreen()); assert(!mSurface); createWindowSurfaceInternal(forceRaster); if (!mSurface) { switch (forceRaster ? RenderRaster : renderMethodToUse()) { case RenderVulkan: SAL_WARN("vcl.skia", "cannot create Vulkan GPU window surface, falling back to Raster"); destroySurface(); // destroys also WindowContext return createWindowSurface(true); // try again case RenderMetal: SAL_WARN("vcl.skia", "cannot create Metal GPU window surface, falling back to Raster"); destroySurface(); // destroys also WindowContext return createWindowSurface(true); // try again case RenderRaster: abort(); // This should not really happen, do not even try to cope with it. } } mIsGPU = mSurface->getCanvas()->recordingContext() != nullptr; #ifdef DBG_UTIL prefillSurface(mSurface); #endif } bool SkiaSalGraphicsImpl::isOffscreen() const { if (mProvider == nullptr || mProvider->IsOffScreen()) return true; // HACK: Sometimes (tdf#131939, tdf#138022, tdf#140288) VCL passes us a zero-sized window, // and zero size is invalid for Skia, so force offscreen surface, where we handle this. if (GetWidth() <= 0 || GetHeight() <= 0) return true; return false; } void SkiaSalGraphicsImpl::createOffscreenSurface() { SkiaZone zone; assert(isOffscreen()); assert(!mSurface); // HACK: See isOffscreen(). int width = std::max(1, GetWidth()); int height = std::max(1, GetHeight()); // We need to use window scaling even for offscreen surfaces, because the common usage is rendering something // into an offscreen surface and then copy it to a window, so without scaling here the result would be originally // drawn without scaling and only upscaled when drawing to a window. mScaling = getWindowScaling(); mSurface = createSkSurface(width * mScaling, height * mScaling); assert(mSurface); mIsGPU = mSurface->getCanvas()->recordingContext() != nullptr; } void SkiaSalGraphicsImpl::destroySurface() { SkiaZone zone; if (mSurface) { // check setClipRegion() invariant assert(mSurface->getCanvas()->getSaveCount() == 3); // if this fails, something forgot to use SkAutoCanvasRestore assert(mSurface->getCanvas()->getTotalMatrix() == SkMatrix::Scale(mScaling, mScaling)); } mSurface.reset(); mWindowContext.reset(); mIsGPU = false; mScaling = 1; } void SkiaSalGraphicsImpl::performFlush() { SkiaZone zone; flushDrawing(); if (mSurface) { // Related: tdf#152703 Eliminate flickering during live resizing of a window // When in live resize, the SkiaSalGraphicsImpl class does not detect that // the window size has changed until after the flush has been called so // call checkSurface() to recreate the SkSurface if needed before flushing. checkSurface(); if (mDirtyRect.intersect(SkIRect::MakeWH(GetWidth(), GetHeight()))) flushSurfaceToWindowContext(); mDirtyRect.setEmpty(); } } void SkiaSalGraphicsImpl::flushSurfaceToWindowContext() { sk_sp screenSurface = mWindowContext->getBackbufferSurface(); if (screenSurface != mSurface) { // GPU-based window contexts require calling getBackbufferSurface() // for every swapBuffers(), for this reason mSurface is an offscreen surface // where we keep the contents (LO does not do full redraws). // So here blit the surface to the window context surface and then swap it. // Raster should always draw directly to backbuffer to save copying // except for small sizes - see renderMethodToUseForSize assert(isGPU() || (mSurface->width() <= 32 && mSurface->height() <= 32)); SkPaint paint; paint.setBlendMode(SkBlendMode::kSrc); // copy as is // We ignore mDirtyRect here, and mSurface already is in screenSurface coordinates, // so no transformation needed. screenSurface->getCanvas()->drawImage(makeCheckedImageSnapshot(mSurface), 0, 0, SkSamplingOptions(), &paint); screenSurface->flushAndSubmit(); // Otherwise the window is not drawn sometimes. mWindowContext->swapBuffers(nullptr); // Must swap the entire surface. } else { // For raster mode use directly the backbuffer surface, it's just a bitmap // surface anyway, and for those there's no real requirement to call // getBackbufferSurface() repeatedly. Using our own surface would duplicate // memory and cost time copying pixels around. assert(!isGPU()); SkIRect dirtyRect = mDirtyRect; if (mScaling != 1) // Adjust to mSurface coordinates if needed. dirtyRect = scaleRect(dirtyRect, mScaling); mWindowContext->swapBuffers(&dirtyRect); } } void SkiaSalGraphicsImpl::DeInit() { destroySurface(); } void SkiaSalGraphicsImpl::preDraw() { DBG_TESTSOLARMUTEX(); SkiaZone::enter(); // matched in postDraw() checkSurface(); checkPendingDrawing(); } void SkiaSalGraphicsImpl::postDraw() { scheduleFlush(); // Skia (at least when using Vulkan) queues drawing commands and executes them only later. // But tdf#136369 leads to creating and queueing many tiny bitmaps, which makes // Skia slow, and may make it even run out of memory. So force a flush if such // a problematic operation has been performed too many times without a flush. // Note that the counter is a static variable, as all drawing shares the same Skia drawing // context (and so the flush here will also flush all drawing). static int maxOperationsToFlush = 1000; if (pendingOperationsToFlush > maxOperationsToFlush) { mSurface->flushAndSubmit(); pendingOperationsToFlush = 0; } SkiaZone::leave(); // matched in preDraw() // If there's a problem with the GPU context, abort. if (GrDirectContext* context = GrAsDirectContext(mSurface->getCanvas()->recordingContext())) { // We don't know the exact status of the surface (and what has or has not been drawn to it). // But let's pretend it was drawn OK, and reduce the flush limit, to try to avoid possible // small HW memory limitation if (context->oomed()) { if (maxOperationsToFlush > 10) { maxOperationsToFlush /= 2; } else { SAL_WARN("vcl.skia", "GPU context has run out of memory, aborting."); abort(); } } // Unrecoverable problem. if (context->abandoned()) { SAL_WARN("vcl.skia", "GPU context has been abandoned, aborting."); abort(); } } } void SkiaSalGraphicsImpl::scheduleFlush() { if (!isOffscreen()) { if (!Application::IsInExecute()) performFlush(); // otherwise nothing would trigger idle rendering else if (!mFlush->IsActive()) mFlush->Start(); } } // VCL can sometimes resize us without telling us, update the surface if needed. // Also create the surface on demand if it has not been created yet (it is a waste // to create it in Init() if it gets recreated later anyway). void SkiaSalGraphicsImpl::checkSurface() { if (!mSurface) { createSurface(); SAL_INFO("vcl.skia.trace", "create(" << this << "): " << Size(mSurface->width(), mSurface->height())); } else if (mInWindowBackingPropertiesChanged || GetWidth() * mScaling != mSurface->width() || GetHeight() * mScaling != mSurface->height()) { if (!avoidRecreateByResize()) { Size oldSize(mSurface->width(), mSurface->height()); // Recreating a surface means that the old SkSurface contents will be lost. // But if a window has been resized the windowing system may send repaint events // only for changed parts and VCL would not repaint the whole area, assuming // that some parts have not changed (this is what seems to cause tdf#131952). // So carry over the old contents for windows, even though generally everything // will be usually repainted anyway. sk_sp snapshot; if (!isOffscreen()) { flushDrawing(); snapshot = makeCheckedImageSnapshot(mSurface); } destroySurface(); createSurface(); if (snapshot) { SkPaint paint; paint.setBlendMode(SkBlendMode::kSrc); // copy as is // Scaling by current mScaling is active, undo that. We assume that the scaling // does not change. resetCanvasScalingAndClipping(); mSurface->getCanvas()->drawImage(snapshot, 0, 0, SkSamplingOptions(), &paint); setCanvasScalingAndClipping(); } SAL_INFO("vcl.skia.trace", "recreate(" << this << "): old " << oldSize << " new " << Size(mSurface->width(), mSurface->height()) << " requested " << Size(GetWidth(), GetHeight())); } } } bool SkiaSalGraphicsImpl::avoidRecreateByResize() const { // Keep the old surface if VCL sends us a broken size (see isOffscreen()). if (GetWidth() == 0 || GetHeight() == 0) return true; return false; } void SkiaSalGraphicsImpl::flushDrawing() { if (!mSurface) return; checkPendingDrawing(); ++pendingOperationsToFlush; } void SkiaSalGraphicsImpl::setCanvasScalingAndClipping() { SkCanvas* canvas = mSurface->getCanvas(); assert(canvas->getSaveCount() == 1); // If HiDPI scaling is active, simply set a scaling matrix for the canvas. This means // that all painting can use VCL coordinates and they'll be automatically translated to mSurface // scaled coordinates. If that is not wanted, the scale() state needs to be temporarily unset. // State such as mDirtyRect is not scaled, the scaling matrix applies to clipping too, // and the rest needs to be handled explicitly. // When reading mSurface contents there's no automatic scaling and it needs to be handled explicitly. canvas->save(); // keep the original state without any scaling canvas->scale(mScaling, mScaling); // SkCanvas::clipRegion() can only further reduce the clip region, // but we need to set the given region, which may extend it. // So handle that by always having the full clip region saved on the stack // and always go back to that. SkCanvas::restore() only affects the clip // and the matrix. canvas->save(); // keep scaled state without clipping setCanvasClipRegion(canvas, mClipRegion); } void SkiaSalGraphicsImpl::resetCanvasScalingAndClipping() { SkCanvas* canvas = mSurface->getCanvas(); assert(canvas->getSaveCount() == 3); canvas->restore(); // undo clipping canvas->restore(); // undo scaling } void SkiaSalGraphicsImpl::setClipRegion(const vcl::Region& region) { if (mClipRegion == region) return; SkiaZone zone; checkPendingDrawing(); checkSurface(); mClipRegion = region; SAL_INFO("vcl.skia.trace", "setclipregion(" << this << "): " << region); SkCanvas* canvas = mSurface->getCanvas(); assert(canvas->getSaveCount() == 3); canvas->restore(); // undo previous clip state, see setCanvasScalingAndClipping() canvas->save(); setCanvasClipRegion(canvas, region); } void SkiaSalGraphicsImpl::setCanvasClipRegion(SkCanvas* canvas, const vcl::Region& region) { SkiaZone zone; SkPath path; // Always use region rectangles, regardless of what the region uses internally. // That's what other VCL backends do, and trying to use addPolyPolygonToPath() // in case a polygon is used leads to off-by-one errors such as tdf#133208. RectangleVector rectangles; region.GetRegionRectangles(rectangles); path.incReserve(rectangles.size() + 1); for (const tools::Rectangle& rectangle : rectangles) path.addRect(SkRect::MakeXYWH(rectangle.getX(), rectangle.getY(), rectangle.GetWidth(), rectangle.GetHeight())); path.setFillType(SkPathFillType::kEvenOdd); canvas->clipPath(path); } void SkiaSalGraphicsImpl::ResetClipRegion() { setClipRegion(vcl::Region(tools::Rectangle(0, 0, GetWidth(), GetHeight()))); } const vcl::Region& SkiaSalGraphicsImpl::getClipRegion() const { return mClipRegion; } sal_uInt16 SkiaSalGraphicsImpl::GetBitCount() const { return 32; } tools::Long SkiaSalGraphicsImpl::GetGraphicsWidth() const { return GetWidth(); } void SkiaSalGraphicsImpl::SetLineColor() { checkPendingDrawing(); moLineColor = std::nullopt; } void SkiaSalGraphicsImpl::SetLineColor(Color nColor) { checkPendingDrawing(); moLineColor = nColor; } void SkiaSalGraphicsImpl::SetFillColor() { checkPendingDrawing(); moFillColor = std::nullopt; } void SkiaSalGraphicsImpl::SetFillColor(Color nColor) { checkPendingDrawing(); moFillColor = nColor; } void SkiaSalGraphicsImpl::SetXORMode(bool set, bool invert) { XorMode newMode = set ? (invert ? XorMode::Invert : XorMode::Xor) : XorMode::None; if (newMode == mXorMode) return; checkPendingDrawing(); SAL_INFO("vcl.skia.trace", "setxormode(" << this << "): " << set << "/" << invert); mXorMode = newMode; } void SkiaSalGraphicsImpl::SetROPLineColor(SalROPColor nROPColor) { checkPendingDrawing(); switch (nROPColor) { case SalROPColor::N0: moLineColor = Color(0, 0, 0); break; case SalROPColor::N1: moLineColor = Color(0xff, 0xff, 0xff); break; case SalROPColor::Invert: moLineColor = Color(0xff, 0xff, 0xff); break; } } void SkiaSalGraphicsImpl::SetROPFillColor(SalROPColor nROPColor) { checkPendingDrawing(); switch (nROPColor) { case SalROPColor::N0: moFillColor = Color(0, 0, 0); break; case SalROPColor::N1: moFillColor = Color(0xff, 0xff, 0xff); break; case SalROPColor::Invert: moFillColor = Color(0xff, 0xff, 0xff); break; } } void SkiaSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY) { drawPixel(nX, nY, *moLineColor); } void SkiaSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY, Color nColor) { preDraw(); SAL_INFO("vcl.skia.trace", "drawpixel(" << this << "): " << Point(nX, nY) << ":" << nColor); addUpdateRegion(SkRect::MakeXYWH(nX, nY, 1, 1)); SkPaint paint = makePixelPaint(nColor); // Apparently drawPixel() is actually expected to set the pixel and not draw it. paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha #ifdef MACOSX // tdf#148569 set extra drawing constraints when scaling // Previously, setting stroke width and cap was only done when running // unit tests. But the same drawing constraints are necessary when running // with a Retina display on macOS. if (mScaling != 1) #else // Related tdf#148569: do not apply macOS fix to non-macOS platforms // Setting the stroke width and cap has a noticeable performance penalty // when running on GTK3. Since tdf#148569 only appears to occur on macOS // Retina displays, revert commit a4488013ee6c87a97501b620dbbf56622fb70246 // for non-macOS platforms. if (mScaling != 1 && isUnitTestRunning()) #endif { // On HiDPI displays, draw a square on the entire non-hidpi "pixel" when running unittests, // since tests often require precise pixel drawing. paint.setStrokeWidth(1); // this will be scaled by mScaling paint.setStrokeCap(SkPaint::kSquare_Cap); } getDrawCanvas()->drawPoint(toSkX(nX), toSkY(nY), paint); postDraw(); } void SkiaSalGraphicsImpl::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2, tools::Long nY2) { if (!moLineColor) return; preDraw(); SAL_INFO("vcl.skia.trace", "drawline(" << this << "): " << Point(nX1, nY1) << "->" << Point(nX2, nY2) << ":" << *moLineColor); addUpdateRegion(SkRect::MakeLTRB(nX1, nY1, nX2, nY2).makeSorted()); SkPaint paint = makeLinePaint(); paint.setAntiAlias(mParent.getAntiAlias()); if (mScaling != 1 && isUnitTestRunning()) { // On HiDPI displays, do not draw hairlines, draw 1-pixel wide lines in order to avoid // smoothing that would confuse unittests. paint.setStrokeWidth(1); // this will be scaled by mScaling paint.setStrokeCap(SkPaint::kSquare_Cap); } getDrawCanvas()->drawLine(toSkX(nX1), toSkY(nY1), toSkX(nX2), toSkY(nY2), paint); postDraw(); } void SkiaSalGraphicsImpl::privateDrawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, double fTransparency, bool blockAA) { preDraw(); SAL_INFO("vcl.skia.trace", "privatedrawrect(" << this << "): " << SkIRect::MakeXYWH(nX, nY, nWidth, nHeight) << ":" << dumpOptionalColor(moLineColor) << ":" << dumpOptionalColor(moFillColor) << ":" << fTransparency); addUpdateRegion(SkRect::MakeXYWH(nX, nY, nWidth, nHeight)); SkCanvas* canvas = getDrawCanvas(); if (moFillColor) { SkPaint paint = makeFillPaint(fTransparency); paint.setAntiAlias(!blockAA && mParent.getAntiAlias()); // HACK: If the polygon is just a line, it still should be drawn. But when filling // Skia doesn't draw empty polygons, so in that case ensure the line is drawn. if (!moLineColor && SkSize::Make(nWidth, nHeight).isEmpty()) paint.setStyle(SkPaint::kStroke_Style); canvas->drawIRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), paint); } if (moLineColor && moLineColor != moFillColor) // otherwise handled by fill { SkPaint paint = makeLinePaint(fTransparency); paint.setAntiAlias(!blockAA && mParent.getAntiAlias()); #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 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* 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 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(pSrcGraphics->GetImpl())); src = static_cast(pSrcGraphics->GetImpl()); src->checkSurface(); src->flushDrawing(); } else { src = this; assert(mXorMode == XorMode::None); } auto srcDebug = [&]() -> std::string { if (src == this) return "(self)"; else { std::ostringstream stream; stream << "(" << src << ")"; return stream.str(); } }; SAL_INFO("vcl.skia.trace", "copybits(" << this << "): " << srcDebug() << ": " << rPosAry); privateCopyBits(rPosAry, src); postDraw(); } void SkiaSalGraphicsImpl::privateCopyBits(const SalTwoRect& rPosAry, SkiaSalGraphicsImpl* src) { assert(mXorMode == XorMode::None); addUpdateRegion(SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight)); SkPaint paint; paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha SkIRect srcRect = SkIRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); SkRect destRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight); if (!SkIRect::Intersects(srcRect, SkIRect::MakeWH(src->GetWidth(), src->GetHeight())) || !SkRect::Intersects(destRect, SkRect::MakeWH(GetWidth(), GetHeight()))) return; if (src == this) { // Copy-to-self means that we'd take a snapshot, which would refcount the data, // and then drawing would result in copy in write, copying the entire surface. // Try to copy less by making a snapshot of only what is needed. // A complication here is that drawImageRect() can handle coordinates outside // of surface fine, but makeImageSnapshot() will crop to the surface area, // so do that manually here in order to adjust also destination rectangle. if (srcRect.x() < 0 || srcRect.y() < 0) { destRect.fLeft += -srcRect.x(); destRect.fTop += -srcRect.y(); srcRect.adjust(-srcRect.x(), -srcRect.y(), 0, 0); } // Note that right() and bottom() are not inclusive (are outside of the rect). if (srcRect.right() - 1 > GetWidth() || srcRect.bottom() - 1 > GetHeight()) { destRect.fRight += GetWidth() - srcRect.right(); destRect.fBottom += GetHeight() - srcRect.bottom(); srcRect.adjust(0, 0, GetWidth() - srcRect.right(), GetHeight() - srcRect.bottom()); } // Scaling for source coordinates must be done manually. if (src->mScaling != 1) srcRect = scaleRect(srcRect, src->mScaling); sk_sp image = makeCheckedImageSnapshot(src->mSurface, srcRect); srcRect.offset(-srcRect.x(), -srcRect.y()); getDrawCanvas()->drawImageRect(image, SkRect::Make(srcRect), destRect, makeSamplingOptions(rPosAry, mScaling, src->mScaling), &paint, SkCanvas::kFast_SrcRectConstraint); } else { // Scaling for source coordinates must be done manually. if (src->mScaling != 1) srcRect = scaleRect(srcRect, src->mScaling); // Do not use makeImageSnapshot(rect), as that one may make a needless data copy. getDrawCanvas()->drawImageRect(makeCheckedImageSnapshot(src->mSurface), SkRect::Make(srcRect), destRect, makeSamplingOptions(rPosAry, mScaling, src->mScaling), &paint, SkCanvas::kFast_SrcRectConstraint); } } bool SkiaSalGraphicsImpl::blendBitmap(const SalTwoRect& rPosAry, const SalBitmap& rBitmap) { if (checkInvalidSourceOrDestination(rPosAry)) return false; assert(dynamic_cast(&rBitmap)); const SkiaSalBitmap& rSkiaBitmap = static_cast(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(&rSourceBitmap)); assert(dynamic_cast(&rMaskBitmap)); assert(dynamic_cast(&rAlphaBitmap)); const SkiaSalBitmap& rSkiaSourceBitmap = static_cast(rSourceBitmap); const SkiaSalBitmap& rSkiaMaskBitmap = static_cast(rMaskBitmap); const SkiaSalBitmap& rSkiaAlphaBitmap = static_cast(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 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 shader = SkShaders::Blend(SkBlendMode::kSrcIn, shaderAlpha, rSkiaSourceBitmap.GetSkShader(samplingOptions)); drawShader(rPosAry, shader); return true; } void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap) { if (checkInvalidSourceOrDestination(rPosAry)) return; assert(dynamic_cast(&rSalBitmap)); const SkiaSalBitmap& rSkiaSourceBitmap = static_cast(rSalBitmap); drawBitmap(rPosAry, rSkiaSourceBitmap); } void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, const SalBitmap& rMaskBitmap) { drawAlphaBitmap(rPosAry, rSalBitmap, rMaskBitmap); } void SkiaSalGraphicsImpl::drawMask(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, Color nMaskColor) { assert(dynamic_cast(&rSalBitmap)); const SkiaSalBitmap& skiaBitmap = static_cast(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 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 image = makeCheckedImageSnapshot( mSurface, scaleRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), mScaling)); std::shared_ptr bitmap = std::make_shared(image); // If the surface is scaled for HiDPI, the bitmap needs to be scaled down, otherwise // it would have incorrect size from the API point of view. The DirectImage::Yes handling // in mergeCacheBitmaps() should access the original unscaled bitmap data to avoid // pointless scaling back and forth. if (mScaling != 1) { if (!isUnitTestRunning()) bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, goodScalingQuality()); else { // Some tests require exact pixel values and would be confused by smooth-scaling. // And some draw something smooth and not smooth-scaling there would break the checks. // When running on macOS with a Retina display, several BackendTest unit tests // also need a lower quality scaling level. if (getWindowScaling() != 1 || isUnitTestRunning("BackendTest__testDrawHaflEllipseAAWithPolyLineB2D_") || isUnitTestRunning("BackendTest__testDrawRectAAWithLine_") || isUnitTestRunning("GraphicsRenderTest__testDrawRectAAWithLine")) { bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, goodScalingQuality()); } else bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, BmpScaleFlag::NearestNeighbor); } } return bitmap; } Color SkiaSalGraphicsImpl::getPixel(tools::Long nX, tools::Long nY) { SkiaZone zone; checkSurface(); SAL_INFO("vcl.skia.trace", "getpixel(" << this << "): " << Point(nX, nY)); flushDrawing(); // This is presumably slow, but getPixel() should be generally used only by unit tests. SkBitmap bitmap; if (!bitmap.tryAllocN32Pixels(mSurface->width(), mSurface->height())) abort(); if (!mSurface->readPixels(bitmap, 0, 0)) abort(); return fromSkColor(bitmap.getColor(nX * mScaling, nY * mScaling)); } void SkiaSalGraphicsImpl::invert(basegfx::B2DPolygon const& rPoly, SalInvert eFlags) { preDraw(); SAL_INFO("vcl.skia.trace", "invert(" << this << "): " << rPoly << ":" << int(eFlags)); assert(mXorMode == XorMode::None); SkPath aPath; aPath.incReserve(rPoly.count()); addPolygonToPath(rPoly, aPath); aPath.setFillType(SkPathFillType::kEvenOdd); addUpdateRegion(aPath.getBounds()); { SkAutoCanvasRestore autoRestore(getDrawCanvas(), true); SkPaint aPaint; // There's no blend mode for inverting as such, but kExclusion is 's + d - 2*s*d', // so with d = 1.0 (all channels) it becomes effectively '1 - s', i.e. inverted color. aPaint.setBlendMode(SkBlendMode::kExclusion); aPaint.setColor(SkColorSetARGB(255, 255, 255, 255)); // TrackFrame just inverts a dashed path around the polygon if (eFlags == SalInvert::TrackFrame) { // TrackFrame is not supposed to paint outside of the polygon (usually rectangle), // but wider stroke width usually results in that, so ensure the requirement // by clipping. getDrawCanvas()->clipRect(aPath.getBounds(), SkClipOp::kIntersect, false); aPaint.setStrokeWidth(2); constexpr float intervals[] = { 4.0f, 4.0f }; aPaint.setStyle(SkPaint::kStroke_Style); aPaint.setPathEffect(SkDashPathEffect::Make(intervals, std::size(intervals), 0)); } else { aPaint.setStyle(SkPaint::kFill_Style); // N50 inverts in checker pattern if (eFlags == SalInvert::N50) { // This creates 2x2 checker pattern bitmap // TODO Use createSkSurface() and cache the image SkBitmap aBitmap; aBitmap.allocN32Pixels(2, 2); const SkPMColor white = SkPreMultiplyARGB(0xFF, 0xFF, 0xFF, 0xFF); const SkPMColor black = SkPreMultiplyARGB(0xFF, 0x00, 0x00, 0x00); SkPMColor* scanline; scanline = aBitmap.getAddr32(0, 0); *scanline++ = white; *scanline++ = black; scanline = aBitmap.getAddr32(0, 1); *scanline++ = black; *scanline++ = white; aBitmap.setImmutable(); // The bitmap is repeated in both directions the checker pattern is as big // as the polygon (usually rectangle) aPaint.setShader(aBitmap.makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkSamplingOptions())); } #ifdef SK_METAL // tdf#153306 prevent subpixel shifting of X coordinate // HACK: for some unknown reason, if the X coordinate of the // path's bounds is more than 1024, SkBlendMode::kExclusion will // shift by about a half a pixel to the right with Skia/Metal on // a Retina display. Weirdly, if the same polygon is repeatedly // drawn, the total shift is cumulative so if the drawn polygon // is more than a few pixels wide, the blinking cursor in Writer // will exhibit this bug but only for one thin vertical slice at // a time. Apparently, shifting drawing a very tiny amount to // the left seems to be enough to quell this runaway cumulative // X coordinate shift. if (isGPU()) { SkMatrix aMatrix; aMatrix.set(SkMatrix::kMTransX, -0.001); getDrawCanvas()->concat(aMatrix); } #endif } getDrawCanvas()->drawPath(aPath, aPaint); } postDraw(); } void SkiaSalGraphicsImpl::invert(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, SalInvert eFlags) { basegfx::B2DRectangle aRectangle(nX, nY, nX + nWidth, nY + nHeight); auto aRect = basegfx::utils::createPolygonFromRect(aRectangle); invert(aRect, eFlags); } void SkiaSalGraphicsImpl::invert(sal_uInt32 nPoints, const Point* pPointArray, SalInvert eFlags) { basegfx::B2DPolygon aPolygon; aPolygon.append(basegfx::B2DPoint(pPointArray[0].getX(), pPointArray[0].getY()), nPoints); for (sal_uInt32 i = 1; i < nPoints; ++i) { aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPointArray[i].getX(), pPointArray[i].getY())); } aPolygon.setClosed(true); invert(aPolygon, eFlags); } bool SkiaSalGraphicsImpl::drawEPS(tools::Long, tools::Long, tools::Long, tools::Long, void*, sal_uInt32) { return false; } // Create SkImage from a bitmap and possibly an alpha mask (the usual VCL one-minus-alpha), // with the given target size. Result will be possibly cached, unless disabled. // Especially in raster mode scaling and alpha blending may be expensive if done repeatedly. sk_sp SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitmap, const SkiaSalBitmap* alphaBitmap, const Size& targetSize) { if (alphaBitmap) assert(bitmap.GetSize() == alphaBitmap->GetSize()); if (targetSize.IsEmpty()) return {}; if (alphaBitmap && alphaBitmap->IsFullyOpaqueAsAlpha()) alphaBitmap = nullptr; // the alpha can be ignored if (bitmap.PreferSkShader() && (!alphaBitmap || alphaBitmap->PreferSkShader())) return {}; // If the bitmap has SkImage that matches the required size, try to use it, even // if it doesn't match bitmap.GetSize(). This can happen with delayed scaling. // This will catch cases such as some code pre-scaling the bitmap, which would make GetSkImage() // scale, changing GetImageKey() in the process so we'd have to re-cache, and then we'd need // to scale again in this function. bool bitmapReady = false; bool alphaBitmapReady = false; if (const sk_sp& image = bitmap.GetSkImage(DirectImage::Yes)) { assert(!bitmap.PreferSkShader()); if (imageSize(image) == targetSize) bitmapReady = true; } // If the image usable and there's no alpha, then it matches exactly what's wanted. if (bitmapReady && !alphaBitmap) return bitmap.GetSkImage(DirectImage::Yes); if (alphaBitmap) { if (!alphaBitmap->GetAlphaSkImage(DirectImage::Yes) && alphaBitmap->GetSkImage(DirectImage::Yes) && imageSize(alphaBitmap->GetSkImage(DirectImage::Yes)) == targetSize) { // There's a usable non-alpha image, try to convert it to alpha. assert(!alphaBitmap->PreferSkShader()); const_cast(alphaBitmap)->TryDirectConvertToAlphaNoScaling(); } if (const sk_sp& image = alphaBitmap->GetAlphaSkImage(DirectImage::Yes)) { assert(!alphaBitmap->PreferSkShader()); if (imageSize(image) == targetSize) alphaBitmapReady = true; } } if (bitmapReady && (!alphaBitmap || alphaBitmapReady)) { // Try to find a cached image based on the already existing images. OString key = makeCachedImageKey(bitmap, alphaBitmap, targetSize, DirectImage::Yes, DirectImage::Yes); if (sk_sp image = findCachedImage(key)) { assert(imageSize(image) == targetSize); return image; } } // Probably not much point in caching of just doing a copy. if (alphaBitmap == nullptr && targetSize == bitmap.GetSize()) return {}; // Image too small to be worth caching if not scaling. if (targetSize == bitmap.GetSize() && targetSize.Width() < 100 && targetSize.Height() < 100) return {}; // GPU-accelerated drawing with SkShader should be fast enough to not need caching. if (isGPU()) { // tdf#140925: But if this is such an extensive downscaling that caching the result // would noticeably reduce amount of data processed by the GPU on repeated usage, do it. int reduceRatio = bitmap.GetSize().Width() * bitmap.GetSize().Height() / targetSize.Width() / targetSize.Height(); if (reduceRatio < 10) return {}; } // Do not cache the result if it would take most of the cache and thus get evicted soon. if (targetSize.Width() * targetSize.Height() * 4 > maxImageCacheSize() * 0.7) return {}; // Use ready direct image if they are both available, now even the size doesn't matter // (we'll scale as necessary and it's better to scale from the original). Require only // that they are the same size, or that one prefers a shader or doesn't exist // (i.e. avoid two images of different size). bitmapReady = bitmap.GetSkImage(DirectImage::Yes) != nullptr; alphaBitmapReady = alphaBitmap && alphaBitmap->GetAlphaSkImage(DirectImage::Yes) != nullptr; if (bitmapReady && alphaBitmap && !alphaBitmapReady && !alphaBitmap->PreferSkShader()) bitmapReady = false; if (alphaBitmapReady && !bitmapReady && bitmap.PreferSkShader()) alphaBitmapReady = false; DirectImage bitmapType = bitmapReady ? DirectImage::Yes : DirectImage::No; DirectImage alphaBitmapType = alphaBitmapReady ? DirectImage::Yes : DirectImage::No; // Try to find a cached result, this time after possible delayed scaling. OString key = makeCachedImageKey(bitmap, alphaBitmap, targetSize, bitmapType, alphaBitmapType); if (sk_sp image = findCachedImage(key)) { assert(imageSize(image) == targetSize); return image; } // In some cases (tdf#134237) the target size may be very large. In that case it's // better to rely on Skia to clip and draw only the necessary, rather than prepare // a very large image only to not use most of it. Do this only after checking whether // the image is already cached, since it might have been already cached in a previous // call that had the draw area large enough to be seen as worth caching. const Size drawAreaSize = mClipRegion.GetBoundRect().GetSize() * mScaling; if (targetSize.Width() > drawAreaSize.Width() || targetSize.Height() > drawAreaSize.Height()) { // This is a bit tricky. The condition above just checks that at least a part of the resulting // image will not be used (it's larger then our drawing area). But this may often happen // when just scrolling a document with a large image, where the caching may very well be worth it. // Since the problem is mainly the cost of upscaling and then the size of the resulting bitmap, // compute a ratio of how much this is going to be scaled up, how much this is larger than // the drawing area, and then refuse to cache if it's too much. const double upscaleRatio = std::max(1.0, 1.0 * targetSize.Width() / bitmap.GetSize().Width() * targetSize.Height() / bitmap.GetSize().Height()); const double oversizeRatio = 1.0 * targetSize.Width() / drawAreaSize.Width() * targetSize.Height() / drawAreaSize.Height(); const double ratio = upscaleRatio * oversizeRatio; if (ratio > 4) { SAL_INFO("vcl.skia.trace", "mergecachebitmaps(" << this << "): not caching, ratio:" << ratio << ", " << bitmap.GetSize() << "->" << targetSize << " in " << drawAreaSize); return {}; } } Size sourceSize; if (bitmapReady) sourceSize = imageSize(bitmap.GetSkImage(DirectImage::Yes)); else if (alphaBitmapReady) sourceSize = imageSize(alphaBitmap->GetAlphaSkImage(DirectImage::Yes)); else sourceSize = bitmap.GetSize(); // Generate a new result and cache it. sk_sp tmpSurface = createSkSurface(targetSize, alphaBitmap ? kPremul_SkAlphaType : bitmap.alphaType()); if (!tmpSurface) return nullptr; SkCanvas* canvas = tmpSurface->getCanvas(); { SkAutoCanvasRestore autoRestore(canvas, true); SkPaint paint; SkSamplingOptions samplingOptions; if (targetSize != sourceSize) { SkMatrix matrix; matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / sourceSize.Width()); matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / sourceSize.Height()); canvas->concat(matrix); if (!isUnitTestRunning()) // unittests want exact pixel values samplingOptions = makeSamplingOptions(matrix, 1); } if (alphaBitmap != nullptr) { canvas->clear(SK_ColorTRANSPARENT); paint.setShader(SkShaders::Blend( SkBlendMode::kDstIn, bitmap.GetSkShader(samplingOptions, bitmapType), alphaBitmap->GetAlphaSkShader(samplingOptions, alphaBitmapType))); canvas->drawPaint(paint); } else if (bitmap.PreferSkShader()) { paint.setShader(bitmap.GetSkShader(samplingOptions, bitmapType)); canvas->drawPaint(paint); } else canvas->drawImage(bitmap.GetSkImage(bitmapType), 0, 0, samplingOptions, &paint); if (isGPU()) SAL_INFO("vcl.skia.trace", "mergecachebitmaps(" << this << "): caching GPU downscaling:" << bitmap.GetSize() << "->" << targetSize); } sk_sp image = makeCheckedImageSnapshot(tmpSurface); addCachedImage(key, image); return image; } OString SkiaSalGraphicsImpl::makeCachedImageKey(const SkiaSalBitmap& bitmap, const SkiaSalBitmap* alphaBitmap, const Size& targetSize, DirectImage bitmapType, DirectImage alphaBitmapType) { OString key = OString::number(targetSize.Width()) + "x" + OString::number(targetSize.Height()) + "_" + bitmap.GetImageKey(bitmapType); if (alphaBitmap) key += "_" + alphaBitmap->GetAlphaImageKey(alphaBitmapType); return key; } bool SkiaSalGraphicsImpl::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSourceBitmap, const SalBitmap& rAlphaBitmap) { assert(dynamic_cast(&rSourceBitmap)); assert(dynamic_cast(&rAlphaBitmap)); const SkiaSalBitmap& rSkiaSourceBitmap = static_cast(rSourceBitmap); const SkiaSalBitmap& rSkiaAlphaBitmap = static_cast(rAlphaBitmap); // Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated // alpha blending or scaling. SalTwoRect imagePosAry(rPosAry); Size imageSize = rSourceBitmap.GetSize(); // If the bitmap will be scaled, prefer to do it in mergeCacheBitmaps(), if possible. if ((rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) && rPosAry.mnSrcX == 0 && rPosAry.mnSrcY == 0 && rPosAry.mnSrcWidth == rSourceBitmap.GetSize().Width() && rPosAry.mnSrcHeight == rSourceBitmap.GetSize().Height()) { imagePosAry.mnSrcWidth = imagePosAry.mnDestWidth; imagePosAry.mnSrcHeight = imagePosAry.mnDestHeight; imageSize = Size(imagePosAry.mnSrcWidth, imagePosAry.mnSrcHeight); } sk_sp image = mergeCacheBitmaps(rSkiaSourceBitmap, &rSkiaAlphaBitmap, imageSize * mScaling); if (image) drawImage(imagePosAry, image, mScaling); else if (rSkiaAlphaBitmap.IsFullyOpaqueAsAlpha() && !rSkiaSourceBitmap.PreferSkShader()) // alpha can be ignored drawBitmap(rPosAry, rSkiaSourceBitmap); else drawShader(rPosAry, SkShaders::Blend( SkBlendMode::kDstIn, rSkiaSourceBitmap.GetSkShader(makeSamplingOptions(rPosAry, mScaling)), rSkiaAlphaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry, mScaling)))); return true; } void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SkiaSalBitmap& bitmap, SkBlendMode blendMode) { // Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated // scaling. SalTwoRect imagePosAry(rPosAry); Size imageSize = bitmap.GetSize(); // If the bitmap will be scaled, prefer to do it in mergeCacheBitmaps(), if possible. if ((rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) && rPosAry.mnSrcX == 0 && rPosAry.mnSrcY == 0 && rPosAry.mnSrcWidth == bitmap.GetSize().Width() && rPosAry.mnSrcHeight == bitmap.GetSize().Height()) { imagePosAry.mnSrcWidth = imagePosAry.mnDestWidth; imagePosAry.mnSrcHeight = imagePosAry.mnDestHeight; imageSize = Size(imagePosAry.mnSrcWidth, imagePosAry.mnSrcHeight); } sk_sp image = mergeCacheBitmaps(bitmap, nullptr, imageSize * mScaling); if (image) drawImage(imagePosAry, image, mScaling, blendMode); else if (bitmap.PreferSkShader()) drawShader(rPosAry, bitmap.GetSkShader(makeSamplingOptions(rPosAry, mScaling)), blendMode); else drawImage(rPosAry, bitmap.GetSkImage(), 1, blendMode); } void SkiaSalGraphicsImpl::drawImage(const SalTwoRect& rPosAry, const sk_sp& aImage, int srcScaling, SkBlendMode eBlendMode) { SkRect aSourceRect = SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); if (srcScaling != 1) aSourceRect = scaleRect(aSourceRect, srcScaling); SkRect aDestinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight); SkPaint aPaint = makeBitmapPaint(); aPaint.setBlendMode(eBlendMode); preDraw(); SAL_INFO("vcl.skia.trace", "drawimage(" << this << "): " << rPosAry << ":" << SkBlendMode_Name(eBlendMode)); addUpdateRegion(aDestinationRect); getDrawCanvas()->drawImageRect(aImage, aSourceRect, aDestinationRect, makeSamplingOptions(rPosAry, mScaling, srcScaling), &aPaint, SkCanvas::kFast_SrcRectConstraint); ++pendingOperationsToFlush; // tdf#136369 postDraw(); } // SkShader can be used to merge multiple bitmaps with appropriate blend modes (e.g. when // merging a bitmap with its alpha mask). void SkiaSalGraphicsImpl::drawShader(const SalTwoRect& rPosAry, const sk_sp& shader, SkBlendMode blendMode) { preDraw(); SAL_INFO("vcl.skia.trace", "drawshader(" << this << "): " << rPosAry); SkRect destinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight); addUpdateRegion(destinationRect); SkPaint paint = makeBitmapPaint(); paint.setBlendMode(blendMode); paint.setShader(shader); SkCanvas* canvas = getDrawCanvas(); // Scaling needs to be done explicitly using a matrix. { SkAutoCanvasRestore autoRestore(canvas, true); SkMatrix matrix = SkMatrix::Translate(rPosAry.mnDestX, rPosAry.mnDestY) * SkMatrix::Scale(1.0 * rPosAry.mnDestWidth / rPosAry.mnSrcWidth, 1.0 * rPosAry.mnDestHeight / rPosAry.mnSrcHeight) * SkMatrix::Translate(-rPosAry.mnSrcX, -rPosAry.mnSrcY); #ifndef NDEBUG // Handle floating point imprecisions, round p1 to 2 decimal places. auto compareRounded = [](const SkPoint& p1, const SkPoint& p2) { return rtl::math::round(p1.x(), 2) == p2.x() && rtl::math::round(p1.y(), 2) == p2.y(); }; #endif assert(compareRounded(matrix.mapXY(rPosAry.mnSrcX, rPosAry.mnSrcY), SkPoint::Make(rPosAry.mnDestX, rPosAry.mnDestY))); assert(compareRounded( matrix.mapXY(rPosAry.mnSrcX + rPosAry.mnSrcWidth, rPosAry.mnSrcY + rPosAry.mnSrcHeight), SkPoint::Make(rPosAry.mnDestX + rPosAry.mnDestWidth, rPosAry.mnDestY + rPosAry.mnDestHeight))); canvas->concat(matrix); SkRect sourceRect = SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); canvas->drawRect(sourceRect, paint); } postDraw(); } bool SkiaSalGraphicsImpl::hasFastDrawTransformedBitmap() const { // Return true even in raster mode, even that way Skia is faster than e.g. GraphicObject // trying to handle stuff manually. return true; } // Whether applying matrix needs image smoothing for the transformation. static bool matrixNeedsHighQuality(const SkMatrix& matrix) { if (matrix.isIdentity()) return false; if (matrix.isScaleTranslate()) { if (abs(matrix.getScaleX()) == 1 && abs(matrix.getScaleY()) == 1) return false; // Only at most flipping and keeping the size. return true; } assert(!matrix.hasPerspective()); // we do not use this if (matrix.getScaleX() == 0 && matrix.getScaleY() == 0) { // Rotating 90 or 270 degrees while keeping the size. if ((matrix.getSkewX() == 1 && matrix.getSkewY() == -1) || (matrix.getSkewX() == -1 && matrix.getSkewY() == 1)) return false; } return true; } namespace SkiaTests { bool matrixNeedsHighQuality(const SkMatrix& matrix) { return ::matrixNeedsHighQuality(matrix); } } bool SkiaSalGraphicsImpl::drawTransformedBitmap(const basegfx::B2DPoint& rNull, const basegfx::B2DPoint& rX, const basegfx::B2DPoint& rY, const SalBitmap& rSourceBitmap, const SalBitmap* pAlphaBitmap, double fAlpha) { assert(dynamic_cast(&rSourceBitmap)); assert(!pAlphaBitmap || dynamic_cast(pAlphaBitmap)); const SkiaSalBitmap& rSkiaBitmap = static_cast(rSourceBitmap); const SkiaSalBitmap* pSkiaAlphaBitmap = static_cast(pAlphaBitmap); if (pSkiaAlphaBitmap && pSkiaAlphaBitmap->IsFullyOpaqueAsAlpha()) pSkiaAlphaBitmap = nullptr; // the alpha can be ignored // Setup the image transformation, // using the rNull, rX, rY points as destinations for the (0,0), (Width,0), (0,Height) source points. const basegfx::B2DVector aXRel = rX - rNull; const basegfx::B2DVector aYRel = rY - rNull; preDraw(); SAL_INFO("vcl.skia.trace", "drawtransformedbitmap(" << this << "): " << rSourceBitmap.GetSize() << " " << rNull << ":" << rX << ":" << rY); addUpdateRegion(SkRect::MakeWH(GetWidth(), GetHeight())); // can't tell, use whole area // Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated // alpha blending or scaling. // The extra fAlpha blending is not cached, with the assumption that it usually gradually changes // for each invocation. // Pass size * mScaling to mergeCacheBitmaps() so that it prepares the size that will be needed // after the mScaling-scaling matrix, but otherwise calculate everything else using the VCL coordinates. Size imageSize(round(aXRel.getLength()), round(aYRel.getLength())); sk_sp imageToDraw = mergeCacheBitmaps(rSkiaBitmap, pSkiaAlphaBitmap, imageSize * mScaling); if (imageToDraw) { SkMatrix matrix; // Round sizes for scaling, so that sub-pixel differences don't // trigger unnecessary scaling. Image has already been scaled // by mergeCacheBitmaps() and we shouldn't scale here again // unless the drawing is also skewed. matrix.set(SkMatrix::kMScaleX, round(aXRel.getX()) / imageSize.Width()); matrix.set(SkMatrix::kMScaleY, round(aYRel.getY()) / imageSize.Height()); matrix.set(SkMatrix::kMSkewY, aXRel.getY() / imageSize.Width()); matrix.set(SkMatrix::kMSkewX, aYRel.getX() / imageSize.Height()); matrix.set(SkMatrix::kMTransX, rNull.getX()); matrix.set(SkMatrix::kMTransY, rNull.getY()); SkCanvas* canvas = getDrawCanvas(); SkAutoCanvasRestore autoRestore(canvas, true); canvas->concat(matrix); SkSamplingOptions samplingOptions; // If the matrix changes geometry, we need to smooth-scale. If there's mScaling, // that's already been handled by mergeCacheBitmaps(). if (matrixNeedsHighQuality(matrix)) samplingOptions = makeSamplingOptions(matrix, 1); if (fAlpha == 1.0) { // Specify sizes to scale the image size back if needed (because of mScaling). SkRect dstRect = SkRect::MakeWH(imageSize.Width(), imageSize.Height()); SkRect srcRect = SkRect::MakeWH(imageToDraw->width(), imageToDraw->height()); SkPaint paint = makeBitmapPaint(); canvas->drawImageRect(imageToDraw, srcRect, dstRect, samplingOptions, &paint, SkCanvas::kFast_SrcRectConstraint); } else { SkPaint paint = makeBitmapPaint(); // Scale the image size back if needed. SkMatrix scale = SkMatrix::Scale(1.0 / mScaling, 1.0 / mScaling); paint.setShader(SkShaders::Blend( SkBlendMode::kDstIn, imageToDraw->makeShader(samplingOptions, &scale), SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0)))); canvas->drawRect(SkRect::MakeWH(imageSize.Width(), imageSize.Height()), paint); } } else { SkMatrix matrix; const Size aSize = rSourceBitmap.GetSize(); matrix.set(SkMatrix::kMScaleX, aXRel.getX() / aSize.Width()); matrix.set(SkMatrix::kMScaleY, aYRel.getY() / aSize.Height()); matrix.set(SkMatrix::kMSkewY, aXRel.getY() / aSize.Width()); matrix.set(SkMatrix::kMSkewX, aYRel.getX() / aSize.Height()); matrix.set(SkMatrix::kMTransX, rNull.getX()); matrix.set(SkMatrix::kMTransY, rNull.getY()); SkCanvas* canvas = getDrawCanvas(); SkAutoCanvasRestore autoRestore(canvas, true); canvas->concat(matrix); SkSamplingOptions samplingOptions; if (matrixNeedsHighQuality(matrix) || (mScaling != 1 && !isUnitTestRunning())) samplingOptions = makeSamplingOptions(matrix, mScaling); if (pSkiaAlphaBitmap) { SkPaint paint = makeBitmapPaint(); paint.setShader(SkShaders::Blend(SkBlendMode::kDstIn, rSkiaBitmap.GetSkShader(samplingOptions), pSkiaAlphaBitmap->GetAlphaSkShader(samplingOptions))); if (fAlpha != 1.0) paint.setShader( SkShaders::Blend(SkBlendMode::kDstIn, paint.refShader(), SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0)))); canvas->drawRect(SkRect::MakeWH(aSize.Width(), aSize.Height()), paint); } else if (rSkiaBitmap.PreferSkShader() || fAlpha != 1.0) { SkPaint paint = makeBitmapPaint(); paint.setShader(rSkiaBitmap.GetSkShader(samplingOptions)); if (fAlpha != 1.0) paint.setShader( SkShaders::Blend(SkBlendMode::kDstIn, paint.refShader(), SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0)))); canvas->drawRect(SkRect::MakeWH(aSize.Width(), aSize.Height()), paint); } else { SkPaint paint = makeBitmapPaint(); canvas->drawImage(rSkiaBitmap.GetSkImage(), 0, 0, samplingOptions, &paint); } } postDraw(); return true; } bool SkiaSalGraphicsImpl::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, sal_uInt8 nTransparency) { privateDrawAlphaRect(nX, nY, nWidth, nHeight, nTransparency / 100.0); return true; } bool SkiaSalGraphicsImpl::drawGradient(const tools::PolyPolygon& rPolyPolygon, const Gradient& rGradient) { if (rGradient.GetStyle() != css::awt::GradientStyle_LINEAR && rGradient.GetStyle() != css::awt::GradientStyle_AXIAL && rGradient.GetStyle() != css::awt::GradientStyle_RADIAL) return false; // unsupported if (rGradient.GetSteps() != 0) return false; // We can't tell Skia how many colors to use in the gradient. preDraw(); SAL_INFO("vcl.skia.trace", "drawgradient(" << this << "): " << rPolyPolygon.getB2DPolyPolygon() << ":" << static_cast(rGradient.GetStyle())); tools::Rectangle boundRect(rPolyPolygon.GetBoundRect()); if (boundRect.IsEmpty()) return true; SkPath path; if (rPolyPolygon.IsRect()) { // Rect->Polygon conversion loses the right and bottom edge, fix that. path.addRect(SkRect::MakeXYWH(boundRect.getX(), boundRect.getY(), boundRect.GetWidth(), boundRect.GetHeight())); boundRect.AdjustRight(1); boundRect.AdjustBottom(1); } else addPolyPolygonToPath(rPolyPolygon.getB2DPolyPolygon(), path); path.setFillType(SkPathFillType::kEvenOdd); addUpdateRegion(path.getBounds()); Gradient aGradient(rGradient); tools::Rectangle aBoundRect; Point aCenter; aGradient.SetAngle(aGradient.GetAngle() + 2700_deg10); aGradient.GetBoundRect(boundRect, aBoundRect, aCenter); SkColor startColor = toSkColorWithIntensity(rGradient.GetStartColor(), rGradient.GetStartIntensity()); SkColor endColor = toSkColorWithIntensity(rGradient.GetEndColor(), rGradient.GetEndIntensity()); sk_sp shader; if (rGradient.GetStyle() == css::awt::GradientStyle_LINEAR) { tools::Polygon aPoly(aBoundRect); aPoly.Rotate(aCenter, aGradient.GetAngle() % 3600_deg10); SkPoint points[2] = { SkPoint::Make(toSkX(aPoly[0].X()), toSkY(aPoly[0].Y())), SkPoint::Make(toSkX(aPoly[1].X()), toSkY(aPoly[1].Y())) }; SkColor colors[2] = { startColor, endColor }; SkScalar pos[2] = { SkDoubleToScalar(aGradient.GetBorder() / 100.0), 1.0 }; shader = SkGradientShader::MakeLinear(points, colors, pos, 2, SkTileMode::kClamp); } else if (rGradient.GetStyle() == css::awt::GradientStyle_AXIAL) { tools::Polygon aPoly(aBoundRect); aPoly.Rotate(aCenter, aGradient.GetAngle() % 3600_deg10); SkPoint points[2] = { SkPoint::Make(toSkX(aPoly[0].X()), toSkY(aPoly[0].Y())), SkPoint::Make(toSkX(aPoly[1].X()), toSkY(aPoly[1].Y())) }; SkColor colors[3] = { endColor, startColor, endColor }; SkScalar border = SkDoubleToScalar(aGradient.GetBorder() / 100.0); SkScalar pos[3] = { std::min(border * 0.5f, 0.5f), 0.5f, std::max(1 - border * 0.5f, 0.5f) }; shader = SkGradientShader::MakeLinear(points, colors, pos, 3, SkTileMode::kClamp); } else { // Move the center by (-1,-1) (the default VCL algorithm is a bit off-center that way, // Skia is the opposite way). SkPoint center = SkPoint::Make(toSkX(aCenter.X()) - 1, toSkY(aCenter.Y()) - 1); SkScalar radius = std::max(aBoundRect.GetWidth() / 2.0, aBoundRect.GetHeight() / 2.0); SkColor colors[2] = { endColor, startColor }; SkScalar pos[2] = { SkDoubleToScalar(aGradient.GetBorder() / 100.0), 1.0 }; shader = SkGradientShader::MakeRadial(center, radius, colors, pos, 2, SkTileMode::kClamp); } SkPaint paint = makeGradientPaint(); paint.setAntiAlias(mParent.getAntiAlias()); paint.setShader(shader); getDrawCanvas()->drawPath(path, paint); postDraw(); return true; } bool SkiaSalGraphicsImpl::implDrawGradient(const basegfx::B2DPolyPolygon& rPolyPolygon, const SalGradient& rGradient) { preDraw(); SAL_INFO("vcl.skia.trace", "impldrawgradient(" << this << "): " << rPolyPolygon << ":" << rGradient.maPoint1 << "->" << rGradient.maPoint2 << ":" << rGradient.maStops.size()); SkPath path; addPolyPolygonToPath(rPolyPolygon, path); path.setFillType(SkPathFillType::kEvenOdd); addUpdateRegion(path.getBounds()); SkPoint points[2] = { SkPoint::Make(toSkX(rGradient.maPoint1.getX()), toSkY(rGradient.maPoint1.getY())), SkPoint::Make(toSkX(rGradient.maPoint2.getX()), toSkY(rGradient.maPoint2.getY())) }; std::vector colors; std::vector pos; for (const SalGradientStop& stop : rGradient.maStops) { colors.emplace_back(toSkColor(stop.maColor)); pos.emplace_back(stop.mfOffset); } sk_sp shader = SkGradientShader::MakeLinear(points, colors.data(), pos.data(), colors.size(), SkTileMode::kDecal); SkPaint paint = makeGradientPaint(); paint.setAntiAlias(mParent.getAntiAlias()); paint.setShader(shader); getDrawCanvas()->drawPath(path, paint); postDraw(); return true; } static double toRadian(Degree10 degree10th) { return toRadians(3600_deg10 - degree10th); } static auto toCos(Degree10 degree10th) { return SkScalarCos(toRadian(degree10th)); } static auto toSin(Degree10 degree10th) { return SkScalarSin(toRadian(degree10th)); } void SkiaSalGraphicsImpl::drawGenericLayout(const GenericSalLayout& layout, Color textColor, const SkFont& font, const SkFont& verticalFont) { SkiaZone zone; std::vector glyphIds; std::vector glyphForms; std::vector verticals; glyphIds.reserve(256); glyphForms.reserve(256); verticals.reserve(256); basegfx::B2DPoint aPos; const GlyphItem* pGlyph; int nStart = 0; auto cos = toCos(layout.GetOrientation()); auto sin = toSin(layout.GetOrientation()); while (layout.GetNextGlyph(&pGlyph, aPos, nStart)) { glyphIds.push_back(pGlyph->glyphId()); verticals.emplace_back(pGlyph->IsVertical()); auto cos1 = pGlyph->IsVertical() ? sin : cos; // cos (x - 90) = sin (x) auto sin1 = pGlyph->IsVertical() ? -cos : sin; // sin (x - 90) = -cos (x) SkRSXform form = SkRSXform::Make(cos1, sin1, aPos.getX(), aPos.getY()); glyphForms.emplace_back(std::move(form)); } if (glyphIds.empty()) return; preDraw(); auto getBoundRect = [&layout]() { basegfx::B2DRectangle rect; layout.GetBoundRect(rect); return rect; }; SAL_INFO("vcl.skia.trace", "drawtextblob(" << this << "): " << getBoundRect() << ", " << glyphIds.size() << " glyphs, " << textColor); // Vertical glyphs need a different font, so split drawing into runs that each // draw only consecutive horizontal or vertical glyphs. std::vector::const_iterator pos = verticals.cbegin(); std::vector::const_iterator end = verticals.cend(); while (pos != end) { bool verticalRun = *pos; std::vector::const_iterator rangeEnd = std::find(pos + 1, end, !verticalRun); size_t index = pos - verticals.cbegin(); size_t count = rangeEnd - pos; sk_sp textBlob = SkTextBlob::MakeFromRSXform( glyphIds.data() + index, count * sizeof(SkGlyphID), glyphForms.data() + index, verticalRun ? verticalFont : font, SkTextEncoding::kGlyphID); addUpdateRegion(textBlob->bounds()); SkPaint paint = makeTextPaint(textColor); getDrawCanvas()->drawTextBlob(textBlob, 0, 0, paint); pos = rangeEnd; } postDraw(); } bool SkiaSalGraphicsImpl::supportsOperation(OutDevSupportType eType) const { switch (eType) { case OutDevSupportType::TransparentRect: return true; default: return false; } } static int getScaling() { // It makes sense to support the debugging flag on all platforms // for unittests purpose, even if the actual windows cannot do it. if (const char* env = getenv("SAL_FORCE_HIDPI_SCALING")) return atoi(env); return 1; } int SkiaSalGraphicsImpl::getWindowScaling() const { static const int scaling = getScaling(); return scaling; } void SkiaSalGraphicsImpl::dump(const char* file) const { assert(mSurface.get()); SkiaHelper::dump(mSurface, file); } void SkiaSalGraphicsImpl::windowBackingPropertiesChanged() { if (mInWindowBackingPropertiesChanged || !isGPU()) return; mInWindowBackingPropertiesChanged = true; performFlush(); mInWindowBackingPropertiesChanged = false; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */