/* -*- 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 .
*/
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, constbool bClosePath, constbool bHasCurves, bool* hasOnlyOrthogonal = nullptr)
{
assert(nFirstIndex < nPointCount || (nFirstIndex == 0 && nPointCount == 0));
assert(nLastIndex <= nPointCount);
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;
} elseif (!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);
// 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()) returntrue; // no curves at all for (constauto& rPolygon : rPolyPolygon)
{ const sal_uInt32 nPointCount(rPolygon.count()); bool bFirst = true;
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))
{ returntrue; // found a straight line
}
}
aPreviousPoint = aCurrentPoint;
nPreviousIndex = nCurrentIndex;
}
} returnfalse; // no straight line found
}
// returns true if the source or destination rectangles are invalid bool checkInvalidSourceOrDestination(SalTwoRect const& rPosAry)
{ return rPosAry.mnSrcWidth <= 0 || rPosAry.mnSrcHeight <= 0 || rPosAry.mnDestWidth <= 0
|| rPosAry.mnDestHeight <= 0;
}
std::string dumpOptionalColor(const std::optional<Color>& c)
{
std::ostringstream oss; if (c)
oss << *c; else
oss << "no color";
return std::move(oss).str(); // optimized in C++20
}
} // end anonymous namespace
// Class that triggers flushing the backing buffer when idle. class SkiaFlushIdle : public Idle
{
SkiaSalGraphicsImpl* mpGraphics; #ifndef NDEBUG char* debugname; #endif
public: explicit SkiaFlushIdle(SkiaSalGraphicsImpl* pGraphics)
: Idle(get_debug_name(pGraphics))
, mpGraphics(pGraphics)
{ #ifdef MACOSX // tdf#165277 Skia needs to flush immediately before POST_PAINT // tasks on macOS
SetPriority(TaskPriority::SKIA_FLUSH); #else // We don't want to be swapping before we've painted.
SetPriority(TaskPriority::POST_PAINT); #endif
} #ifndef NDEBUG virtual ~SkiaFlushIdle() { free(debugname); } #endif constchar* get_debug_name(SkiaSalGraphicsImpl* pGraphics)
{ #ifndef NDEBUG // Idle keeps just a pointer, so we need to store the string
debugname = strdup(
OString("skia idle 0x" + OString::number(reinterpret_cast<sal_uIntPtr>(pGraphics), 16))
.getStr()); return debugname; #else
(void)pGraphics; return"skia idle"; #endif
}
virtualvoid Invoke() override
{
mpGraphics->performFlush();
Stop(); #ifdef MACOSX // tdf#157312 and tdf#163945 Lower Skia flush timer priority on macOS // On macOS, flushing with Skia/Metal is noticeably slower than // with Skia/Raster. So lower the flush timer priority to // TaskPriority::SKIA_FLUSH so that the flush timer runs less // frequently but each pass copies a more up-to-date offscreen // surface. // tdf#165277 Skia needs to flush immediately before POST_PAINT // tasks on macOS
SetPriority(TaskPriority::SKIA_FLUSH); #else
SetPriority(TaskPriority::HIGHEST); #endif
}
};
// We don't want to be swapping before we've painted.
mFlush->Stop(); #ifdef MACOSX // tdf#165277 Skia needs to flush immediately before POST_PAINT // tasks on macOS
mFlush->SetPriority(TaskPriority::SKIA_FLUSH); #else
mFlush->SetPriority(TaskPriority::POST_PAINT); #endif
}
void SkiaSalGraphicsImpl::createWindowSurface(bool forceRaster)
{
SkiaZone zone;
assert(!isOffscreen());
assert(!mSurface);
createWindowSurfaceInternal(forceRaster); if (!mSurface)
{ switch (forceRaster ? RenderRaster : renderMethodToUse())
{ case RenderVulkan:
SAL_WARN("vcl.skia", "cannot create Vulkan GPU window surface, falling back to Raster");
destroySurface(); // destroys also WindowContext return createWindowSurface(true); // try again case RenderMetal:
SAL_WARN("vcl.skia", "cannot create Metal GPU window surface, falling back to Raster");
destroySurface(); // destroys also WindowContext return createWindowSurface(true); // try again case RenderRaster:
abort(); // This should not really happen, do not even try to cope with it.
}
}
mIsGPU = mSurface->getCanvas()->recordingContext() != nullptr; #ifdef DBG_UTIL
prefillSurface(mSurface); #endif
}
bool SkiaSalGraphicsImpl::isOffscreen() const
{ if (mProvider == nullptr || mProvider->IsOffScreen()) returntrue; // 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) returntrue; returnfalse;
}
void SkiaSalGraphicsImpl::createOffscreenSurface()
{
SkiaZone zone;
assert(isOffscreen());
assert(!mSurface); // HACK: See isOffscreen(). int width = std::max(1, GetWidth()); int height = std::max(1, GetHeight()); // We need to use window scaling even for offscreen surfaces, because the common usage is rendering something // into an offscreen surface and then copy it to a window, so without scaling here the result would be originally // drawn without scaling and only upscaled when drawing to a window.
mScaling = getWindowScaling();
mSurface = createSkSurface(width * mScaling, height * mScaling);
assert(mSurface);
mIsGPU = mSurface->getCanvas()->recordingContext() != nullptr;
}
void SkiaSalGraphicsImpl::destroySurface()
{
SkiaZone zone; if (mSurface)
{ // check setClipRegion() invariant
assert(mSurface->getCanvas()->getSaveCount() == 3); // if this fails, something forgot to use SkAutoCanvasRestore
assert(mSurface->getCanvas()->getTotalMatrix() == SkMatrix::Scale(mScaling, mScaling));
}
mSurface.reset();
mWindowContext.reset();
mIsGPU = false;
mScaling = 1;
}
void SkiaSalGraphicsImpl::performFlush()
{
SkiaZone zone;
flushDrawing(); if (mSurface)
{ // Related: tdf#152703 Eliminate flickering during live resizing of a window // When in live resize, the SkiaSalGraphicsImpl class does not detect that // the window size has changed until after the flush has been called so // call checkSurface() to recreate the SkSurface if needed before flushing.
checkSurface(); if (mDirtyRect.intersect(SkIRect::MakeWH(GetWidth(), GetHeight())))
flushSurfaceToWindowContext();
mDirtyRect.setEmpty();
}
}
void SkiaSalGraphicsImpl::flushSurfaceToWindowContext()
{
sk_sp<SkSurface> screenSurface = mWindowContext->getBackbufferSurface(); if (screenSurface != mSurface)
{ // GPU-based window contexts require calling getBackbufferSurface() // for every swapBuffers(), for this reason mSurface is an offscreen surface // where we keep the contents (LO does not do full redraws). // So here blit the surface to the window context surface and then swap it.
// Raster should always draw directly to backbuffer to save copying // except for small sizes - see renderMethodToUseForSize
assert(isGPU() || (mSurface->width() <= 32 && mSurface->height() <= 32));
SkPaint paint;
paint.setBlendMode(SkBlendMode::kSrc); // copy as is // We ignore mDirtyRect here, and mSurface already is in screenSurface coordinates, // so no transformation needed.
screenSurface->getCanvas()->drawImage(makeCheckedImageSnapshot(mSurface), 0, 0,
SkSamplingOptions(), &paint); // Otherwise the window is not drawn sometimes. if (auto dContext = GrAsDirectContext(screenSurface->getCanvas()->recordingContext()))
dContext->flushAndSubmit();
mWindowContext->swapBuffers(nullptr); // Must swap the entire surface.
} else
{ // For raster mode use directly the backbuffer surface, it's just a bitmap // surface anyway, and for those there's no real requirement to call // getBackbufferSurface() repeatedly. Using our own surface would duplicate // memory and cost time copying pixels around.
assert(!isGPU());
SkIRect dirtyRect = mDirtyRect; if (mScaling != 1) // Adjust to mSurface coordinates if needed.
dirtyRect = scaleRect(dirtyRect, mScaling);
mWindowContext->swapBuffers(&dirtyRect);
}
}
void SkiaSalGraphicsImpl::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). staticint maxOperationsToFlush = 1000; if (pendingOperationsToFlush > maxOperationsToFlush)
{ if (auto dContext = GrAsDirectContext(mSurface->getCanvas()->recordingContext()))
dContext->flushAndSubmit();
pendingOperationsToFlush = 0;
}
SkiaZone::leave(); // matched in preDraw() // If there's a problem with the GPU context, abort. if (GrDirectContext* context = GrAsDirectContext(mSurface->getCanvas()->recordingContext()))
{ // We don't know the exact status of the surface (and what has or has not been drawn to it). // But let's pretend it was drawn OK, and reduce the flush limit, to try to avoid possible // small HW memory limitation if (context->oomed())
{ if (maxOperationsToFlush > 10)
{
maxOperationsToFlush /= 2;
} else
{
SAL_WARN("vcl.skia", "GPU context has run out of memory, aborting.");
abort();
}
} // Unrecoverable problem. if (context->abandoned())
{
SAL_WARN("vcl.skia", "GPU context has been abandoned, aborting.");
abort();
}
}
}
void SkiaSalGraphicsImpl::scheduleFlush()
{ if (!isOffscreen())
{ if (!Application::IsInExecute())
performFlush(); // otherwise nothing would trigger idle rendering elseif (!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()));
} elseif (mInWindowBackingPropertiesChanged || GetWidth() * mScaling != mSurface->width()
|| GetHeight() * mScaling != mSurface->height())
{ if (!avoidRecreateByResize())
{
Size oldSize(mSurface->width(), mSurface->height()); // Recreating a surface means that the old SkSurface contents will be lost. // But if a window has been resized the windowing system may send repaint events // only for changed parts and VCL would not repaint the whole area, assuming // that some parts have not changed (this is what seems to cause tdf#131952). // So carry over the old contents for windows, even though generally everything // will be usually repainted anyway.
sk_sp<SkImage> snapshot; if (!isOffscreen())
{
flushDrawing();
snapshot = makeCheckedImageSnapshot(mSurface);
}
destroySurface();
createSurface();
if (snapshot)
{
SkPaint paint;
paint.setBlendMode(SkBlendMode::kSrc); // copy as is // Scaling by current mScaling is active, undo that. We assume that the scaling // does not change.
resetCanvasScalingAndClipping();
mSurface->getCanvas()->drawImage(snapshot, 0, 0, SkSamplingOptions(), &paint);
setCanvasScalingAndClipping();
}
SAL_INFO("vcl.skia.trace", "recreate(" << this << "): old " << oldSize << " new "
<< Size(mSurface->width(), mSurface->height())
<< " requested "
<< Size(GetWidth(), GetHeight()));
}
}
}
bool SkiaSalGraphicsImpl::avoidRecreateByResize() const
{ // Keep the old surface if VCL sends us a broken size (see isOffscreen()). if (GetWidth() == 0 || GetHeight() == 0) returntrue; returnfalse;
}
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::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::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();
}
// 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();
}
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()) returnfalse; // Only filled polygons without an outline are problematic. if (!moFillColor || moLineColor) returnfalse; // Merge only simple polygons, real polypolygons most likely aren't needlessly split, // so they do not need joining. if (aPolyPolygon.count() != 1) returnfalse; // If the polygon is not closed, it doesn't mark an area to be filled. if (!aPolyPolygon.isClosed()) returnfalse; // 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)) returnfalse;
if (!mLastPolyPolygonInfo.polygons.empty()
&& (mLastPolyPolygonInfo.transparency != fTransparency
|| !mLastPolyPolygonInfo.bounds.overlaps(aPolyPolygon.getB2DRange())))
{
checkPendingDrawing(); // Cannot be parts of the same larger polygon, draw the last and reset.
} if (!mLastPolyPolygonInfo.polygons.empty())
{
assert(aPolyPolygon.count() == 1);
assert(mLastPolyPolygonInfo.polygons.back().count() == 1); // Check if the new and the previous polygon share at least one point. If not, then they // cannot be adjacent polygons, so there's no point in trying to merge them. bool sharePoint = false; const basegfx::B2DPolygon& poly1 = aPolyPolygon.getB2DPolygon(0); const basegfx::B2DPolygon& poly2 = mLastPolyPolygonInfo.polygons.back().getB2DPolygon(0);
o3tl::sorted_vector<basegfx::B2DPoint, LessThan> poly1Points; // for O(n log n)
poly1Points.reserve(poly1.count()); for (sal_uInt32 i = 0; i < poly1.count(); ++i)
poly1Points.insert(poly1.getB2DPoint(i)); for (sal_uInt32 i = 0; i < poly2.count(); ++i) if (poly1Points.find(poly2.getB2DPoint(i)) != poly1Points.end())
{
sharePoint = true; break;
} if (!sharePoint)
checkPendingDrawing(); // Draw the previous one and reset.
} // Collect the polygons that can be possibly merged. Do the merging only once at the end, // because it's not a cheap operation.
mLastPolyPolygonInfo.polygons.push_back(aPolyPolygon);
mLastPolyPolygonInfo.bounds.expand(aPolyPolygon.getB2DRange());
mLastPolyPolygonInfo.transparency = fTransparency; returntrue;
}
// Tdf#140848 - basegfx::utils::mergeToSinglePolyPolygon() seems to have rounding // errors that sometimes cause it to merge incorrectly. staticvoid 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);
}
}
}
// Adjust line width for object-to-device scale.
fLineWidth = (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength(); #ifdef MACOSX // tdf#162646 suppressing drawing hairlines when scaling // Previously, drawing of hairlines (i.e. zero line width) was only // suppressed when running unit tests. But drawing hairlines causes // unexpected shifting of the lines when using a Retina display on // macOS and antialiasing is disabled. if (fLineWidth == 0 && mScaling != 1 && (isUnitTestRunning() || !mParent.getAntiAlias())) #else // On HiDPI displays, do not draw hairlines, draw 1-pixel wide lines in order to avoid // smoothing that would confuse unittests. if (fLineWidth == 0 && mScaling != 1 && isUnitTestRunning()) #endif
fLineWidth = 1; // this will be scaled by mScaling
// Transform to DeviceCoordinates, get DeviceLineWidth, execute PixelSnapHairline
basegfx::B2DPolygon aPolyLine(rPolyLine);
aPolyLine.transform(rObjectToDevice); if (bPixelSnapHairline)
{
aPolyLine = basegfx::utils::snapPointsOfHorizontalOrVerticalEdges(aPolyLine);
}
SkPaint aPaint = makeLinePaint(fTransparency);
switch (eLineJoin)
{ case basegfx::B2DLineJoin::Bevel:
aPaint.setStrokeJoin(SkPaint::kBevel_Join); break; case basegfx::B2DLineJoin::Round:
aPaint.setStrokeJoin(SkPaint::kRound_Join); break; case basegfx::B2DLineJoin::NONE: break; case basegfx::B2DLineJoin::Miter:
aPaint.setStrokeJoin(SkPaint::kMiter_Join); // convert miter minimum angle to miter limit
aPaint.setStrokeMiter(1.0 / std::sin(fMiterMinimumAngle / 2.0)); break;
}
switch (eLineCap)
{ case css::drawing::LineCap_ROUND:
aPaint.setStrokeCap(SkPaint::kRound_Cap); break; case css::drawing::LineCap_SQUARE:
aPaint.setStrokeCap(SkPaint::kSquare_Cap); break; default: // css::drawing::LineCap_BUTT:
aPaint.setStrokeCap(SkPaint::kButt_Cap); break;
}
aPaint.setStrokeWidth(fLineWidth);
aPaint.setAntiAlias(mParent.getAntiAlias()); // See the tdf#134346 comment above. const SkScalar posFix = mParent.getAntiAlias() ? toSkXYFix : 0;
if (pStroke && std::accumulate(pStroke->begin(), pStroke->end(), 0.0) != 0)
{
std::vector<SkScalar> intervals; // Transform size by the matrix. for (double stroke : *pStroke)
intervals.push_back((rObjectToDevice * basegfx::B2DVector(stroke, 0)).getLength());
aPaint.setPathEffect(SkDashPathEffect::Make(intervals.data(), intervals.size(), 0));
}
// Skia does not support basegfx::B2DLineJoin::NONE, so in that case batch only if lines // are not wider than a pixel. if (eLineJoin != basegfx::B2DLineJoin::NONE || fLineWidth <= 1.0)
{
SkPath aPath;
aPath.incReserve(aPolyLine.count() * 3); // because cubicTo is 3 elements
addPolygonToPath(aPolyLine, aPath);
aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr);
addUpdateRegion(aPath.getBounds());
getDrawCanvas()->drawPath(aPath, aPaint);
} else
{
sal_uInt32 nPoints = aPolyLine.count(); bool bClosed = aPolyLine.isClosed(); bool bHasCurves = aPolyLine.areControlPointsUsed(); for (sal_uInt32 j = 0; j < nPoints; ++j)
{
SkPath aPath;
aPath.incReserve(2 * 3); // because cubicTo is 3 elements
addPolygonToPath(aPolyLine, aPath, j, j + 1, nPoints, bClosed, bHasCurves);
aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr);
addUpdateRegion(aPath.getBounds());
getDrawCanvas()->drawPath(aPath, aPaint);
}
}
if (!SkIRect::Intersects(srcRect, SkIRect::MakeWH(src->GetWidth(), src->GetHeight()))
|| !SkRect::Intersects(destRect, SkRect::MakeWH(GetWidth(), GetHeight()))) return;
if (src == this)
{ // Copy-to-self means that we'd take a snapshot, which would refcount the data, // and then drawing would result in copy in write, copying the entire surface. // Try to copy less by making a snapshot of only what is needed. // A complication here is that drawImageRect() can handle coordinates outside // of surface fine, but makeImageSnapshot() will crop to the surface area, // so do that manually here in order to adjust also destination rectangle. if (srcRect.x() < 0 || srcRect.y() < 0)
{
destRect.fLeft += -srcRect.x();
destRect.fTop += -srcRect.y();
srcRect.adjust(-srcRect.x(), -srcRect.y(), 0, 0);
} // Note that right() and bottom() are not inclusive (are outside of the rect). if (srcRect.right() - 1 > GetWidth() || srcRect.bottom() - 1 > GetHeight())
{
destRect.fRight += GetWidth() - srcRect.right();
destRect.fBottom += GetHeight() - srcRect.bottom();
srcRect.adjust(0, 0, GetWidth() - srcRect.right(), GetHeight() - srcRect.bottom());
} // Scaling for source coordinates must be done manually. if (src->mScaling != 1)
srcRect = scaleRect(srcRect, src->mScaling);
sk_sp<SkImage> image = makeCheckedImageSnapshot(src->mSurface, srcRect);
srcRect.offset(-srcRect.x(), -srcRect.y());
getDrawCanvas()->drawImageRect(image, SkRect::Make(srcRect), destRect,
makeSamplingOptions(rPosAry, mScaling, src->mScaling),
&paint, SkCanvas::kFast_SrcRectConstraint);
} else
{ // Scaling for source coordinates must be done manually. if (src->mScaling != 1)
srcRect = scaleRect(srcRect, src->mScaling); // Do not use makeImageSnapshot(rect), as that one may make a needless data copy.
getDrawCanvas()->drawImageRect(makeCheckedImageSnapshot(src->mSurface),
SkRect::Make(srcRect), destRect,
makeSamplingOptions(rPosAry, mScaling, src->mScaling),
&paint, SkCanvas::kFast_SrcRectConstraint);
}
}
bool SkiaSalGraphicsImpl::blendBitmap(const SalTwoRect& rPosAry, const SalBitmap& rBitmap)
{ if (checkInvalidSourceOrDestination(rPosAry)) returnfalse;
assert(dynamic_cast<const SkiaSalBitmap*>(&rBitmap)); const SkiaSalBitmap& rSkiaBitmap = static_cast<const SkiaSalBitmap&>(rBitmap); // This is used by VirtualDevice in the alpha mode for the "alpha" layer // So the result is transparent only if both the inputs // are transparent. Which seems to be what SkBlendMode::kModulate does, // so use that. // See also blendAlphaBitmap(). if (rSkiaBitmap.IsFullyOpaqueAsAlpha())
{ // Optimization. If the bitmap means fully opaque, it's all one's. In CPU // mode it should be faster to just copy instead of SkBlendMode::kMultiply.
drawBitmap(rPosAry, rSkiaBitmap);
} else
drawBitmap(rPosAry, rSkiaBitmap, SkBlendMode::kModulate); returntrue;
}
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()) returnfalse;
if (checkInvalidSourceOrDestination(rPosAry)) returnfalse;
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); returntrue;
} // This was originally implemented for the OpenGL drawing method and it is poorly documented. // The source and mask bitmaps are the usual data and alpha bitmaps, and 'alpha' // is the "alpha" layer of the VirtualDevice (the alpha in VirtualDevice is also stored // as a separate bitmap). Now if I understand it correctly these two alpha masks first need // to be combined into the actual alpha mask to be used. The formula for TYPE_BLEND // in opengl's combinedTextureFragmentShader.glsl is // "result_alpha = 1.0 - (1.0 - floor(alpha)) * mask". // See also blendBitmap().
SkSamplingOptions samplingOptions = makeSamplingOptions(rPosAry, mScaling); // First do the "( 1 - alpha ) * mask" // (no idea how to do "floor", but hopefully not needed in practice).
sk_sp<SkShader> shaderAlpha
= SkShaders::Blend(SkBlendMode::kDstIn, rSkiaMaskBitmap.GetAlphaSkShader(samplingOptions),
rSkiaAlphaBitmap.GetAlphaSkShader(samplingOptions)); // And now draw the bitmap with "1 - x", where x is the "( 1 - alpha ) * mask".
sk_sp<SkShader> shader = SkShaders::Blend(SkBlendMode::kSrcIn, shaderAlpha,
rSkiaSourceBitmap.GetSkShader(samplingOptions));
drawShader(rPosAry, shader); returntrue;
}
void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap)
{ if (checkInvalidSourceOrDestination(rPosAry)) return;
void SkiaSalGraphicsImpl::drawMask(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap,
Color nMaskColor)
{
assert(dynamic_cast<const SkiaSalBitmap*>(&rSalBitmap)); const SkiaSalBitmap& skiaBitmap = static_cast<const SkiaSalBitmap&>(rSalBitmap); // SkBlendMode::kDstOut must be used instead of SkBlendMode::kDstIn because // the alpha channel of what is drawn appears to get inverted at some point // after it is drawn
drawShader(
rPosAry,
SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is alpha.
SkShaders::Color(toSkColor(nMaskColor)),
skiaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry, mScaling))));
}
std::shared_ptr<SalBitmap> SkiaSalGraphicsImpl::getBitmap(tools::Long nX, tools::LongnY,
tools::Long nWidth, tools::Long nHeight)
{
SkiaZone zone;
checkSurface();
SAL_INFO("vcl.skia.trace", "getbitmap(" << this << "): " << SkIRect::MakeXYWH(nX, nY, nWidth, nHeight));
flushDrawing(); // TODO makeImageSnapshot(rect) may copy the data, which may be a waste if this is used // e.g. for VirtualDevice's lame alpha blending, in which case the image will eventually end up // in blendAlphaBitmap(), where we could simply use the proper rect of the image.
sk_sp<SkImage> image = makeCheckedImageSnapshot(
mSurface, scaleRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), mScaling));
std::shared_ptr<SkiaSalBitmap> bitmap = std::make_shared<SkiaSalBitmap>(image); // If the surface is scaled for HiDPI, the bitmap needs to be scaled down, otherwise // it would have incorrect size from the API point of view. The DirectImage::Yes handling // in mergeCacheBitmaps() should access the original unscaled bitmap data to avoid // pointless scaling back and forth. if (mScaling != 1)
{ if (!isUnitTestRunning())
bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, goodScalingQuality()); else
{ // Some tests require exact pixel values and would be confused by smooth-scaling. // And some draw something smooth and not smooth-scaling there would break the checks. // When running on macOS with a Retina display, several BackendTest unit tests // also need a lower quality scaling level. if (getWindowScaling() != 1
|| isUnitTestRunning("BackendTest__testDrawHaflEllipseAAWithPolyLineB2D_")
|| isUnitTestRunning("BackendTest__testDrawRectAAWithLine_")
|| isUnitTestRunning("GraphicsRenderTest__testDrawRectAAWithLine"))
{
bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, goodScalingQuality());
} else
bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, BmpScaleFlag::NearestNeighbor);
}
} return bitmap;
}
Color SkiaSalGraphicsImpl::getPixel(tools::Long nX, tools::Long nY)
{
SkiaZone zone;
checkSurface();
SAL_INFO("vcl.skia.trace", "getpixel(" << this << "): " << Point(nX, nY));
flushDrawing(); // This is presumably slow, but getPixel() should be generally used only by unit tests.
SkBitmap bitmap; if (!bitmap.tryAllocN32Pixels(mSurface->width(), mSurface->height()))
abort(); if (!mSurface->readPixels(bitmap, 0, 0))
abort(); return fromSkColor(bitmap.getColor(nX * mScaling, nY * mScaling));
}
void SkiaSalGraphicsImpl::invert(basegfx::B2DPolygon const& rPoly, SalInvert eFlags)
{
preDraw();
SAL_INFO("vcl.skia.trace", "invert(" << this << "): " << rPoly << ":" << int(eFlags));
assert(mXorMode == XorMode::None);
SkPath aPath;
aPath.incReserve(rPoly.count());
addPolygonToPath(rPoly, aPath);
aPath.setFillType(SkPathFillType::kEvenOdd);
addUpdateRegion(aPath.getBounds());
{
SkAutoCanvasRestore autoRestore(getDrawCanvas(), true);
SkPaint aPaint; // There's no blend mode for inverting as such, but kExclusion is 's + d - 2*s*d', // so with d = 1.0 (all channels) it becomes effectively '1 - s', i.e. inverted color.
aPaint.setBlendMode(SkBlendMode::kExclusion);
aPaint.setColor(SkColorSetARGB(255, 255, 255, 255)); // TrackFrame just inverts a dashed path around the polygon if (eFlags == SalInvert::TrackFrame)
{ // TrackFrame is not supposed to paint outside of the polygon (usually rectangle), // but wider stroke width usually results in that, so ensure the requirement // by clipping.
getDrawCanvas()->clipRect(aPath.getBounds(), SkClipOp::kIntersect, false);
aPaint.setStrokeWidth(2); static constexpr float intervals[] = { 4.0f, 4.0f };
aPaint.setStyle(SkPaint::kStroke_Style);
aPaint.setPathEffect(SkDashPathEffect::Make(intervals, std::size(intervals), 0));
} else
{
aPaint.setStyle(SkPaint::kFill_Style);
// N50 inverts in checker pattern if (eFlags == SalInvert::N50)
{ // This creates 2x2 checker pattern bitmap // TODO Use createSkSurface() and cache the image
SkBitmap aBitmap;
aBitmap.allocN32Pixels(2, 2); const SkPMColor white = SkPreMultiplyARGB(0xFF, 0xFF, 0xFF, 0xFF); const SkPMColor black = SkPreMultiplyARGB(0xFF, 0x00, 0x00, 0x00);
SkPMColor* scanline;
scanline = aBitmap.getAddr32(0, 0);
*scanline++ = white;
*scanline++ = black;
scanline = aBitmap.getAddr32(0, 1);
*scanline++ = black;
*scanline++ = white;
aBitmap.setImmutable(); // The bitmap is repeated in both directions the checker pattern is as big // as the polygon (usually rectangle)
aPaint.setShader(aBitmap.makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat,
SkSamplingOptions()));
}
#ifdef SK_METAL // tdf#153306 prevent subpixel shifting of X coordinate // HACK: for some unknown reason, if the X coordinate of the // path's bounds is more than 1024, SkBlendMode::kExclusion will // shift by about a half a pixel to the right with Skia/Metal on // a Retina display. Weirdly, if the same polygon is repeatedly // drawn, the total shift is cumulative so if the drawn polygon // is more than a few pixels wide, the blinking cursor in Writer // will exhibit this bug but only for one thin vertical slice at // a time. Apparently, shifting drawing a very tiny amount to // the left seems to be enough to quell this runaway cumulative // X coordinate shift. if (isGPU())
{
SkMatrix aMatrix;
aMatrix.set(SkMatrix::kMTransX, -0.001);
getDrawCanvas()->concat(aMatrix);
} #endif
}
getDrawCanvas()->drawPath(aPath, aPaint);
}
postDraw();
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.