diff options
Diffstat (limited to 'vcl/quartz')
-rw-r--r-- | vcl/quartz/AquaGraphicsBackend.cxx | 1346 | ||||
-rw-r--r-- | vcl/quartz/CoreTextFont.cxx | 235 | ||||
-rw-r--r-- | vcl/quartz/CoreTextFontFace.cxx | 98 | ||||
-rw-r--r-- | vcl/quartz/SystemFontList.cxx | 301 | ||||
-rw-r--r-- | vcl/quartz/cgutils.mm | 131 | ||||
-rw-r--r-- | vcl/quartz/salbmp.cxx | 677 | ||||
-rw-r--r-- | vcl/quartz/salgdi.cxx | 495 | ||||
-rw-r--r-- | vcl/quartz/salgdicommon.cxx | 234 | ||||
-rw-r--r-- | vcl/quartz/salvd.cxx | 177 | ||||
-rw-r--r-- | vcl/quartz/utils.cxx | 242 |
10 files changed, 3936 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: */ diff --git a/vcl/quartz/CoreTextFont.cxx b/vcl/quartz/CoreTextFont.cxx new file mode 100644 index 0000000000..6248a255c6 --- /dev/null +++ b/vcl/quartz/CoreTextFont.cxx @@ -0,0 +1,235 @@ +/* -*- 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 <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> + +#ifdef MACOSX +#include <osx/saldata.hxx> +#include <osx/salinst.h> +#endif +#include <font/LogicalFontInstance.hxx> +#include <impglyphitem.hxx> +#include <quartz/CoreTextFont.hxx> +#include <quartz/CoreTextFontFace.hxx> +#include <quartz/salgdi.h> +#include <quartz/utils.h> +#include <hb.h> + +CoreTextFont::CoreTextFont(const CoreTextFontFace& rPFF, const vcl::font::FontSelectPattern& rFSP) + : LogicalFontInstance(rPFF, rFSP) + , mfFontStretch(1.0) + , mfFontRotation(0.0) + , mpCTFont(nullptr) +{ + double fScaledFontHeight = rFSP.mfExactHeight; + + // convert font rotation to radian + mfFontRotation = toRadians(rFSP.mnOrientation); + + // dummy matrix so we can use CGAffineTransformConcat() below + CGAffineTransform aMatrix = CGAffineTransformMakeTranslation(0, 0); + + // handle font stretching if any + if ((rFSP.mnWidth != 0) && (rFSP.mnWidth != rFSP.mnHeight)) + { + mfFontStretch = float(rFSP.mnWidth) / rFSP.mnHeight; + aMatrix = CGAffineTransformConcat(aMatrix, CGAffineTransformMakeScale(mfFontStretch, 1.0F)); + } + + // artificial italic + if (NeedsArtificialItalic()) + aMatrix = CGAffineTransformConcat( + aMatrix, CGAffineTransformMake(1, 0, ARTIFICIAL_ITALIC_SKEW, 1, 0, 0)); + + CTFontDescriptorRef pFontDesc = rPFF.GetFontDescriptorRef(); + mpCTFont = CTFontCreateWithFontDescriptor(pFontDesc, fScaledFontHeight, &aMatrix); +} + +CoreTextFont::~CoreTextFont() +{ + if (mpCTFont) + CFRelease(mpCTFont); +} + +void CoreTextFont::GetFontMetric(FontMetricDataRef const& rxFontMetric) +{ + rxFontMetric->ImplCalcLineSpacing(this); + rxFontMetric->ImplInitBaselines(this); + + // since FontMetricData::mnWidth is only used for stretching/squeezing fonts + // setting this width to the pixel height of the fontsize is good enough + // it also makes the calculation of the stretch factor simple + rxFontMetric->SetWidth(lrint(CTFontGetSize(mpCTFont) * mfFontStretch)); + + rxFontMetric->SetMinKashida(GetKashidaWidth()); +} + +namespace +{ +// callbacks from CTFontCreatePathForGlyph+CGPathApply for GetGlyphOutline() +struct GgoData +{ + basegfx::B2DPolygon maPolygon; + basegfx::B2DPolyPolygon* mpPolyPoly; +}; +} + +static void MyCGPathApplierFunc(void* pData, const CGPathElement* pElement) +{ + basegfx::B2DPolygon& rPolygon = static_cast<GgoData*>(pData)->maPolygon; + const int nPointCount = rPolygon.count(); + + switch (pElement->type) + { + case kCGPathElementCloseSubpath: + case kCGPathElementMoveToPoint: + if (nPointCount > 0) + { + static_cast<GgoData*>(pData)->mpPolyPoly->append(rPolygon); + rPolygon.clear(); + } + // fall through for kCGPathElementMoveToPoint: + if (pElement->type != kCGPathElementMoveToPoint) + { + break; + } + [[fallthrough]]; + case kCGPathElementAddLineToPoint: + rPolygon.append(basegfx::B2DPoint(+pElement->points[0].x, -pElement->points[0].y)); + break; + + case kCGPathElementAddCurveToPoint: + rPolygon.append(basegfx::B2DPoint(+pElement->points[2].x, -pElement->points[2].y)); + rPolygon.setNextControlPoint( + nPointCount - 1, basegfx::B2DPoint(pElement->points[0].x, -pElement->points[0].y)); + rPolygon.setPrevControlPoint( + nPointCount + 0, basegfx::B2DPoint(pElement->points[1].x, -pElement->points[1].y)); + break; + + case kCGPathElementAddQuadCurveToPoint: + { + const basegfx::B2DPoint aStartPt = rPolygon.getB2DPoint(nPointCount - 1); + const basegfx::B2DPoint aCtrPt1((aStartPt.getX() + 2 * pElement->points[0].x) / 3.0, + (aStartPt.getY() - 2 * pElement->points[0].y) / 3.0); + const basegfx::B2DPoint aCtrPt2( + (+2 * pElement->points[0].x + pElement->points[1].x) / 3.0, + (-2 * pElement->points[0].y - pElement->points[1].y) / 3.0); + rPolygon.append(basegfx::B2DPoint(+pElement->points[1].x, -pElement->points[1].y)); + rPolygon.setNextControlPoint(nPointCount - 1, aCtrPt1); + rPolygon.setPrevControlPoint(nPointCount + 0, aCtrPt2); + } + break; + } +} + +bool CoreTextFont::GetGlyphOutline(sal_GlyphId nId, basegfx::B2DPolyPolygon& rResult, bool) const +{ + rResult.clear(); + + CGGlyph nCGGlyph = nId; + + SAL_WNODEPRECATED_DECLARATIONS_PUSH + const CTFontOrientation aFontOrientation = kCTFontDefaultOrientation; + SAL_WNODEPRECATED_DECLARATIONS_POP + CGRect aCGRect + = CTFontGetBoundingRectsForGlyphs(mpCTFont, aFontOrientation, &nCGGlyph, nullptr, 1); + + if (!CGRectIsNull(aCGRect) && CGRectIsEmpty(aCGRect)) + { + // CTFontCreatePathForGlyph returns NULL for blank glyphs, but we want + // to return true for them. + return true; + } + + CGPathRef xPath = CTFontCreatePathForGlyph(mpCTFont, nCGGlyph, nullptr); + if (!xPath) + { + return false; + } + + GgoData aGgoData; + aGgoData.mpPolyPoly = &rResult; + CGPathApply(xPath, static_cast<void*>(&aGgoData), MyCGPathApplierFunc); +#if 0 // TODO: does OSX ensure that the last polygon is always closed? + const CGPathElement aClosingElement = { kCGPathElementCloseSubpath, NULL }; + MyCGPathApplierFunc( (void*)&aGgoData, &aClosingElement ); +#endif + CFRelease(xPath); + + return true; +} + +hb_blob_t* CoreTextFontFace::GetHbTable(hb_tag_t nTag) const +{ + hb_blob_t* pBlob = nullptr; + CTFontRef pFont = CTFontCreateWithFontDescriptor(mxFontDescriptor, 0.0, nullptr); + + if (!nTag) + { + // If nTag is 0, the whole font data is requested. CoreText does not + // give us that, so we will construct an HarfBuzz face from CoreText + // table data and return the blob of that face. + + auto pTags = CTFontCopyAvailableTables(pFont, kCTFontTableOptionNoOptions); + CFIndex nTags = pTags ? CFArrayGetCount(pTags) : 0; + if (nTags > 0) + { + hb_face_t* pHbFace = hb_face_builder_create(); + for (CFIndex i = 0; i < nTags; i++) + { + auto nTable = reinterpret_cast<intptr_t>(CFArrayGetValueAtIndex(pTags, i)); + assert(nTable); + auto pTable = GetHbTable(nTable); + assert(pTable); + hb_face_builder_add_table(pHbFace, nTable, pTable); + } + pBlob = hb_face_reference_blob(pHbFace); + + hb_face_destroy(pHbFace); + } + if (pTags) + CFRelease(pTags); + } + else + { + CFDataRef pData = CTFontCopyTable(pFont, nTag, kCTFontTableOptionNoOptions); + const CFIndex nLength = pData ? CFDataGetLength(pData) : 0; + if (nLength > 0) + { + auto pBuffer = new UInt8[nLength]; + const CFRange aRange = CFRangeMake(0, nLength); + CFDataGetBytes(pData, aRange, pBuffer); + + pBlob = hb_blob_create(reinterpret_cast<const char*>(pBuffer), nLength, + HB_MEMORY_MODE_READONLY, pBuffer, + [](void* data) { delete[] static_cast<UInt8*>(data); }); + } + if (pData) + CFRelease(pData); + } + + CFRelease(pFont); + return pBlob; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/CoreTextFontFace.cxx b/vcl/quartz/CoreTextFontFace.cxx new file mode 100644 index 0000000000..662b439a9e --- /dev/null +++ b/vcl/quartz/CoreTextFontFace.cxx @@ -0,0 +1,98 @@ +/* -*- 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> + +#ifdef MACOSX +#include <osx/saldata.hxx> +#include <osx/salinst.h> +#endif +#include <font/LogicalFontInstance.hxx> +#include <quartz/CoreTextFont.hxx> +#include <quartz/CoreTextFontFace.hxx> +#include <quartz/salgdi.h> +#include <quartz/utils.h> + +CoreTextFontFace::CoreTextFontFace(const FontAttributes& rDFA, CTFontDescriptorRef xFontDescriptor) + : vcl::font::PhysicalFontFace(rDFA) + , mxFontDescriptor(xFontDescriptor) +{ + CFRetain(mxFontDescriptor); +} + +CoreTextFontFace::~CoreTextFontFace() { CFRelease(mxFontDescriptor); } + +sal_IntPtr CoreTextFontFace::GetFontId() const +{ + return reinterpret_cast<sal_IntPtr>(mxFontDescriptor); +} + +const std::vector<hb_variation_t>& CoreTextFontFace::GetVariations(const LogicalFontInstance&) const +{ + CTFontRef pFont = CTFontCreateWithFontDescriptor(mxFontDescriptor, 0.0, nullptr); + + if (!mxVariations) + { + mxVariations.emplace(); + CFArrayRef pAxes = CTFontCopyVariationAxes(pFont); + if (pAxes) + { + CFDictionaryRef pVariations = CTFontCopyVariation(pFont); + if (pVariations) + { + CFIndex nAxes = CFArrayGetCount(pAxes); + for (CFIndex i = 0; i < nAxes; ++i) + { + auto pAxis = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(pAxes, i)); + if (pAxis) + { + hb_tag_t nTag; + auto pTag = static_cast<CFNumberRef>( + CFDictionaryGetValue(pAxis, kCTFontVariationAxisIdentifierKey)); + if (!pTag) + continue; + CFNumberGetValue(pTag, kCFNumberIntType, &nTag); + + float fValue; + auto pValue + = static_cast<CFNumberRef>(CFDictionaryGetValue(pVariations, pTag)); + if (!pValue) + continue; + CFNumberGetValue(pValue, kCFNumberFloatType, &fValue); + + mxVariations->push_back({ nTag, fValue }); + } + } + CFRelease(pVariations); + } + CFRelease(pAxes); + } + } + + return *mxVariations; +} + +rtl::Reference<LogicalFontInstance> +CoreTextFontFace::CreateFontInstance(const vcl::font::FontSelectPattern& rFSD) const +{ + return new CoreTextFont(*this, rFSD); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/SystemFontList.cxx b/vcl/quartz/SystemFontList.cxx new file mode 100644 index 0000000000..e068caf80f --- /dev/null +++ b/vcl/quartz/SystemFontList.cxx @@ -0,0 +1,301 @@ +/* -*- 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 <tools/long.hxx> + + +#include <quartz/SystemFontList.hxx> +#include <impfont.hxx> +#ifdef MACOSX +#include <osx/saldata.hxx> +#include <osx/salinst.h> +#endif +#include <fontattributes.hxx> +#include <font/PhysicalFontCollection.hxx> +#include <quartz/CoreTextFontFace.hxx> +#include <quartz/salgdi.h> +#include <quartz/utils.h> +#include <sallayout.hxx> + + +FontAttributes DevFontFromCTFontDescriptor( CTFontDescriptorRef pFD, bool* bFontEnabled ) +{ + // all CoreText fonts are device fonts that can rotate just fine + FontAttributes rDFA; + rDFA.SetQuality( 0 ); + + // reset the font attributes + rDFA.SetFamilyType( FAMILY_DONTKNOW ); + rDFA.SetPitch( PITCH_VARIABLE ); + rDFA.SetWidthType( WIDTH_NORMAL ); + rDFA.SetWeight( WEIGHT_NORMAL ); + rDFA.SetItalic( ITALIC_NONE ); + rDFA.SetMicrosoftSymbolEncoded( false ); + + // get font name +#ifdef MACOSX + CFStringRef pLang = nullptr; + CFStringRef pFamilyName = static_cast<CFStringRef>( + CTFontDescriptorCopyLocalizedAttribute( pFD, kCTFontFamilyNameAttribute, &pLang )); + + if ( !pLang ) + { + if( pFamilyName ) + { + CFRelease( pFamilyName ); + } + pFamilyName = static_cast<CFStringRef>(CTFontDescriptorCopyAttribute( pFD, kCTFontFamilyNameAttribute )); + } +#else + // No "Application" on iOS. And it is unclear whether this code + // snippet will actually ever get invoked on iOS anyway. So just + // use the old code that uses a non-localized font name. + CFStringRef pFamilyName = (CFStringRef)CTFontDescriptorCopyAttribute( pFD, kCTFontFamilyNameAttribute ); +#endif + + rDFA.SetFamilyName( GetOUString( pFamilyName ) ); + + // get font style + CFStringRef pStyleName = static_cast<CFStringRef>(CTFontDescriptorCopyAttribute( pFD, kCTFontStyleNameAttribute )); + rDFA.SetStyleName( GetOUString( pStyleName ) ); + + // get font-enabled status + if( bFontEnabled ) + { + int bEnabled = TRUE; // by default (and when we're on macOS < 10.6) it's "enabled" + CFNumberRef pEnabled = static_cast<CFNumberRef>(CTFontDescriptorCopyAttribute( pFD, kCTFontEnabledAttribute )); + CFNumberGetValue( pEnabled, kCFNumberIntType, &bEnabled ); + *bFontEnabled = bEnabled; + } + + // get font attributes + CFDictionaryRef pAttrDict = static_cast<CFDictionaryRef>(CTFontDescriptorCopyAttribute( pFD, kCTFontTraitsAttribute )); + + if (bFontEnabled && *bFontEnabled) + { + // Ignore font formats not supported. + int nFormat; + CFNumberRef pFormat = static_cast<CFNumberRef>(CTFontDescriptorCopyAttribute(pFD, kCTFontFormatAttribute)); + CFNumberGetValue(pFormat, kCFNumberIntType, &nFormat); + if (nFormat == kCTFontFormatUnrecognized || nFormat == kCTFontFormatPostScript || nFormat == kCTFontFormatBitmap) + { + SAL_INFO("vcl.fonts", "Ignoring font with unsupported format: " << rDFA.GetFamilyName()); + *bFontEnabled = false; + } + CFRelease(pFormat); + } + + // get symbolic trait + // TODO: use other traits such as MonoSpace/Condensed/Expanded or Vertical too + SInt64 nSymbolTrait = 0; + CFNumberRef pSymbolNum = nullptr; + if( CFDictionaryGetValueIfPresent( pAttrDict, kCTFontSymbolicTrait, reinterpret_cast<const void**>(&pSymbolNum) ) ) + { + CFNumberGetValue( pSymbolNum, kCFNumberSInt64Type, &nSymbolTrait ); + if (nSymbolTrait & kCTFontMonoSpaceTrait) + rDFA.SetPitch(PITCH_FIXED); + } + + // get the font weight + double fWeight = 0; + CFNumberRef pWeightNum = static_cast<CFNumberRef>(CFDictionaryGetValue( pAttrDict, kCTFontWeightTrait )); + // tdf#140401 check if attribute is a nullptr + if( pWeightNum ) + CFNumberGetValue( pWeightNum, kCFNumberDoubleType, &fWeight ); + int nInt = WEIGHT_NORMAL; + + // Special case fixes + + // tdf#67744: Courier Std Medium is always bold. We get a kCTFontWeightTrait of 0.23 which + // surely must be wrong. + if (rDFA.GetFamilyName() == "Courier Std" && + (rDFA.GetStyleName() == "Medium" || rDFA.GetStyleName() == "Medium Oblique") && + fWeight > 0.2) + { + fWeight = 0; + } + + // tdf#68889: Ditto for Gill Sans MT Pro. Here I can kinda understand it, maybe the + // kCTFontWeightTrait is intended to give a subjective "optical" impression of how the font + // looks, and Gill Sans MT Pro Medium is kinda heavy. But with the way LibreOffice uses fonts, + // we still should think of it as being "medium" weight. + if (rDFA.GetFamilyName() == "Gill Sans MT Pro" && + (rDFA.GetStyleName() == "Medium" || rDFA.GetStyleName() == "Medium Italic") && + fWeight > 0.2) + { + fWeight = 0; + } + + if( fWeight > 0 ) + { + nInt = rint(int(WEIGHT_NORMAL) + fWeight * ((WEIGHT_BLACK - WEIGHT_NORMAL)/0.68)); + if( nInt > WEIGHT_BLACK ) + { + nInt = WEIGHT_BLACK; + } + } + else if( fWeight < 0 ) + { + nInt = rint(int(WEIGHT_NORMAL) + fWeight * ((WEIGHT_NORMAL - WEIGHT_THIN)/0.8)); + if( nInt < WEIGHT_THIN ) + { + nInt = WEIGHT_THIN; + } + } + rDFA.SetWeight( static_cast<FontWeight>(nInt) ); + + // get the font slant + double fSlant = 0; + CFNumberRef pSlantNum = static_cast<CFNumberRef>(CFDictionaryGetValue( pAttrDict, kCTFontSlantTrait )); + // tdf#140401 check if attribute is a nullptr + if( pSlantNum ) + CFNumberGetValue( pSlantNum, kCFNumberDoubleType, &fSlant ); + if( fSlant >= 0.035 ) + { + rDFA.SetItalic( ITALIC_NORMAL ); + } + // get width trait + double fWidth = 0; + CFNumberRef pWidthNum = static_cast<CFNumberRef>(CFDictionaryGetValue( pAttrDict, kCTFontWidthTrait )); + // tdf#140401 check if attribute is a nullptr + if( pWidthNum ) + CFNumberGetValue( pWidthNum, kCFNumberDoubleType, &fWidth ); + nInt = WIDTH_NORMAL; + + if( fWidth > 0 ) + { + nInt = rint( int(WIDTH_NORMAL) + fWidth * ((WIDTH_ULTRA_EXPANDED - WIDTH_NORMAL)/0.4)); + if( nInt > WIDTH_ULTRA_EXPANDED ) + { + nInt = WIDTH_ULTRA_EXPANDED; + } + } + else if( fWidth < 0 ) + { + nInt = rint( int(WIDTH_NORMAL) + fWidth * ((WIDTH_NORMAL - WIDTH_ULTRA_CONDENSED)/0.5)); + if( nInt < WIDTH_ULTRA_CONDENSED ) + { + nInt = WIDTH_ULTRA_CONDENSED; + } + } + rDFA.SetWidthType( static_cast<FontWidth>(nInt) ); + + // release the attribute dict that we had copied + CFRelease( pAttrDict ); + + // TODO? also use the HEAD table if available to get more attributes +// CFDataRef CTFontCopyTable( CTFontRef, kCTFontTableHead, /*kCTFontTableOptionNoOptions*/kCTFontTableOptionExcludeSynthetic ); + + return rDFA; +} + +static void fontEnumCallBack( const void* pValue, void* pContext ) +{ + CTFontDescriptorRef pFD = static_cast<CTFontDescriptorRef>(pValue); + + bool bFontEnabled; + FontAttributes rDFA = DevFontFromCTFontDescriptor( pFD, &bFontEnabled ); + + if( bFontEnabled) + { + rtl::Reference<CoreTextFontFace> pFontData = new CoreTextFontFace( rDFA, pFD ); + SystemFontList* pFontList = static_cast<SystemFontList*>(pContext); + pFontList->AddFont( pFontData.get() ); + } +} + +SystemFontList::SystemFontList() + : mpCTFontCollection( nullptr ) + , mpCTFontArray( nullptr ) +{} + +SystemFontList::~SystemFontList() +{ + maFontContainer.clear(); + + if( mpCTFontArray ) + { + CFRelease( mpCTFontArray ); + } + if( mpCTFontCollection ) + { + CFRelease( mpCTFontCollection ); + } +} + +void SystemFontList::AddFont( CoreTextFontFace* pFontData ) +{ + sal_IntPtr nFontId = pFontData->GetFontId(); + maFontContainer[ nFontId ] = pFontData; +} + +void SystemFontList::AnnounceFonts( vcl::font::PhysicalFontCollection& rFontCollection ) const +{ + for(const auto& rEntry : maFontContainer ) + { + rFontCollection.Add( rEntry.second.get() ); + } +} + +CoreTextFontFace* SystemFontList::GetFontDataFromId( sal_IntPtr nFontId ) const +{ + auto it = maFontContainer.find( nFontId ); + if( it == maFontContainer.end() ) + { + return nullptr; + } + return (*it).second.get(); +} + +bool SystemFontList::Init() +{ + // enumerate available system fonts + static const int nMaxDictEntries = 8; + CFMutableDictionaryRef pCFDict = CFDictionaryCreateMutable( nullptr, + nMaxDictEntries, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks ); + + CFDictionaryAddValue( pCFDict, kCTFontCollectionRemoveDuplicatesOption, kCFBooleanTrue ); + mpCTFontCollection = CTFontCollectionCreateFromAvailableFonts( pCFDict ); + CFRelease( pCFDict ); + mpCTFontArray = CTFontCollectionCreateMatchingFontDescriptors( mpCTFontCollection ); + + const int nFontCount = CFArrayGetCount( mpCTFontArray ); + const CFRange aFullRange = CFRangeMake( 0, nFontCount ); + CFArrayApplyFunction( mpCTFontArray, aFullRange, fontEnumCallBack, this ); + + return true; +} + +std::unique_ptr<SystemFontList> GetCoretextFontList() +{ + std::unique_ptr<SystemFontList> pList(new SystemFontList()); + if( !pList->Init() ) + { + return nullptr; + } + + return pList; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/cgutils.mm b/vcl/quartz/cgutils.mm new file mode 100644 index 0000000000..0d611bec11 --- /dev/null +++ b/vcl/quartz/cgutils.mm @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <quartz/cgutils.h> + +#include <salbmp.hxx> +#ifdef MACOSX +#include <osx/saldata.hxx> +#else +#include <ios/iosinst.hxx> +#endif + +#ifdef MACOSX +#include <premac.h> +#include <Metal/Metal.h> +#include <postmac.h> +#endif + +static void CFRTLFree(void* /*info*/, const void* data, size_t /*size*/) +{ + std::free( const_cast<void*>(data) ); +} + +CGImageRef CreateWithSalBitmapAndMask( const SalBitmap& rBitmap, const SalBitmap& rMask, int nX, int nY, int nWidth, int nHeight ) +{ + CGImageRef xImage( rBitmap.CreateCroppedImage( nX, nY, nWidth, nHeight ) ); + if( !xImage ) + return nullptr; + + CGImageRef xMask = rMask.CreateCroppedImage( nX, nY, nWidth, nHeight ); + if( !xMask ) + return xImage; + + // If xMask is an image (i.e. not a mask), it must be greyscale - a requirement of the + // CGImageCreateWithMask() function. + if( !CGImageIsMask(xMask) && CGImageGetColorSpace(xMask) != GetSalData()->mxGraySpace ) + { + CGImageRef xGrayMask = CGImageCreateCopyWithColorSpace(xMask, GetSalData()->mxGraySpace); + if (xGrayMask) + { + CFRelease(xMask); + xMask = xGrayMask; + } + else + { + // Many gallery images will fail to be converted to a grayscale + // colorspace so fall back to old mask creation code + const CGRect xImageRect=CGRectMake( 0, 0, nWidth, nHeight );//the rect has no offset + + // create the alpha mask image fitting our image + // TODO: is caching the full mask or the subimage mask worth it? + int nMaskBytesPerRow = ((nWidth + 3) & ~3); + void* pMaskMem = std::malloc( nMaskBytesPerRow * nHeight ); + CGContextRef xMaskContext = CGBitmapContextCreate( pMaskMem, + nWidth, nHeight, 8, nMaskBytesPerRow, GetSalData()->mxGraySpace, kCGImageAlphaNone ); + CGContextDrawImage( xMaskContext, xImageRect, xMask ); + CFRelease( xMask ); + CGDataProviderRef xDataProvider( CGDataProviderCreateWithData( nullptr, + pMaskMem, nHeight * nMaskBytesPerRow, &CFRTLFree ) ); + + static const CGFloat* pDecode = nullptr; + xMask = CGImageMaskCreate( nWidth, nHeight, 8, 8, nMaskBytesPerRow, xDataProvider, pDecode, false ); + CFRelease( xDataProvider ); + CFRelease( xMaskContext ); + } + } + + if( !xMask ) + return xImage; + + // combine image and alpha mask + CGImageRef xMaskedImage = CGImageCreateWithMask( xImage, xMask ); + CFRelease( xMask ); + CFRelease( xImage ); + return xMaskedImage; +} + +#ifdef MACOSX + +bool DefaultMTLDeviceIsSupported() +{ + id<MTLDevice> pMetalDevice = MTLCreateSystemDefaultDevice(); + if (!pMetalDevice || !pMetalDevice.name) + { + SAL_WARN("vcl.skia", "MTLCreateSystemDefaultDevice() returned nil"); + return false; + } + + SAL_WARN("vcl.skia", "Default MTLDevice is \"" << [pMetalDevice.name UTF8String] << "\""); + + bool bRet = true; + + // tdf#156881 Disable Metal with AMD Radeon Pro 5XXX GPUs on macOS Catalina + // When running macOS Catalina on a 2019 MacBook Pro, unexpected drawing + // artifacts are drawn so disable Metal for the AMD Radeon Pro GPUs listed + // for that model in https://support.apple.com/kb/SP809. + if (@available(macOS 11, *)) + { + // No known problems with macOS Big Sur or later + } + else + { + static NSString* pAMDRadeonPro5300Prefix = @"AMD Radeon Pro 5300M"; + static NSString* pAMDRadeonPro5500Prefix = @"AMD Radeon Pro 5500M"; + if ([pMetalDevice.name hasPrefix:pAMDRadeonPro5300Prefix] || [pMetalDevice.name hasPrefix:pAMDRadeonPro5500Prefix]) + bRet = false; + } + + [pMetalDevice release]; + return bRet; +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/salbmp.cxx b/vcl/quartz/salbmp.cxx new file mode 100644 index 0000000000..ab435c0acd --- /dev/null +++ b/vcl/quartz/salbmp.cxx @@ -0,0 +1,677 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> +#include <sal/log.hxx> +#include <osl/diagnose.h> + +#include <cstddef> +#include <limits> + +#include <o3tl/make_shared.hxx> +#include <tools/color.hxx> +#include <vcl/bitmap.hxx> +#include <vcl/BitmapAccessMode.hxx> +#include <vcl/BitmapBuffer.hxx> +#include <vcl/BitmapColor.hxx> +#include <vcl/BitmapPalette.hxx> +#include <vcl/ColorMask.hxx> +#include <vcl/Scanline.hxx> + +#include <bitmap/bmpfast.hxx> +#include <quartz/cgutils.h> +#include <quartz/salbmp.h> +#include <quartz/utils.h> +#include <bitmap/ScanlineTools.hxx> + +#ifdef MACOSX +#include <osx/saldata.hxx> +#else +#include <ios/iosinst.hxx> +#endif + +const unsigned long k32BitRedColorMask = 0x00ff0000; +const unsigned long k32BitGreenColorMask = 0x0000ff00; +const unsigned long k32BitBlueColorMask = 0x000000ff; + +QuartzSalBitmap::QuartzSalBitmap() + : mxCachedImage( nullptr ) + , mnBits(0) + , mnWidth(0) + , mnHeight(0) + , mnBytesPerRow(0) +{ +} + +QuartzSalBitmap::~QuartzSalBitmap() +{ + doDestroy(); +} + +bool QuartzSalBitmap::Create( const Size& rSize, vcl::PixelFormat ePixelFormat, const BitmapPalette& rBitmapPalette ) +{ + if (ePixelFormat == vcl::PixelFormat::INVALID) + return false; + + maPalette = rBitmapPalette; + mnBits = vcl::pixelFormatBitCount(ePixelFormat); + mnWidth = rSize.Width(); + mnHeight = rSize.Height(); + return AllocateUserData(); +} + +bool QuartzSalBitmap::Create( const SalBitmap& rSalBmp ) +{ + vcl::PixelFormat ePixelFormat = vcl::bitDepthToPixelFormat(rSalBmp.GetBitCount()); + return Create( rSalBmp, ePixelFormat); +} + +bool QuartzSalBitmap::Create( const SalBitmap& rSalBmp, SalGraphics* pGraphics ) +{ + vcl::PixelFormat ePixelFormat = vcl::PixelFormat::INVALID; + if (pGraphics) + ePixelFormat = vcl::bitDepthToPixelFormat(pGraphics->GetBitCount()); + else + ePixelFormat = vcl::bitDepthToPixelFormat(rSalBmp.GetBitCount()); + + return Create( rSalBmp, ePixelFormat); +} + +bool QuartzSalBitmap::Create( const SalBitmap& rSalBmp, vcl::PixelFormat eNewPixelFormat ) +{ + const QuartzSalBitmap& rSourceBitmap = static_cast<const QuartzSalBitmap&>(rSalBmp); + + if (eNewPixelFormat != vcl::PixelFormat::INVALID && rSourceBitmap.m_pUserBuffer) + { + mnBits = vcl::pixelFormatBitCount(eNewPixelFormat); + mnWidth = rSourceBitmap.mnWidth; + mnHeight = rSourceBitmap.mnHeight; + maPalette = rSourceBitmap.maPalette; + + if( AllocateUserData() ) + { + ConvertBitmapData( mnWidth, mnHeight, mnBits, mnBytesPerRow, maPalette, + m_pUserBuffer.get(), rSourceBitmap.mnBits, + rSourceBitmap.mnBytesPerRow, rSourceBitmap.maPalette, + rSourceBitmap.m_pUserBuffer.get() ); + return true; + } + } + return false; +} + +bool QuartzSalBitmap::Create( const css::uno::Reference< css::rendering::XBitmapCanvas >& /*xBitmapCanvas*/, + Size& /*rSize*/, bool /*bMask*/ ) +{ + return false; +} + +void QuartzSalBitmap::Destroy() +{ + doDestroy(); +} + +void QuartzSalBitmap::doDestroy() +{ + DestroyContext(); + m_pUserBuffer.reset(); +} + +void QuartzSalBitmap::DestroyContext() +{ + if( mxCachedImage ) + { + CGImageRelease( mxCachedImage ); + mxCachedImage = nullptr; + } + + if (maGraphicContext.isSet()) + { + CGContextRelease(maGraphicContext.get()); + maGraphicContext.set(nullptr); + m_pContextBuffer.reset(); + } +} + +bool QuartzSalBitmap::CreateContext() +{ + DestroyContext(); + + // prepare graphics context + // convert image from user input if available + const bool bSkipConversion = !m_pUserBuffer; + if( bSkipConversion ) + AllocateUserData(); + + // default to RGBA color space + CGColorSpaceRef aCGColorSpace = GetSalData()->mxRGBSpace; + CGBitmapInfo aCGBmpInfo = kCGImageAlphaNoneSkipFirst; + + // convert data into something accepted by CGBitmapContextCreate() + size_t bitsPerComponent = 8; + sal_uInt32 nContextBytesPerRow = mnBytesPerRow; + if( mnBits == 32 ) + { + // no conversion needed for truecolor + m_pContextBuffer = m_pUserBuffer; + } + else if( mnBits == 8 && maPalette.IsGreyPalette8Bit() ) + { + // no conversion needed for grayscale + m_pContextBuffer = m_pUserBuffer; + aCGColorSpace = GetSalData()->mxGraySpace; + aCGBmpInfo = kCGImageAlphaNone; + bitsPerComponent = mnBits; + } + // TODO: is special handling for 1bit input buffers worth it? + else + { + // convert user data to 32 bit + nContextBytesPerRow = mnWidth << 2; + try + { + m_pContextBuffer = o3tl::make_shared_array<sal_uInt8>(mnHeight * nContextBytesPerRow); + + if( !bSkipConversion ) + { + ConvertBitmapData( mnWidth, mnHeight, + 32, nContextBytesPerRow, maPalette, m_pContextBuffer.get(), + mnBits, mnBytesPerRow, maPalette, m_pUserBuffer.get() ); + } + } + catch( const std::bad_alloc& ) + { + maGraphicContext.set(nullptr); + } + } + + if (m_pContextBuffer) + { + maGraphicContext.set(CGBitmapContextCreate(m_pContextBuffer.get(), mnWidth, mnHeight, + bitsPerComponent, nContextBytesPerRow, + aCGColorSpace, aCGBmpInfo)); + } + + if (!maGraphicContext.isSet()) + m_pContextBuffer.reset(); + + return maGraphicContext.isSet(); +} + +bool QuartzSalBitmap::AllocateUserData() +{ + Destroy(); + + if( mnWidth && mnHeight ) + { + mnBytesPerRow = 0; + + switch( mnBits ) + { + case 1: mnBytesPerRow = (mnWidth + 7) >> 3; break; + case 8: mnBytesPerRow = mnWidth; break; + case 24: mnBytesPerRow = (mnWidth << 1) + mnWidth; break; + case 32: mnBytesPerRow = mnWidth << 2; break; + default: + assert(false && "vcl::QuartzSalBitmap::AllocateUserData(), illegal bitcount!"); + } + } + + bool alloc = false; + if (mnBytesPerRow != 0 && + mnBytesPerRow <= std::numeric_limits<sal_uInt32>::max() / mnHeight) + { + try + { + m_pUserBuffer = o3tl::make_shared_array<sal_uInt8>(mnBytesPerRow * mnHeight); + alloc = true; + } + catch (std::bad_alloc &) {} + } + if (!alloc) + { + SAL_WARN( "vcl.quartz", "bad_alloc: " << mnWidth << "x" << mnHeight << " (" << mnBytesPerRow * mnHeight << " bytes)"); + m_pUserBuffer.reset(); + mnBytesPerRow = 0; + } + + return bool(m_pUserBuffer); +} + +void QuartzSalBitmap::ConvertBitmapData( sal_uInt32 nWidth, sal_uInt32 nHeight, + sal_uInt16 nDestBits, sal_uInt32 nDestBytesPerRow, + const BitmapPalette& rDestPalette, sal_uInt8* pDestData, + sal_uInt16 nSrcBits, sal_uInt32 nSrcBytesPerRow, + const BitmapPalette& rSrcPalette, sal_uInt8* pSrcData ) + +{ + if( (nDestBytesPerRow == nSrcBytesPerRow) && + (nDestBits == nSrcBits) && ((nSrcBits != 8) || (rDestPalette.operator==( rSrcPalette ))) ) + { + // simple case, same format, so just copy + memcpy( pDestData, pSrcData, nHeight * nDestBytesPerRow ); + return; + } + + // try accelerated conversion if possible + // TODO: are other truecolor conversions except BGR->ARGB worth it? + bool bConverted = false; + if( (nSrcBits == 24) && (nDestBits == 32) ) + { + // TODO: extend bmpfast.cxx with a method that can be directly used here + BitmapBuffer aSrcBuf; + aSrcBuf.mnFormat = ScanlineFormat::N24BitTcBgr; + aSrcBuf.mpBits = pSrcData; + aSrcBuf.mnBitCount = nSrcBits; + aSrcBuf.mnScanlineSize = nSrcBytesPerRow; + BitmapBuffer aDstBuf; + aDstBuf.mnFormat = ScanlineFormat::N32BitTcArgb; + aDstBuf.mpBits = pDestData; + aDstBuf.mnBitCount = nDestBits; + aDstBuf.mnScanlineSize = nDestBytesPerRow; + + aSrcBuf.mnWidth = aDstBuf.mnWidth = nWidth; + aSrcBuf.mnHeight = aDstBuf.mnHeight = nHeight; + + SalTwoRect aTwoRects(0, 0, mnWidth, mnHeight, 0, 0, mnWidth, mnHeight); + bConverted = ::ImplFastBitmapConversion( aDstBuf, aSrcBuf, aTwoRects ); + } + + if( !bConverted ) + { + // TODO: this implementation is for clarity, not for speed + + auto pTarget = vcl::bitmap::getScanlineTransformer(nDestBits, rDestPalette); + auto pSource = vcl::bitmap::getScanlineTransformer(nSrcBits, rSrcPalette); + + if (pTarget && pSource) + { + sal_uInt32 nY = nHeight; + while( nY-- ) + { + pTarget->startLine(pDestData); + pSource->startLine(pSrcData); + + sal_uInt32 nX = nWidth; + while( nX-- ) + { + pTarget->writePixel(pSource->readPixel()); + } + pSrcData += nSrcBytesPerRow; + pDestData += nDestBytesPerRow; + } + } + } +} + +Size QuartzSalBitmap::GetSize() const +{ + return Size( mnWidth, mnHeight ); +} + +sal_uInt16 QuartzSalBitmap::GetBitCount() const +{ + return mnBits; +} + +namespace { + +struct pal_entry +{ + sal_uInt8 mnRed; + sal_uInt8 mnGreen; + sal_uInt8 mnBlue; +}; + +} + +pal_entry const aImplSalSysPalEntryAry[ 16 ] = +{ +{ 0, 0, 0 }, +{ 0, 0, 0x80 }, +{ 0, 0x80, 0 }, +{ 0, 0x80, 0x80 }, +{ 0x80, 0, 0 }, +{ 0x80, 0, 0x80 }, +{ 0x80, 0x80, 0 }, +{ 0x80, 0x80, 0x80 }, +{ 0xC0, 0xC0, 0xC0 }, +{ 0, 0, 0xFF }, +{ 0, 0xFF, 0 }, +{ 0, 0xFF, 0xFF }, +{ 0xFF, 0, 0 }, +{ 0xFF, 0, 0xFF }, +{ 0xFF, 0xFF, 0 }, +{ 0xFF, 0xFF, 0xFF } +}; + +static const BitmapPalette& GetDefaultPalette( int mnBits, bool bMonochrome ) +{ + if( bMonochrome ) + return Bitmap::GetGreyPalette( 1U << mnBits ); + + // at this point we should provide some kind of default palette + // since all other platforms do so, too. + static bool bDefPalInit = false; + static BitmapPalette aDefPalette256; + static BitmapPalette aDefPalette2; + if( ! bDefPalInit ) + { + bDefPalInit = true; + aDefPalette256.SetEntryCount( 256 ); + aDefPalette2.SetEntryCount( 2 ); + + // Standard colors + unsigned int i; + for( i = 0; i < 16; i++ ) + { + aDefPalette256[i] = BitmapColor( aImplSalSysPalEntryAry[i].mnRed, + aImplSalSysPalEntryAry[i].mnGreen, + aImplSalSysPalEntryAry[i].mnBlue ); + } + + aDefPalette2[0] = BitmapColor( 0, 0, 0 ); + aDefPalette2[1] = BitmapColor( 0xff, 0xff, 0xff ); + + // own palette (6/6/6) + const int DITHER_PAL_STEPS = 6; + const sal_uInt8 DITHER_PAL_DELTA = 51; + int nB, nG, nR; + sal_uInt8 nRed, nGreen, nBlue; + for( nB=0, nBlue=0; nB < DITHER_PAL_STEPS; nB++, nBlue += DITHER_PAL_DELTA ) + { + for( nG=0, nGreen=0; nG < DITHER_PAL_STEPS; nG++, nGreen += DITHER_PAL_DELTA ) + { + for( nR=0, nRed=0; nR < DITHER_PAL_STEPS; nR++, nRed += DITHER_PAL_DELTA ) + { + aDefPalette256[ i ] = BitmapColor( nRed, nGreen, nBlue ); + i++; + } + } + } + } + + // now fill in appropriate palette + switch( mnBits ) + { + case 1: return aDefPalette2; + case 8: return aDefPalette256; + default: break; + } + + const static BitmapPalette aEmptyPalette; + return aEmptyPalette; +} + +BitmapBuffer* QuartzSalBitmap::AcquireBuffer( BitmapAccessMode /*nMode*/ ) +{ + // TODO: AllocateUserData(); + if (!m_pUserBuffer) + return nullptr; + + BitmapBuffer* pBuffer = new BitmapBuffer; + pBuffer->mnWidth = mnWidth; + pBuffer->mnHeight = mnHeight; + pBuffer->maPalette = maPalette; + pBuffer->mnScanlineSize = mnBytesPerRow; + pBuffer->mpBits = m_pUserBuffer.get(); + pBuffer->mnBitCount = mnBits; + switch( mnBits ) + { + case 1: + pBuffer->mnFormat = ScanlineFormat::N1BitMsbPal; + break; + case 8: + pBuffer->mnFormat = ScanlineFormat::N8BitPal; + break; + case 24: + pBuffer->mnFormat = ScanlineFormat::N24BitTcBgr; + break; + case 32: + { + pBuffer->mnFormat = ScanlineFormat::N32BitTcArgb; + ColorMaskElement aRedMask(k32BitRedColorMask); + aRedMask.CalcMaskShift(); + ColorMaskElement aGreenMask(k32BitGreenColorMask); + aGreenMask.CalcMaskShift(); + ColorMaskElement aBlueMask(k32BitBlueColorMask); + aBlueMask.CalcMaskShift(); + pBuffer->maColorMask = ColorMask(aRedMask, aGreenMask, aBlueMask); + break; + } + default: assert(false); + } + + // some BitmapBuffer users depend on a complete palette + if( (mnBits <= 8) && !maPalette ) + pBuffer->maPalette = GetDefaultPalette( mnBits, true ); + + return pBuffer; +} + +void QuartzSalBitmap::ReleaseBuffer( BitmapBuffer* pBuffer, BitmapAccessMode nMode ) +{ + // invalidate graphic context if we have different data + if( nMode == BitmapAccessMode::Write ) + { + maPalette = pBuffer->maPalette; + if (maGraphicContext.isSet()) + { + DestroyContext(); + } + InvalidateChecksum(); + } + + delete pBuffer; +} + +CGImageRef QuartzSalBitmap::CreateCroppedImage( int nX, int nY, int nNewWidth, int nNewHeight ) const +{ + if( !mxCachedImage ) + { + if (!maGraphicContext.isSet()) + { + if( !const_cast<QuartzSalBitmap*>(this)->CreateContext() ) + { + return nullptr; + } + } + mxCachedImage = CGBitmapContextCreateImage(maGraphicContext.get()); + } + + CGImageRef xCroppedImage = nullptr; + // short circuit if there is nothing to crop + if( !nX && !nY && (mnWidth == nNewWidth) && (mnHeight == nNewHeight) ) + { + xCroppedImage = mxCachedImage; + CFRetain( xCroppedImage ); + } + else + { + nY = mnHeight - (nY + nNewHeight); // adjust for y-mirrored context + const CGRect aCropRect = { { static_cast<CGFloat>(nX), static_cast<CGFloat>(nY) }, { static_cast<CGFloat>(nNewWidth), static_cast<CGFloat>(nNewHeight) } }; + xCroppedImage = CGImageCreateWithImageInRect( mxCachedImage, aCropRect ); + } + + return xCroppedImage; +} + +static void CFRTLFree(void* /*info*/, const void* data, size_t /*size*/) +{ + std::free( const_cast<void*>(data) ); +} + +CGImageRef QuartzSalBitmap::CreateWithMask( const SalBitmap& rMask, + int nX, int nY, int nWidth, int nHeight ) const +{ + return CreateWithSalBitmapAndMask( *this, rMask, nX, nY, nWidth, nHeight ); +} + +/** creates an image from the given rectangle, replacing all black pixels + with nMaskColor and make all other full transparent */ +CGImageRef QuartzSalBitmap::CreateColorMask( int nX, int nY, int nWidth, + int nHeight, Color nMaskColor ) const +{ + CGImageRef xMask = nullptr; + if (m_pUserBuffer && (nX + nWidth <= mnWidth) && (nY + nHeight <= mnHeight)) + { + auto pSourcePixels = vcl::bitmap::getScanlineTransformer(mnBits, maPalette); + // Don't allocate destination buffer if there is no scanline transformer + if( !pSourcePixels ) + return xMask; + + const sal_uInt32 nDestBytesPerRow = nWidth << 2; + std::unique_ptr<sal_uInt32[]> pMaskBuffer(new (std::nothrow) sal_uInt32[ nHeight * nDestBytesPerRow / 4] ); + if( pMaskBuffer ) + { + sal_uInt32 nColor; + reinterpret_cast<sal_uInt8*>(&nColor)[0] = 0xff; + reinterpret_cast<sal_uInt8*>(&nColor)[1] = nMaskColor.GetRed(); + reinterpret_cast<sal_uInt8*>(&nColor)[2] = nMaskColor.GetGreen(); + reinterpret_cast<sal_uInt8*>(&nColor)[3] = nMaskColor.GetBlue(); + + sal_uInt8* pSource = m_pUserBuffer.get(); + sal_uInt32* pDest = pMaskBuffer.get(); + // First to nY on y-axis, as that is our starting point (sub-image) + if( nY ) + pSource += nY * mnBytesPerRow; + + int y = nHeight; + while( y-- ) + { + pSourcePixels->startLine( pSource ); + pSourcePixels->skipPixel(nX); // Skip on x axis to nX + sal_uInt32 x = nWidth; + while( x-- ) + { + // Fix failure to generate the correct color mask + // OutputDevice::ImplDrawRotateText() draws black text but + // that will generate gray pixels due to antialiasing so + // count dark gray the same as black, light gray the same + // as white, and the rest as medium gray. + // The results are not smooth since LibreOffice appears to + // redraw these semi-transparent masks repeatedly without + // clearing the background so the semi-transparent pixels + // will grow darker with repeatedly redraws due to + // cumulative blending. But it is now better than before. + sal_uInt8 nAlpha = 255 - pSourcePixels->readPixel().GetRed(); + sal_uInt32 nPremultColor = nColor; + if ( nAlpha < 192 ) + { + if ( nAlpha < 64 ) + { + nPremultColor = 0; + } + else + { + reinterpret_cast<sal_uInt8*>(&nPremultColor)[0] /= 2; + reinterpret_cast<sal_uInt8*>(&nPremultColor)[1] /= 2; + reinterpret_cast<sal_uInt8*>(&nPremultColor)[2] /= 2; + reinterpret_cast<sal_uInt8*>(&nPremultColor)[3] /= 2; + } + } + *pDest++ = nPremultColor; + } + pSource += mnBytesPerRow; + } + + CGDataProviderRef xDataProvider( CGDataProviderCreateWithData(nullptr, pMaskBuffer.release(), nHeight * nDestBytesPerRow, &CFRTLFree) ); + xMask = CGImageCreate(nWidth, nHeight, 8, 32, nDestBytesPerRow, GetSalData()->mxRGBSpace, kCGImageAlphaPremultipliedFirst, xDataProvider, nullptr, true, kCGRenderingIntentDefault); + CFRelease(xDataProvider); + } + } + return xMask; +} + +/** QuartzSalBitmap::GetSystemData Get platform native image data from existing image + * + * @param rData struct BitmapSystemData, defined in vcl/inc/bitmap.hxx + * @return true if successful +**/ +bool QuartzSalBitmap::GetSystemData( BitmapSystemData& rData ) +{ + bool bRet = false; + + if (!maGraphicContext.isSet()) + CreateContext(); + + if (maGraphicContext.isSet()) + { + bRet = true; + + if ((CGBitmapContextGetBitsPerPixel(maGraphicContext.get()) == 32) && + (CGBitmapContextGetBitmapInfo(maGraphicContext.get()) & kCGBitmapByteOrderMask) != kCGBitmapByteOrder32Host) + { + /** + * We need to hack things because VCL does not use kCGBitmapByteOrder32Host, while Cairo requires it. + * + * Not sure what the above comment means. We don't use Cairo on macOS or iOS. + * + * This whole if statement was originally (before 2011) inside #ifdef CAIRO. Did we use Cairo on Mac back then? + * Anyway, nowadays (since many years, I think) we don't, so should this if statement be dropped? Fun. + */ + + CGImageRef xImage = CGBitmapContextCreateImage(maGraphicContext.get()); + + // re-create the context with single change: include kCGBitmapByteOrder32Host flag. + CGContextHolder aGraphicContextNew(CGBitmapContextCreate(CGBitmapContextGetData(maGraphicContext.get()), + CGBitmapContextGetWidth(maGraphicContext.get()), + CGBitmapContextGetHeight(maGraphicContext.get()), + CGBitmapContextGetBitsPerComponent(maGraphicContext.get()), + CGBitmapContextGetBytesPerRow(maGraphicContext.get()), + CGBitmapContextGetColorSpace(maGraphicContext.get()), + CGBitmapContextGetBitmapInfo(maGraphicContext.get()) | kCGBitmapByteOrder32Host)); + CFRelease(maGraphicContext.get()); + + // Needs to be flipped + aGraphicContextNew.saveState(); + CGContextTranslateCTM (aGraphicContextNew.get(), 0, CGBitmapContextGetHeight(aGraphicContextNew.get())); + CGContextScaleCTM (aGraphicContextNew.get(), 1.0, -1.0); + + CGContextDrawImage(aGraphicContextNew.get(), CGRectMake( 0, 0, CGImageGetWidth(xImage), CGImageGetHeight(xImage)), xImage); + + // Flip back + CGContextRestoreGState( aGraphicContextNew.get() ); + CGImageRelease( xImage ); + maGraphicContext = aGraphicContextNew; + } + + rData.mnWidth = mnWidth; + rData.mnHeight = mnHeight; + } + + return bRet; +} + +bool QuartzSalBitmap::ScalingSupported() const +{ + return false; +} + +bool QuartzSalBitmap::Scale( const double& /*rScaleX*/, const double& /*rScaleY*/, BmpScaleFlag /*nScaleFlag*/ ) +{ + return false; +} + +bool QuartzSalBitmap::Replace( const Color& /*rSearchColor*/, const Color& /*rReplaceColor*/, sal_uInt8 /*nTol*/ ) +{ + return false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/salgdi.cxx b/vcl/quartz/salgdi.cxx new file mode 100644 index 0000000000..0522ff8d58 --- /dev/null +++ b/vcl/quartz/salgdi.cxx @@ -0,0 +1,495 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <memory> + +#include <sal/log.hxx> +#include <config_folders.h> + +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/range/b2drectangle.hxx> +#include <osl/file.hxx> +#include <osl/process.h> +#include <rtl/bootstrap.h> +#include <rtl/strbuf.hxx> +#include <rtl/ustrbuf.hxx> +#include <tools/long.hxx> +#include <comphelper/lok.hxx> + +#include <vcl/metric.hxx> +#include <vcl/fontcharmap.hxx> +#include <vcl/svapp.hxx> +#include <vcl/sysdata.hxx> + +#include <fontsubset.hxx> +#include <impfont.hxx> +#include <font/FontMetricData.hxx> +#include <font/fontsubstitution.hxx> +#include <font/PhysicalFontCollection.hxx> + +#ifdef MACOSX +#include <osx/salframe.h> +#endif +#include <quartz/utils.h> +#ifdef IOS +#include <ios/iosinst.hxx> +#endif +#include <sallayout.hxx> + +#include <config_features.h> +#include <vcl/skia/SkiaHelper.hxx> +#if HAVE_FEATURE_SKIA +#include <skia/osx/gdiimpl.hxx> +#endif + +#include <quartz/SystemFontList.hxx> +#include <quartz/CoreTextFont.hxx> +#include <quartz/CoreTextFontFace.hxx> + +using namespace vcl; + +namespace { + +class CoreTextGlyphFallbackSubstititution +: public vcl::font::GlyphFallbackFontSubstitution +{ +public: + bool FindFontSubstitute(vcl::font::FontSelectPattern&, LogicalFontInstance* pLogicalFont, OUString&) const override; +}; + +bool FontHasCharacter(CTFontRef pFont, const OUString& rString, sal_Int32 nIndex, sal_Int32 nLen) +{ + auto const glyphs = std::make_unique<CGGlyph[]>(nLen); + return CTFontGetGlyphsForCharacters(pFont, reinterpret_cast<const UniChar*>(rString.getStr() + nIndex), glyphs.get(), nLen); +} + +} + +bool CoreTextGlyphFallbackSubstititution::FindFontSubstitute(vcl::font::FontSelectPattern& rPattern, LogicalFontInstance* pLogicalFont, + OUString& rMissingChars) const +{ + bool bFound = false; + CoreTextFont* pFont = static_cast<CoreTextFont*>(pLogicalFont); + CFStringRef pStr = CreateCFString(rMissingChars); + if (pStr) + { + CTFontRef pFallback = CTFontCreateForString(pFont->GetCTFont(), pStr, CFRangeMake(0, CFStringGetLength(pStr))); + if (pFallback) + { + bFound = true; + + // tdf#148470 remove the resolved chars from rMissing to flag which ones are still missing + // for an attempt with another font + OUStringBuffer aStillMissingChars; + for (sal_Int32 nStrIndex = 0; nStrIndex < rMissingChars.getLength();) + { + sal_Int32 nOldStrIndex = nStrIndex; + rMissingChars.iterateCodePoints(&nStrIndex); + sal_Int32 nCharLength = nStrIndex - nOldStrIndex; + if (!FontHasCharacter(pFallback, rMissingChars, nOldStrIndex, nCharLength)) + aStillMissingChars.append(rMissingChars.getStr() + nOldStrIndex, nCharLength); + } + rMissingChars = aStillMissingChars.toString(); + + CTFontDescriptorRef pDesc = CTFontCopyFontDescriptor(pFallback); + FontAttributes rAttr = DevFontFromCTFontDescriptor(pDesc, nullptr); + + rPattern.maSearchName = rAttr.GetFamilyName(); + + CFRelease(pFallback); + CFRelease(pDesc); + } + CFRelease(pStr); + } + + return bFound; +} + +AquaSalGraphics::AquaSalGraphics(bool bPrinter) + : mnRealDPIX( 0 ) + , mnRealDPIY( 0 ) +{ + SAL_INFO( "vcl.quartz", "AquaSalGraphics::AquaSalGraphics() this=" << this ); + +#if HAVE_FEATURE_SKIA + // tdf#146842 Do not use Skia for printing + // Skia does not work with a native print graphics contexts. I am not sure + // why but from what I can see, the Skia implementation drawing to a bitmap + // buffer. However, in an NSPrintOperation, the print view's backing buffer + // is CGPDFContext so even if this bug could be solved by blitting the + // Skia bitmap buffer, the printed PDF would not have selectable text so + // always disable Skia for print graphics contexts. + if(!bPrinter && SkiaHelper::isVCLSkiaEnabled()) + mpBackend.reset(new AquaSkiaSalGraphicsImpl(*this, maShared)); +#else + (void)bPrinter; + if(false) + ; +#endif + else + mpBackend.reset(new AquaGraphicsBackend(maShared)); + + for (int i = 0; i < MAX_FALLBACK; ++i) + mpFont[i] = nullptr; + + if (comphelper::LibreOfficeKit::isActive()) + initWidgetDrawBackends(true); +} + +AquaSalGraphics::~AquaSalGraphics() +{ + SAL_INFO( "vcl.quartz", "AquaSalGraphics::~AquaSalGraphics() this=" << this ); + + maShared.unsetClipPath(); + + ReleaseFonts(); + + maShared.mpXorEmulation.reset(); + +#ifdef IOS + if (maShared.mbForeignContext) + return; +#endif + if (maShared.maLayer.isSet()) + { + CGLayerRelease(maShared.maLayer.get()); + } + else if (maShared.maContextHolder.isSet() +#ifdef MACOSX + && maShared.mbWindow +#endif + ) + { + // destroy backbuffer bitmap context that we created ourself + CGContextRelease(maShared.maContextHolder.get()); + maShared.maContextHolder.set(nullptr); + } +} + +SalGraphicsImpl* AquaSalGraphics::GetImpl() const +{ + return mpBackend->GetImpl(); +} + +void AquaSalGraphics::SetTextColor( Color nColor ) +{ + maShared.maTextColor = nColor; +} + +void AquaSalGraphics::GetFontMetric(FontMetricDataRef& rxFontMetric, int nFallbackLevel) +{ + if (nFallbackLevel < MAX_FALLBACK && mpFont[nFallbackLevel]) + { + mpFont[nFallbackLevel]->GetFontMetric(rxFontMetric); + } +} + +static bool AddTempDevFont(const OUString& rFontFileURL) +{ + OUString aUSystemPath; + OSL_VERIFY( !osl::FileBase::getSystemPathFromFileURL( rFontFileURL, aUSystemPath ) ); + OString aCFileName = OUStringToOString( aUSystemPath, RTL_TEXTENCODING_UTF8 ); + + CFStringRef rFontPath = CFStringCreateWithCString(nullptr, aCFileName.getStr(), kCFStringEncodingUTF8); + CFURLRef rFontURL = CFURLCreateWithFileSystemPath(nullptr, rFontPath, kCFURLPOSIXPathStyle, true); + + CFErrorRef error; + bool success = CTFontManagerRegisterFontsForURL(rFontURL, kCTFontManagerScopeProcess, &error); + if (!success) + { + CFRelease(error); + } + CFRelease(rFontPath); + CFRelease(rFontURL); + + return success; +} + +static void AddTempFontDir( const OUString &rFontDirUrl ) +{ + osl::Directory aFontDir( rFontDirUrl ); + osl::FileBase::RC rcOSL = aFontDir.open(); + if( rcOSL == osl::FileBase::E_None ) + { + osl::DirectoryItem aDirItem; + + while( aFontDir.getNextItem( aDirItem, 10 ) == osl::FileBase::E_None ) + { + osl::FileStatus aFileStatus( osl_FileStatus_Mask_FileURL ); + rcOSL = aDirItem.getFileStatus( aFileStatus ); + if ( rcOSL == osl::FileBase::E_None ) + { + AddTempDevFont(aFileStatus.getFileURL()); + } + } + } +} + +static void AddLocalTempFontDirs() +{ + static bool bFirst = true; + if( !bFirst ) + return; + + bFirst = false; + + // add private font files + + OUString aBrandStr( "$BRAND_BASE_DIR" ); + rtl_bootstrap_expandMacros( &aBrandStr.pData ); + + // internal font resources, required for normal operation, like OpenSymbol + AddTempFontDir( aBrandStr + "/" LIBO_SHARE_RESOURCE_FOLDER "/common/fonts/" ); + + AddTempFontDir( aBrandStr + "/" LIBO_SHARE_FOLDER "/fonts/truetype/" ); +} + +void AquaSalGraphics::GetDevFontList(vcl::font::PhysicalFontCollection* pFontCollection) +{ + SAL_WARN_IF( !pFontCollection, "vcl", "AquaSalGraphics::GetDevFontList(NULL) !"); + + AddLocalTempFontDirs(); + + SalData* pSalData = GetSalData(); + pSalData->mpFontList = GetCoretextFontList(); + + // Copy all PhysicalFontFace objects contained in the SystemFontList + pSalData->mpFontList->AnnounceFonts( *pFontCollection ); + + static CoreTextGlyphFallbackSubstititution aSubstFallback; + pFontCollection->SetFallbackHook(&aSubstFallback); +} + +void AquaSalGraphics::ClearDevFontCache() +{ + SalData* pSalData = GetSalData(); + pSalData->mpFontList.reset(); +} + +bool AquaSalGraphics::AddTempDevFont(vcl::font::PhysicalFontCollection*, + const OUString& rFontFileURL, const OUString& /*rFontName*/) +{ + return ::AddTempDevFont(rFontFileURL); +} + +void AquaSalGraphics::DrawTextLayout(const GenericSalLayout& rLayout) +{ + mpBackend->drawTextLayout(rLayout); +} + +void AquaGraphicsBackend::drawTextLayout(const GenericSalLayout& rLayout) +{ +#ifdef IOS + if (!mrShared.checkContext()) + { + SAL_WARN("vcl.quartz", "AquaSalGraphics::DrawTextLayout() without context"); + return; + } +#endif + + const CoreTextFont& rFont = *static_cast<const CoreTextFont*>(&rLayout.GetFont()); + const vcl::font::FontSelectPattern& rFontSelect = rFont.GetFontSelectPattern(); + if (rFontSelect.mnHeight == 0) + { + SAL_WARN("vcl.quartz", "AquaSalGraphics::DrawTextLayout(): rFontSelect.mnHeight is zero!?"); + return; + } + + CTFontRef pCTFont = rFont.GetCTFont(); + CGAffineTransform aRotMatrix = CGAffineTransformMakeRotation(-rFont.mfFontRotation); + + basegfx::B2DPoint aPos; + const GlyphItem* pGlyph; + std::vector<CGGlyph> aGlyphIds; + std::vector<CGPoint> aGlyphPos; + std::vector<bool> aGlyphOrientation; + int nStart = 0; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nStart)) + { + CGPoint aGCPos = CGPointMake(aPos.getX(), -aPos.getY()); + + // Whether the glyph should be upright in vertical mode or not + bool bUprightGlyph = false; + + if (rFont.mfFontRotation) + { + if (pGlyph->IsVertical()) + bUprightGlyph = true; + else + // Transform the position of rotated glyphs. + aGCPos = CGPointApplyAffineTransform(aGCPos, aRotMatrix); + } + + aGlyphIds.push_back(pGlyph->glyphId()); + aGlyphPos.push_back(aGCPos); + aGlyphOrientation.push_back(bUprightGlyph); + } + + if (aGlyphIds.empty()) + return; + + assert(aGlyphIds.size() == aGlyphPos.size()); +#if 0 + std::cerr << "aGlyphIds:["; + for (unsigned i = 0; i < aGlyphIds.size(); i++) + { + if (i > 0) + std::cerr << ","; + std::cerr << aGlyphIds[i]; + } + std::cerr << "]\n"; + std::cerr << "aGlyphPos:["; + for (unsigned i = 0; i < aGlyphPos.size(); i++) + { + if (i > 0) + std::cerr << ","; + std::cerr << aGlyphPos[i]; + } + std::cerr << "]\n"; +#endif + + mrShared.maContextHolder.saveState(); + RGBAColor textColor( mrShared.maTextColor ); + + // The view is vertically flipped (no idea why), flip it back. + CGContextScaleCTM(mrShared.maContextHolder.get(), 1.0, -1.0); + CGContextSetShouldAntialias(mrShared.maContextHolder.get(), !mrShared.mbNonAntialiasedText); + CGContextSetFillColor(mrShared.maContextHolder.get(), textColor.AsArray()); + + if (rFont.NeedsArtificialBold()) + { + + float fSize = rFontSelect.mnHeight / 23.0f; + CGContextSetStrokeColor(mrShared.maContextHolder.get(), textColor.AsArray()); + CGContextSetLineWidth(mrShared.maContextHolder.get(), fSize); + CGContextSetTextDrawingMode(mrShared.maContextHolder.get(), kCGTextFillStroke); + } + + if (rLayout.GetSubpixelPositioning()) + { + CGContextSetAllowsFontSubpixelQuantization(mrShared.maContextHolder.get(), false); + CGContextSetShouldSubpixelQuantizeFonts(mrShared.maContextHolder.get(), false); + CGContextSetAllowsFontSubpixelPositioning(mrShared.maContextHolder.get(), true); + CGContextSetShouldSubpixelPositionFonts(mrShared.maContextHolder.get(), true); + } + + auto aIt = aGlyphOrientation.cbegin(); + while (aIt != aGlyphOrientation.cend()) + { + bool bUprightGlyph = *aIt; + // Find the boundary of the run of glyphs with the same rotation, to be + // drawn together. + auto aNext = std::find(aIt, aGlyphOrientation.cend(), !bUprightGlyph); + size_t nStartIndex = std::distance(aGlyphOrientation.cbegin(), aIt); + size_t nLen = std::distance(aIt, aNext); + + mrShared.maContextHolder.saveState(); + if (rFont.mfFontRotation && !bUprightGlyph) + { + CGContextRotateCTM(mrShared.maContextHolder.get(), rFont.mfFontRotation); + } + CTFontDrawGlyphs(pCTFont, &aGlyphIds[nStartIndex], &aGlyphPos[nStartIndex], nLen, mrShared.maContextHolder.get()); + mrShared.maContextHolder.restoreState(); + + aIt = aNext; + } + + mrShared.maContextHolder.restoreState(); +} + +void AquaSalGraphics::SetFont(LogicalFontInstance* pReqFont, int nFallbackLevel) +{ + // release the font + for (int i = nFallbackLevel; i < MAX_FALLBACK; ++i) + { + if (!mpFont[i]) + break; + mpFont[i].clear(); + } + + if (!pReqFont) + return; + + // update the font + mpFont[nFallbackLevel] = static_cast<CoreTextFont*>(pReqFont); +} + +std::unique_ptr<GenericSalLayout> AquaSalGraphics::GetTextLayout(int nFallbackLevel) +{ + assert(mpFont[nFallbackLevel]); + if (!mpFont[nFallbackLevel]) + return nullptr; + return std::make_unique<GenericSalLayout>(*mpFont[nFallbackLevel]); +} + +FontCharMapRef AquaSalGraphics::GetFontCharMap() const +{ + if (!mpFont[0]) + { + return FontCharMapRef( new FontCharMap() ); + } + + return mpFont[0]->GetFontFace()->GetFontCharMap(); +} + +bool AquaSalGraphics::GetFontCapabilities(vcl::FontCapabilities &rFontCapabilities) const +{ + if (!mpFont[0]) + return false; + + return mpFont[0]->GetFontFace()->GetFontCapabilities(rFontCapabilities); +} + +void AquaSalGraphics::Flush() +{ + mpBackend->Flush(); +} + +void AquaSalGraphics::Flush( const tools::Rectangle& rRect ) +{ + mpBackend->Flush( rRect ); +} + +void AquaSalGraphics::WindowBackingPropertiesChanged() +{ + mpBackend->WindowBackingPropertiesChanged(); +} + +#ifdef IOS + +bool AquaSharedAttributes::checkContext() +{ + if (mbForeignContext) + { + SAL_INFO("vcl.ios", "CheckContext() this=" << this << ", mbForeignContext, return true"); + return true; + } + + SAL_INFO( "vcl.ios", "CheckContext() this=" << this << ", not foreign, return false"); + return false; +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/salgdicommon.cxx b/vcl/quartz/salgdicommon.cxx new file mode 100644 index 0000000000..98ff40a7dc --- /dev/null +++ b/vcl/quartz/salgdicommon.cxx @@ -0,0 +1,234 @@ +/* -*- 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 <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 <quartz/salbmp.h> +#ifdef MACOSX +#include <quartz/salgdi.h> +#endif +#include <quartz/utils.h> +#ifdef IOS +#include <svdata.hxx> +#endif + +using namespace vcl; + +#ifndef IOS + +void AquaSalGraphics::copyResolution( AquaSalGraphics& rGraphics ) +{ + if (!rGraphics.mnRealDPIY && rGraphics.maShared.mbWindow && rGraphics.maShared.mpFrame) + { + rGraphics.initResolution(rGraphics.maShared.mpFrame->getNSWindow()); + } + mnRealDPIX = rGraphics.mnRealDPIX; + mnRealDPIY = rGraphics.mnRealDPIY; +} + +#endif + +SystemGraphicsData AquaSalGraphics::GetGraphicsData() const +{ + SystemGraphicsData aRes; + aRes.nSize = sizeof(aRes); + aRes.rCGContext = maShared.maContextHolder.get(); + return aRes; +} + +#ifndef IOS + +void AquaSalGraphics::initResolution(NSWindow* nsWindow) +{ + if (!nsWindow) + { + if (Application::IsBitmapRendering()) + mnRealDPIX = mnRealDPIY = 96; + return; + } + + // #i100617# read DPI only once; there is some kind of weird caching going on + // if the main screen changes + // FIXME: this is really unfortunate and needs to be investigated + + SalData* pSalData = GetSalData(); + if( pSalData->mnDPIX == 0 || pSalData->mnDPIY == 0 ) + { + NSScreen* pScreen = nil; + + /* #i91301# + many woes went into the try to have different resolutions + on different screens. The result of these trials is that OOo is not ready + for that yet, vcl and applications would need to be adapted. + + Unfortunately this is not possible in the 3.0 timeframe. + So let's stay with one resolution for all Windows and VirtualDevices + which is the resolution of the main screen + + This of course also means that measurements are exact only on the main screen. + For activating different resolutions again just comment out the two lines below. + + if( pWin ) + pScreen = [pWin screen]; + */ + if( pScreen == nil ) + { + NSArray* pScreens = [NSScreen screens]; + if( pScreens && [pScreens count] > 0) + { + pScreen = [pScreens objectAtIndex: 0]; + } + } + + mnRealDPIX = mnRealDPIY = 96; + if( pScreen ) + { + NSDictionary* pDev = [pScreen deviceDescription]; + if( pDev ) + { + NSNumber* pVal = [pDev objectForKey: @"NSScreenNumber"]; + if( pVal ) + { + // FIXME: casting a long to CGDirectDisplayID is evil, but + // Apple suggest to do it this way + const CGDirectDisplayID nDisplayID = static_cast<CGDirectDisplayID>([pVal longValue]); + const CGSize aSize = CGDisplayScreenSize( nDisplayID ); // => result is in millimeters + mnRealDPIX = static_cast<sal_Int32>((CGDisplayPixelsWide( nDisplayID ) * 25.4) / aSize.width); + mnRealDPIY = static_cast<sal_Int32>((CGDisplayPixelsHigh( nDisplayID ) * 25.4) / aSize.height); + } + else + { + OSL_FAIL( "no resolution found in device description" ); + } + } + else + { + OSL_FAIL( "no device description" ); + } + } + else + { + OSL_FAIL( "no screen found" ); + } + + // #i107076# maintaining size-WYSIWYG-ness causes many problems for + // low-DPI, high-DPI or for mis-reporting devices + // => it is better to limit the calculation result then + static const int nMinDPI = 72; + if( (mnRealDPIX < nMinDPI) || (mnRealDPIY < nMinDPI) ) + { + mnRealDPIX = mnRealDPIY = nMinDPI; + } + // Note that on a Retina display, the "mnRealDPIX" as + // calculated above is not the true resolution of the display, + // but the "logical" one, or whatever the correct terminology + // is. (For instance on a 5K 27in iMac, it's 108.) So at + // least currently, it won't be over 200. I don't know whether + // this test is a "sanity check", or whether there is some + // real reason to limit this to 200. + static const int nMaxDPI = 200; + if( (mnRealDPIX > nMaxDPI) || (mnRealDPIY > nMaxDPI) ) + { + mnRealDPIX = mnRealDPIY = nMaxDPI; + } + // for OSX any anisotropy reported for the display resolution is best ignored (e.g. TripleHead2Go) + mnRealDPIX = mnRealDPIY = (mnRealDPIX + mnRealDPIY + 1) / 2; + + pSalData->mnDPIX = mnRealDPIX; + pSalData->mnDPIY = mnRealDPIY; + } + else + { + mnRealDPIX = pSalData->mnDPIX; + mnRealDPIY = pSalData->mnDPIY; + } +} + +#endif + +void AquaSharedAttributes::setState() +{ + maContextHolder.restoreState(); + maContextHolder.saveState(); + + // setup clipping + if (mxClipPath) + { + CGContextBeginPath(maContextHolder.get()); // discard any existing path + CGContextAddPath(maContextHolder.get(), mxClipPath); // set the current path to the clipping path + CGContextClip(maContextHolder.get()); // use it for clipping + } + + // set RGB colorspace and line and fill colors + CGContextSetFillColor(maContextHolder.get(), maFillColor.AsArray() ); + + CGContextSetStrokeColor(maContextHolder.get(), maLineColor.AsArray() ); + CGContextSetShouldAntialias(maContextHolder.get(), false ); + if (mnXorMode == 2) + { + CGContextSetBlendMode(maContextHolder.get(), kCGBlendModeDifference ); + } +} + +#ifndef IOS + +void AquaSalGraphics::updateResolution() +{ + SAL_WARN_IF(!maShared.mbWindow, "vcl", "updateResolution on inappropriate graphics"); + + initResolution((maShared.mbWindow && maShared.mpFrame) ? maShared.mpFrame->getNSWindow() : nil); +} + +#endif + +XorEmulation::XorEmulation() + : m_xTargetLayer( nullptr ) + , m_xTargetContext( nullptr ) + , m_xMaskContext( nullptr ) + , m_xTempContext( nullptr ) + , m_pMaskBuffer( nullptr ) + , m_pTempBuffer( nullptr ) + , m_nBufferLongs( 0 ) + , m_bIsEnabled( false ) +{ + SAL_INFO( "vcl.quartz", "XorEmulation::XorEmulation() this=" << this ); +} + +XorEmulation::~XorEmulation() +{ + SAL_INFO( "vcl.quartz", "XorEmulation::~XorEmulation() this=" << this ); + Disable(); + SetTarget( 0, 0, 0, nullptr, nullptr ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/salvd.cxx b/vcl/quartz/salvd.cxx new file mode 100644 index 0000000000..4e0c295a17 --- /dev/null +++ b/vcl/quartz/salvd.cxx @@ -0,0 +1,177 @@ +/* -*- 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 <vcl/svapp.hxx> +#include <vcl/sysdata.hxx> + +#ifdef MACOSX +#include <osx/salinst.h> +#include <osx/saldata.hxx> +#include <osx/salframe.h> +#else +#include <ios/iosinst.hxx> +#include "headless/svpframe.hxx" +#include "headless/svpinst.hxx" +#include "headless/svpvd.hxx" +#endif +#include <quartz/salgdi.h> +#include <quartz/salvd.h> +#include <quartz/utils.h> + +std::unique_ptr<SalVirtualDevice> AquaSalInstance::CreateVirtualDevice( SalGraphics& rGraphics, + tools::Long &nDX, tools::Long &nDY, + DeviceFormat eFormat, + const SystemGraphicsData *pData ) +{ + // #i92075# can be called first in a thread + SalData::ensureThreadAutoreleasePool(); + +#ifdef IOS + if( pData ) + { + return std::unique_ptr<SalVirtualDevice>(new AquaSalVirtualDevice( static_cast< AquaSalGraphics* >(&rGraphics), + nDX, nDY, eFormat, pData )); + } + else + { + std::unique_ptr<SalVirtualDevice> pNew(new AquaSalVirtualDevice( NULL, nDX, nDY, eFormat, NULL )); + pNew->SetSize( nDX, nDY ); + return pNew; + } +#else + return std::unique_ptr<SalVirtualDevice>(new AquaSalVirtualDevice( static_cast< AquaSalGraphics* >(&rGraphics), + nDX, nDY, eFormat, pData )); +#endif +} + +AquaSalVirtualDevice::AquaSalVirtualDevice( + AquaSalGraphics* pGraphic, tools::Long &nDX, tools::Long &nDY, + DeviceFormat eFormat, const SystemGraphicsData *pData ) + : mbGraphicsUsed( false ) + , mnBitmapDepth( 0 ) + , mnWidth(0) + , mnHeight(0) +{ + SAL_INFO( "vcl.virdev", "AquaSalVirtualDevice::AquaSalVirtualDevice() this=" << this + << " size=(" << nDX << "x" << nDY << ") bitcount=" << static_cast<int>(eFormat) << + " pData=" << pData << " context=" << (pData ? pData->rCGContext : nullptr) ); + + if( pGraphic && pData && pData->rCGContext ) + { + // Create virtual device based on existing SystemGraphicsData + // We ignore nDx and nDY, as the desired size comes from the SystemGraphicsData. + // the mxContext is from pData (what "mxContext"? there is no such field anywhere in vcl;) + mbForeignContext = true; + mpGraphics = new AquaSalGraphics( /*pGraphic*/ ); + if (nDX == 0) + { + nDX = 1; + } + if (nDY == 0) + { + nDY = 1; + } + maLayer.set(CGLayerCreateWithContext(pData->rCGContext, CGSizeMake(nDX, nDY), nullptr)); + // Interrogate the context as to its real size + if (maLayer.isSet()) + { + const CGSize aSize = CGLayerGetSize(maLayer.get()); + nDX = static_cast<tools::Long>(aSize.width); + nDY = static_cast<tools::Long>(aSize.height); + } + else + { + nDX = 0; + nDY = 0; + } + + mpGraphics->SetVirDevGraphics(this, maLayer, pData->rCGContext); + + SAL_INFO("vcl.virdev", "AquaSalVirtualDevice::AquaSalVirtualDevice() this=" << this << + " (" << nDX << "x" << nDY << ") mbForeignContext=" << (mbForeignContext ? "YES" : "NO")); + + } + else + { + // create empty new virtual device + mbForeignContext = false; // the mxContext is created within VCL + mpGraphics = new AquaSalGraphics(); // never fails + switch (eFormat) + { +#ifdef IOS + case DeviceFormat::GRAYSCALE: + mnBitmapDepth = 8; + break; +#endif + default: + mnBitmapDepth = 0; + break; + } +#ifdef MACOSX + // inherit resolution from reference device + if( pGraphic ) + { + AquaSalFrame* pFrame = pGraphic->getGraphicsFrame(); + if( pFrame && AquaSalFrame::isAlive( pFrame ) ) + { + mpGraphics->setGraphicsFrame( pFrame ); + mpGraphics->copyResolution( *pGraphic ); + } + } +#endif + if( nDX && nDY ) + { + SetSize( nDX, nDY ); + } + // NOTE: if SetSize does not succeed, we just ignore the nDX and nDY + } +} + +AquaSalVirtualDevice::~AquaSalVirtualDevice() +{ + SAL_INFO( "vcl.virdev", "AquaSalVirtualDevice::~AquaSalVirtualDevice() this=" << this ); + + if( mpGraphics ) + { + mpGraphics->SetVirDevGraphics( this, nullptr, nullptr ); + delete mpGraphics; + mpGraphics = nullptr; + } + Destroy(); +} + +SalGraphics* AquaSalVirtualDevice::AcquireGraphics() +{ + if( mbGraphicsUsed || !mpGraphics ) + { + return nullptr; + } + mbGraphicsUsed = true; + return mpGraphics; +} + +void AquaSalVirtualDevice::ReleaseGraphics( SalGraphics* ) +{ + mbGraphicsUsed = false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/utils.cxx b/vcl/quartz/utils.cxx new file mode 100644 index 0000000000..b07e68f746 --- /dev/null +++ b/vcl/quartz/utils.cxx @@ -0,0 +1,242 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <iostream> +#include <iomanip> + +#include <rtl/alloc.h> +#include <rtl/ustrbuf.hxx> + +#include <quartz/utils.h> + +OUString GetOUString( CFStringRef rStr ) +{ + if( rStr == nullptr ) + { + return OUString(); + } + + CFIndex nLength = CFStringGetLength( rStr ); + if( nLength == 0 ) + { + return OUString(); + } + + const UniChar* pConstStr = CFStringGetCharactersPtr( rStr ); + if( pConstStr ) + { + return OUString( reinterpret_cast<sal_Unicode const *>(pConstStr), nLength ); + } + + std::unique_ptr<UniChar[]> pStr(new UniChar[nLength]); + CFRange aRange = { 0, nLength }; + CFStringGetCharacters( rStr, aRange, pStr.get() ); + + OUString aRet( reinterpret_cast<sal_Unicode *>(pStr.get()), nLength ); + return aRet; +} + +OUString GetOUString( const NSString* pStr ) +{ + if( ! pStr ) + { + return OUString(); + } + + int nLen = [pStr length]; + if( nLen == 0 ) + { + return OUString(); + } + + OUStringBuffer aBuf( nLen+1 ); + aBuf.setLength( nLen ); + [pStr getCharacters: + reinterpret_cast<unichar *>(const_cast<sal_Unicode*>(aBuf.getStr()))]; + + return aBuf.makeStringAndClear(); +} + +CFStringRef CreateCFString( const OUString& rStr ) +{ + return CFStringCreateWithCharacters(kCFAllocatorDefault, reinterpret_cast<UniChar const *>(rStr.getStr()), rStr.getLength() ); +} + +NSString* CreateNSString( const OUString& rStr ) +{ + return [[NSString alloc] initWithCharacters: reinterpret_cast<unichar const *>(rStr.getStr()) length: rStr.getLength()]; +} + +OUString NSStringArrayToOUString(NSArray* array) +{ + OUString result = "["; + OUString sep; + for (NSUInteger i = 0; i < [array count]; i++) + { + result = result + sep + OUString::fromUtf8([[array objectAtIndex:i] UTF8String]); + sep = ","; + } + result = result + "]"; + return result; +} + +OUString NSDictionaryKeysToOUString(NSDictionary* dict) +{ + OUString result = "{"; + OUString sep; + for (NSString *key in dict) + { + result = result + sep + OUString::fromUtf8([key UTF8String]); + sep = ","; + } + result = result + "}"; + return result; +} + +std::ostream &operator <<(std::ostream& s, const CGRect &rRect) +{ +#ifndef SAL_LOG_INFO + (void) rRect; +#else + if (CGRectIsNull(rRect)) + { + s << "NULL"; + } + else + { + s << rRect.size << "@" << rRect.origin; + } +#endif + return s; +} + +std::ostream &operator <<(std::ostream& s, const CGPoint &rPoint) +{ +#ifndef SAL_LOG_INFO + (void) rPoint; +#else + s << "(" << rPoint.x << "," << rPoint.y << ")"; +#endif + return s; +} + +std::ostream &operator <<(std::ostream& s, const CGSize &rSize) +{ +#ifndef SAL_LOG_INFO + (void) rSize; +#else + s << rSize.width << "x" << rSize.height; +#endif + return s; +} + +std::ostream &operator <<(std::ostream& s, CGColorRef pColor) +{ +#ifndef SAL_LOG_INFO + (void) pColor; +#else + CFStringRef colorString = CFCopyDescription(pColor); + if (colorString) + { + s << GetOUString(colorString); + CFRelease(colorString); + } + else + { + s << "NULL"; + } +#endif + return s; +} + +std::ostream &operator <<(std::ostream& s, const CGAffineTransform &aXform) +{ +#ifndef SAL_LOG_INFO + (void) aXform; +#else + if (CGAffineTransformIsIdentity(aXform)) + { + s << "IDENT"; + } + else + { + s << "[" << aXform.a << "," << aXform.b << "," << aXform.c << "," << aXform.d << "," << aXform.tx << "," << aXform.ty << "]"; + } +#endif + return s; +} + +std::ostream &operator <<(std::ostream& s, CGColorSpaceRef cs) +{ +#ifndef SAL_LOG_INFO + (void) cs; +#else + if (cs == nullptr) + { + s << "null"; + return s; + } + + CGColorSpaceModel model = CGColorSpaceGetModel(cs); + switch (model) + { + case kCGColorSpaceModelUnknown: + s << "Unknown"; + break; + case kCGColorSpaceModelMonochrome: + s << "Monochrome"; + break; + case kCGColorSpaceModelRGB: + s << "RGB"; + if (CGColorSpaceIsWideGamutRGB(cs)) + s << " (wide gamut)"; + break; + case kCGColorSpaceModelCMYK: + s << "CMYK"; + break; + case kCGColorSpaceModelLab: + s << "Lab"; + break; + case kCGColorSpaceModelDeviceN: + s << "DeviceN"; + break; + case kCGColorSpaceModelIndexed: + s << "Indexed (" << CGColorSpaceGetColorTableCount(cs) << ")"; + break; + case kCGColorSpaceModelPattern: + s << "Pattern"; + break; + case kCGColorSpaceModelXYZ: + s << "XYZ"; + break; + default: + s << "?(" << model << ")"; + break; + } + + CFStringRef name = CGColorSpaceCopyName(cs); + if (name != nullptr) + s << " (" << [static_cast<NSString *>(name) UTF8String] << ")"; +#endif + return s; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |