diff options
Diffstat (limited to '')
-rw-r--r-- | vcl/quartz/AquaGraphicsBackend.cxx | 1495 | ||||
-rw-r--r-- | vcl/quartz/ctfonts.cxx | 559 | ||||
-rw-r--r-- | vcl/quartz/salbmp.cxx | 751 | ||||
-rw-r--r-- | vcl/quartz/salgdi.cxx | 843 | ||||
-rw-r--r-- | vcl/quartz/salgdicommon.cxx | 287 | ||||
-rw-r--r-- | vcl/quartz/salvd.cxx | 176 | ||||
-rw-r--r-- | vcl/quartz/utils.cxx | 242 |
7 files changed, 4353 insertions, 0 deletions
diff --git a/vcl/quartz/AquaGraphicsBackend.cxx b/vcl/quartz/AquaGraphicsBackend.cxx new file mode 100644 index 000000000..822c1820c --- /dev/null +++ b/vcl/quartz/AquaGraphicsBackend.cxx @@ -0,0 +1,1495 @@ +/* -*- 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 <svdata.hxx> +#endif +#include <sft.hxx> + +#if HAVE_FEATURE_SKIA +#include <vcl/skia/SkiaHelper.hxx> +#include <skia/salbmp.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); +} + +#if HAVE_FEATURE_SKIA + +// Related: tdf#146842 Convert SkiaSalBitmap to QuartzSalBitmap +// Commit de3f13e2175564316eb5a62dee65e9ff8f31b460 disabled Skia for printing. +// However, since all SalBitmaps created are either all QuartzSalBitmaps or all +// SkiaSalBitmaps, a crash occurs whenever a SkiaSalBitmap is passed to a +// printer's SalGraphics instance which is now always non-Skia. +QuartzSalBitmap* checkAndConvertToQuartzSalBitmap(const SalTwoRect& rPosAry, + const SalBitmap& rSalBitmap, + SalTwoRect* pAdjustedSrcPosAry) +{ + QuartzSalBitmap* pRet = nullptr; + + if (SkiaHelper::isVCLSkiaEnabled() && dynamic_cast<const SkiaSalBitmap*>(&rSalBitmap)) + { + const SkiaSalBitmap& rSkiaBitmap = static_cast<const SkiaSalBitmap&>(rSalBitmap); + + SalTwoRect aSrcPosAry(rPosAry); + aSrcPosAry.mnDestX = 0; + aSrcPosAry.mnDestY = 0; + + pRet = new QuartzSalBitmap; + if (pRet) + { + // Ignore any failures as returning a nullptr will lead to a crash + pRet->Create(rSkiaBitmap, aSrcPosAry); + + if (pAdjustedSrcPosAry) + { + pAdjustedSrcPosAry->mnSrcX = 0; + pAdjustedSrcPosAry->mnSrcY = 0; + pAdjustedSrcPosAry->mnSrcWidth = aSrcPosAry.mnDestWidth; + pAdjustedSrcPosAry->mnSrcHeight = aSrcPosAry.mnDestHeight; + } + } + } + + return pRet; +} + +#endif +} + +AquaGraphicsBackend::AquaGraphicsBackend(AquaSharedAttributes& rShared) + : AquaGraphicsBackendBase(rShared) +{ +} + +AquaGraphicsBackend::~AquaGraphicsBackend() {} + +void AquaGraphicsBackend::Init() {} +void AquaGraphicsBackend::freeResources() {} + +bool 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(); + + return true; +} + +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.nWidth; + } + } +#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); +} + +bool AquaGraphicsBackend::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolyPolygon& rPolyPolygon, + double fTransparency) +{ +#ifdef IOS + if (!mrShared.maContextHolder.isSet()) + return true; +#endif + + // short circuit if there is nothing to do + if (rPolyPolygon.count() == 0) + return true; + + // ignore invisible polygons + if ((fTransparency >= 1.0) || (fTransparency < 0)) + return true; + + // 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 true; + } + + // 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); + + return true; +} + +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; + +#if HAVE_FEATURE_SKIA + if (mrShared.mbPrinter) + { + SAL_INFO("vcl.print", "Printing with Skia bitmaps: AquaGraphicsBackend::drawBitmap"); + SalTwoRect aDestPosAry(rPosAry); + std::unique_ptr<QuartzSalBitmap> pSalBitmap( + checkAndConvertToQuartzSalBitmap(rPosAry, rSalBitmap, &aDestPosAry)); + if (pSalBitmap) + { + drawBitmap(aDestPosAry, *pSalBitmap); + return; + } + } +#endif + + const QuartzSalBitmap& rBitmap = static_cast<const QuartzSalBitmap&>(rSalBitmap); + CGImageRef xImage = rBitmap.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; + +#if HAVE_FEATURE_SKIA + if (mrShared.mbPrinter) + { + SAL_INFO( + "vcl.print", + "Printing with Skia bitmaps: AquaGraphicsBackend::drawBitmapWithTranspraentBitmap"); + SalTwoRect aDestPosAry(rPosAry); + std::unique_ptr<QuartzSalBitmap> pSalBitmap( + checkAndConvertToQuartzSalBitmap(rPosAry, rSalBitmap, &aDestPosAry)); + if (pSalBitmap) + { + drawBitmap(aDestPosAry, *pSalBitmap, rTransparentBitmap); + return; + } + + pSalBitmap.reset( + checkAndConvertToQuartzSalBitmap(rPosAry, rTransparentBitmap, &aDestPosAry)); + if (pSalBitmap) + { + drawBitmap(aDestPosAry, rSalBitmap, *pSalBitmap); + return; + } + } +#endif + + const QuartzSalBitmap& rBitmap = static_cast<const QuartzSalBitmap&>(rSalBitmap); + const QuartzSalBitmap& rMask = static_cast<const QuartzSalBitmap&>(rTransparentBitmap); + + CGImageRef xMaskedImage(rBitmap.CreateWithMask(rMask, 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; + +#if HAVE_FEATURE_SKIA + if (mrShared.mbPrinter) + { + SAL_INFO("vcl.print", "Printing with Skia bitmaps: AquaGraphicsBackend::drawMask"); + SalTwoRect aDestPosAry(rPosAry); + std::unique_ptr<QuartzSalBitmap> pSalBitmap( + checkAndConvertToQuartzSalBitmap(rPosAry, rSalBitmap, &aDestPosAry)); + if (pSalBitmap) + { + drawMask(aDestPosAry, *pSalBitmap, nMaskColor); + return; + } + } +#endif + + const QuartzSalBitmap& rBitmap = static_cast<const QuartzSalBitmap&>(rSalBitmap); + CGImageRef xImage = rBitmap.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)]; + NSImageRep* xEpsImage = [NSEPSImageRep imageRepWithData:xNSData]; + 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; + +#if HAVE_FEATURE_SKIA + if (!mrShared.checkContext()) + return false; + + if (mrShared.mbPrinter) + { + SAL_INFO("vcl.print", "Printing with Skia bitmaps: AquaGraphicsBackend::drawAlphaBitmap"); + SalTwoRect aDestPosAry(rTR); + std::unique_ptr<QuartzSalBitmap> pSalBitmap( + checkAndConvertToQuartzSalBitmap(rTR, rSrcBitmap, &aDestPosAry)); + if (pSalBitmap) + return drawAlphaBitmap(aDestPosAry, *pSalBitmap, rAlphaBmp); + + pSalBitmap.reset(checkAndConvertToQuartzSalBitmap(rTR, rAlphaBmp, &aDestPosAry)); + if (pSalBitmap) + return drawAlphaBitmap(aDestPosAry, rSrcBitmap, *pSalBitmap); + } +#endif + + const QuartzSalBitmap& rSrcSalBmp = static_cast<const QuartzSalBitmap&>(rSrcBitmap); + const QuartzSalBitmap& rMaskSalBmp = static_cast<const QuartzSalBitmap&>(rAlphaBmp); + CGImageRef xMaskedImage = rSrcSalBmp.CreateWithMask(rMaskSalBmp, 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; + +#if HAVE_FEATURE_SKIA + if (mrShared.mbPrinter) + { + SAL_INFO("vcl.print", + "Printing with Skia bitmaps: AquaGraphicsBackend::drawTransformedBitmap"); + const Size aSize = rSrcBitmap.GetSize(); + SalTwoRect aTR(0, 0, aSize.Width(), aSize.Height(), 0, 0, aSize.Width(), aSize.Height()); + std::unique_ptr<QuartzSalBitmap> pSalBitmap( + checkAndConvertToQuartzSalBitmap(aTR, rSrcBitmap, nullptr)); + if (pSalBitmap) + return drawTransformedBitmap(rNull, rX, rY, *pSalBitmap, pAlphaBmp, fAlpha); + + pSalBitmap.reset(checkAndConvertToQuartzSalBitmap(aTR, *pAlphaBmp, nullptr)); + if (pSalBitmap) + return drawTransformedBitmap(rNull, rX, rY, rSrcBitmap, pSalBitmap.get(), fAlpha); + } +#endif + + // get the Quartz image + CGImageRef xImage = nullptr; + const Size aSize = rSrcBitmap.GetSize(); + const QuartzSalBitmap& rSrcSalBmp = static_cast<const QuartzSalBitmap&>(rSrcBitmap); + const QuartzSalBitmap* pMaskSalBmp = static_cast<const QuartzSalBitmap*>(pAlphaBmp); + + if (!pMaskSalBmp) + xImage = rSrcSalBmp.CreateCroppedImage(0, 0, int(aSize.Width()), int(aSize.Height())); + else + xImage = rSrcSalBmp.CreateWithMask(*pMaskSalBmp, 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: + case OutDevSupportType::B2DDraw: + return true; + default: + break; + } + return false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/ctfonts.cxx b/vcl/quartz/ctfonts.cxx new file mode 100644 index 000000000..7985905cc --- /dev/null +++ b/vcl/quartz/ctfonts.cxx @@ -0,0 +1,559 @@ +/* -*- 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> +#include <tools/long.hxx> +#include <vcl/settings.hxx> + + +#include <quartz/ctfonts.hxx> +#include <impfont.hxx> +#ifdef MACOSX +#include <osx/saldata.hxx> +#include <osx/salinst.h> +#endif +#include <fontinstance.hxx> +#include <fontattributes.hxx> +#include <impglyphitem.hxx> +#include <font/PhysicalFontCollection.hxx> +#include <quartz/salgdi.h> +#include <quartz/utils.h> +#include <sallayout.hxx> +#include <hb-coretext.h> + +CoreTextStyle::CoreTextStyle(const vcl::font::PhysicalFontFace& rPFF, const vcl::font::FontSelectPattern& rFSP) + : LogicalFontInstance(rPFF, rFSP) + , mfFontStretch( 1.0 ) + , mfFontRotation( 0.0 ) + , mbFauxBold(false) + , mpStyleDict( 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)); + } + + // create the style object for CoreText font attributes + static const CFIndex nMaxDictSize = 16; // TODO: does this really suffice? + mpStyleDict = CFDictionaryCreateMutable( nullptr, nMaxDictSize, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks ); + + CFBooleanRef pCFVertBool = rFSP.mbVertical ? kCFBooleanTrue : kCFBooleanFalse; + CFDictionarySetValue( mpStyleDict, kCTVerticalFormsAttributeName, pCFVertBool ); + + // fake bold + if ( (rFSP.GetWeight() >= WEIGHT_BOLD) && + ((rPFF.GetWeight() < WEIGHT_SEMIBOLD) && + (rPFF.GetWeight() != WEIGHT_DONTKNOW)) ) + { + mbFauxBold = true; + } + + // fake italic + if (((rFSP.GetItalic() == ITALIC_NORMAL) || + (rFSP.GetItalic() == ITALIC_OBLIQUE)) && + (rPFF.GetItalic() == ITALIC_NONE)) + { + aMatrix = CGAffineTransformConcat(aMatrix, CGAffineTransformMake(1, 0, basegfx::deg2rad(12), 1, 0, 0)); + } + + CTFontDescriptorRef pFontDesc = reinterpret_cast<CTFontDescriptorRef>(rPFF.GetFontId()); + CTFontRef pNewCTFont = CTFontCreateWithFontDescriptor( pFontDesc, fScaledFontHeight, &aMatrix ); + CFDictionarySetValue( mpStyleDict, kCTFontAttributeName, pNewCTFont ); + CFRelease( pNewCTFont); +} + +CoreTextStyle::~CoreTextStyle() +{ + if( mpStyleDict ) + CFRelease( mpStyleDict ); +} + +void CoreTextStyle::GetFontMetric( ImplFontMetricDataRef const & rxFontMetric ) +{ + // get the matching CoreText font handle + // TODO: is it worth it to cache the CTFontRef in SetFont() and reuse it here? + CTFontRef aCTFontRef = static_cast<CTFontRef>(CFDictionaryGetValue( mpStyleDict, kCTFontAttributeName )); + + rxFontMetric->ImplCalcLineSpacing(this); + rxFontMetric->ImplInitBaselines(this); + + // since ImplFontMetricData::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( aCTFontRef ) * mfFontStretch) ); + + rxFontMetric->SetMinKashida(GetKashidaWidth()); +} + +bool CoreTextStyle::ImplGetGlyphBoundRect(sal_GlyphId nId, tools::Rectangle& rRect, bool bVertical) const +{ + CGGlyph nCGGlyph = nId; + CTFontRef aCTFontRef = static_cast<CTFontRef>(CFDictionaryGetValue( mpStyleDict, kCTFontAttributeName )); + + SAL_WNODEPRECATED_DECLARATIONS_PUSH //TODO: 10.11 kCTFontDefaultOrientation + const CTFontOrientation aFontOrientation = kCTFontDefaultOrientation; // TODO: horz/vert + SAL_WNODEPRECATED_DECLARATIONS_POP + CGRect aCGRect = CTFontGetBoundingRectsForGlyphs(aCTFontRef, aFontOrientation, &nCGGlyph, nullptr, 1); + + // Apply font rotation to non-vertical glyphs. + if (mfFontRotation && !bVertical) + aCGRect = CGRectApplyAffineTransform(aCGRect, CGAffineTransformMakeRotation(mfFontRotation)); + + tools::Long xMin = floor(aCGRect.origin.x); + tools::Long yMin = floor(aCGRect.origin.y); + tools::Long xMax = ceil(aCGRect.origin.x + aCGRect.size.width); + tools::Long yMax = ceil(aCGRect.origin.y + aCGRect.size.height); + rRect = tools::Rectangle(xMin, -yMax, xMax, -yMin); + return true; +} + +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 CoreTextStyle::GetGlyphOutline(sal_GlyphId nId, basegfx::B2DPolyPolygon& rResult, bool) const +{ + rResult.clear(); + + CGGlyph nCGGlyph = nId; + CTFontRef pCTFont = static_cast<CTFontRef>(CFDictionaryGetValue( mpStyleDict, kCTFontAttributeName )); + + SAL_WNODEPRECATED_DECLARATIONS_PUSH + const CTFontOrientation aFontOrientation = kCTFontDefaultOrientation; + SAL_WNODEPRECATED_DECLARATIONS_POP + CGRect aCGRect = CTFontGetBoundingRectsForGlyphs(pCTFont, 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( pCTFont, 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; +} + +static hb_blob_t* getFontTable(hb_face_t* /*face*/, hb_tag_t nTableTag, void* pUserData) +{ + sal_uLong nLength = 0; + unsigned char* pBuffer = nullptr; + CoreTextFontFace* pFont = static_cast<CoreTextFontFace*>(pUserData); + nLength = pFont->GetFontTable(nTableTag, nullptr); + if (nLength > 0) + { + pBuffer = new unsigned char[nLength]; + pFont->GetFontTable(nTableTag, pBuffer); + } + + hb_blob_t* pBlob = nullptr; + if (pBuffer != nullptr) + pBlob = hb_blob_create(reinterpret_cast<const char*>(pBuffer), nLength, HB_MEMORY_MODE_READONLY, + pBuffer, [](void* data){ delete[] static_cast<unsigned char*>(data); }); + return pBlob; +} + +hb_font_t* CoreTextStyle::ImplInitHbFont() +{ + hb_face_t* pHbFace = hb_face_create_for_tables(getFontTable, GetFontFace(), nullptr); + + return InitHbFont(pHbFace); +} + +rtl::Reference<LogicalFontInstance> CoreTextFontFace::CreateFontInstance(const vcl::font::FontSelectPattern& rFSD) const +{ + return new CoreTextStyle(*this, rFSD); +} + +int CoreTextFontFace::GetFontTable( const char pTagName[5], unsigned char* pResultBuf ) const +{ + SAL_WARN_IF( pTagName[4]!='\0', "vcl", "CoreTextFontFace::GetFontTable with invalid tagname!" ); + + const CTFontTableTag nTagCode = (pTagName[0]<<24) + (pTagName[1]<<16) + (pTagName[2]<<8) + (pTagName[3]<<0); + + return GetFontTable(nTagCode, pResultBuf); +} + +int CoreTextFontFace::GetFontTable(uint32_t nTagCode, unsigned char* pResultBuf ) const +{ + // get the raw table length + CTFontDescriptorRef pFontDesc = reinterpret_cast<CTFontDescriptorRef>( GetFontId()); + CTFontRef rCTFont = CTFontCreateWithFontDescriptor( pFontDesc, 0.0, nullptr); + const uint32_t opts( kCTFontTableOptionNoOptions ); + CFDataRef pDataRef = CTFontCopyTable( rCTFont, nTagCode, opts); + CFRelease( rCTFont); + if( !pDataRef) + return 0; + + const CFIndex nByteLength = CFDataGetLength( pDataRef); + + // get the raw table data if requested + if( pResultBuf && (nByteLength > 0)) + { + const CFRange aFullRange = CFRangeMake( 0, nByteLength); + CFDataGetBytes( pDataRef, aFullRange, reinterpret_cast<UInt8*>(pResultBuf)); + } + + CFRelease( pDataRef); + + return static_cast<int>(nByteLength); +} + +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.SetSymbolFlag( 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 ); + rDFA.SetSymbolFlag( (nSymbolTrait & kCTFontClassMaskTrait) == kCTFontSymbolicClass ); + + if (nSymbolTrait & kCTFontMonoSpaceTrait) + rDFA.SetPitch(PITCH_FIXED); + } + + // get the font weight + double fWeight = 0; + CFNumberRef pWeightNum = static_cast<CFNumberRef>(CFDictionaryGetValue( pAttrDict, kCTFontWeightTrait )); + 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 )); + 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 )); + 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) + { + const sal_IntPtr nFontId = reinterpret_cast<sal_IntPtr>(pValue); + rtl::Reference<CoreTextFontFace> pFontData = new CoreTextFontFace( rDFA, nFontId ); + 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; +} + +SystemFontList* GetCoretextFontList() +{ + SystemFontList* pList = new SystemFontList(); + if( !pList->Init() ) + { + delete pList; + return nullptr; + } + + return pList; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/salbmp.cxx b/vcl/quartz/salbmp.cxx new file mode 100644 index 000000000..c8f7bd7f3 --- /dev/null +++ b/vcl/quartz/salbmp.cxx @@ -0,0 +1,751 @@ +/* -*- 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/salbmp.h> +#include <quartz/utils.h> +#include <bitmap/ScanlineTools.hxx> + +#ifdef MACOSX +#include <osx/saldata.hxx> +#else +#include <svdata.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; +} + +#if HAVE_FEATURE_SKIA + +bool QuartzSalBitmap::Create( const SkiaSalBitmap& rSalBmp, const SalTwoRect& rPosAry ) +{ + bool bRet = false; + + // Ugly but necessary to acquire the bitmap buffer because all of the + // SalBitmap instances that callers pass are already const. At least we + // only need to read, not write to the bitmap paramter. + SkiaSalBitmap& rSkiaSalBmp = const_cast<SkiaSalBitmap&>( rSalBmp ); + + BitmapBuffer *pSrcBuffer = rSkiaSalBmp.AcquireBuffer( BitmapAccessMode::Read ); + if ( !pSrcBuffer ) + return bRet; + + if ( !pSrcBuffer->mpBits ) + { + rSkiaSalBmp.ReleaseBuffer( pSrcBuffer, BitmapAccessMode::Read ); + return bRet; + } + + // Create only a 1 pixel buffer as it will always be discarded + mnBits = 32; + mnWidth = 1; + mnHeight = 1; + if( AllocateUserData() ) + { + BitmapBuffer *pDestBuffer = AcquireBuffer( BitmapAccessMode::Read ); + if ( pDestBuffer ) + { + std::unique_ptr<BitmapBuffer> pConvertedBuffer = StretchAndConvert( *pSrcBuffer, rPosAry, pDestBuffer->mnFormat, pDestBuffer->maPalette, &pDestBuffer->maColorMask ); + bool bUseDestBuffer = ( pConvertedBuffer && + pConvertedBuffer->mpBits && + pConvertedBuffer->mnFormat == pDestBuffer->mnFormat && + pConvertedBuffer->mnWidth == rPosAry.mnDestWidth && + pConvertedBuffer->mnHeight == rPosAry.mnDestHeight ); + + ReleaseBuffer( pDestBuffer, BitmapAccessMode::Read ); + + if ( bUseDestBuffer ) + { + // Surprisingly, BitmapBuffer does not delete the bits so + // discard our 1 pixel buffer and take ownership of the bits + DestroyContext(); + m_pUserBuffer.reset( pConvertedBuffer->mpBits ); + mnWidth = pConvertedBuffer->mnWidth; + mnHeight = pConvertedBuffer->mnHeight; + mnBytesPerRow = pConvertedBuffer->mnScanlineSize; + bRet = true; + } + } + } + + rSkiaSalBmp.ReleaseBuffer( pSrcBuffer, BitmapAccessMode::Read ); + + return bRet; +} + +#endif + +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 4: mnBytesPerRow = (mnWidth + 1) >> 1; 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 aDefPalette16; + static BitmapPalette aDefPalette2; + if( ! bDefPalInit ) + { + bDefPalInit = true; + aDefPalette256.SetEntryCount( 256 ); + aDefPalette16.SetEntryCount( 16 ); + aDefPalette2.SetEntryCount( 2 ); + + // Standard colors + unsigned int i; + for( i = 0; i < 16; i++ ) + { + aDefPalette16[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 4: return aDefPalette16; + 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 QuartzSalBitmap& rMask, + int nX, int nY, int nWidth, int nHeight ) const +{ + CGImageRef xImage( CreateCroppedImage( nX, nY, nWidth, nHeight ) ); + if( !xImage ) + return nullptr; + + CGImageRef xMask = rMask.CreateCroppedImage( nX, nY, nWidth, nHeight ); + if( !xMask ) + return xImage; + + // CGImageCreateWithMask() only likes masks or greyscale images => convert if needed + // TODO: isolate in an extra method? + if( !CGImageIsMask(xMask) || rMask.GetBitCount() != 8)//(CGImageGetColorSpace(xMask) != GetSalData()->mxGraySpace) ) + { + 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; +} + +/** 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)) + { + const sal_uInt32 nDestBytesPerRow = nWidth << 2; + std::unique_ptr<sal_uInt32[]> pMaskBuffer(new (std::nothrow) sal_uInt32[ nHeight * nDestBytesPerRow / 4] ); + sal_uInt32* pDest = pMaskBuffer.get(); + + auto pSourcePixels = vcl::bitmap::getScanlineTransformer(mnBits, maPalette); + + if( pMaskBuffer && pSourcePixels ) + { + 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(); + // 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-- ) + { + *pDest++ = (pSourcePixels->readPixel() == 0) ? nColor : 0; + } + 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 000000000..d4ecc85cc --- /dev/null +++ b/vcl/quartz/salgdi.cxx @@ -0,0 +1,843 @@ +/* -*- 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 <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 <quartz/ctfonts.hxx> +#include <fontsubset.hxx> +#include <impfont.hxx> +#include <impfontcharmap.hxx> +#include <impfontmetricdata.hxx> +#include <font/fontsubstitution.hxx> +#include <font/PhysicalFontCollection.hxx> + +#ifdef MACOSX +#include <osx/salframe.h> +#endif +#include <quartz/utils.h> +#ifdef IOS +#include <svdata.hxx> +#endif +#include <sallayout.hxx> +#include <sft.hxx> + +#include <config_features.h> +#include <vcl/skia/SkiaHelper.hxx> +#if HAVE_FEATURE_SKIA +#include <skia/osx/gdiimpl.hxx> +#endif + +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) +{ + CGGlyph glyphs[nLen]; + return CTFontGetGlyphsForCharacters(pFont, reinterpret_cast<const UniChar*>(rString.getStr() + nIndex), glyphs, nLen); +} + +} + +bool CoreTextGlyphFallbackSubstititution::FindFontSubstitute(vcl::font::FontSelectPattern& rPattern, LogicalFontInstance* pLogicalFont, + OUString& rMissingChars) const +{ + bool bFound = false; + CoreTextStyle* pStyle = static_cast<CoreTextStyle*>(pLogicalFont); + CTFontRef pFont = static_cast<CTFontRef>(CFDictionaryGetValue(pStyle->GetStyleDict(), kCTFontAttributeName)); + CFStringRef pStr = CreateCFString(rMissingChars); + if (pStr) + { + CTFontRef pFallback = CTFontCreateForString(pFont, 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(); + + rPattern.SetWeight(rAttr.GetWeight()); + rPattern.SetItalic(rAttr.GetItalic()); + rPattern.SetPitch(rAttr.GetPitch()); + rPattern.SetWidthType(rAttr.GetWidthType()); + + CFRelease(pFallback); + CFRelease(pDesc); + } + CFRelease(pStr); + } + + return bFound; +} + +CoreTextFontFace::CoreTextFontFace( const FontAttributes& rDFA, sal_IntPtr nFontId ) + : vcl::font::PhysicalFontFace( rDFA ) + , mnFontId( nFontId ) + , mbFontCapabilitiesRead( false ) +{ +} + +CoreTextFontFace::~CoreTextFontFace() +{ +} + +sal_IntPtr CoreTextFontFace::GetFontId() const +{ + return mnFontId; +} + +FontCharMapRef CoreTextFontFace::GetFontCharMap() const +{ + // return the cached charmap + if( mxCharMap.is() ) + return mxCharMap; + + // set the default charmap + FontCharMapRef pCharMap( new FontCharMap() ); + mxCharMap = pCharMap; + + // get the CMAP byte size + // allocate a buffer for the CMAP raw data + const int nBufSize = GetFontTable( "cmap", nullptr ); + SAL_WARN_IF( (nBufSize <= 0), "vcl", "CoreTextFontFace::GetFontCharMap : GetFontTable1 failed!"); + if( nBufSize <= 0 ) + return mxCharMap; + + // get the CMAP raw data + std::vector<unsigned char> aBuffer( nBufSize ); + const int nRawLength = GetFontTable( "cmap", aBuffer.data() ); + SAL_WARN_IF( (nRawLength <= 0), "vcl", "CoreTextFontFace::GetFontCharMap : GetFontTable2 failed!"); + if( nRawLength <= 0 ) + return mxCharMap; + + SAL_WARN_IF( (nBufSize!=nRawLength), "vcl", "CoreTextFontFace::GetFontCharMap : ByteCount mismatch!"); + + // parse the CMAP + CmapResult aCmapResult; + if( ParseCMAP( aBuffer.data(), nRawLength, aCmapResult ) ) + { + FontCharMapRef xDefFontCharMap( new FontCharMap(aCmapResult) ); + // create the matching charmap + mxCharMap = xDefFontCharMap; + } + + return mxCharMap; +} + +bool CoreTextFontFace::GetFontCapabilities(vcl::FontCapabilities &rFontCapabilities) const +{ + // read this only once per font + if( mbFontCapabilitiesRead ) + { + rFontCapabilities = maFontCapabilities; + return rFontCapabilities.oUnicodeRange || rFontCapabilities.oCodePageRange; + } + mbFontCapabilitiesRead = true; + + int nBufSize = GetFontTable( "OS/2", nullptr ); + if( nBufSize > 0 ) + { + // allocate a buffer for the OS/2 raw data + std::vector<unsigned char> aBuffer( nBufSize ); + // get the OS/2 raw data + const int nRawLength = GetFontTable( "OS/2", aBuffer.data() ); + if( nRawLength > 0 ) + { + const unsigned char* pOS2Table = aBuffer.data(); + vcl::getTTCoverage( maFontCapabilities.oUnicodeRange, + maFontCapabilities.oCodePageRange, + pOS2Table, nRawLength); + } + } + rFontCapabilities = maFontCapabilities; + return rFontCapabilities.oUnicodeRange || rFontCapabilities.oCodePageRange; +} + +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 + if(false) + ; +#endif + else + mpBackend.reset(new AquaGraphicsBackend(maShared)); + + for (int i = 0; i < MAX_FALLBACK; ++i) + mpTextStyle[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(ImplFontMetricDataRef& rxFontMetric, int nFallbackLevel) +{ + if (nFallbackLevel < MAX_FALLBACK && mpTextStyle[nFallbackLevel]) + { + mpTextStyle[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(); + + // The idea is to cache the list of system fonts once it has been generated. + // SalData seems to be a good place for this caching. However we have to + // carefully make the access to the font list thread-safe. If we register + // a font-change event handler to update the font list in case fonts have + // changed on the system we have to lock access to the list. The right + // way to do that is the solar mutex since GetDevFontList is protected + // through it as should be all event handlers + + SalData* pSalData = GetSalData(); + if( !pSalData->mpFontList ) + 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(); + delete pSalData->mpFontList; + pSalData->mpFontList = nullptr; +} + +bool AquaSalGraphics::AddTempDevFont(vcl::font::PhysicalFontCollection*, + const OUString& rFontFileURL, const OUString& /*rFontName*/) +{ + return ::AddTempDevFont(rFontFileURL); +} + +void AquaSalGraphics::DrawTextLayout(const GenericSalLayout& rLayout) +{ + mpBackend->drawTextLayout(rLayout, getTextRenderModeForResolutionIndependentLayoutEnabled()); +} + +void AquaGraphicsBackend::drawTextLayout(const GenericSalLayout& rLayout, bool bTextRenderModeForResolutionIndependentLayout) +{ +#ifdef IOS + if (!mrShared.checkContext()) + { + SAL_WARN("vcl.quartz", "AquaSalGraphics::DrawTextLayout() without context"); + return; + } +#endif + + const CoreTextStyle& rStyle = *static_cast<const CoreTextStyle*>(&rLayout.GetFont()); + const vcl::font::FontSelectPattern& rFontSelect = rStyle.GetFontSelectPattern(); + if (rFontSelect.mnHeight == 0) + { + SAL_WARN("vcl.quartz", "AquaSalGraphics::DrawTextLayout(): rFontSelect.mnHeight is zero!?"); + return; + } + + CTFontRef pFont = static_cast<CTFontRef>(CFDictionaryGetValue(rStyle.GetStyleDict(), kCTFontAttributeName)); + CGAffineTransform aRotMatrix = CGAffineTransformMakeRotation(-rStyle.mfFontRotation); + + DevicePoint 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 (rStyle.mfFontRotation) + { + if (pGlyph->IsVertical()) + { + bUprightGlyph = true; + // Adjust the position of upright (vertical) glyphs. + aGCPos.y -= CTFontGetAscent(pFont) - CTFontGetDescent(pFont); + } + 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 (rStyle.mbFauxBold) + { + + float fSize = rFontSelect.mnHeight / 23.0f; + CGContextSetStrokeColor(mrShared.maContextHolder.get(), textColor.AsArray()); + CGContextSetLineWidth(mrShared.maContextHolder.get(), fSize); + CGContextSetTextDrawingMode(mrShared.maContextHolder.get(), kCGTextFillStroke); + } + + if (bTextRenderModeForResolutionIndependentLayout) + { + 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 (rStyle.mfFontRotation && !bUprightGlyph) + { + CGContextRotateCTM(mrShared.maContextHolder.get(), rStyle.mfFontRotation); + } + CTFontDrawGlyphs(pFont, &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 text style + for (int i = nFallbackLevel; i < MAX_FALLBACK; ++i) + { + if (!mpTextStyle[i]) + break; + mpTextStyle[i].clear(); + } + + if (!pReqFont) + return; + + // update the text style + mpTextStyle[nFallbackLevel] = static_cast<CoreTextStyle*>(pReqFont); +} + +std::unique_ptr<GenericSalLayout> AquaSalGraphics::GetTextLayout(int nFallbackLevel) +{ + assert(mpTextStyle[nFallbackLevel]); + if (!mpTextStyle[nFallbackLevel]) + return nullptr; + return std::make_unique<GenericSalLayout>(*mpTextStyle[nFallbackLevel]); +} + +FontCharMapRef AquaSalGraphics::GetFontCharMap() const +{ + if (!mpTextStyle[0]) + { + return FontCharMapRef( new FontCharMap() ); + } + + return static_cast<const CoreTextFontFace*>(mpTextStyle[0]->GetFontFace())->GetFontCharMap(); +} + +bool AquaSalGraphics::GetFontCapabilities(vcl::FontCapabilities &rFontCapabilities) const +{ + if (!mpTextStyle[0]) + return false; + + return static_cast<const CoreTextFontFace*>(mpTextStyle[0]->GetFontFace())->GetFontCapabilities(rFontCapabilities); +} + +// fake a SFNT font directory entry for a font table +// see http://developer.apple.com/fonts/TTRefMan/RM06/Chap6.html#Directory +static void FakeDirEntry( const char aTag[5], ByteCount nOfs, ByteCount nLen, + const unsigned char* /*pData*/, unsigned char*& rpDest ) +{ + // write entry tag + rpDest[ 0] = aTag[0]; + rpDest[ 1] = aTag[1]; + rpDest[ 2] = aTag[2]; + rpDest[ 3] = aTag[3]; + // TODO: get entry checksum and write it + // not too important since the subsetter doesn't care currently + // for( pData+nOfs ... pData+nOfs+nLen ) + // write entry offset + rpDest[ 8] = static_cast<char>(nOfs >> 24); + rpDest[ 9] = static_cast<char>(nOfs >> 16); + rpDest[10] = static_cast<char>(nOfs >> 8); + rpDest[11] = static_cast<char>(nOfs >> 0); + // write entry length + rpDest[12] = static_cast<char>(nLen >> 24); + rpDest[13] = static_cast<char>(nLen >> 16); + rpDest[14] = static_cast<char>(nLen >> 8); + rpDest[15] = static_cast<char>(nLen >> 0); + // advance to next entry + rpDest += 16; +} + +// fake a TTF or CFF font as directly accessing font file is not possible +// when only the fontid is known. This approach also handles *.font fonts. +bool AquaSalGraphics::GetRawFontData( const vcl::font::PhysicalFontFace* pFontData, + std::vector<unsigned char>& rBuffer, bool* pJustCFF ) +{ + const CoreTextFontFace* pMacFont = static_cast<const CoreTextFontFace*>(pFontData); + + // short circuit for CFF-only fonts + const int nCffSize = pMacFont->GetFontTable( "CFF ", nullptr); + if( pJustCFF != nullptr ) + { + *pJustCFF = (nCffSize > 0); + if( *pJustCFF) + { + rBuffer.resize( nCffSize); + const int nCffRead = pMacFont->GetFontTable( "CFF ", rBuffer.data()); + if( nCffRead != nCffSize) + { + return false; + } + return true; + } + } + + // get font table availability and size in bytes + const int nHeadSize = pMacFont->GetFontTable( "head", nullptr); + if( nHeadSize <= 0) + return false; + + const int nMaxpSize = pMacFont->GetFontTable( "maxp", nullptr); + if( nMaxpSize <= 0) + return false; + + const int nCmapSize = pMacFont->GetFontTable( "cmap", nullptr); + if( nCmapSize <= 0) + return false; + + const int nNameSize = pMacFont->GetFontTable( "name", nullptr); + if( nNameSize <= 0) + return false; + + const int nHheaSize = pMacFont->GetFontTable( "hhea", nullptr); + if( nHheaSize <= 0) + return false; + + const int nHmtxSize = pMacFont->GetFontTable( "hmtx", nullptr); + if( nHmtxSize <= 0) + return false; + + // get the ttf-glyf outline tables + int nLocaSize = 0; + int nGlyfSize = 0; + if( nCffSize <= 0) + { + nLocaSize = pMacFont->GetFontTable( "loca", nullptr); + if( nLocaSize <= 0) + return false; + + nGlyfSize = pMacFont->GetFontTable( "glyf", nullptr); + if( nGlyfSize <= 0) + return false; + } + + int nPrepSize = 0, nCvtSize = 0, nFpgmSize = 0; + if( nGlyfSize) // TODO: reduce PDF size by making hint subsetting optional + { + nPrepSize = pMacFont->GetFontTable( "prep", nullptr); + nCvtSize = pMacFont->GetFontTable( "cvt ", nullptr); + nFpgmSize = pMacFont->GetFontTable( "fpgm", nullptr); + } + + // prepare a byte buffer for a fake font + int nTableCount = 7; + nTableCount += (nPrepSize>0?1:0) + (nCvtSize>0?1:0) + (nFpgmSize>0?1:0) + (nGlyfSize>0?1:0); + const ByteCount nFdirSize = 12 + 16*nTableCount; + ByteCount nTotalSize = nFdirSize; + nTotalSize += nHeadSize + nMaxpSize + nNameSize + nCmapSize; + + if( nGlyfSize ) + { + nTotalSize += nLocaSize + nGlyfSize; + } + else + { + nTotalSize += nCffSize; + } + nTotalSize += nHheaSize + nHmtxSize; + nTotalSize += nPrepSize + nCvtSize + nFpgmSize; + rBuffer.resize( nTotalSize ); + + // fake a SFNT font directory header + if( nTableCount < 16 ) + { + int nLog2 = 0; + while( (nTableCount >> nLog2) > 1 ) ++nLog2; + rBuffer[ 1] = 1; // Win-TTF style scaler + rBuffer[ 5] = nTableCount; // table count + rBuffer[ 7] = nLog2*16; // searchRange + rBuffer[ 9] = nLog2; // entrySelector + rBuffer[11] = (nTableCount-nLog2)*16; // rangeShift + } + + // get font table raw data and update the fake directory entries + ByteCount nOfs = nFdirSize; + unsigned char* pFakeEntry = &rBuffer[12]; + if( nCmapSize != pMacFont->GetFontTable( "cmap", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "cmap", nOfs, nCmapSize, rBuffer.data(), pFakeEntry ); + nOfs += nCmapSize; + if( nCvtSize ) + { + if( nCvtSize != pMacFont->GetFontTable( "cvt ", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "cvt ", nOfs, nCvtSize, rBuffer.data(), pFakeEntry ); + nOfs += nCvtSize; + } + if( nFpgmSize ) + { + if( nFpgmSize != pMacFont->GetFontTable( "fpgm", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "fpgm", nOfs, nFpgmSize, rBuffer.data(), pFakeEntry ); + nOfs += nFpgmSize; + } + if( nCffSize ) + { + if( nCffSize != pMacFont->GetFontTable( "CFF ", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "CFF ", nOfs, nCffSize, rBuffer.data(), pFakeEntry ); + nOfs += nGlyfSize; + } + else + { + if( nGlyfSize != pMacFont->GetFontTable( "glyf", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "glyf", nOfs, nGlyfSize, rBuffer.data(), pFakeEntry ); + nOfs += nGlyfSize; + + if( nLocaSize != pMacFont->GetFontTable( "loca", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "loca", nOfs, nLocaSize, rBuffer.data(), pFakeEntry ); + nOfs += nLocaSize; + } + if( nHeadSize != pMacFont->GetFontTable( "head", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "head", nOfs, nHeadSize, rBuffer.data(), pFakeEntry ); + nOfs += nHeadSize; + + if( nHheaSize != pMacFont->GetFontTable( "hhea", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "hhea", nOfs, nHheaSize, rBuffer.data(), pFakeEntry ); + nOfs += nHheaSize; + if( nHmtxSize != pMacFont->GetFontTable( "hmtx", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "hmtx", nOfs, nHmtxSize, rBuffer.data(), pFakeEntry ); + nOfs += nHmtxSize; + if( nMaxpSize != pMacFont->GetFontTable( "maxp", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "maxp", nOfs, nMaxpSize, rBuffer.data(), pFakeEntry ); + nOfs += nMaxpSize; + if( nNameSize != pMacFont->GetFontTable( "name", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "name", nOfs, nNameSize, rBuffer.data(), pFakeEntry ); + nOfs += nNameSize; + if( nPrepSize ) + { + if( nPrepSize != pMacFont->GetFontTable( "prep", &rBuffer[nOfs])) + return false; + + FakeDirEntry( "prep", nOfs, nPrepSize, rBuffer.data(), pFakeEntry ); + nOfs += nPrepSize; + } + + SAL_WARN_IF( (nOfs!=nTotalSize), "vcl", "AquaSalGraphics::GetRawFontData (nOfs!=nTotalSize)"); + + return true; +} + +void AquaSalGraphics::GetGlyphWidths( const vcl::font::PhysicalFontFace* pFontData, bool bVertical, + std::vector< sal_Int32 >& rGlyphWidths, Ucs2UIntMap& rUnicodeEnc ) +{ + rGlyphWidths.clear(); + rUnicodeEnc.clear(); + + std::vector<unsigned char> aBuffer; + if( !GetRawFontData( pFontData, aBuffer, nullptr ) ) + return; + + // TODO: modernize psprint's horrible fontsubset C-API + // this probably only makes sense after the switch to another SCM + // that can preserve change history after file renames + + // use the font subsetter to get the widths + TrueTypeFont* pSftFont = nullptr; + SFErrCodes nRC = ::OpenTTFontBuffer(static_cast<void*>(aBuffer.data()), aBuffer.size(), 0, &pSftFont, + pFontData->GetFontCharMap()); + if( nRC != SFErrCodes::Ok ) + return; + + SalGraphics::GetGlyphWidths(*pSftFont, *pFontData, bVertical, rGlyphWidths, rUnicodeEnc); + + ::CloseTTFont( pSftFont ); +} + +const void* AquaSalGraphics::GetEmbedFontData(const vcl::font::PhysicalFontFace*, tools::Long* /*pDataLen*/) +{ + return nullptr; +} + +void AquaSalGraphics::FreeEmbedFontData( const void* pData, tools::Long /*nDataLen*/ ) +{ + // TODO: implementing this only makes sense when the implementation of + // AquaSalGraphics::GetEmbedFontData() returns non-NULL + SAL_WARN_IF( (pData==nullptr), "vcl", "AquaSalGraphics::FreeEmbedFontData() is not implemented"); +} + +void AquaSalGraphics::Flush() +{ + mpBackend->Flush(); +} + +void AquaSalGraphics::Flush( const tools::Rectangle& rRect ) +{ + mpBackend->Flush( rRect ); +} + +#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 000000000..197f4b38f --- /dev/null +++ b/vcl/quartz/salgdicommon.cxx @@ -0,0 +1,287 @@ +/* -*- 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 <fontsubset.hxx> +#include <quartz/salbmp.h> +#ifdef MACOSX +#include <quartz/salgdi.h> +#endif +#include <quartz/utils.h> +#ifdef IOS +#include <svdata.hxx> +#endif +#include <sft.hxx> + +using namespace vcl; + +bool AquaSalGraphics::CreateFontSubset( const OUString& rToFile, + const vcl::font::PhysicalFontFace* pFontData, + const sal_GlyphId* pGlyphIds, const sal_uInt8* pEncoding, + sal_Int32* pGlyphWidths, const int nGlyphCount, + FontSubsetInfo& rInfo ) +{ + // TODO: move more of the functionality here into the generic subsetter code + + // prepare the requested file name for writing the font-subset file + OUString aSysPath; + if( osl_File_E_None != osl_getSystemPathFromFileURL( rToFile.pData, &aSysPath.pData ) ) + return false; + + // get the raw-bytes from the font to be subset + std::vector<unsigned char> aBuffer; + bool bCffOnly = false; + if( !GetRawFontData( pFontData, aBuffer, &bCffOnly ) ) + return false; + const OString aToFile( OUStringToOString( aSysPath, + osl_getThreadTextEncoding())); + + // handle CFF-subsetting + // NOTE: assuming that all glyphids requested on Aqua are fully translated + if (bCffOnly) + return SalGraphics::CreateCFFfontSubset(aBuffer.data(), aBuffer.size(), aToFile, pGlyphIds, + pEncoding, pGlyphWidths, nGlyphCount, rInfo); + + // TODO: modernize psprint's horrible fontsubset C-API + // this probably only makes sense after the switch to another SCM + // that can preserve change history after file renames + + // prepare data for psprint's font subsetter + TrueTypeFont* pSftFont = nullptr; + if (::OpenTTFontBuffer( static_cast<void*>(aBuffer.data()), aBuffer.size(), 0, &pSftFont) + != SFErrCodes::Ok) + return false; + + // get details about the subsetted font + TTGlobalFontInfo aTTInfo; + ::GetTTGlobalFontInfo( pSftFont, &aTTInfo ); + OUString aPSName(aTTInfo.psname, std::strlen(aTTInfo.psname), RTL_TEXTENCODING_UTF8); + FillFontSubsetInfo(aTTInfo, aPSName, rInfo); + + // write subset into destination file + bool bRet + = SalGraphics::CreateTTFfontSubset(*pSftFont, aToFile, false /* use FontSelectPattern? */, + pGlyphIds, pEncoding, pGlyphWidths, nGlyphCount); + ::CloseTTFont(pSftFont); + return bRet; +} + +#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 000000000..7cd82fbcb --- /dev/null +++ b/vcl/quartz/salvd.cxx @@ -0,0 +1,176 @@ +/* -*- 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 "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 000000000..b07e68f74 --- /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: */ |