/* -*- 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef MACOSX #include #endif #include #ifdef IOS #include #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(i_pIn->getX()) + 0.5; o_fY = static_cast(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->GetWidth(); } } #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(); 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* 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(rPosAry.mnSrcX), static_cast(rPosAry.mnSrcY), static_cast(rPosAry.mnSrcWidth), static_cast(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 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 pBitmap = std::make_shared(); 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(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: */