diff options
Diffstat (limited to 'vcl/quartz/AquaGraphicsBackend.cxx')
-rw-r--r-- | vcl/quartz/AquaGraphicsBackend.cxx | 1346 |
1 files changed, 1346 insertions, 0 deletions
diff --git a/vcl/quartz/AquaGraphicsBackend.cxx b/vcl/quartz/AquaGraphicsBackend.cxx new file mode 100644 index 0000000000..4badefacf4 --- /dev/null +++ b/vcl/quartz/AquaGraphicsBackend.cxx @@ -0,0 +1,1346 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * 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 <sal/config.h> +#include <sal/log.hxx> + +#include <cassert> +#include <cstring> +#include <numeric> +#include <utility> + +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <osl/endian.h> +#include <osl/file.hxx> +#include <sal/types.h> +#include <tools/long.hxx> +#include <vcl/sysdata.hxx> + +#include <fontsubset.hxx> +#include <quartz/salbmp.h> +#ifdef MACOSX +#include <quartz/salgdi.h> +#endif +#include <quartz/utils.h> +#ifdef IOS +#include <ios/iosinst.hxx> +#endif + +using namespace vcl; + +namespace +{ +const basegfx::B2DPoint aHalfPointOfs(0.5, 0.5); + +void AddPolygonToPath(CGMutablePathRef xPath, const basegfx::B2DPolygon& rPolygon, bool bClosePath, + bool bPixelSnap, bool bLineDraw) +{ + // short circuit if there is nothing to do + const int nPointCount = rPolygon.count(); + if (nPointCount <= 0) + { + return; + } + + const bool bHasCurves = rPolygon.areControlPointsUsed(); + for (int nPointIdx = 0, nPrevIdx = 0;; nPrevIdx = nPointIdx++) + { + int nClosedIdx = nPointIdx; + if (nPointIdx >= nPointCount) + { + // prepare to close last curve segment if needed + if (bClosePath && (nPointIdx == nPointCount)) + { + nClosedIdx = 0; + } + else + { + break; + } + } + + basegfx::B2DPoint aPoint = rPolygon.getB2DPoint(nClosedIdx); + + if (bPixelSnap) + { + // snap device coordinates to full pixels + aPoint.setX(basegfx::fround(aPoint.getX())); + aPoint.setY(basegfx::fround(aPoint.getY())); + } + + if (bLineDraw) + { + aPoint += aHalfPointOfs; + } + if (!nPointIdx) + { + // first point => just move there + CGPathMoveToPoint(xPath, nullptr, aPoint.getX(), aPoint.getY()); + continue; + } + + bool bPendingCurve = false; + if (bHasCurves) + { + bPendingCurve = rPolygon.isNextControlPointUsed(nPrevIdx); + bPendingCurve |= rPolygon.isPrevControlPointUsed(nClosedIdx); + } + + if (!bPendingCurve) // line segment + { + CGPathAddLineToPoint(xPath, nullptr, aPoint.getX(), aPoint.getY()); + } + else // cubic bezier segment + { + basegfx::B2DPoint aCP1 = rPolygon.getNextControlPoint(nPrevIdx); + basegfx::B2DPoint aCP2 = rPolygon.getPrevControlPoint(nClosedIdx); + if (bLineDraw) + { + aCP1 += aHalfPointOfs; + aCP2 += aHalfPointOfs; + } + CGPathAddCurveToPoint(xPath, nullptr, aCP1.getX(), aCP1.getY(), aCP2.getX(), + aCP2.getY(), aPoint.getX(), aPoint.getY()); + } + } + + if (bClosePath) + { + CGPathCloseSubpath(xPath); + } +} + +void alignLinePoint(const Point* i_pIn, float& o_fX, float& o_fY) +{ + o_fX = static_cast<float>(i_pIn->getX()) + 0.5; + o_fY = static_cast<float>(i_pIn->getY()) + 0.5; +} + +void getBoundRect(sal_uInt32 nPoints, const Point* pPtAry, tools::Long& rX, tools::Long& rY, + tools::Long& rWidth, tools::Long& rHeight) +{ + tools::Long nX1 = pPtAry->getX(); + tools::Long nX2 = nX1; + tools::Long nY1 = pPtAry->getY(); + tools::Long nY2 = nY1; + + for (sal_uInt32 n = 1; n < nPoints; n++) + { + if (pPtAry[n].getX() < nX1) + { + nX1 = pPtAry[n].getX(); + } + else if (pPtAry[n].getX() > nX2) + { + nX2 = pPtAry[n].getX(); + } + if (pPtAry[n].getY() < nY1) + { + nY1 = pPtAry[n].getY(); + } + else if (pPtAry[n].getY() > nY2) + { + nY2 = pPtAry[n].getY(); + } + } + rX = nX1; + rY = nY1; + rWidth = nX2 - nX1 + 1; + rHeight = nY2 - nY1 + 1; +} + +Color ImplGetROPColor(SalROPColor nROPColor) +{ + Color nColor; + if (nROPColor == SalROPColor::N0) + { + nColor = Color(0, 0, 0); + } + else + { + nColor = Color(255, 255, 255); + } + return nColor; +} + +void drawPattern50(void*, CGContextRef rContext) +{ + static const CGRect aRects[2] = { { { 0, 0 }, { 2, 2 } }, { { 2, 2 }, { 2, 2 } } }; + CGContextAddRects(rContext, aRects, 2); + CGContextFillPath(rContext); +} +} + +AquaGraphicsBackend::AquaGraphicsBackend(AquaSharedAttributes& rShared) + : AquaGraphicsBackendBase(rShared, this) +{ +} + +AquaGraphicsBackend::~AquaGraphicsBackend() {} + +void AquaGraphicsBackend::Init() {} +void AquaGraphicsBackend::freeResources() {} + +void AquaGraphicsBackend::setClipRegion(vcl::Region const& rRegion) +{ + // release old clip path + mrShared.unsetClipPath(); + mrShared.mxClipPath = CGPathCreateMutable(); + + // set current path, either as polypolgon or sequence of rectangles + RectangleVector aRectangles; + rRegion.GetRegionRectangles(aRectangles); + + for (const auto& rRect : aRectangles) + { + const tools::Long nW(rRect.Right() - rRect.Left() + 1); // uses +1 logic in original + + if (nW) + { + const tools::Long nH(rRect.Bottom() - rRect.Top() + 1); // uses +1 logic in original + + if (nH) + { + const CGRect aRect = CGRectMake(rRect.Left(), rRect.Top(), nW, nH); + CGPathAddRect(mrShared.mxClipPath, nullptr, aRect); + } + } + } + // set the current path as clip region + if (mrShared.checkContext()) + mrShared.setState(); +} + +void AquaGraphicsBackend::ResetClipRegion() +{ + // release old path and indicate no clipping + mrShared.unsetClipPath(); + + if (mrShared.checkContext()) + { + mrShared.setState(); + } +} + +sal_uInt16 AquaGraphicsBackend::GetBitCount() const +{ + sal_uInt16 nBits = mrShared.mnBitmapDepth ? mrShared.mnBitmapDepth : 32; //24; + return nBits; +} + +tools::Long AquaGraphicsBackend::GetGraphicsWidth() const +{ + tools::Long width = 0; + if (mrShared.maContextHolder.isSet() + && ( +#ifndef IOS + mrShared.mbWindow || +#endif + mrShared.mbVirDev)) + { + width = mrShared.mnWidth; + } + +#ifndef IOS + if (width == 0) + { + if (mrShared.mbWindow && mrShared.mpFrame) + { + width = mrShared.mpFrame->maGeometry.width(); + } + } +#endif + return width; +} + +void AquaGraphicsBackend::SetLineColor() +{ + mrShared.maLineColor.SetAlpha(0.0); // transparent + if (mrShared.checkContext()) + { + CGContextSetRGBStrokeColor(mrShared.maContextHolder.get(), mrShared.maLineColor.GetRed(), + mrShared.maLineColor.GetGreen(), mrShared.maLineColor.GetBlue(), + mrShared.maLineColor.GetAlpha()); + } +} + +void AquaGraphicsBackend::SetLineColor(Color nColor) +{ + mrShared.maLineColor = RGBAColor(nColor); + if (mrShared.checkContext()) + { + CGContextSetRGBStrokeColor(mrShared.maContextHolder.get(), mrShared.maLineColor.GetRed(), + mrShared.maLineColor.GetGreen(), mrShared.maLineColor.GetBlue(), + mrShared.maLineColor.GetAlpha()); + } +} + +void AquaGraphicsBackend::SetFillColor() +{ + mrShared.maFillColor.SetAlpha(0.0); // transparent + if (mrShared.checkContext()) + { + CGContextSetRGBFillColor(mrShared.maContextHolder.get(), mrShared.maFillColor.GetRed(), + mrShared.maFillColor.GetGreen(), mrShared.maFillColor.GetBlue(), + mrShared.maFillColor.GetAlpha()); + } +} + +void AquaGraphicsBackend::SetFillColor(Color nColor) +{ + mrShared.maFillColor = RGBAColor(nColor); + if (mrShared.checkContext()) + { + CGContextSetRGBFillColor(mrShared.maContextHolder.get(), mrShared.maFillColor.GetRed(), + mrShared.maFillColor.GetGreen(), mrShared.maFillColor.GetBlue(), + mrShared.maFillColor.GetAlpha()); + } +} + +void AquaGraphicsBackend::SetXORMode(bool bSet, bool bInvertOnly) +{ + // return early if XOR mode remains unchanged + if (mrShared.mbPrinter) + { + return; + } + if (!bSet && mrShared.mnXorMode == 2) + { + CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeNormal); + mrShared.mnXorMode = 0; + return; + } + else if (bSet && bInvertOnly && mrShared.mnXorMode == 0) + { + CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference); + mrShared.mnXorMode = 2; + return; + } + + if (!mrShared.mpXorEmulation && !bSet) + { + return; + } + if (mrShared.mpXorEmulation && bSet == mrShared.mpXorEmulation->IsEnabled()) + { + return; + } + if (!mrShared.checkContext()) + { + return; + } + // prepare XOR emulation + if (!mrShared.mpXorEmulation) + { + mrShared.mpXorEmulation = std::make_unique<XorEmulation>(); + mrShared.mpXorEmulation->SetTarget(mrShared.mnWidth, mrShared.mnHeight, + mrShared.mnBitmapDepth, mrShared.maContextHolder.get(), + mrShared.maLayer.get()); + } + + // change the XOR mode + if (bSet) + { + mrShared.mpXorEmulation->Enable(); + mrShared.maContextHolder.set(mrShared.mpXorEmulation->GetMaskContext()); + mrShared.mnXorMode = 1; + } + else + { + mrShared.mpXorEmulation->UpdateTarget(); + mrShared.mpXorEmulation->Disable(); + mrShared.maContextHolder.set(mrShared.mpXorEmulation->GetTargetContext()); + mrShared.mnXorMode = 0; + } +} + +void AquaGraphicsBackend::SetROPFillColor(SalROPColor nROPColor) +{ + if (!mrShared.mbPrinter) + { + SetFillColor(ImplGetROPColor(nROPColor)); + } +} + +void AquaGraphicsBackend::SetROPLineColor(SalROPColor nROPColor) +{ + if (!mrShared.mbPrinter) + { + SetLineColor(ImplGetROPColor(nROPColor)); + } +} + +void AquaGraphicsBackend::drawPixelImpl(tools::Long nX, tools::Long nY, const RGBAColor& rColor) +{ + if (!mrShared.checkContext()) + return; + + // overwrite the fill color + CGContextSetFillColor(mrShared.maContextHolder.get(), rColor.AsArray()); + // draw 1x1 rect, there is no pixel drawing in Quartz + const CGRect aDstRect = CGRectMake(nX, nY, 1, 1); + CGContextFillRect(mrShared.maContextHolder.get(), aDstRect); + + refreshRect(aDstRect); + + // reset the fill color + CGContextSetFillColor(mrShared.maContextHolder.get(), mrShared.maFillColor.AsArray()); +} + +void AquaGraphicsBackend::drawPixel(tools::Long nX, tools::Long nY) +{ + // draw pixel with current line color + drawPixelImpl(nX, nY, mrShared.maLineColor); +} + +void AquaGraphicsBackend::drawPixel(tools::Long nX, tools::Long nY, Color nColor) +{ + const RGBAColor aPixelColor(nColor); + drawPixelImpl(nX, nY, aPixelColor); +} + +void AquaGraphicsBackend::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2, + tools::Long nY2) +{ + if (nX1 == nX2 && nY1 == nY2) + { + // #i109453# platform independent code expects at least one pixel to be drawn + drawPixel(nX1, nY1); + return; + } + + if (!mrShared.checkContext()) + return; + + CGContextBeginPath(mrShared.maContextHolder.get()); + CGContextMoveToPoint(mrShared.maContextHolder.get(), float(nX1) + 0.5, float(nY1) + 0.5); + CGContextAddLineToPoint(mrShared.maContextHolder.get(), float(nX2) + 0.5, float(nY2) + 0.5); + CGContextDrawPath(mrShared.maContextHolder.get(), kCGPathStroke); + + tools::Rectangle aRefreshRect(nX1, nY1, nX2, nY2); + (void)aRefreshRect; + // Is a call to RefreshRect( aRefreshRect ) missing here? +} + +void AquaGraphicsBackend::drawRect(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight) +{ + if (!mrShared.checkContext()) + return; + + CGRect aRect = CGRectMake(nX, nY, nWidth, nHeight); + if (mrShared.isPenVisible()) + { + aRect.origin.x += 0.5; + aRect.origin.y += 0.5; + aRect.size.width -= 1; + aRect.size.height -= 1; + } + + if (mrShared.isBrushVisible()) + { + CGContextFillRect(mrShared.maContextHolder.get(), aRect); + } + if (mrShared.isPenVisible()) + { + CGContextStrokeRect(mrShared.maContextHolder.get(), aRect); + } + mrShared.refreshRect(nX, nY, nWidth, nHeight); +} + +void AquaGraphicsBackend::drawPolyLine(sal_uInt32 nPoints, const Point* pPointArray) +{ + if (nPoints < 1) + return; + + if (!mrShared.checkContext()) + return; + + tools::Long nX = 0, nY = 0, nWidth = 0, nHeight = 0; + getBoundRect(nPoints, pPointArray, nX, nY, nWidth, nHeight); + + float fX, fY; + CGContextBeginPath(mrShared.maContextHolder.get()); + alignLinePoint(pPointArray, fX, fY); + CGContextMoveToPoint(mrShared.maContextHolder.get(), fX, fY); + pPointArray++; + + for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPointArray++) + { + alignLinePoint(pPointArray, fX, fY); + CGContextAddLineToPoint(mrShared.maContextHolder.get(), fX, fY); + } + CGContextStrokePath(mrShared.maContextHolder.get()); + + mrShared.refreshRect(nX, nY, nWidth, nHeight); +} + +void AquaGraphicsBackend::drawPolygon(sal_uInt32 nPoints, const Point* pPointArray) +{ + if (nPoints <= 1) + return; + + if (!mrShared.checkContext()) + return; + + tools::Long nX = 0, nY = 0, nWidth = 0, nHeight = 0; + getBoundRect(nPoints, pPointArray, nX, nY, nWidth, nHeight); + + CGPathDrawingMode eMode; + if (mrShared.isBrushVisible() && mrShared.isPenVisible()) + { + eMode = kCGPathEOFillStroke; + } + else if (mrShared.isPenVisible()) + { + eMode = kCGPathStroke; + } + else if (mrShared.isBrushVisible()) + { + eMode = kCGPathEOFill; + } + else + { + SAL_WARN("vcl.quartz", "Neither pen nor brush visible"); + return; + } + + CGContextBeginPath(mrShared.maContextHolder.get()); + + if (mrShared.isPenVisible()) + { + float fX, fY; + alignLinePoint(pPointArray, fX, fY); + CGContextMoveToPoint(mrShared.maContextHolder.get(), fX, fY); + pPointArray++; + for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPointArray++) + { + alignLinePoint(pPointArray, fX, fY); + CGContextAddLineToPoint(mrShared.maContextHolder.get(), fX, fY); + } + } + else + { + CGContextMoveToPoint(mrShared.maContextHolder.get(), pPointArray->getX(), + pPointArray->getY()); + pPointArray++; + for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPointArray++) + { + CGContextAddLineToPoint(mrShared.maContextHolder.get(), pPointArray->getX(), + pPointArray->getY()); + } + } + + CGContextClosePath(mrShared.maContextHolder.get()); + CGContextDrawPath(mrShared.maContextHolder.get(), eMode); + + mrShared.refreshRect(nX, nY, nWidth, nHeight); +} + +void AquaGraphicsBackend::drawPolyPolygon(sal_uInt32 nPolyCount, const sal_uInt32* pPoints, + const Point** ppPtAry) +{ + if (nPolyCount <= 0) + return; + + if (!mrShared.checkContext()) + return; + + // find bound rect + tools::Long leftX = 0, topY = 0, maxWidth = 0, maxHeight = 0; + + getBoundRect(pPoints[0], ppPtAry[0], leftX, topY, maxWidth, maxHeight); + + for (sal_uInt32 n = 1; n < nPolyCount; n++) + { + tools::Long nX = leftX, nY = topY, nW = maxWidth, nH = maxHeight; + getBoundRect(pPoints[n], ppPtAry[n], nX, nY, nW, nH); + if (nX < leftX) + { + maxWidth += leftX - nX; + leftX = nX; + } + if (nY < topY) + { + maxHeight += topY - nY; + topY = nY; + } + if (nX + nW > leftX + maxWidth) + { + maxWidth = nX + nW - leftX; + } + if (nY + nH > topY + maxHeight) + { + maxHeight = nY + nH - topY; + } + } + + // prepare drawing mode + CGPathDrawingMode eMode; + if (mrShared.isBrushVisible() && mrShared.isPenVisible()) + { + eMode = kCGPathEOFillStroke; + } + else if (mrShared.isPenVisible()) + { + eMode = kCGPathStroke; + } + else if (mrShared.isBrushVisible()) + { + eMode = kCGPathEOFill; + } + else + { + SAL_WARN("vcl.quartz", "Neither pen nor brush visible"); + return; + } + + // convert to CGPath + CGContextBeginPath(mrShared.maContextHolder.get()); + if (mrShared.isPenVisible()) + { + for (sal_uInt32 nPoly = 0; nPoly < nPolyCount; nPoly++) + { + const sal_uInt32 nPoints = pPoints[nPoly]; + if (nPoints > 1) + { + const Point* pPtAry = ppPtAry[nPoly]; + float fX, fY; + + alignLinePoint(pPtAry, fX, fY); + CGContextMoveToPoint(mrShared.maContextHolder.get(), fX, fY); + pPtAry++; + + for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++) + { + alignLinePoint(pPtAry, fX, fY); + CGContextAddLineToPoint(mrShared.maContextHolder.get(), fX, fY); + } + CGContextClosePath(mrShared.maContextHolder.get()); + } + } + } + else + { + for (sal_uInt32 nPoly = 0; nPoly < nPolyCount; nPoly++) + { + const sal_uInt32 nPoints = pPoints[nPoly]; + if (nPoints > 1) + { + const Point* pPtAry = ppPtAry[nPoly]; + CGContextMoveToPoint(mrShared.maContextHolder.get(), pPtAry->getX(), + pPtAry->getY()); + pPtAry++; + for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++) + { + CGContextAddLineToPoint(mrShared.maContextHolder.get(), pPtAry->getX(), + pPtAry->getY()); + } + CGContextClosePath(mrShared.maContextHolder.get()); + } + } + } + + CGContextDrawPath(mrShared.maContextHolder.get(), eMode); + + mrShared.refreshRect(leftX, topY, maxWidth, maxHeight); +} + +void AquaGraphicsBackend::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolyPolygon& rPolyPolygon, + double fTransparency) +{ +#ifdef IOS + if (!mrShared.maContextHolder.isSet()) + return; +#endif + + // short circuit if there is nothing to do + if (rPolyPolygon.count() == 0) + return; + + // ignore invisible polygons + if ((fTransparency >= 1.0) || (fTransparency < 0)) + return; + + // Fallback: Transform to DeviceCoordinates + basegfx::B2DPolyPolygon aPolyPolygon(rPolyPolygon); + aPolyPolygon.transform(rObjectToDevice); + + // setup poly-polygon path + CGMutablePathRef xPath = CGPathCreateMutable(); + // tdf#120252 Use the correct, already transformed PolyPolygon (as long as + // the transformation is not used here...) + for (auto const& rPolygon : std::as_const(aPolyPolygon)) + { + AddPolygonToPath(xPath, rPolygon, true, !getAntiAlias(), mrShared.isPenVisible()); + } + + const CGRect aRefreshRect = CGPathGetBoundingBox(xPath); + // #i97317# workaround for Quartz having problems with drawing small polygons + if (aRefreshRect.size.width > 0.125 || aRefreshRect.size.height > 0.125) + { + // prepare drawing mode + CGPathDrawingMode eMode; + if (mrShared.isBrushVisible() && mrShared.isPenVisible()) + { + eMode = kCGPathEOFillStroke; + } + else if (mrShared.isPenVisible()) + { + eMode = kCGPathStroke; + } + else if (mrShared.isBrushVisible()) + { + eMode = kCGPathEOFill; + } + else + { + SAL_WARN("vcl.quartz", "Neither pen nor brush visible"); + CGPathRelease(xPath); + return; + } + + // use the path to prepare the graphics context + mrShared.maContextHolder.saveState(); + CGContextBeginPath(mrShared.maContextHolder.get()); + CGContextAddPath(mrShared.maContextHolder.get(), xPath); + + // draw path with antialiased polygon + CGContextSetShouldAntialias(mrShared.maContextHolder.get(), getAntiAlias()); + CGContextSetAlpha(mrShared.maContextHolder.get(), 1.0 - fTransparency); + CGContextDrawPath(mrShared.maContextHolder.get(), eMode); + mrShared.maContextHolder.restoreState(); + + // mark modified rectangle as updated + refreshRect(aRefreshRect); + } + + CGPathRelease(xPath); +} + +bool AquaGraphicsBackend::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolygon& rPolyLine, double fTransparency, + double fLineWidth, + const std::vector<double>* pStroke, // MM01 + basegfx::B2DLineJoin eLineJoin, + css::drawing::LineCap eLineCap, double fMiterMinimumAngle, + bool bPixelSnapHairline) +{ + // MM01 check done for simple reasons + if (!rPolyLine.count() || fTransparency < 0.0 || fTransparency > 1.0) + { + return true; + } + +#ifdef IOS + if (!mrShared.checkContext()) + return false; +#endif + + // tdf#124848 get correct LineWidth in discrete coordinates, + if (fLineWidth == 0) // hairline + fLineWidth = 1.0; + else // Adjust line width for object-to-device scale. + fLineWidth = (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength(); + + // #i101491# Aqua does not support B2DLineJoin::NONE; return false to use + // the fallback (own geometry preparation) + // #i104886# linejoin-mode and thus the above only applies to "fat" lines + if ((basegfx::B2DLineJoin::NONE == eLineJoin) && (fLineWidth > 1.3)) + return false; + + // MM01 need to do line dashing as fallback stuff here now + const double fDotDashLength( + nullptr != pStroke ? std::accumulate(pStroke->begin(), pStroke->end(), 0.0) : 0.0); + const bool bStrokeUsed(0.0 != fDotDashLength); + assert(!bStrokeUsed || (bStrokeUsed && pStroke)); + basegfx::B2DPolyPolygon aPolyPolygonLine; + + if (bStrokeUsed) + { + // apply LineStyle + basegfx::utils::applyLineDashing(rPolyLine, // source + *pStroke, // pattern + &aPolyPolygonLine, // target for lines + nullptr, // target for gaps + fDotDashLength); // full length if available + } + else + { + // no line dashing, just copy + aPolyPolygonLine.append(rPolyLine); + } + + // Transform to DeviceCoordinates, get DeviceLineWidth, execute PixelSnapHairline + aPolyPolygonLine.transform(rObjectToDevice); + if (bPixelSnapHairline) + { + aPolyPolygonLine = basegfx::utils::snapPointsOfHorizontalOrVerticalEdges(aPolyPolygonLine); + } + + // setup line attributes + CGLineJoin aCGLineJoin = kCGLineJoinMiter; + switch (eLineJoin) + { + case basegfx::B2DLineJoin::NONE: + aCGLineJoin = /*TODO?*/ kCGLineJoinMiter; + break; + case basegfx::B2DLineJoin::Bevel: + aCGLineJoin = kCGLineJoinBevel; + break; + case basegfx::B2DLineJoin::Miter: + aCGLineJoin = kCGLineJoinMiter; + break; + case basegfx::B2DLineJoin::Round: + aCGLineJoin = kCGLineJoinRound; + break; + } + // convert miter minimum angle to miter limit + CGFloat fCGMiterLimit = 1.0 / sin(std::max(fMiterMinimumAngle, 0.01 * M_PI) / 2.0); + // setup cap attribute + CGLineCap aCGLineCap(kCGLineCapButt); + + switch (eLineCap) + { + default: // css::drawing::LineCap_BUTT: + { + aCGLineCap = kCGLineCapButt; + break; + } + case css::drawing::LineCap_ROUND: + { + aCGLineCap = kCGLineCapRound; + break; + } + case css::drawing::LineCap_SQUARE: + { + aCGLineCap = kCGLineCapSquare; + break; + } + } + + // setup poly-polygon path + CGMutablePathRef xPath = CGPathCreateMutable(); + + // MM01 todo - I assume that this is OKAY to be done in one run for quartz + // but this NEEDS to be checked/verified + for (sal_uInt32 a(0); a < aPolyPolygonLine.count(); a++) + { + const basegfx::B2DPolygon aPolyLine(aPolyPolygonLine.getB2DPolygon(a)); + AddPolygonToPath(xPath, aPolyLine, aPolyLine.isClosed(), !getAntiAlias(), true); + } + + const CGRect aRefreshRect = CGPathGetBoundingBox(xPath); + // #i97317# workaround for Quartz having problems with drawing small polygons + if ((aRefreshRect.size.width > 0.125) || (aRefreshRect.size.height > 0.125)) + { + // use the path to prepare the graphics context + mrShared.maContextHolder.saveState(); + CGContextBeginPath(mrShared.maContextHolder.get()); + CGContextAddPath(mrShared.maContextHolder.get(), xPath); + // draw path with antialiased line + CGContextSetShouldAntialias(mrShared.maContextHolder.get(), getAntiAlias()); + CGContextSetAlpha(mrShared.maContextHolder.get(), 1.0 - fTransparency); + CGContextSetLineJoin(mrShared.maContextHolder.get(), aCGLineJoin); + CGContextSetLineCap(mrShared.maContextHolder.get(), aCGLineCap); + CGContextSetLineWidth(mrShared.maContextHolder.get(), fLineWidth); + CGContextSetMiterLimit(mrShared.maContextHolder.get(), fCGMiterLimit); + CGContextDrawPath(mrShared.maContextHolder.get(), kCGPathStroke); + mrShared.maContextHolder.restoreState(); + + // mark modified rectangle as updated + refreshRect(aRefreshRect); + } + + CGPathRelease(xPath); + + return true; +} + +bool AquaGraphicsBackend::drawPolyLineBezier(sal_uInt32 /*nPoints*/, const Point* /*pPointArray*/, + const PolyFlags* /*pFlagArray*/) +{ + return false; +} + +bool AquaGraphicsBackend::drawPolygonBezier(sal_uInt32 /*nPoints*/, const Point* /*pPointArray*/, + const PolyFlags* /*pFlagArray*/) +{ + return false; +} + +bool AquaGraphicsBackend::drawPolyPolygonBezier(sal_uInt32 /*nPoly*/, const sal_uInt32* /*pPoints*/, + const Point* const* /*pPointArray*/, + const PolyFlags* const* /*pFlagArray*/) +{ + return false; +} + +void AquaGraphicsBackend::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap) +{ + if (!mrShared.checkContext()) + return; + + CGImageRef xImage = rSalBitmap.CreateCroppedImage( + static_cast<int>(rPosAry.mnSrcX), static_cast<int>(rPosAry.mnSrcY), + static_cast<int>(rPosAry.mnSrcWidth), static_cast<int>(rPosAry.mnSrcHeight)); + if (!xImage) + return; + + const CGRect aDstRect + = CGRectMake(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight); + CGContextDrawImage(mrShared.maContextHolder.get(), aDstRect, xImage); + + CGImageRelease(xImage); + refreshRect(aDstRect); +} + +void AquaGraphicsBackend::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, + const SalBitmap& rTransparentBitmap) +{ + if (!mrShared.checkContext()) + return; + + CGImageRef xMaskedImage(rSalBitmap.CreateWithMask(rTransparentBitmap, rPosAry.mnSrcX, + rPosAry.mnSrcY, rPosAry.mnSrcWidth, + rPosAry.mnSrcHeight)); + if (!xMaskedImage) + return; + + const CGRect aDstRect + = CGRectMake(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight); + CGContextDrawImage(mrShared.maContextHolder.get(), aDstRect, xMaskedImage); + CGImageRelease(xMaskedImage); + refreshRect(aDstRect); +} + +void AquaGraphicsBackend::drawMask(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, + Color nMaskColor) +{ + if (!mrShared.checkContext()) + return; + + CGImageRef xImage = rSalBitmap.CreateColorMask( + rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight, nMaskColor); + if (!xImage) + return; + + const CGRect aDstRect + = CGRectMake(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight); + CGContextDrawImage(mrShared.maContextHolder.get(), aDstRect, xImage); + CGImageRelease(xImage); + refreshRect(aDstRect); +} + +std::shared_ptr<SalBitmap> AquaGraphicsBackend::getBitmap(tools::Long nX, tools::Long nY, + tools::Long nDX, tools::Long nDY) +{ + SAL_WARN_IF(!mrShared.maLayer.isSet(), "vcl.quartz", + "AquaSalGraphics::getBitmap() with no layer this=" << this); + + mrShared.applyXorContext(); + + std::shared_ptr<QuartzSalBitmap> pBitmap = std::make_shared<QuartzSalBitmap>(); + if (!pBitmap->Create(mrShared.maLayer, mrShared.mnBitmapDepth, nX, nY, nDX, nDY, + mrShared.isFlipped())) + { + pBitmap = nullptr; + } + return pBitmap; +} + +Color AquaGraphicsBackend::getPixel(tools::Long nX, tools::Long nY) +{ + // return default value on printers or when out of bounds + if (!mrShared.maLayer.isSet() || (nX < 0) || (nX >= mrShared.mnWidth) || (nY < 0) + || (nY >= mrShared.mnHeight)) + { + return COL_BLACK; + } + + // prepare creation of matching a CGBitmapContext +#if defined OSL_BIGENDIAN + struct + { + unsigned char b, g, r, a; + } aPixel; +#else + struct + { + unsigned char a, r, g, b; + } aPixel; +#endif + + // create a one-pixel bitmap context + // TODO: is it worth to cache it? + CGContextRef xOnePixelContext = CGBitmapContextCreate( + &aPixel, 1, 1, 8, 32, GetSalData()->mxRGBSpace, + uint32_t(kCGImageAlphaNoneSkipFirst) | uint32_t(kCGBitmapByteOrder32Big)); + + // update this graphics layer + mrShared.applyXorContext(); + + // copy the requested pixel into the bitmap context + if (mrShared.isFlipped()) + { + nY = mrShared.mnHeight - nY; + } + const CGPoint aCGPoint = CGPointMake(-nX, -nY); + CGContextDrawLayerAtPoint(xOnePixelContext, aCGPoint, mrShared.maLayer.get()); + + CGContextRelease(xOnePixelContext); + + Color nColor(aPixel.r, aPixel.g, aPixel.b); + return nColor; +} + +void AquaSalGraphics::GetResolution(sal_Int32& rDPIX, sal_Int32& rDPIY) +{ +#ifndef IOS + if (!mnRealDPIY) + { + initResolution((maShared.mbWindow && maShared.mpFrame) ? maShared.mpFrame->getNSWindow() + : nil); + } + + rDPIX = mnRealDPIX; + rDPIY = mnRealDPIY; +#else + // This *must* be 96 or else the iOS app will behave very badly (tiles are scaled wrongly and + // don't match each others at their boundaries, and other issues). But *why* it must be 96 I + // have no idea. The commit that changed it to 96 from (the arbitrary) 200 did not say. If you + // know where else 96 is explicitly or implicitly hard-coded, please modify this comment. + + // Follow-up: It might be this: in 'online', loleaflet/src/map/Map.js: + // 15 = 1440 twips-per-inch / 96 dpi. + // Chosen to match previous hardcoded value of 3840 for + // the current tile pixel size of 256. + rDPIX = rDPIY = 96; +#endif +} + +void AquaGraphicsBackend::pattern50Fill() +{ + static const CGFloat aFillCol[4] = { 1, 1, 1, 1 }; + static const CGPatternCallbacks aCallback = { 0, &drawPattern50, nullptr }; + static const CGColorSpaceRef mxP50Space = CGColorSpaceCreatePattern(GetSalData()->mxRGBSpace); + static const CGPatternRef mxP50Pattern + = CGPatternCreate(nullptr, CGRectMake(0, 0, 4, 4), CGAffineTransformIdentity, 4, 4, + kCGPatternTilingConstantSpacing, false, &aCallback); + SAL_WARN_IF(!mrShared.maContextHolder.get(), "vcl.quartz", "maContextHolder.get() is NULL"); + CGContextSetFillColorSpace(mrShared.maContextHolder.get(), mxP50Space); + CGContextSetFillPattern(mrShared.maContextHolder.get(), mxP50Pattern, aFillCol); + CGContextFillPath(mrShared.maContextHolder.get()); +} + +void AquaGraphicsBackend::invert(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight, SalInvert nFlags) +{ + if (mrShared.checkContext()) + { + CGRect aCGRect = CGRectMake(nX, nY, nWidth, nHeight); + mrShared.maContextHolder.saveState(); + if (nFlags & SalInvert::TrackFrame) + { + const CGFloat dashLengths[2] = { 4.0, 4.0 }; // for drawing dashed line + CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference); + CGContextSetRGBStrokeColor(mrShared.maContextHolder.get(), 1.0, 1.0, 1.0, 1.0); + CGContextSetLineDash(mrShared.maContextHolder.get(), 0, dashLengths, 2); + CGContextSetLineWidth(mrShared.maContextHolder.get(), 2.0); + CGContextStrokeRect(mrShared.maContextHolder.get(), aCGRect); + } + else if (nFlags & SalInvert::N50) + { + //CGContextSetAllowsAntialiasing( maContextHolder.get(), false ); + CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference); + CGContextAddRect(mrShared.maContextHolder.get(), aCGRect); + pattern50Fill(); + } + else // just invert + { + CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference); + CGContextSetRGBFillColor(mrShared.maContextHolder.get(), 1.0, 1.0, 1.0, 1.0); + CGContextFillRect(mrShared.maContextHolder.get(), aCGRect); + } + mrShared.maContextHolder.restoreState(); + refreshRect(aCGRect); + } +} + +namespace +{ +CGPoint* makeCGptArray(sal_uInt32 nPoints, const Point* pPtAry) +{ + CGPoint* CGpoints = new CGPoint[nPoints]; + for (sal_uLong i = 0; i < nPoints; i++) + { + CGpoints[i].x = pPtAry[i].getX(); + CGpoints[i].y = pPtAry[i].getY(); + } + return CGpoints; +} + +} // end anonymous ns + +void AquaGraphicsBackend::invert(sal_uInt32 nPoints, const Point* pPtAry, SalInvert nSalFlags) +{ + if (mrShared.checkContext()) + { + mrShared.maContextHolder.saveState(); + CGPoint* CGpoints = makeCGptArray(nPoints, pPtAry); + CGContextAddLines(mrShared.maContextHolder.get(), CGpoints, nPoints); + if (nSalFlags & SalInvert::TrackFrame) + { + const CGFloat dashLengths[2] = { 4.0, 4.0 }; // for drawing dashed line + CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference); + CGContextSetRGBStrokeColor(mrShared.maContextHolder.get(), 1.0, 1.0, 1.0, 1.0); + CGContextSetLineDash(mrShared.maContextHolder.get(), 0, dashLengths, 2); + CGContextSetLineWidth(mrShared.maContextHolder.get(), 2.0); + CGContextStrokePath(mrShared.maContextHolder.get()); + } + else if (nSalFlags & SalInvert::N50) + { + CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference); + pattern50Fill(); + } + else // just invert + { + CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference); + CGContextSetRGBFillColor(mrShared.maContextHolder.get(), 1.0, 1.0, 1.0, 1.0); + CGContextFillPath(mrShared.maContextHolder.get()); + } + const CGRect aRefreshRect = CGContextGetClipBoundingBox(mrShared.maContextHolder.get()); + mrShared.maContextHolder.restoreState(); + delete[] CGpoints; + refreshRect(aRefreshRect); + } +} + +#ifndef IOS +bool AquaGraphicsBackend::drawEPS(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight, void* pEpsData, sal_uInt32 nByteCount) +{ + // convert the raw data to an NSImageRef + NSData* xNSData = [NSData dataWithBytes:pEpsData length:static_cast<int>(nByteCount)]; + SAL_WNODEPRECATED_DECLARATIONS_PUSH + // 'NSEPSImageRep' is deprecated: first deprecated in macOS 14.0 - `NSEPSImageRep` instances + // cannot be created on macOS 14.0 and later + NSImageRep* xEpsImage = [NSEPSImageRep imageRepWithData:xNSData]; + SAL_WNODEPRECATED_DECLARATIONS_POP + if (!xEpsImage) + { + return false; + } + // get the target context + if (!mrShared.checkContext()) + { + return false; + } + // NOTE: flip drawing, else the nsimage would be drawn upside down + mrShared.maContextHolder.saveState(); + // CGContextTranslateCTM( maContextHolder.get(), 0, +mnHeight ); + CGContextScaleCTM(mrShared.maContextHolder.get(), +1, -1); + nY = /*mnHeight*/ -(nY + nHeight); + + // prepare the target context + NSGraphicsContext* pOrigNSCtx = [NSGraphicsContext currentContext]; + [pOrigNSCtx retain]; + + // create new context + NSGraphicsContext* pDrawNSCtx = + [NSGraphicsContext graphicsContextWithCGContext:mrShared.maContextHolder.get() + flipped:mrShared.isFlipped()]; + // set it, setCurrentContext also releases the previously set one + [NSGraphicsContext setCurrentContext:pDrawNSCtx]; + + // draw the EPS + const NSRect aDstRect = NSMakeRect(nX, nY, nWidth, nHeight); + const bool bOK = [xEpsImage drawInRect:aDstRect]; + + // restore the NSGraphicsContext + [NSGraphicsContext setCurrentContext:pOrigNSCtx]; + [pOrigNSCtx release]; // restore the original retain count + + mrShared.maContextHolder.restoreState(); + // mark the destination rectangle as updated + refreshRect(aDstRect); + + return bOK; +} +#else +bool AquaGraphicsBackend::drawEPS(tools::Long /*nX*/, tools::Long /*nY*/, tools::Long /*nWidth*/, + tools::Long /*nHeight*/, void* /*pEpsData*/, + sal_uInt32 /*nByteCount*/) +{ + return false; +} +#endif + +bool AquaGraphicsBackend::blendBitmap(const SalTwoRect& /*rPosAry*/, const SalBitmap& /*rBitmap*/) +{ + return false; +} + +bool AquaGraphicsBackend::blendAlphaBitmap(const SalTwoRect& /*rPosAry*/, + const SalBitmap& /*rSrcBitmap*/, + const SalBitmap& /*rMaskBitmap*/, + const SalBitmap& /*rAlphaBitmap*/) +{ + return false; +} + +bool AquaGraphicsBackend::drawAlphaBitmap(const SalTwoRect& rTR, const SalBitmap& rSrcBitmap, + const SalBitmap& rAlphaBmp) +{ + // An image mask can't have a depth > 8 bits (should be 1 to 8 bits) + if (rAlphaBmp.GetBitCount() > 8) + return false; + + // are these two tests really necessary? (see vcl/unx/source/gdi/salgdi2.cxx) + // horizontal/vertical mirroring not implemented yet + if (rTR.mnDestWidth < 0 || rTR.mnDestHeight < 0) + return false; + + CGImageRef xMaskedImage = rSrcBitmap.CreateWithMask(rAlphaBmp, rTR.mnSrcX, rTR.mnSrcY, + rTR.mnSrcWidth, rTR.mnSrcHeight); + if (!xMaskedImage) + return false; + + if (mrShared.checkContext()) + { + const CGRect aDstRect + = CGRectMake(rTR.mnDestX, rTR.mnDestY, rTR.mnDestWidth, rTR.mnDestHeight); + CGContextDrawImage(mrShared.maContextHolder.get(), aDstRect, xMaskedImage); + refreshRect(aDstRect); + } + + CGImageRelease(xMaskedImage); + + return true; +} + +bool AquaGraphicsBackend::drawTransformedBitmap(const basegfx::B2DPoint& rNull, + const basegfx::B2DPoint& rX, + const basegfx::B2DPoint& rY, + const SalBitmap& rSrcBitmap, + const SalBitmap* pAlphaBmp, double fAlpha) +{ + if (!mrShared.checkContext()) + return true; + + if (fAlpha != 1.0) + return false; + + // get the Quartz image + CGImageRef xImage = nullptr; + const Size aSize = rSrcBitmap.GetSize(); + + if (!pAlphaBmp) + xImage = rSrcBitmap.CreateCroppedImage(0, 0, int(aSize.Width()), int(aSize.Height())); + else + xImage + = rSrcBitmap.CreateWithMask(*pAlphaBmp, 0, 0, int(aSize.Width()), int(aSize.Height())); + + if (!xImage) + return false; + + // setup the image transformation + // using the rNull,rX,rY points as destinations for the (0,0),(0,Width),(Height,0) source points + mrShared.maContextHolder.saveState(); + const basegfx::B2DVector aXRel = rX - rNull; + const basegfx::B2DVector aYRel = rY - rNull; + const CGAffineTransform aCGMat = CGAffineTransformMake( + aXRel.getX() / aSize.Width(), aXRel.getY() / aSize.Width(), aYRel.getX() / aSize.Height(), + aYRel.getY() / aSize.Height(), rNull.getX(), rNull.getY()); + + CGContextConcatCTM(mrShared.maContextHolder.get(), aCGMat); + + // draw the transformed image + const CGRect aSrcRect = CGRectMake(0, 0, aSize.Width(), aSize.Height()); + CGContextDrawImage(mrShared.maContextHolder.get(), aSrcRect, xImage); + + CGImageRelease(xImage); + + // restore the Quartz graphics state + mrShared.maContextHolder.restoreState(); + + // mark the destination as painted + const CGRect aDstRect = CGRectApplyAffineTransform(aSrcRect, aCGMat); + refreshRect(aDstRect); + + return true; +} + +bool AquaGraphicsBackend::hasFastDrawTransformedBitmap() const { return false; } + +bool AquaGraphicsBackend::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight, sal_uInt8 nTransparency) +{ + if (!mrShared.checkContext()) + return true; + + // save the current state + mrShared.maContextHolder.saveState(); + CGContextSetAlpha(mrShared.maContextHolder.get(), (100 - nTransparency) * (1.0 / 100)); + + CGRect aRect = CGRectMake(nX, nY, nWidth - 1, nHeight - 1); + if (mrShared.isPenVisible()) + { + aRect.origin.x += 0.5; + aRect.origin.y += 0.5; + } + + CGContextBeginPath(mrShared.maContextHolder.get()); + CGContextAddRect(mrShared.maContextHolder.get(), aRect); + CGContextDrawPath(mrShared.maContextHolder.get(), kCGPathFill); + + mrShared.maContextHolder.restoreState(); + refreshRect(aRect); + + return true; +} + +bool AquaGraphicsBackend::drawGradient(const tools::PolyPolygon& /*rPolygon*/, + const Gradient& /*rGradient*/) +{ + return false; +} + +bool AquaGraphicsBackend::implDrawGradient(basegfx::B2DPolyPolygon const& /*rPolyPolygon*/, + SalGradient const& /*rGradient*/) +{ + return false; +} + +bool AquaGraphicsBackend::supportsOperation(OutDevSupportType eType) const +{ + switch (eType) + { + case OutDevSupportType::TransparentRect: + return true; + default: + break; + } + return false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |