diff options
Diffstat (limited to 'vcl/quartz')
-rw-r--r-- | vcl/quartz/ctfonts.cxx | 560 | ||||
-rw-r--r-- | vcl/quartz/salbmp.cxx | 963 | ||||
-rw-r--r-- | vcl/quartz/salgdi.cxx | 888 | ||||
-rw-r--r-- | vcl/quartz/salgdicommon.cxx | 2047 | ||||
-rw-r--r-- | vcl/quartz/salgdiutils.cxx | 265 | ||||
-rw-r--r-- | vcl/quartz/salvd.cxx | 303 | ||||
-rw-r--r-- | vcl/quartz/utils.cxx | 161 |
7 files changed, 5187 insertions, 0 deletions
diff --git a/vcl/quartz/ctfonts.cxx b/vcl/quartz/ctfonts.cxx new file mode 100644 index 000000000..eb67d981f --- /dev/null +++ b/vcl/quartz/ctfonts.cxx @@ -0,0 +1,560 @@ +/* -*- 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 <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 <PhysicalFontCollection.hxx> +#include <quartz/salgdi.h> +#include <quartz/utils.h> +#include <sallayout.hxx> +#include <hb-coretext.h> + +static double toRadian(int nDegree) +{ + return nDegree * (M_PI / 1800.0); +} + +CoreTextStyle::CoreTextStyle(const PhysicalFontFace& rPFF, const 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 = toRadian(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, toRadian(120), 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); + + // 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)); + + long xMin = floor(aCGRect.origin.x); + long yMin = floor(aCGRect.origin.y); + long xMax = ceil(aCGRect.origin.x + aCGRect.size.width); + 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 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 ); + } + + // 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( 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..8dd09d176 --- /dev/null +++ b/vcl/quartz/salbmp.cxx @@ -0,0 +1,963 @@ +/* -*- 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 <basegfx/vector/b2ivector.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 <bmpfast.hxx> +#include <quartz/salbmp.h> +#include <quartz/utils.h> + +#ifdef MACOSX +#include <osx/saldata.hxx> +#else +#include "saldatabasic.hxx" +#endif + +static const unsigned long k32BitRedColorMask = 0x00ff0000; +static const unsigned long k32BitGreenColorMask = 0x0000ff00; +static const unsigned long k32BitBlueColorMask = 0x000000ff; + +static bool isValidBitCount( sal_uInt16 nBitCount ) +{ + return (nBitCount == 1) || (nBitCount == 4) || (nBitCount == 8) || + (nBitCount == 24) || (nBitCount == 32); +} + +QuartzSalBitmap::QuartzSalBitmap() + : mxCachedImage( nullptr ) + , mnBits(0) + , mnWidth(0) + , mnHeight(0) + , mnBytesPerRow(0) +{ +} + +QuartzSalBitmap::~QuartzSalBitmap() +{ + doDestroy(); +} + +bool QuartzSalBitmap::Create(CGLayerHolder const & rLayerHolder, int nBitmapBits, int nX, int nY, int nWidth, int nHeight, bool bFlipped) +{ + SAL_WARN_IF(!rLayerHolder.isSet(), "vcl", "QuartzSalBitmap::Create() from non-layered context"); + + // sanitize input parameters + if( nX < 0 ) { + nWidth += nX; + nX = 0; + } + + if( nY < 0 ) { + nHeight += nY; + nY = 0; + } + + const CGSize aLayerSize = CGLayerGetSize(rLayerHolder.get()); + + if( nWidth >= static_cast<int>(aLayerSize.width) - nX ) + nWidth = static_cast<int>(aLayerSize.width) - nX; + + if( nHeight >= static_cast<int>(aLayerSize.height) - nY ) + nHeight = static_cast<int>(aLayerSize.height) - nY; + + if( (nWidth < 0) || (nHeight < 0) ) + nWidth = nHeight = 0; + + // initialize properties + mnWidth = nWidth; + mnHeight = nHeight; + mnBits = nBitmapBits ? nBitmapBits : 32; + + // initialize drawing context + CreateContext(); + + // copy layer content into the bitmap buffer + const CGPoint aSrcPoint = { static_cast<CGFloat>(-nX), static_cast<CGFloat>(-nY) }; + if (maGraphicContext.isSet()) // remove warning + { + if( bFlipped ) + { + CGContextTranslateCTM( maGraphicContext.get(), 0, +mnHeight ); + + CGContextScaleCTM( maGraphicContext.get(), +1, -1 ); + } + + CGContextDrawLayerAtPoint(maGraphicContext.get(), aSrcPoint, rLayerHolder.get()); + } + return true; +} + +bool QuartzSalBitmap::Create( const Size& rSize, sal_uInt16 nBits, const BitmapPalette& rBitmapPalette ) +{ + if( !isValidBitCount( nBits ) ) + return false; + + maPalette = rBitmapPalette; + mnBits = nBits; + mnWidth = rSize.Width(); + mnHeight = rSize.Height(); + return AllocateUserData(); +} + +bool QuartzSalBitmap::Create( const SalBitmap& rSalBmp ) +{ + return Create( rSalBmp, rSalBmp.GetBitCount() ); +} + +bool QuartzSalBitmap::Create( const SalBitmap& rSalBmp, SalGraphics* pGraphics ) +{ + return Create( rSalBmp, pGraphics ? pGraphics->GetBitCount() : rSalBmp.GetBitCount() ); +} + +bool QuartzSalBitmap::Create( const SalBitmap& rSalBmp, sal_uInt16 nNewBitCount ) +{ + const QuartzSalBitmap& rSourceBitmap = static_cast<const QuartzSalBitmap&>(rSalBmp); + + if (isValidBitCount(nNewBitCount) && rSourceBitmap.m_pUserBuffer.get()) + { + mnBits = nNewBitCount; + mnWidth = rSourceBitmap.mnWidth; + mnHeight = rSourceBitmap.mnHeight; + maPalette = rSourceBitmap.maPalette; + + if( AllocateUserData() ) + { + ConvertBitmapData( mnWidth, mnHeight, mnBits, mnBytesPerRow, maPalette, + m_pUserBuffer.get(), rSourceBitmap.mnBits, + rSourceBitmap.mnBytesPerRow, rSourceBitmap.maPalette, + rSourceBitmap.m_pUserBuffer.get() ); + return true; + } + } + return false; +} + +bool QuartzSalBitmap::Create( const css::uno::Reference< css::rendering::XBitmapCanvas >& /*xBitmapCanvas*/, + Size& /*rSize*/, bool /*bMask*/ ) +{ + return false; +} + +void QuartzSalBitmap::Destroy() +{ + doDestroy(); +} + +void QuartzSalBitmap::doDestroy() +{ + DestroyContext(); + m_pUserBuffer.reset(); +} + +void QuartzSalBitmap::DestroyContext() +{ + if( mxCachedImage ) + { + CGImageRelease( mxCachedImage ); + mxCachedImage = nullptr; + } + + if (maGraphicContext.isSet()) + { + CGContextRelease(maGraphicContext.get()); + maGraphicContext.set(nullptr); + m_pContextBuffer.reset(); + } +} + +bool QuartzSalBitmap::CreateContext() +{ + DestroyContext(); + + // prepare graphics context + // convert image from user input if available + const bool bSkipConversion = !m_pUserBuffer; + if( bSkipConversion ) + AllocateUserData(); + + // default to RGBA color space + CGColorSpaceRef aCGColorSpace = GetSalData()->mxRGBSpace; + CGBitmapInfo aCGBmpInfo = kCGImageAlphaNoneSkipFirst; + + // convert data into something accepted by CGBitmapContextCreate() + size_t bitsPerComponent = 8; + sal_uInt32 nContextBytesPerRow = mnBytesPerRow; + if( mnBits == 32 ) + { + // no conversion needed for truecolor + m_pContextBuffer = m_pUserBuffer; + } + else if( mnBits == 8 && maPalette.IsGreyPalette8Bit() ) + { + // no conversion needed for grayscale + m_pContextBuffer = m_pUserBuffer; + aCGColorSpace = GetSalData()->mxGraySpace; + aCGBmpInfo = kCGImageAlphaNone; + bitsPerComponent = mnBits; + } + // TODO: is special handling for 1bit input buffers worth it? + else + { + // convert user data to 32 bit + nContextBytesPerRow = mnWidth << 2; + try + { + m_pContextBuffer = o3tl::make_shared_array<sal_uInt8>(mnHeight * nContextBytesPerRow); + + if( !bSkipConversion ) + { + ConvertBitmapData( mnWidth, mnHeight, + 32, nContextBytesPerRow, maPalette, m_pContextBuffer.get(), + mnBits, mnBytesPerRow, maPalette, m_pUserBuffer.get() ); + } + } + catch( const std::bad_alloc& ) + { + maGraphicContext.set(nullptr); + } + } + + if (m_pContextBuffer.get()) + { + 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 m_pUserBuffer.get() != nullptr; +} + +namespace { + +class ImplPixelFormat +{ +public: + static std::unique_ptr<ImplPixelFormat> GetFormat( sal_uInt16 nBits, const BitmapPalette& rPalette ); + + virtual void StartLine( sal_uInt8* pLine ) = 0; + virtual void SkipPixel( sal_uInt32 nPixel ) = 0; + virtual Color ReadPixel() = 0; + virtual void WritePixel( Color nColor ) = 0; + virtual ~ImplPixelFormat() { } +}; + +class ImplPixelFormat32 : public ImplPixelFormat +// currently ARGB-format for 32bit depth +{ + sal_uInt8* pData; +public: + virtual void StartLine( sal_uInt8* pLine ) override { pData = pLine; } + virtual void SkipPixel( sal_uInt32 nPixel ) override + { + pData += nPixel << 2; + } + virtual Color ReadPixel() override + { + const Color c( pData[1], pData[2], pData[3] ); + pData += 4; + return c; + } + virtual void WritePixel( Color nColor ) override + { + *pData++ = 0; + *pData++ = nColor.GetRed(); + *pData++ = nColor.GetGreen(); + *pData++ = nColor.GetBlue(); + } +}; + +class ImplPixelFormat24 : public ImplPixelFormat +// currently BGR-format for 24bit depth +{ + sal_uInt8* pData; +public: + virtual void StartLine( sal_uInt8* pLine ) override { pData = pLine; } + virtual void SkipPixel( sal_uInt32 nPixel ) override + { + pData += (nPixel << 1) + nPixel; + } + virtual Color ReadPixel() override + { + const Color c( pData[2], pData[1], pData[0] ); + pData += 3; + return c; + } + virtual void WritePixel( Color nColor ) override + { + *pData++ = nColor.GetBlue(); + *pData++ = nColor.GetGreen(); + *pData++ = nColor.GetRed(); + } +}; + +class ImplPixelFormat8 : public ImplPixelFormat +{ +private: + sal_uInt8* pData; + const BitmapPalette& mrPalette; + const sal_uInt16 mnPaletteCount; + +public: + explicit ImplPixelFormat8( const BitmapPalette& rPalette ) + : pData(nullptr) + , mrPalette(rPalette) + , mnPaletteCount(rPalette.GetEntryCount()) + { + } + virtual void StartLine( sal_uInt8* pLine ) override { pData = pLine; } + virtual void SkipPixel( sal_uInt32 nPixel ) override + { + pData += nPixel; + } + virtual Color ReadPixel() override + { + const sal_uInt8 nIndex(*pData++); + + // Caution(!) rPalette.GetEntryCount() may be != (depth^^2)-1 (!) + if(nIndex < mnPaletteCount) + return mrPalette[nIndex]; + else + return COL_BLACK; + } + virtual void WritePixel( Color nColor ) override + { + *pData++ = static_cast< sal_uInt8 >( mrPalette.GetBestIndex( nColor ) ); + } +}; + +class ImplPixelFormat4 : public ImplPixelFormat +{ +private: + sal_uInt8* pData; + const BitmapPalette& mrPalette; + const sal_uInt16 mnPaletteCount; + sal_uInt32 mnX; + sal_uInt32 mnShift; + +public: + explicit ImplPixelFormat4( const BitmapPalette& rPalette ) + : pData(nullptr) + , mrPalette(rPalette) + , mnPaletteCount(rPalette.GetEntryCount()) + , mnX(0) + , mnShift(0) + { + } + virtual void SkipPixel( sal_uInt32 nPixel ) override + { + mnX += nPixel; + if( nPixel & 1 ) + { + mnShift ^= 4; + } + } + virtual void StartLine( sal_uInt8* pLine ) override + { + pData = pLine; + mnX = 0; + mnShift = 4; + } + virtual Color ReadPixel() override + { + // Caution(!) rPalette.GetEntryCount() may be != (depth^^2)-1 (!) + const sal_uInt8 nIndex(( pData[mnX >> 1] >> mnShift) & 0x0f); + mnX++; + mnShift ^= 4; + + if(nIndex < mnPaletteCount) + return mrPalette[nIndex]; + else + return COL_BLACK; + } + virtual void WritePixel( Color nColor ) override + { + pData[mnX>>1] &= (0xf0 >> mnShift); + pData[mnX>>1] |= (static_cast< sal_uInt8 >( mrPalette.GetBestIndex( nColor ) ) & 0x0f); + mnX++; + mnShift ^= 4; + } +}; + +class ImplPixelFormat1 : public ImplPixelFormat +{ +private: + sal_uInt8* pData; + const BitmapPalette& mrPalette; + const sal_uInt16 mnPaletteCount; + sal_uInt32 mnX; + +public: + explicit ImplPixelFormat1( const BitmapPalette& rPalette ) + : pData(nullptr) + , mrPalette(rPalette) + , mnPaletteCount(rPalette.GetEntryCount()) + , mnX(0) + { + } + virtual void SkipPixel( sal_uInt32 nPixel ) override + { + mnX += nPixel; + } + virtual void StartLine( sal_uInt8* pLine ) override + { + pData = pLine; + mnX = 0; + } + virtual Color ReadPixel() override + { + // Caution(!) rPalette.GetEntryCount() may be != (depth^^2)-1 (!) + const sal_uInt8 nIndex( (pData[mnX >> 3 ] >> ( 7 - ( mnX & 7 ) )) & 1); + mnX++; + + if(nIndex < mnPaletteCount) + return mrPalette[nIndex]; + else + return COL_BLACK; + } + virtual void WritePixel( Color nColor ) override + { + if( mrPalette.GetBestIndex( nColor ) & 1 ) + { + pData[ mnX >> 3 ] |= 1 << ( 7 - ( mnX & 7 ) ); + } + else + { + pData[ mnX >> 3 ] &= ~( 1 << ( 7 - ( mnX & 7 ) ) ); + } + mnX++; + } +}; + +std::unique_ptr<ImplPixelFormat> ImplPixelFormat::GetFormat( sal_uInt16 nBits, const BitmapPalette& rPalette ) +{ + switch( nBits ) + { + case 1: return std::make_unique<ImplPixelFormat1>( rPalette ); + case 4: return std::make_unique<ImplPixelFormat4>( rPalette ); + case 8: return std::make_unique<ImplPixelFormat8>( rPalette ); + case 24: return std::make_unique<ImplPixelFormat24>(); + case 32: return std::make_unique<ImplPixelFormat32>(); + default: + assert(false); + return nullptr; + } + + return nullptr; +} + +} // namespace + +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 + + std::unique_ptr<ImplPixelFormat> pD = ImplPixelFormat::GetFormat( nDestBits, rDestPalette ); + std::unique_ptr<ImplPixelFormat> pS = ImplPixelFormat::GetFormat( nSrcBits, rSrcPalette ); + + if( pD && pS ) + { + sal_uInt32 nY = nHeight; + while( nY-- ) + { + pD->StartLine( pDestData ); + pS->StartLine( pSrcData ); + + sal_uInt32 nX = nWidth; + while( nX-- ) + { + pD->WritePixel( pS->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; +}; + +} + +static 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.get()) + 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 4: + pBuffer->mnFormat = ScanlineFormat::N4BitMsnPal; + 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.get() && (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(); + + std::unique_ptr<ImplPixelFormat> pSourcePixels = ImplPixelFormat::GetFormat( 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 maGraphicContextNew(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 + maGraphicContextNew.saveState(); + CGContextTranslateCTM (maGraphicContextNew.get(), 0, CGBitmapContextGetHeight(maGraphicContextNew.get())); + CGContextScaleCTM (maGraphicContextNew.get(), 1.0, -1.0); + + CGContextDrawImage(maGraphicContextNew.get(), CGRectMake( 0, 0, CGImageGetWidth(xImage), CGImageGetHeight(xImage)), xImage); + + // Flip back + CGContextRestoreGState( maGraphicContextNew.get() ); + CGImageRelease( xImage ); + maGraphicContext = maGraphicContextNew; + } + + 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..83aebe2ab --- /dev/null +++ b/vcl/quartz/salgdi.cxx @@ -0,0 +1,888 @@ +/* -*- 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 <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 <outdev.h> +#include <PhysicalFontCollection.hxx> + +#ifdef MACOSX +#include <osx/salframe.h> +#endif +#include <quartz/utils.h> +#ifdef IOS +#include "saldatabasic.hxx" +#endif +#include <sallayout.hxx> +#include <sft.hxx> + +using namespace vcl; + +namespace { + +class CoreTextGlyphFallbackSubstititution +: public ImplGlyphFallbackFontSubstitution +{ +public: + bool FindFontSubstitute(FontSelectPattern&, LogicalFontInstance* pLogicalFont, OUString&) const override; +}; + +} + +bool CoreTextGlyphFallbackSubstititution::FindFontSubstitute(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; + + 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 ) + : 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() + : mpXorEmulation( nullptr ) + , mnXorMode( 0 ) + , mnWidth( 0 ) + , mnHeight( 0 ) + , mnBitmapDepth( 0 ) + , mnRealDPIX( 0 ) + , mnRealDPIY( 0 ) + , mxClipPath( nullptr ) + , maLineColor( COL_WHITE ) + , maFillColor( COL_BLACK ) + , maTextColor( COL_BLACK ) + , mbNonAntialiasedText( false ) +#ifdef MACOSX + , mpFrame( nullptr ) +#endif + , mbPrinter( false ) + , mbVirDev( false ) +#ifdef MACOSX + , mbWindow( false ) +#else + , mbForeignContext( false ) +#endif +{ + SAL_INFO( "vcl.quartz", "AquaSalGraphics::AquaSalGraphics() this=" << this ); + + 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 ); + + if( mxClipPath ) + { + CGPathRelease( mxClipPath ); + } + + ReleaseFonts(); + + if( mpXorEmulation ) + delete mpXorEmulation; + +#ifdef IOS + if (mbForeignContext) + return; +#endif + if (maLayer.isSet()) + { + CGLayerRelease(maLayer.get()); + } + else if (maContextHolder.isSet() +#ifdef MACOSX + && mbWindow +#endif + ) + { + // destroy backbuffer bitmap context that we created ourself + CGContextRelease(maContextHolder.get()); + maContextHolder.set(nullptr); + } +} + +SalGraphicsImpl* AquaSalGraphics::GetImpl() const +{ + return nullptr; +} + +void AquaSalGraphics::SetTextColor( Color nColor ) +{ + maTextColor = RGBAColor( nColor ); + // SAL_ DEBUG(std::hex << nColor << std::dec << "={" << maTextColor.GetRed() << ", " << maTextColor.GetGreen() << ", " << maTextColor.GetBlue() << ", " << maTextColor.GetAlpha() << "}"); +} + +void AquaSalGraphics::GetFontMetric(ImplFontMetricDataRef& rxFontMetric, int nFallbackLevel) +{ + if (nFallbackLevel < MAX_FALLBACK && mpTextStyle[nFallbackLevel]) + { + mpTextStyle[nFallbackLevel]->GetFontMetric(rxFontMetric); + } +} + +static bool AddTempDevFont(const OUString& rFontFileURL) +{ + OUString aUSytemPath; + OSL_VERIFY( !osl::FileBase::getSystemPathFromFileURL( rFontFileURL, aUSytemPath ) ); + OString aCFileName = OUStringToOString( aUSytemPath, 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( 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( PhysicalFontCollection*, + const OUString& rFontFileURL, const OUString& /*rFontName*/ ) +{ + return ::AddTempDevFont(rFontFileURL); +} + +void AquaSalGraphics::DrawTextLayout(const GenericSalLayout& rLayout) +{ +#ifdef IOS + if (!CheckContext()) + { + SAL_WARN("vcl.quartz", "AquaSalGraphics::DrawTextLayout() without context"); + return; + } +#endif + + const CoreTextStyle& rStyle = *static_cast<const CoreTextStyle*>(&rLayout.GetFont()); + const 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); + + Point 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.X(), -aPos.Y()); + + // 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 + + maContextHolder.saveState(); + + // The view is vertically flipped (no idea why), flip it back. + CGContextScaleCTM(maContextHolder.get(), 1.0, -1.0); + CGContextSetShouldAntialias(maContextHolder.get(), !mbNonAntialiasedText); + CGContextSetFillColor(maContextHolder.get(), maTextColor.AsArray()); + + if (rStyle.mbFauxBold) + { + + float fSize = rFontSelect.mnHeight / 23.0f; + CGContextSetStrokeColor(maContextHolder.get(), maTextColor.AsArray()); + CGContextSetLineWidth(maContextHolder.get(), fSize); + CGContextSetTextDrawingMode(maContextHolder.get(), kCGTextFillStroke); + } + + 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); + + maContextHolder.saveState(); + if (rStyle.mfFontRotation && !bUprightGlyph) + { + CGContextRotateCTM(maContextHolder.get(), rStyle.mfFontRotation); + } + CTFontDrawGlyphs(pFont, &aGlyphIds[nStartIndex], &aGlyphPos[nStartIndex], nLen, maContextHolder.get()); + maContextHolder.restoreState(); + + aIt = aNext; + } + + 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 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 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); + if( nRC != SFErrCodes::Ok ) + return; + + const int nGlyphCount = ::GetTTGlyphCount( pSftFont ); + if( nGlyphCount > 0 ) + { + // get glyph metrics + rGlyphWidths.resize(nGlyphCount); + std::vector<sal_uInt16> aGlyphIds(nGlyphCount); + for( int i = 0; i < nGlyphCount; i++ ) + { + aGlyphIds[i] = static_cast<sal_uInt16>(i); + } + + std::unique_ptr<sal_uInt16[]> pGlyphMetrics = ::GetTTSimpleGlyphMetrics( pSftFont, aGlyphIds.data(), + nGlyphCount, bVertical ); + if( pGlyphMetrics ) + { + for( int i = 0; i < nGlyphCount; ++i ) + { + rGlyphWidths[i] = pGlyphMetrics[i]; + } + pGlyphMetrics.reset(); + } + + rtl::Reference<CoreTextFontFace> rCTFontData(new CoreTextFontFace(*pFontData, pFontData->GetFontId())); + FontCharMapRef xFCMap = rCTFontData->GetFontCharMap(); + SAL_WARN_IF( !xFCMap.is() || !xFCMap->GetCharCount(), "vcl", "no charmap" ); + + // get unicode<->glyph encoding + // TODO? avoid sft mapping by using the xFCMap itself + int nCharCount = xFCMap->GetCharCount(); + sal_uInt32 nChar = xFCMap->GetFirstChar(); + for( ; --nCharCount >= 0; nChar = xFCMap->GetNextChar( nChar ) ) + { + if( nChar > 0xFFFF ) // TODO: allow UTF-32 chars + break; + + sal_Ucs nUcsChar = static_cast<sal_Ucs>(nChar); + sal_uInt32 nGlyph = ::MapChar( pSftFont, nUcsChar ); + if( nGlyph > 0 ) + { + rUnicodeEnc[ nUcsChar ] = nGlyph; + } + } + + xFCMap = nullptr; + } + + ::CloseTTFont( pSftFont ); +} + +const void* AquaSalGraphics::GetEmbedFontData(const PhysicalFontFace*, long* /*pDataLen*/) +{ + return nullptr; +} + +void AquaSalGraphics::FreeEmbedFontData( const void* pData, 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"); +} + +bool AquaSalGraphics::IsFlipped() const +{ +#ifdef MACOSX + return mbWindow; +#else + return false; +#endif +} + +void AquaSalGraphics::RefreshRect(float lX, float lY, float lWidth, float lHeight) +{ +#ifdef MACOSX + if( ! mbWindow ) // view only on Window graphics + return; + + if( mpFrame ) + { + // update a little more around the designated rectangle + // this helps with antialiased rendering + // Rounding down x and width can accumulate a rounding error of up to 2 + // The decrementing of x, the rounding error and the antialiasing border + // require that the width and the height need to be increased by four + const tools::Rectangle aVclRect(Point(static_cast<long int>(lX-1), + static_cast<long int>(lY-1) ), + Size( static_cast<long int>(lWidth+4), + static_cast<long int>(lHeight+4) ) ); + mpFrame->maInvalidRect.Union( aVclRect ); + } +#else + (void) lX; + (void) lY; + (void) lWidth; + (void) lHeight; + return; +#endif +} + +#ifdef IOS + +bool AquaSalGraphics::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; +} + +CGContextRef AquaSalGraphics::GetContext() +{ + if (!maContextHolder.isSet()) + CheckContext(); + + return maContextHolder.get(); +} + +#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..8b51e91c6 --- /dev/null +++ b/vcl/quartz/salgdicommon.cxx @@ -0,0 +1,2047 @@ +/* -*- 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 <vcl/sysdata.hxx> + +#include <fontsubset.hxx> +#include <quartz/salbmp.h> +#ifdef MACOSX +#include <quartz/salgdi.h> +#endif +#include <quartz/utils.h> +#ifdef IOS +#include "saldatabasic.hxx" +#endif +#include <sft.hxx> + +using namespace vcl; + +static const basegfx::B2DPoint aHalfPointOfs ( 0.5, 0.5 ); + +static 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 ); + } +} + +static void AddPolyPolygonToPath( CGMutablePathRef xPath, + const basegfx::B2DPolyPolygon& rPolyPoly, + bool bPixelSnap, bool bLineDraw ) +{ + // short circuit if there is nothing to do + if( rPolyPoly.count() == 0 ) + { + return; + } + for(auto const& rPolygon : rPolyPoly) + { + AddPolygonToPath( xPath, rPolygon, true, bPixelSnap, bLineDraw ); + } +} + +bool AquaSalGraphics::CreateFontSubset( const OUString& rToFile, + const PhysicalFontFace* pFontData, + const sal_GlyphId* pGlyphIds, const sal_uInt8* pEncoding, + sal_Int32* pGlyphWidths, 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 + if( bCffOnly ) + { + // provide the raw-CFF data to the subsetter + ByteCount nCffLen = aBuffer.size(); + rInfo.LoadFont( FontType::CFF_FONT, aBuffer.data(), nCffLen ); + + // NOTE: assuming that all glyphids requested on Aqua are fully translated + + // make the subsetter provide the requested subset + FILE* pOutFile = fopen( aToFile.getStr(), "wb" ); + bool bRC = rInfo.CreateFontSubset( FontType::TYPE1_PFB, pOutFile, nullptr, + pGlyphIds, pEncoding, nGlyphCount, pGlyphWidths ); + fclose( pOutFile ); + return bRC; + } + + // 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; + SFErrCodes nRC = ::OpenTTFontBuffer( static_cast<void*>(aBuffer.data()), aBuffer.size(), 0, &pSftFont); + if( nRC != SFErrCodes::Ok ) + { + return false; + } + // get details about the subsetted font + TTGlobalFontInfo aTTInfo; + ::GetTTGlobalFontInfo( pSftFont, &aTTInfo ); + rInfo.m_nFontType = FontType::SFNT_TTF; + rInfo.m_aPSName = OUString( aTTInfo.psname, std::strlen(aTTInfo.psname), + RTL_TEXTENCODING_UTF8 ); + rInfo.m_aFontBBox = tools::Rectangle( Point( aTTInfo.xMin, aTTInfo.yMin ), + Point( aTTInfo.xMax, aTTInfo.yMax ) ); + rInfo.m_nCapHeight = aTTInfo.yMax; // Well ... + rInfo.m_nAscent = aTTInfo.winAscent; + rInfo.m_nDescent = aTTInfo.winDescent; + // mac fonts usually do not have an OS2-table + // => get valid ascent/descent values from other tables + if( !rInfo.m_nAscent ) + { + rInfo.m_nAscent = +aTTInfo.typoAscender; + } + if( !rInfo.m_nAscent ) + { + rInfo.m_nAscent = +aTTInfo.ascender; + } + if( !rInfo.m_nDescent ) + { + rInfo.m_nDescent = +aTTInfo.typoDescender; + } + if( !rInfo.m_nDescent ) + { + rInfo.m_nDescent = -aTTInfo.descender; + } + + // subset glyphs and get their properties + // take care that subset fonts require the NotDef glyph in pos 0 + int nOrigCount = nGlyphCount; + sal_uInt16 aShortIDs[ 257 ]; + sal_uInt8 aTempEncs[ 257 ]; + int nNotDef = -1; + + assert( (nGlyphCount <= 256 && "too many glyphs for subsetting" )); + + for( int i = 0; i < nGlyphCount; ++i ) + { + aTempEncs[i] = pEncoding[i]; + + sal_GlyphId aGlyphId(pGlyphIds[i]); + aShortIDs[i] = static_cast<sal_uInt16>( aGlyphId ); + if( !aGlyphId && nNotDef < 0 ) + { + nNotDef = i; // first NotDef glyph found + } + } + + if( nNotDef != 0 ) + { + // add fake NotDef glyph if needed + if( nNotDef < 0 ) + { + nNotDef = nGlyphCount++; + } + // NotDef glyph must be in pos 0 => swap glyphids + aShortIDs[ nNotDef ] = aShortIDs[0]; + aTempEncs[ nNotDef ] = aTempEncs[0]; + aShortIDs[0] = 0; + aTempEncs[0] = 0; + } + + // TODO: where to get bVertical? + const bool bVertical = false; + + // fill the pGlyphWidths array + // while making sure that the NotDef glyph is at index==0 + std::unique_ptr<sal_uInt16[]> pGlyphMetrics = ::GetTTSimpleGlyphMetrics( pSftFont, aShortIDs, + nGlyphCount, bVertical ); + if( !pGlyphMetrics ) + { + return false; + } + + sal_uInt16 nNotDefAdv = pGlyphMetrics[0]; + pGlyphMetrics[0] = pGlyphMetrics[nNotDef]; + pGlyphMetrics[nNotDef] = nNotDefAdv; + for( int i = 0; i < nOrigCount; ++i ) + { + pGlyphWidths[i] = pGlyphMetrics[i]; + } + pGlyphMetrics.reset(); + + // write subset into destination file + nRC = ::CreateTTFromTTGlyphs( pSftFont, aToFile.getStr(), aShortIDs, + aTempEncs, nGlyphCount ); + ::CloseTTFont(pSftFont); + return (nRC == SFErrCodes::Ok); +} + +static void alignLinePoint( const SalPoint* i_pIn, float& o_fX, float& o_fY ) +{ + o_fX = static_cast<float>(i_pIn->mnX ) + 0.5; + o_fY = static_cast<float>(i_pIn->mnY ) + 0.5; +} + +void AquaSalGraphics::copyBits( const SalTwoRect& rPosAry, SalGraphics *pSrcGraphics ) +{ + + if( !pSrcGraphics ) + { + pSrcGraphics = this; + } + //from unix salgdi2.cxx + //[FIXME] find a better way to prevent calc from crashing when width and height are negative + if( rPosAry.mnSrcWidth <= 0 || + rPosAry.mnSrcHeight <= 0 || + rPosAry.mnDestWidth <= 0 || + rPosAry.mnDestHeight <= 0 ) + { + return; + } + +#ifdef IOS + // If called from idle layout, maContextHolder.get() is NULL, no idea what to do + if (!maContextHolder.isSet()) + return; +#endif + + // accelerate trivial operations + /*const*/ AquaSalGraphics* pSrc = static_cast<AquaSalGraphics*>(pSrcGraphics); + const bool bSameGraphics = (this == pSrc) +#ifdef MACOSX + || (mbWindow && mpFrame && pSrc->mbWindow && (mpFrame == pSrc->mpFrame)) +#endif + ; + + if( bSameGraphics && + (rPosAry.mnSrcWidth == rPosAry.mnDestWidth) && + (rPosAry.mnSrcHeight == rPosAry.mnDestHeight)) + { + // short circuit if there is nothing to do + if( (rPosAry.mnSrcX == rPosAry.mnDestX) && + (rPosAry.mnSrcY == rPosAry.mnDestY)) + { + return; + } + // use copyArea() if source and destination context are identical + copyArea( rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnSrcX, rPosAry.mnSrcY, + rPosAry.mnSrcWidth, rPosAry.mnSrcHeight, false/*bWindowInvalidate*/ ); + return; + } + + ApplyXorContext(); + pSrc->ApplyXorContext(); + + SAL_WARN_IF (!pSrc->maLayer.isSet(), "vcl.quartz", + "AquaSalGraphics::copyBits() from non-layered graphics this=" << this); + + const CGPoint aDstPoint = CGPointMake(+rPosAry.mnDestX - rPosAry.mnSrcX, rPosAry.mnDestY - rPosAry.mnSrcY); + if ((rPosAry.mnSrcWidth == rPosAry.mnDestWidth && + rPosAry.mnSrcHeight == rPosAry.mnDestHeight) && + (!mnBitmapDepth || (aDstPoint.x + pSrc->mnWidth) <= mnWidth) + && pSrc->maLayer.isSet()) // workaround for a Quartz crash + { + // in XOR mode the drawing context is redirected to the XOR mask + // if source and target are identical then copyBits() paints onto the target context though + CGContextHolder aCopyContext = maContextHolder; + if( mpXorEmulation && mpXorEmulation->IsEnabled() ) + { + if( pSrcGraphics == this ) + { + aCopyContext.set(mpXorEmulation->GetTargetContext()); + } + } + aCopyContext.saveState(); + + const CGRect aDstRect = CGRectMake(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight); + CGContextClipToRect(aCopyContext.get(), aDstRect); + + // draw at new destination + // NOTE: flipped drawing gets disabled for this, else the subimage would be drawn upside down + if( pSrc->IsFlipped() ) + { + CGContextTranslateCTM( aCopyContext.get(), 0, +mnHeight ); + CGContextScaleCTM( aCopyContext.get(), +1, -1 ); + } + + // TODO: pSrc->size() != this->size() + CGContextDrawLayerAtPoint(aCopyContext.get(), aDstPoint, pSrc->maLayer.get()); + + aCopyContext.restoreState(); + // mark the destination rectangle as updated + RefreshRect( aDstRect ); + } + else + { + std::shared_ptr<SalBitmap> pBitmap = pSrc->getBitmap( rPosAry.mnSrcX, rPosAry.mnSrcY, + rPosAry.mnSrcWidth, rPosAry.mnSrcHeight ); + if( pBitmap ) + { + SalTwoRect aPosAry( rPosAry ); + aPosAry.mnSrcX = 0; + aPosAry.mnSrcY = 0; + drawBitmap( aPosAry, *pBitmap ); + } + } +} + +static void DrawPattern50( void*, CGContextRef rContext ) +{ + static const CGRect aRects[2] = { { {0,0}, { 2, 2 } }, { { 2, 2 }, { 2, 2 } } }; + CGContextAddRects( rContext, aRects, 2 ); + CGContextFillPath( rContext ); +} + +static void getBoundRect( sal_uInt32 nPoints, const SalPoint *pPtAry, + long &rX, long& rY, long& rWidth, long& rHeight ) +{ + long nX1 = pPtAry->mnX; + long nX2 = nX1; + long nY1 = pPtAry->mnY; + long nY2 = nY1; + + for( sal_uInt32 n = 1; n < nPoints; n++ ) + { + if( pPtAry[n].mnX < nX1 ) + { + nX1 = pPtAry[n].mnX; + } + else if( pPtAry[n].mnX > nX2 ) + { + nX2 = pPtAry[n].mnX; + } + if( pPtAry[n].mnY < nY1 ) + { + nY1 = pPtAry[n].mnY; + } + else if( pPtAry[n].mnY > nY2 ) + { + nY2 = pPtAry[n].mnY; + } + } + rX = nX1; + rY = nY1; + rWidth = nX2 - nX1 + 1; + rHeight = nY2 - nY1 + 1; +} + +static Color ImplGetROPColor( SalROPColor nROPColor ) +{ + Color nColor; + if ( nROPColor == SalROPColor::N0 ) + { + nColor = Color( 0, 0, 0 ); + } + else + { + nColor = Color( 255, 255, 255 ); + } + return nColor; +} + +// apply the XOR mask to the target context if active and dirty +void AquaSalGraphics::ApplyXorContext() +{ + if( !mpXorEmulation ) + { + return; + } + if( mpXorEmulation->UpdateTarget() ) + { + RefreshRect( 0, 0, mnWidth, mnHeight ); // TODO: refresh minimal changerect + } +} + +void AquaSalGraphics::copyArea( long nDstX, long nDstY,long nSrcX, long nSrcY, + long nSrcWidth, long nSrcHeight, bool /*bWindowInvalidate*/ ) +{ + SAL_WARN_IF (!maLayer.isSet(), "vcl.quartz", + "AquaSalGraphics::copyArea() for non-layered graphics this=" << this); + +#ifdef IOS + if (!maLayer.isSet()) + return; +#endif + float fScale = maLayer.getScale(); + + long nScaledSourceX = nSrcX * fScale; + long nScaledSourceY = nSrcY * fScale; + + long nScaledTargetX = nDstX * fScale; + long nScaledTargetY = nDstY * fScale; + + long nScaledSourceWidth = nSrcWidth * fScale; + long nScaledSourceHeight = nSrcHeight * fScale; + + ApplyXorContext(); + + maContextHolder.saveState(); + + // in XOR mode the drawing context is redirected to the XOR mask + // copyArea() always works on the target context though + CGContextRef xCopyContext = maContextHolder.get(); + + if( mpXorEmulation && mpXorEmulation->IsEnabled() ) + { + xCopyContext = mpXorEmulation->GetTargetContext(); + } + + // If we have a scaled layer, we need to revert the scaling or else + // it will interfere with the coordinate calculation + CGContextScaleCTM(xCopyContext, 1.0 / fScale, 1.0 / fScale); + + // drawing a layer onto its own context causes trouble on OSX => copy it first + // TODO: is it possible to get rid of this unneeded copy more often? + // e.g. on OSX>=10.5 only this situation causes problems: + // mnBitmapDepth && (aDstPoint.x + pSrc->mnWidth) > mnWidth + + CGLayerHolder sSourceLayerHolder(maLayer); + { + const CGSize aSrcSize = CGSizeMake(nScaledSourceWidth, nScaledSourceHeight); + sSourceLayerHolder.set(CGLayerCreateWithContext(xCopyContext, aSrcSize, nullptr)); + + const CGContextRef xSrcContext = CGLayerGetContext(sSourceLayerHolder.get()); + + CGPoint aSrcPoint = CGPointMake(-nScaledSourceX, -nScaledSourceY); + if( IsFlipped() ) + { + CGContextTranslateCTM( xSrcContext, 0, +nScaledSourceHeight ); + CGContextScaleCTM( xSrcContext, +1, -1 ); + aSrcPoint.y = (nScaledSourceY + nScaledSourceHeight) - (mnHeight * fScale); + } + CGContextSetBlendMode(xSrcContext, kCGBlendModeCopy); + + CGContextDrawLayerAtPoint(xSrcContext, aSrcPoint, maLayer.get()); + } + + // draw at new destination + const CGRect aTargetRect = CGRectMake(nScaledTargetX, nScaledTargetY, nScaledSourceWidth, nScaledSourceHeight); + CGContextSetBlendMode(xCopyContext, kCGBlendModeCopy); + CGContextDrawLayerInRect(xCopyContext, aTargetRect, sSourceLayerHolder.get()); + + maContextHolder.restoreState(); + + // cleanup + if (sSourceLayerHolder.get() != maLayer.get()) + { + CGLayerRelease(sSourceLayerHolder.get()); + } + // mark the destination rectangle as updated + RefreshRect( nDstX, nDstY, nSrcWidth, nSrcHeight ); +} + +#ifndef IOS + +void AquaSalGraphics::copyResolution( AquaSalGraphics& rGraphics ) +{ + if( !rGraphics.mnRealDPIY && rGraphics.mbWindow && rGraphics.mpFrame ) + { + rGraphics.initResolution( rGraphics.mpFrame->getNSWindow() ); + } + mnRealDPIX = rGraphics.mnRealDPIX; + mnRealDPIY = rGraphics.mnRealDPIY; +} + +#endif + +bool AquaSalGraphics::blendBitmap( const SalTwoRect&, + const SalBitmap& ) +{ + return false; +} + +bool AquaSalGraphics::blendAlphaBitmap( const SalTwoRect&, + const SalBitmap&, + const SalBitmap&, + const SalBitmap& ) +{ + return false; +} + +bool AquaSalGraphics::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; + + 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 ( CheckContext() ) + { + const CGRect aDstRect = CGRectMake( rTR.mnDestX, rTR.mnDestY, rTR.mnDestWidth, rTR.mnDestHeight); + CGContextDrawImage( maContextHolder.get(), aDstRect, xMaskedImage ); + RefreshRect( aDstRect ); + } + + CGImageRelease(xMaskedImage); + + return true; +} + +bool AquaSalGraphics::drawTransformedBitmap( + const basegfx::B2DPoint& rNull, const basegfx::B2DPoint& rX, const basegfx::B2DPoint& rY, + const SalBitmap& rSrcBitmap, const SalBitmap* pAlphaBmp ) +{ + if( !CheckContext() ) + return true; + + // 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, static_cast<int>(aSize.Width()), static_cast<int>(aSize.Height()) ); + else + xImage = rSrcSalBmp.CreateWithMask( *pMaskSalBmp, 0, 0, static_cast<int>(aSize.Width()), static_cast<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 + 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( maContextHolder.get(), aCGMat ); + + // draw the transformed image + const CGRect aSrcRect = CGRectMake(0, 0, aSize.Width(), aSize.Height()); + CGContextDrawImage( maContextHolder.get(), aSrcRect, xImage ); + + CGImageRelease( xImage ); + // restore the Quartz graphics state + maContextHolder.restoreState(); + + // mark the destination as painted + const CGRect aDstRect = CGRectApplyAffineTransform( aSrcRect, aCGMat ); + RefreshRect( aDstRect ); + + return true; +} + +bool AquaSalGraphics::drawAlphaRect( long nX, long nY, long nWidth, + long nHeight, sal_uInt8 nTransparency ) +{ + if( !CheckContext() ) + return true; + + // save the current state + maContextHolder.saveState(); + CGContextSetAlpha( maContextHolder.get(), (100-nTransparency) * (1.0/100) ); + + CGRect aRect = CGRectMake(nX, nY, nWidth-1, nHeight-1); + if( IsPenVisible() ) + { + aRect.origin.x += 0.5; + aRect.origin.y += 0.5; + } + + CGContextBeginPath( maContextHolder.get() ); + CGContextAddRect( maContextHolder.get(), aRect ); + CGContextDrawPath( maContextHolder.get(), kCGPathFill ); + + maContextHolder.restoreState(); + RefreshRect( aRect ); + + return true; +} + +void AquaSalGraphics::drawBitmap( const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap ) +{ + if( !CheckContext() ) + return; + + 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( maContextHolder.get(), aDstRect, xImage ); + + CGImageRelease( xImage ); + RefreshRect( aDstRect ); +} + +void AquaSalGraphics::drawBitmap( const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, + const SalBitmap& rTransparentBitmap ) +{ + if( !CheckContext() ) + return; + + 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( maContextHolder.get(), aDstRect, xMaskedImage ); + CGImageRelease( xMaskedImage ); + RefreshRect( aDstRect ); +} + +#ifndef IOS + +bool AquaSalGraphics::drawEPS( long nX, long nY, long nWidth, 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( !CheckContext() ) + { + return false; + } + // NOTE: flip drawing, else the nsimage would be drawn upside down + maContextHolder.saveState(); +// CGContextTranslateCTM( maContextHolder.get(), 0, +mnHeight ); + CGContextScaleCTM( 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: maContextHolder.get() flipped: 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 + + maContextHolder.restoreState(); + // mark the destination rectangle as updated + RefreshRect( aDstRect ); + + return bOK; +} + +#endif + +void AquaSalGraphics::drawLine( long nX1, long nY1, long nX2, long nY2 ) +{ + if( nX1 == nX2 && nY1 == nY2 ) + { + // #i109453# platform independent code expects at least one pixel to be drawn + drawPixel( nX1, nY1 ); + + return; + } + + if( !CheckContext() ) + return; + + CGContextBeginPath( maContextHolder.get() ); + CGContextMoveToPoint( maContextHolder.get(), static_cast<float>(nX1)+0.5, static_cast<float>(nY1)+0.5 ); + CGContextAddLineToPoint( maContextHolder.get(), static_cast<float>(nX2)+0.5, static_cast<float>(nY2)+0.5 ); + CGContextDrawPath( maContextHolder.get(), kCGPathStroke ); + + tools::Rectangle aRefreshRect( nX1, nY1, nX2, nY2 ); + (void) aRefreshRect; + // Is a call to RefreshRect( aRefreshRect ) missing here? +} + +void AquaSalGraphics::drawMask( const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, Color nMaskColor ) +{ + if( !CheckContext() ) + return; + + 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( maContextHolder.get(), aDstRect, xImage ); + CGImageRelease( xImage ); + RefreshRect( aDstRect ); +} + +void AquaSalGraphics::drawPixel( long nX, long nY ) +{ + // draw pixel with current line color + ImplDrawPixel( nX, nY, maLineColor ); +} + +void AquaSalGraphics::drawPixel( long nX, long nY, Color nColor ) +{ + const RGBAColor aPixelColor( nColor ); + ImplDrawPixel( nX, nY, aPixelColor ); +} + +bool AquaSalGraphics::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( !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(fMiterMinimumAngle / 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(), + !getAntiAliasB2DDraw(), + 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 + maContextHolder.saveState(); + CGContextBeginPath( maContextHolder.get() ); + CGContextAddPath( maContextHolder.get(), xPath ); + // draw path with antialiased line + CGContextSetShouldAntialias( maContextHolder.get(), true ); + CGContextSetAlpha( maContextHolder.get(), 1.0 - fTransparency ); + CGContextSetLineJoin( maContextHolder.get(), aCGLineJoin ); + CGContextSetLineCap( maContextHolder.get(), aCGLineCap ); + CGContextSetLineWidth( maContextHolder.get(), fLineWidth ); + CGContextSetMiterLimit(maContextHolder.get(), fCGMiterLimit); + CGContextDrawPath( maContextHolder.get(), kCGPathStroke ); + maContextHolder.restoreState(); + + // mark modified rectangle as updated + RefreshRect( aRefreshRect ); + } + + CGPathRelease( xPath ); + + return true; +} + +bool AquaSalGraphics::drawPolyLineBezier( sal_uInt32, const SalPoint*, const PolyFlags* ) +{ + return false; +} + +bool AquaSalGraphics::drawPolyPolygon( + const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolyPolygon& rPolyPolygon, + double fTransparency) +{ +#ifdef IOS + if (!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 : aPolyPolygon) + { + AddPolygonToPath( xPath, rPolygon, true, !getAntiAliasB2DDraw(), 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( IsBrushVisible() && IsPenVisible() ) + { + eMode = kCGPathEOFillStroke; + } + else if( IsPenVisible() ) + { + eMode = kCGPathStroke; + } + else if( 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 + maContextHolder.saveState(); + CGContextBeginPath( maContextHolder.get() ); + CGContextAddPath( maContextHolder.get(), xPath ); + + // draw path with antialiased polygon + CGContextSetShouldAntialias( maContextHolder.get(), true ); + CGContextSetAlpha( maContextHolder.get(), 1.0 - fTransparency ); + CGContextDrawPath( maContextHolder.get(), eMode ); + maContextHolder.restoreState(); + + // mark modified rectangle as updated + RefreshRect( aRefreshRect ); + } + + CGPathRelease( xPath ); + + return true; +} + +void AquaSalGraphics::drawPolyPolygon( sal_uInt32 nPolyCount, const sal_uInt32 *pPoints, PCONSTSALPOINT *ppPtAry ) +{ + if( nPolyCount <= 0 ) + return; + + if( !CheckContext() ) + return; + + // find bound rect + 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++ ) + { + 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( IsBrushVisible() && IsPenVisible() ) + { + eMode = kCGPathEOFillStroke; + } + else if( IsPenVisible() ) + { + eMode = kCGPathStroke; + } + else if( IsBrushVisible() ) + { + eMode = kCGPathEOFill; + } + else + { + SAL_WARN( "vcl.quartz", "Neither pen nor brush visible" ); + return; + } + + // convert to CGPath + CGContextBeginPath( maContextHolder.get() ); + if( IsPenVisible() ) + { + for( sal_uInt32 nPoly = 0; nPoly < nPolyCount; nPoly++ ) + { + const sal_uInt32 nPoints = pPoints[nPoly]; + if( nPoints > 1 ) + { + const SalPoint *pPtAry = ppPtAry[nPoly]; + float fX, fY; + + alignLinePoint( pPtAry, fX, fY ); + CGContextMoveToPoint( maContextHolder.get(), fX, fY ); + pPtAry++; + + for( sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++ ) + { + alignLinePoint( pPtAry, fX, fY ); + CGContextAddLineToPoint( maContextHolder.get(), fX, fY ); + } + CGContextClosePath(maContextHolder.get()); + } + } + } + else + { + for( sal_uInt32 nPoly = 0; nPoly < nPolyCount; nPoly++ ) + { + const sal_uInt32 nPoints = pPoints[nPoly]; + if( nPoints > 1 ) + { + const SalPoint *pPtAry = ppPtAry[nPoly]; + CGContextMoveToPoint( maContextHolder.get(), pPtAry->mnX, pPtAry->mnY ); + pPtAry++; + for( sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++ ) + { + CGContextAddLineToPoint( maContextHolder.get(), pPtAry->mnX, pPtAry->mnY ); + } + CGContextClosePath(maContextHolder.get()); + } + } + } + + CGContextDrawPath( maContextHolder.get(), eMode ); + + RefreshRect( leftX, topY, maxWidth, maxHeight ); +} + +void AquaSalGraphics::drawPolygon( sal_uInt32 nPoints, const SalPoint *pPtAry ) +{ + if( nPoints <= 1 ) + return; + + if( !CheckContext() ) + return; + + long nX = 0, nY = 0, nWidth = 0, nHeight = 0; + getBoundRect( nPoints, pPtAry, nX, nY, nWidth, nHeight ); + + CGPathDrawingMode eMode; + if( IsBrushVisible() && IsPenVisible() ) + { + eMode = kCGPathEOFillStroke; + } + else if( IsPenVisible() ) + { + eMode = kCGPathStroke; + } + else if( IsBrushVisible() ) + { + eMode = kCGPathEOFill; + } + else + { + SAL_WARN( "vcl.quartz", "Neither pen nor brush visible" ); + return; + } + + CGContextBeginPath( maContextHolder.get() ); + + if( IsPenVisible() ) + { + float fX, fY; + alignLinePoint( pPtAry, fX, fY ); + CGContextMoveToPoint( maContextHolder.get(), fX, fY ); + pPtAry++; + for( sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++ ) + { + alignLinePoint( pPtAry, fX, fY ); + CGContextAddLineToPoint( maContextHolder.get(), fX, fY ); + } + } + else + { + CGContextMoveToPoint( maContextHolder.get(), pPtAry->mnX, pPtAry->mnY ); + pPtAry++; + for( sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++ ) + { + CGContextAddLineToPoint( maContextHolder.get(), pPtAry->mnX, pPtAry->mnY ); + } + } + + CGContextClosePath( maContextHolder.get() ); + CGContextDrawPath( maContextHolder.get(), eMode ); + RefreshRect( nX, nY, nWidth, nHeight ); +} + +bool AquaSalGraphics::drawPolygonBezier( sal_uInt32, const SalPoint*, const PolyFlags* ) +{ + return false; +} + +bool AquaSalGraphics::drawPolyPolygonBezier( sal_uInt32, const sal_uInt32*, + const SalPoint* const*, const PolyFlags* const* ) +{ + return false; +} + +void AquaSalGraphics::drawRect( long nX, long nY, long nWidth, long nHeight ) +{ + if( !CheckContext() ) + return; + + CGRect aRect( CGRectMake(nX, nY, nWidth, nHeight) ); + if( IsPenVisible() ) + { + aRect.origin.x += 0.5; + aRect.origin.y += 0.5; + aRect.size.width -= 1; + aRect.size.height -= 1; + } + + if( IsBrushVisible() ) + { + CGContextFillRect( maContextHolder.get(), aRect ); + } + if( IsPenVisible() ) + { + CGContextStrokeRect( maContextHolder.get(), aRect ); + } + RefreshRect( nX, nY, nWidth, nHeight ); +} + +void AquaSalGraphics::drawPolyLine( sal_uInt32 nPoints, const SalPoint *pPtAry ) +{ + if( nPoints < 1 ) + return; + + if( !CheckContext() ) + return; + + long nX = 0, nY = 0, nWidth = 0, nHeight = 0; + getBoundRect( nPoints, pPtAry, nX, nY, nWidth, nHeight ); + + float fX, fY; + CGContextBeginPath( maContextHolder.get() ); + alignLinePoint( pPtAry, fX, fY ); + CGContextMoveToPoint( maContextHolder.get(), fX, fY ); + pPtAry++; + + for( sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++ ) + { + alignLinePoint( pPtAry, fX, fY ); + CGContextAddLineToPoint( maContextHolder.get(), fX, fY ); + } + CGContextStrokePath(maContextHolder.get()); + + RefreshRect( nX, nY, nWidth, nHeight ); +} + +sal_uInt16 AquaSalGraphics::GetBitCount() const +{ + sal_uInt16 nBits = mnBitmapDepth ? mnBitmapDepth : 32;//24; + return nBits; +} + +std::shared_ptr<SalBitmap> AquaSalGraphics::getBitmap( long nX, long nY, long nDX, long nDY ) +{ + SAL_WARN_IF(!maLayer.isSet(), "vcl.quartz", "AquaSalGraphics::getBitmap() with no layer this=" << this); + + ApplyXorContext(); + + std::shared_ptr<QuartzSalBitmap> pBitmap = std::make_shared<QuartzSalBitmap>(); + if (!pBitmap->Create(maLayer, mnBitmapDepth, nX, nY, nDX, nDY, IsFlipped())) + { + pBitmap = nullptr; + } + return pBitmap; +} + +SystemGraphicsData AquaSalGraphics::GetGraphicsData() const +{ + SystemGraphicsData aRes; + aRes.nSize = sizeof(aRes); + aRes.rCGContext = maContextHolder.get(); + return aRes; +} + +long AquaSalGraphics::GetGraphicsWidth() const +{ + long w = 0; + if( maContextHolder.isSet() && ( +#ifndef IOS + mbWindow || +#endif + mbVirDev) ) + { + w = mnWidth; + } + +#ifndef IOS + if( w == 0 ) + { + if( mbWindow && mpFrame ) + { + w = mpFrame->maGeometry.nWidth; + } + } +#endif + return w; +} + +Color AquaSalGraphics::getPixel( long nX, long nY ) +{ + // return default value on printers or when out of bounds + if (!maLayer.isSet() || (nX < 0) || (nX >= mnWidth) || + (nY < 0) || (nY >= mnHeight)) + { + return sal_uInt32(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 + ApplyXorContext(); + + // copy the requested pixel into the bitmap context + if( IsFlipped() ) + { + nY = mnHeight - nY; + } + const CGPoint aCGPoint = CGPointMake(-nX, -nY); + CGContextDrawLayerAtPoint(xOnePixelContext, aCGPoint, 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( (mbWindow && mpFrame) ? 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 AquaSalGraphics::ImplDrawPixel( long nX, long nY, const RGBAColor& rColor ) +{ + if( !CheckContext() ) + { + return; + } + // overwrite the fill color + CGContextSetFillColor( maContextHolder.get(), rColor.AsArray() ); + // draw 1x1 rect, there is no pixel drawing in Quartz + const CGRect aDstRect = CGRectMake(nX, nY, 1, 1); + CGContextFillRect( maContextHolder.get(), aDstRect ); + RefreshRect( aDstRect ); + // reset the fill color + CGContextSetFillColor( maContextHolder.get(), maFillColor.AsArray() ); +} + +#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<long>((CGDisplayPixelsWide( nDisplayID ) * 25.4) / aSize.width); + mnRealDPIY = static_cast<long>((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 AquaSalGraphics::invert( long nX, long nY, long nWidth, long nHeight, SalInvert nFlags ) +{ + if ( CheckContext() ) + { + CGRect aCGRect = CGRectMake( nX, nY, nWidth, nHeight); + maContextHolder.saveState(); + if ( nFlags & SalInvert::TrackFrame ) + { + const CGFloat dashLengths[2] = { 4.0, 4.0 }; // for drawing dashed line + CGContextSetBlendMode( maContextHolder.get(), kCGBlendModeDifference ); + CGContextSetRGBStrokeColor ( maContextHolder.get(), 1.0, 1.0, 1.0, 1.0 ); + CGContextSetLineDash ( maContextHolder.get(), 0, dashLengths, 2 ); + CGContextSetLineWidth( maContextHolder.get(), 2.0); + CGContextStrokeRect ( maContextHolder.get(), aCGRect ); + } + else if ( nFlags & SalInvert::N50 ) + { + //CGContextSetAllowsAntialiasing( maContextHolder.get(), false ); + CGContextSetBlendMode(maContextHolder.get(), kCGBlendModeDifference); + CGContextAddRect( maContextHolder.get(), aCGRect ); + Pattern50Fill(); + } + else // just invert + { + CGContextSetBlendMode(maContextHolder.get(), kCGBlendModeDifference); + CGContextSetRGBFillColor ( maContextHolder.get(),1.0, 1.0, 1.0 , 1.0 ); + CGContextFillRect ( maContextHolder.get(), aCGRect ); + } + maContextHolder.restoreState(); + RefreshRect( aCGRect ); + } +} + +namespace { + +CGPoint* makeCGptArray(sal_uInt32 nPoints, const SalPoint* pPtAry) +{ + CGPoint *CGpoints = new CGPoint[nPoints]; + for(sal_uLong i=0;i<nPoints;i++) + { + CGpoints[i].x = pPtAry[i].mnX; + CGpoints[i].y = pPtAry[i].mnY; + } + return CGpoints; +} + +} + +void AquaSalGraphics::invert( sal_uInt32 nPoints, const SalPoint* pPtAry, SalInvert nSalFlags ) +{ + if ( CheckContext() ) + { + maContextHolder.saveState(); + CGPoint* CGpoints = makeCGptArray(nPoints,pPtAry); + CGContextAddLines ( maContextHolder.get(), CGpoints, nPoints ); + if ( nSalFlags & SalInvert::TrackFrame ) + { + const CGFloat dashLengths[2] = { 4.0, 4.0 }; // for drawing dashed line + CGContextSetBlendMode( maContextHolder.get(), kCGBlendModeDifference ); + CGContextSetRGBStrokeColor ( maContextHolder.get(), 1.0, 1.0, 1.0, 1.0 ); + CGContextSetLineDash ( maContextHolder.get(), 0, dashLengths, 2 ); + CGContextSetLineWidth( maContextHolder.get(), 2.0); + CGContextStrokePath ( maContextHolder.get() ); + } + else if ( nSalFlags & SalInvert::N50 ) + { + CGContextSetBlendMode(maContextHolder.get(), kCGBlendModeDifference); + Pattern50Fill(); + } + else // just invert + { + CGContextSetBlendMode( maContextHolder.get(), kCGBlendModeDifference ); + CGContextSetRGBFillColor( maContextHolder.get(), 1.0, 1.0, 1.0, 1.0 ); + CGContextFillPath( maContextHolder.get() ); + } + const CGRect aRefreshRect = CGContextGetClipBoundingBox(maContextHolder.get()); + maContextHolder.restoreState(); + delete [] CGpoints; + RefreshRect( aRefreshRect ); + } +} + +void AquaSalGraphics::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( !maContextHolder.get(), "vcl.quartz", "maContextHolder.get() is NULL" ); + CGContextSetFillColorSpace( maContextHolder.get(), mxP50Space ); + CGContextSetFillPattern( maContextHolder.get(), mxP50Pattern, aFillCol ); + CGContextFillPath( maContextHolder.get() ); +} + +void AquaSalGraphics::ResetClipRegion() +{ + // release old path and indicate no clipping + if( mxClipPath ) + { + CGPathRelease( mxClipPath ); + mxClipPath = nullptr; + } + if( CheckContext() ) + { + SetState(); + } +} + +void AquaSalGraphics::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 ); + } +} + +void AquaSalGraphics::SetLineColor() +{ + maLineColor.SetAlpha( 0.0 ); // transparent + if( CheckContext() ) + { + CGContextSetRGBStrokeColor( maContextHolder.get(), maLineColor.GetRed(), maLineColor.GetGreen(), + maLineColor.GetBlue(), maLineColor.GetAlpha() ); + } +} + +void AquaSalGraphics::SetLineColor( Color nColor ) +{ + maLineColor = RGBAColor( nColor ); + if( CheckContext() ) + { + CGContextSetRGBStrokeColor( maContextHolder.get(), maLineColor.GetRed(), maLineColor.GetGreen(), + maLineColor.GetBlue(), maLineColor.GetAlpha() ); + } +} + +void AquaSalGraphics::SetFillColor() +{ + maFillColor.SetAlpha( 0.0 ); // transparent + if( CheckContext() ) + { + CGContextSetRGBFillColor( maContextHolder.get(), maFillColor.GetRed(), maFillColor.GetGreen(), + maFillColor.GetBlue(), maFillColor.GetAlpha() ); + } +} + +void AquaSalGraphics::SetFillColor( Color nColor ) +{ + maFillColor = RGBAColor( nColor ); + if( CheckContext() ) + { + CGContextSetRGBFillColor( maContextHolder.get(), maFillColor.GetRed(), maFillColor.GetGreen(), + maFillColor.GetBlue(), maFillColor.GetAlpha() ); + } +} + +bool AquaSalGraphics::supportsOperation( OutDevSupportType eType ) const +{ + bool bRet = false; + switch( eType ) + { + case OutDevSupportType::TransparentRect: + case OutDevSupportType::B2DDraw: + bRet = true; + break; + default: + break; + } + return bRet; +} + +bool AquaSalGraphics::setClipRegion( const vcl::Region& i_rClip ) +{ + // release old clip path + if( mxClipPath ) + { + CGPathRelease( mxClipPath ); + mxClipPath = nullptr; + } + mxClipPath = CGPathCreateMutable(); + + // set current path, either as polypolgon or sequence of rectangles + if(i_rClip.HasPolyPolygonOrB2DPolyPolygon()) + { + const basegfx::B2DPolyPolygon aClip(i_rClip.GetAsB2DPolyPolygon()); + + AddPolyPolygonToPath( mxClipPath, aClip, !getAntiAliasB2DDraw(), false ); + } + else + { + RectangleVector aRectangles; + i_rClip.GetRegionRectangles(aRectangles); + + for(const auto& rRect : aRectangles) + { + const long nW(rRect.Right() - rRect.Left() + 1); // uses +1 logic in original + + if(nW) + { + const 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( mxClipPath, nullptr, aRect ); + } + } + } + } + // set the current path as clip region + if( CheckContext() ) + { + SetState(); + } + return true; +} + +void AquaSalGraphics::SetROPFillColor( SalROPColor nROPColor ) +{ + if( ! mbPrinter ) + { + SetFillColor( ImplGetROPColor( nROPColor ) ); + } +} + +void AquaSalGraphics::SetROPLineColor( SalROPColor nROPColor ) +{ + if( ! mbPrinter ) + { + SetLineColor( ImplGetROPColor( nROPColor ) ); + } +} + +void AquaSalGraphics::SetXORMode( bool bSet, bool bInvertOnly ) +{ + // return early if XOR mode remains unchanged + if( mbPrinter ) + { + return; + } + if( ! bSet && mnXorMode == 2 ) + { + CGContextSetBlendMode( maContextHolder.get(), kCGBlendModeNormal ); + mnXorMode = 0; + return; + } + else if( bSet && bInvertOnly && mnXorMode == 0) + { + CGContextSetBlendMode( maContextHolder.get(), kCGBlendModeDifference ); + mnXorMode = 2; + return; + } + + if( (mpXorEmulation == nullptr) && !bSet ) + { + return; + } + if( (mpXorEmulation != nullptr) && (bSet == mpXorEmulation->IsEnabled()) ) + { + return; + } + if( !CheckContext() ) + { + return; + } + // prepare XOR emulation + if( !mpXorEmulation ) + { + mpXorEmulation = new XorEmulation(); + mpXorEmulation->SetTarget(mnWidth, mnHeight, mnBitmapDepth, maContextHolder.get(), maLayer.get()); + } + + // change the XOR mode + if( bSet ) + { + mpXorEmulation->Enable(); + maContextHolder.set(mpXorEmulation->GetMaskContext()); + mnXorMode = 1; + } + else + { + mpXorEmulation->UpdateTarget(); + mpXorEmulation->Disable(); + maContextHolder.set(mpXorEmulation->GetTargetContext()); + mnXorMode = 0; + } +} + +#ifndef IOS + +void AquaSalGraphics::updateResolution() +{ + SAL_WARN_IF( !mbWindow, "vcl", "updateResolution on inappropriate graphics" ); + + initResolution( (mbWindow && mpFrame) ? 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 ); +} + +void XorEmulation::SetTarget( int nWidth, int nHeight, int nTargetDepth, + CGContextRef xTargetContext, CGLayerRef xTargetLayer ) +{ + SAL_INFO( "vcl.quartz", "XorEmulation::SetTarget() this=" << this << + " (" << nWidth << "x" << nHeight << ") depth=" << nTargetDepth << + " context=" << xTargetContext << " layer=" << xTargetLayer ); + + // prepare to replace old mask+temp context + if( m_xMaskContext ) + { + // cleanup the mask context + CGContextRelease( m_xMaskContext ); + delete[] m_pMaskBuffer; + m_xMaskContext = nullptr; + m_pMaskBuffer = nullptr; + + // cleanup the temp context if needed + if( m_xTempContext ) + { + CGContextRelease( m_xTempContext ); + delete[] m_pTempBuffer; + m_xTempContext = nullptr; + m_pTempBuffer = nullptr; + } + } + + // return early if there is nothing more to do + if( !xTargetContext ) + { + return; + } + // retarget drawing operations to the XOR mask + m_xTargetLayer = xTargetLayer; + m_xTargetContext = xTargetContext; + + // prepare creation of matching CGBitmaps + CGColorSpaceRef aCGColorSpace = GetSalData()->mxRGBSpace; + CGBitmapInfo aCGBmpInfo = kCGImageAlphaNoneSkipFirst; + int nBitDepth = nTargetDepth; + if( !nBitDepth ) + { + nBitDepth = 32; + } + int nBytesPerRow = 4; + const size_t nBitsPerComponent = (nBitDepth == 16) ? 5 : 8; + if( nBitDepth <= 8 ) + { + aCGColorSpace = GetSalData()->mxGraySpace; + aCGBmpInfo = kCGImageAlphaNone; + nBytesPerRow = 1; + } + nBytesPerRow *= nWidth; + m_nBufferLongs = (nHeight * nBytesPerRow + sizeof(sal_uLong)-1) / sizeof(sal_uLong); + + // create a XorMask context + m_pMaskBuffer = new sal_uLong[ m_nBufferLongs ]; + m_xMaskContext = CGBitmapContextCreate( m_pMaskBuffer, + nWidth, nHeight, + nBitsPerComponent, nBytesPerRow, + aCGColorSpace, aCGBmpInfo ); + SAL_WARN_IF( !m_xMaskContext, "vcl.quartz", "mask context creation failed" ); + + // reset the XOR mask to black + memset( m_pMaskBuffer, 0, m_nBufferLongs * sizeof(sal_uLong) ); + + // a bitmap context will be needed for manual XORing + // create one unless the target context is a bitmap context + if( nTargetDepth ) + { + m_pTempBuffer = static_cast<sal_uLong*>(CGBitmapContextGetData( m_xTargetContext )); + } + if( !m_pTempBuffer ) + { + // create a bitmap context matching to the target context + m_pTempBuffer = new sal_uLong[ m_nBufferLongs ]; + m_xTempContext = CGBitmapContextCreate( m_pTempBuffer, + nWidth, nHeight, + nBitsPerComponent, nBytesPerRow, + aCGColorSpace, aCGBmpInfo ); + SAL_WARN_IF( !m_xTempContext, "vcl.quartz", "temp context creation failed" ); + } + + // initialize XOR mask context for drawing + CGContextSetFillColorSpace( m_xMaskContext, aCGColorSpace ); + CGContextSetStrokeColorSpace( m_xMaskContext, aCGColorSpace ); + CGContextSetShouldAntialias( m_xMaskContext, false ); + + // improve the XorMask's XOR emulation a little + // NOTE: currently only enabled for monochrome contexts + if( aCGColorSpace == GetSalData()->mxGraySpace ) + { + CGContextSetBlendMode( m_xMaskContext, kCGBlendModeDifference ); + } + // initialize the transformation matrix to the drawing target + const CGAffineTransform aCTM = CGContextGetCTM( xTargetContext ); + CGContextConcatCTM( m_xMaskContext, aCTM ); + if( m_xTempContext ) + { + CGContextConcatCTM( m_xTempContext, aCTM ); + } + // initialize the default XorMask graphics state + CGContextSaveGState( m_xMaskContext ); +} + +bool XorEmulation::UpdateTarget() +{ + SAL_INFO( "vcl.quartz", "XorEmulation::UpdateTarget() this=" << this ); + + if( !IsEnabled() ) + { + return false; + } + // update the temp bitmap buffer if needed + if( m_xTempContext ) + { + SAL_WARN_IF( m_xTargetContext == nullptr, "vcl.quartz", "Target layer is NULL"); + CGContextDrawLayerAtPoint( m_xTempContext, CGPointZero, m_xTargetLayer ); + } + // do a manual XOR with the XorMask + // this approach suffices for simple color manipulations + // and also the complex-clipping-XOR-trick used in metafiles + const sal_uLong* pSrc = m_pMaskBuffer; + sal_uLong* pDst = m_pTempBuffer; + for( int i = m_nBufferLongs; --i >= 0;) + { + *(pDst++) ^= *(pSrc++); + } + // write back the XOR results to the target context + if( m_xTempContext ) + { + CGImageRef xXorImage = CGBitmapContextCreateImage( m_xTempContext ); + const int nWidth = static_cast<int>(CGImageGetWidth( xXorImage )); + const int nHeight = static_cast<int>(CGImageGetHeight( xXorImage )); + // TODO: update minimal changerect + const CGRect aFullRect = CGRectMake(0, 0, nWidth, nHeight); + CGContextDrawImage( m_xTargetContext, aFullRect, xXorImage ); + CGImageRelease( xXorImage ); + } + + // reset the XorMask to black again + // TODO: not needed for last update + memset( m_pMaskBuffer, 0, m_nBufferLongs * sizeof(sal_uLong) ); + + // TODO: return FALSE if target was not changed + return true; +} + +void AquaSalGraphics::SetVirDevGraphics(CGLayerHolder const & rLayer, CGContextRef xContext, + int nBitmapDepth) +{ + SAL_INFO( "vcl.quartz", "SetVirDevGraphics() this=" << this << " layer=" << rLayer.get() << " context=" << xContext ); + +#ifndef IOS + mbWindow = false; +#endif + mbPrinter = false; + mbVirDev = true; + + // set graphics properties + maLayer = rLayer; + maContextHolder.set(xContext); + + mnBitmapDepth = nBitmapDepth; + +#ifdef IOS + mbForeignContext = xContext != NULL; +#endif + + // return early if the virdev is being destroyed + if( !xContext ) + return; + + // get new graphics properties + if (!maLayer.isSet()) + { + mnWidth = CGBitmapContextGetWidth( maContextHolder.get() ); + mnHeight = CGBitmapContextGetHeight( maContextHolder.get() ); + } + else + { + const CGSize aSize = CGLayerGetSize(maLayer.get()); + mnWidth = static_cast<int>(aSize.width); + mnHeight = static_cast<int>(aSize.height); + } + + // prepare graphics for drawing + const CGColorSpaceRef aCGColorSpace = GetSalData()->mxRGBSpace; + CGContextSetFillColorSpace( maContextHolder.get(), aCGColorSpace ); + CGContextSetStrokeColorSpace( maContextHolder.get(), aCGColorSpace ); + + // re-enable XorEmulation for the new context + if( mpXorEmulation ) + { + mpXorEmulation->SetTarget(mnWidth, mnHeight, mnBitmapDepth, maContextHolder.get(), maLayer.get()); + if( mpXorEmulation->IsEnabled() ) + { + maContextHolder.set(mpXorEmulation->GetMaskContext()); + } + } + + // initialize stack of CGContext states + maContextHolder.saveState(); + SetState(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/quartz/salgdiutils.cxx b/vcl/quartz/salgdiutils.cxx new file mode 100644 index 000000000..57953e536 --- /dev/null +++ b/vcl/quartz/salgdiutils.cxx @@ -0,0 +1,265 @@ +/* -*- 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 <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/range/b2drectangle.hxx> +#include <basegfx/range/b2irange.hxx> +#include <basegfx/vector/b2ivector.hxx> +#include <vcl/svapp.hxx> + +#include <quartz/salgdi.h> +#include <quartz/utils.h> +#include <osx/salframe.h> +#include <osx/saldata.hxx> + +void AquaSalGraphics::SetWindowGraphics( AquaSalFrame* pFrame ) +{ + mpFrame = pFrame; + mbWindow = true; + mbPrinter = false; + mbVirDev = false; +} + +void AquaSalGraphics::SetPrinterGraphics( CGContextRef xContext, long nDPIX, long nDPIY ) +{ + mbWindow = false; + mbPrinter = true; + mbVirDev = false; + + maContextHolder.set(xContext); + mnRealDPIX = nDPIX; + mnRealDPIY = nDPIY; + + // a previously set clip path is now invalid + if( mxClipPath ) + { + CGPathRelease( mxClipPath ); + mxClipPath = nullptr; + } + + if (maContextHolder.isSet()) + { + CGContextSetFillColorSpace( maContextHolder.get(), GetSalData()->mxRGBSpace ); + CGContextSetStrokeColorSpace( maContextHolder.get(), GetSalData()->mxRGBSpace ); + CGContextSaveGState( maContextHolder.get() ); + SetState(); + } +} + +void AquaSalGraphics::InvalidateContext() +{ + UnsetState(); + + CGContextRelease(maContextHolder.get()); + CGContextRelease(maBGContextHolder.get()); + CGContextRelease(maCSContextHolder.get()); + + maContextHolder.set(nullptr); + maCSContextHolder.set(nullptr); + maBGContextHolder.set(nullptr); +} + +void AquaSalGraphics::UnsetState() +{ + if (maBGContextHolder.isSet()) + { + CGContextRelease(maBGContextHolder.get()); + maBGContextHolder.set(nullptr); + } + if (maCSContextHolder.isSet()) + { + CGContextRelease(maCSContextHolder.get()); + maBGContextHolder.set(nullptr); + } + if (maContextHolder.isSet()) + { + maContextHolder.restoreState(); + maContextHolder.set(nullptr); + } + if( mxClipPath ) + { + CGPathRelease( mxClipPath ); + mxClipPath = nullptr; + } +} + +/** + * (re-)create the off-screen maLayer we render everything to if + * necessary: eg. not initialized yet, or it has an incorrect size. + */ +bool AquaSalGraphics::CheckContext() +{ + if (mbWindow && mpFrame && (mpFrame->getNSWindow() || Application::IsBitmapRendering())) + { + const unsigned int nWidth = mpFrame->maGeometry.nWidth; + const unsigned int nHeight = mpFrame->maGeometry.nHeight; + + // Let's get the window scaling factor if possible, or use 1.0 + // as the scaling factor. + float fScale = 1.0f; + if (mpFrame->getNSWindow()) + fScale = [mpFrame->getNSWindow() backingScaleFactor]; + + CGLayerRef rReleaseLayer = nullptr; + + // check if a new drawing context is needed (e.g. after a resize) + if( (unsigned(mnWidth) != nWidth) || (unsigned(mnHeight) != nHeight) ) + { + mnWidth = nWidth; + mnHeight = nHeight; + // prepare to release the corresponding resources + if (maLayer.isSet()) + { + rReleaseLayer = maLayer.get(); + } + else if (maContextHolder.isSet()) + { + CGContextRelease(maContextHolder.get()); + } + CGContextRelease(maBGContextHolder.get()); + CGContextRelease(maCSContextHolder.get()); + + maContextHolder.set(nullptr); + maBGContextHolder.set(nullptr); + maCSContextHolder.set(nullptr); + maLayer.set(nullptr); + } + + if (!maContextHolder.isSet()) + { + const int nBitmapDepth = 32; + + float nScaledWidth = mnWidth * fScale; + float nScaledHeight = mnHeight * fScale; + + const CGSize aLayerSize = { static_cast<CGFloat>(nScaledWidth), static_cast<CGFloat>(nScaledHeight) }; + + const int nBytesPerRow = (nBitmapDepth * nScaledWidth) / 8; + int nFlags = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host; + maBGContextHolder.set(CGBitmapContextCreate( + NULL, nScaledWidth, nScaledHeight, 8, nBytesPerRow, GetSalData()->mxRGBSpace, nFlags)); + + maLayer.set(CGLayerCreateWithContext(maBGContextHolder.get(), aLayerSize, nullptr)); + maLayer.setScale(fScale); + + nFlags = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + maCSContextHolder.set(CGBitmapContextCreate( + NULL, nScaledWidth, nScaledHeight, 8, nBytesPerRow, GetSalData()->mxRGBSpace, nFlags)); + + CGContextRef xDrawContext = CGLayerGetContext(maLayer.get()); + maContextHolder = xDrawContext; + + if (rReleaseLayer) + { + // copy original layer to resized layer + if (maContextHolder.isSet()) + { + CGContextDrawLayerAtPoint(maContextHolder.get(), CGPointZero, rReleaseLayer); + } + CGLayerRelease(rReleaseLayer); + } + + if (maContextHolder.isSet()) + { + CGContextTranslateCTM(maContextHolder.get(), 0, nScaledHeight); + CGContextScaleCTM(maContextHolder.get(), 1.0, -1.0); + CGContextSetFillColorSpace(maContextHolder.get(), GetSalData()->mxRGBSpace); + CGContextSetStrokeColorSpace(maContextHolder.get(), GetSalData()->mxRGBSpace); + // apply a scale matrix so everything is auto-magically scaled + CGContextScaleCTM(maContextHolder.get(), fScale, fScale); + maContextHolder.saveState(); + SetState(); + + // re-enable XOR emulation for the new context + if (mpXorEmulation) + mpXorEmulation->SetTarget(mnWidth, mnHeight, mnBitmapDepth, maContextHolder.get(), maLayer.get()); + } + } + } + + SAL_WARN_IF(!maContextHolder.isSet() && !mbPrinter, "vcl", "<<<WARNING>>> AquaSalGraphics::CheckContext() FAILED!!!!"); + + return maContextHolder.isSet(); +} + +CGContextRef AquaSalGraphics::GetContext() +{ + if (!maContextHolder.isSet()) + { + CheckContext(); + } + return maContextHolder.get(); +} + +/** + * Blit the contents of our internal maLayer state to the + * associated window, if any; cf. drawRect event handling + * on the frame. + */ +void AquaSalGraphics::UpdateWindow( NSRect& ) +{ + if( !mpFrame ) + { + return; + } + + NSGraphicsContext* pContext = [NSGraphicsContext currentContext]; + if (maLayer.isSet() && pContext != nullptr) + { + CGContextHolder rCGContextHolder([pContext CGContext]); + + rCGContextHolder.saveState(); + + CGMutablePathRef rClip = mpFrame->getClipPath(); + if (rClip) + { + CGContextBeginPath(rCGContextHolder.get()); + CGContextAddPath(rCGContextHolder.get(), rClip ); + CGContextClip(rCGContextHolder.get()); + } + + ApplyXorContext(); + + const CGSize aSize = maLayer.getSizePoints(); + const CGRect aRect = CGRectMake(0, 0, aSize.width, aSize.height); + const CGRect aRectPoints = { CGPointZero, maLayer.getSizePixels() }; + CGContextSetBlendMode(maCSContextHolder.get(), kCGBlendModeCopy); + CGContextDrawLayerInRect(maCSContextHolder.get(), aRectPoints, maLayer.get()); + + CGImageRef img = CGBitmapContextCreateImage(maCSContextHolder.get()); + CGImageRef displayColorSpaceImage = CGImageCreateCopyWithColorSpace(img, [[mpFrame->getNSWindow() colorSpace] CGColorSpace]); + CGContextSetBlendMode(rCGContextHolder.get(), kCGBlendModeCopy); + CGContextDrawImage(rCGContextHolder.get(), aRect, displayColorSpaceImage); + + CGImageRelease(img); + CGImageRelease(displayColorSpaceImage); + + rCGContextHolder.restoreState(); + } + else + { + SAL_WARN_IF( !mpFrame->mbInitShow, "vcl", "UpdateWindow called on uneligible graphics" ); + } +} + +/* 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..c06ba33c5 --- /dev/null +++ b/vcl/quartz/salvd.cxx @@ -0,0 +1,303 @@ +/* -*- 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 <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* pGraphics, + long &nDX, 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* >( pGraphics ), + 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* >( pGraphics ), + nDX, nDY, eFormat, pData )); +#endif +} + +AquaSalVirtualDevice::AquaSalVirtualDevice( AquaSalGraphics* pGraphic, long &nDX, 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<long>(aSize.width); + nDY = static_cast<long>(aSize.height); + } + else + { + nDX = 0; + nDY = 0; + } + + mpGraphics->SetVirDevGraphics(maLayer, pData->rCGContext); + } + else + { + // create empty new virtual device + mbForeignContext = false; // the mxContext is created within VCL + mpGraphics = new AquaSalGraphics(); // never fails + switch (eFormat) + { + case DeviceFormat::BITMASK: + mnBitmapDepth = 1; + break; +#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( nullptr, nullptr ); + delete mpGraphics; + mpGraphics = nullptr; + } + Destroy(); +} + +void AquaSalVirtualDevice::Destroy() +{ + SAL_INFO( "vcl.virdev", "AquaSalVirtualDevice::Destroy() this=" << this << " mbForeignContext=" << mbForeignContext ); + + if( mbForeignContext ) + { + // Do not delete mxContext that we have received from outside VCL + maLayer.set(nullptr); + return; + } + + if (maLayer.isSet()) + { + if( mpGraphics ) + { + mpGraphics->SetVirDevGraphics(nullptr, nullptr); + } + CGLayerRelease(maLayer.get()); + maLayer.set(nullptr); + } + + if (maBitmapContext.isSet()) + { + void* pRawData = CGBitmapContextGetData(maBitmapContext.get()); + std::free(pRawData); + CGContextRelease(maBitmapContext.get()); + maBitmapContext.set(nullptr); + } +} + +SalGraphics* AquaSalVirtualDevice::AcquireGraphics() +{ + if( mbGraphicsUsed || !mpGraphics ) + { + return nullptr; + } + mbGraphicsUsed = true; + return mpGraphics; +} + +void AquaSalVirtualDevice::ReleaseGraphics( SalGraphics* ) +{ + mbGraphicsUsed = false; +} + +bool AquaSalVirtualDevice::SetSize( long nDX, long nDY ) +{ + SAL_INFO( "vcl.virdev", "AquaSalVirtualDevice::SetSize() this=" << this << + " (" << nDX << "x" << nDY << ") mbForeignContext=" << (mbForeignContext ? "YES" : "NO")); + + if( mbForeignContext ) + { + // Do not delete/resize mxContext that we have received from outside VCL + return true; + } + + if (maLayer.isSet()) + { + const CGSize aSize = CGLayerGetSize(maLayer.get()); + if( (nDX == aSize.width) && (nDY == aSize.height) ) + { + // Yay, we do not have to do anything :) + return true; + } + } + + Destroy(); + + mnWidth = nDX; + mnHeight = nDY; + + // create a Quartz layer matching to the intended virdev usage + CGContextHolder xCGContextHolder; + if( mnBitmapDepth && (mnBitmapDepth < 16) ) + { + mnBitmapDepth = 8; // TODO: are 1bit vdevs worth it? + const int nBytesPerRow = (mnBitmapDepth * nDX + 7) / 8; + + void* pRawData = std::malloc( nBytesPerRow * nDY ); + maBitmapContext.set(CGBitmapContextCreate( pRawData, nDX, nDY, + mnBitmapDepth, nBytesPerRow, + GetSalData()->mxGraySpace, kCGImageAlphaNone)); + xCGContextHolder = maBitmapContext; + } + else + { +#ifdef MACOSX + // default to a NSView target context + AquaSalFrame* pSalFrame = mpGraphics->getGraphicsFrame(); + if( !pSalFrame || !AquaSalFrame::isAlive( pSalFrame )) + { + pSalFrame = static_cast<AquaSalFrame*>( GetSalData()->mpInstance->anyFrame() ); + if ( pSalFrame ) + // update the frame reference + mpGraphics->setGraphicsFrame( pSalFrame ); + } + if( pSalFrame ) + { + // #i91990# + NSWindow* pNSWindow = pSalFrame->getNSWindow(); + if ( pNSWindow ) + { + NSGraphicsContext* pNSContext = [NSGraphicsContext graphicsContextWithWindow: pNSWindow]; + if( pNSContext ) + { + xCGContextHolder.set([pNSContext CGContext]); + } + } + } +#endif + + if (!xCGContextHolder.isSet()) + { + // assert(Application::IsBitmapRendering()); + mnBitmapDepth = 32; + + const int nBytesPerRow = (mnBitmapDepth * nDX) / 8; + void* pRawData = std::malloc( nBytesPerRow * nDY ); +#ifdef MACOSX + const int nFlags = kCGImageAlphaNoneSkipFirst; +#else + const int nFlags = kCGImageAlphaNoneSkipFirst | kCGImageByteOrder32Little; +#endif + maBitmapContext.set(CGBitmapContextCreate(pRawData, nDX, nDY, 8, nBytesPerRow, + GetSalData()->mxRGBSpace, nFlags)); + xCGContextHolder = maBitmapContext; + } + } + + SAL_WARN_IF(!xCGContextHolder.isSet(), "vcl.quartz", "No context"); + + const CGSize aNewSize = { static_cast<CGFloat>(nDX), static_cast<CGFloat>(nDY) }; + maLayer.set(CGLayerCreateWithContext(xCGContextHolder.get(), aNewSize, nullptr)); + + if (maLayer.isSet() && mpGraphics) + { + // get the matching Quartz context + CGContextRef xDrawContext = CGLayerGetContext( maLayer.get() ); + mpGraphics->SetVirDevGraphics(maLayer.get(), xDrawContext, mnBitmapDepth); + } + + return maLayer.isSet(); +} + +/* 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..0e0ac8f4d --- /dev/null +++ b/vcl/quartz/utils.cxx @@ -0,0 +1,161 @@ +/* -*- 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()]; +} + +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; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |