Version 6.4.0.0.beta1, tag libreoffice-6.4.0.0.beta1
[LibreOffice.git] / vcl / quartz / ctfonts.cxx
blob252720a0aa4ec8e84ab8ad83746ea6536d8c7389
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include <sal/config.h>
21 #include <sal/log.hxx>
23 #include <basegfx/polygon/b2dpolygon.hxx>
24 #include <basegfx/matrix/b2dhommatrix.hxx>
26 #include <vcl/settings.hxx>
29 #include <quartz/ctfonts.hxx>
30 #include <impfont.hxx>
31 #ifdef MACOSX
32 #include <osx/saldata.hxx>
33 #include <osx/salinst.h>
34 #endif
35 #include <fontinstance.hxx>
36 #include <fontattributes.hxx>
37 #include <impglyphitem.hxx>
38 #include <PhysicalFontCollection.hxx>
39 #include <quartz/salgdi.h>
40 #include <quartz/utils.h>
41 #include <sallayout.hxx>
42 #include <hb-coretext.h>
44 static double toRadian(int nDegree)
46 return nDegree * (M_PI / 1800.0);
49 CoreTextStyle::CoreTextStyle(const PhysicalFontFace& rPFF, const FontSelectPattern& rFSP)
50 : LogicalFontInstance(rPFF, rFSP)
51 , mfFontStretch( 1.0 )
52 , mfFontRotation( 0.0 )
53 , mbFauxBold(false)
54 , mpStyleDict( nullptr )
56 double fScaledFontHeight = rFSP.mfExactHeight;
58 // convert font rotation to radian
59 mfFontRotation = toRadian(rFSP.mnOrientation);
61 // dummy matrix so we can use CGAffineTransformConcat() below
62 CGAffineTransform aMatrix = CGAffineTransformMakeTranslation(0, 0);
64 // handle font stretching if any
65 if( (rFSP.mnWidth != 0) && (rFSP.mnWidth != rFSP.mnHeight) )
67 mfFontStretch = float(rFSP.mnWidth) / rFSP.mnHeight;
68 aMatrix = CGAffineTransformConcat(aMatrix, CGAffineTransformMakeScale(mfFontStretch, 1.0F));
71 // create the style object for CoreText font attributes
72 static const CFIndex nMaxDictSize = 16; // TODO: does this really suffice?
73 mpStyleDict = CFDictionaryCreateMutable( nullptr, nMaxDictSize,
74 &kCFTypeDictionaryKeyCallBacks,
75 &kCFTypeDictionaryValueCallBacks );
77 CFBooleanRef pCFVertBool = rFSP.mbVertical ? kCFBooleanTrue : kCFBooleanFalse;
78 CFDictionarySetValue( mpStyleDict, kCTVerticalFormsAttributeName, pCFVertBool );
80 // fake bold
81 if ( (rFSP.GetWeight() >= WEIGHT_BOLD) &&
82 ((rPFF.GetWeight() < WEIGHT_SEMIBOLD) &&
83 (rPFF.GetWeight() != WEIGHT_DONTKNOW)) )
85 mbFauxBold = true;
88 // fake italic
89 if (((rFSP.GetItalic() == ITALIC_NORMAL) ||
90 (rFSP.GetItalic() == ITALIC_OBLIQUE)) &&
91 (rPFF.GetItalic() == ITALIC_NONE))
93 aMatrix = CGAffineTransformConcat(aMatrix, CGAffineTransformMake(1, 0, toRadian(120), 1, 0, 0));
96 CTFontDescriptorRef pFontDesc = reinterpret_cast<CTFontDescriptorRef>(rPFF.GetFontId());
97 CTFontRef pNewCTFont = CTFontCreateWithFontDescriptor( pFontDesc, fScaledFontHeight, &aMatrix );
98 CFDictionarySetValue( mpStyleDict, kCTFontAttributeName, pNewCTFont );
99 CFRelease( pNewCTFont);
102 CoreTextStyle::~CoreTextStyle()
104 if( mpStyleDict )
105 CFRelease( mpStyleDict );
108 void CoreTextStyle::GetFontMetric( ImplFontMetricDataRef const & rxFontMetric )
110 // get the matching CoreText font handle
111 // TODO: is it worth it to cache the CTFontRef in SetFont() and reuse it here?
112 CTFontRef aCTFontRef = static_cast<CTFontRef>(CFDictionaryGetValue( mpStyleDict, kCTFontAttributeName ));
114 rxFontMetric->ImplCalcLineSpacing(this);
116 // since ImplFontMetricData::mnWidth is only used for stretching/squeezing fonts
117 // setting this width to the pixel height of the fontsize is good enough
118 // it also makes the calculation of the stretch factor simple
119 rxFontMetric->SetWidth( lrint( CTFontGetSize( aCTFontRef ) * mfFontStretch) );
121 rxFontMetric->SetMinKashida(GetKashidaWidth());
124 bool CoreTextStyle::ImplGetGlyphBoundRect(sal_GlyphId nId, tools::Rectangle& rRect, bool bVertical) const
126 CGGlyph nCGGlyph = nId;
127 CTFontRef aCTFontRef = static_cast<CTFontRef>(CFDictionaryGetValue( mpStyleDict, kCTFontAttributeName ));
129 SAL_WNODEPRECATED_DECLARATIONS_PUSH //TODO: 10.11 kCTFontDefaultOrientation
130 const CTFontOrientation aFontOrientation = kCTFontDefaultOrientation; // TODO: horz/vert
131 SAL_WNODEPRECATED_DECLARATIONS_POP
132 CGRect aCGRect = CTFontGetBoundingRectsForGlyphs(aCTFontRef, aFontOrientation, &nCGGlyph, nullptr, 1);
134 // Apply font rotation to non-vertical glyphs.
135 if (mfFontRotation && !bVertical)
136 aCGRect = CGRectApplyAffineTransform(aCGRect, CGAffineTransformMakeRotation(mfFontRotation));
138 long xMin = floor(aCGRect.origin.x);
139 long yMin = floor(aCGRect.origin.y);
140 long xMax = ceil(aCGRect.origin.x + aCGRect.size.width);
141 long yMax = ceil(aCGRect.origin.y + aCGRect.size.height);
142 rRect = tools::Rectangle(xMin, -yMax, xMax, -yMin);
143 return true;
146 // callbacks from CTFontCreatePathForGlyph+CGPathApply for GetGlyphOutline()
147 struct GgoData { basegfx::B2DPolygon maPolygon; basegfx::B2DPolyPolygon* mpPolyPoly; };
149 static void MyCGPathApplierFunc( void* pData, const CGPathElement* pElement )
151 basegfx::B2DPolygon& rPolygon = static_cast<GgoData*>(pData)->maPolygon;
152 const int nPointCount = rPolygon.count();
154 switch( pElement->type )
156 case kCGPathElementCloseSubpath:
157 case kCGPathElementMoveToPoint:
158 if( nPointCount > 0 )
160 static_cast<GgoData*>(pData)->mpPolyPoly->append( rPolygon );
161 rPolygon.clear();
163 // fall through for kCGPathElementMoveToPoint:
164 if( pElement->type != kCGPathElementMoveToPoint )
166 break;
168 [[fallthrough]];
169 case kCGPathElementAddLineToPoint:
170 rPolygon.append( basegfx::B2DPoint( +pElement->points[0].x, -pElement->points[0].y ) );
171 break;
173 case kCGPathElementAddCurveToPoint:
174 rPolygon.append( basegfx::B2DPoint( +pElement->points[2].x, -pElement->points[2].y ) );
175 rPolygon.setNextControlPoint( nPointCount - 1,
176 basegfx::B2DPoint( pElement->points[0].x,
177 -pElement->points[0].y ) );
178 rPolygon.setPrevControlPoint( nPointCount + 0,
179 basegfx::B2DPoint( pElement->points[1].x,
180 -pElement->points[1].y ) );
181 break;
183 case kCGPathElementAddQuadCurveToPoint:
185 const basegfx::B2DPoint aStartPt = rPolygon.getB2DPoint( nPointCount-1 );
186 const basegfx::B2DPoint aCtrPt1( (aStartPt.getX() + 2 * pElement->points[0].x) / 3.0,
187 (aStartPt.getY() - 2 * pElement->points[0].y) / 3.0 );
188 const basegfx::B2DPoint aCtrPt2( (+2 * pElement->points[0].x + pElement->points[1].x) / 3.0,
189 (-2 * pElement->points[0].y - pElement->points[1].y) / 3.0 );
190 rPolygon.append( basegfx::B2DPoint( +pElement->points[1].x, -pElement->points[1].y ) );
191 rPolygon.setNextControlPoint( nPointCount-1, aCtrPt1 );
192 rPolygon.setPrevControlPoint( nPointCount+0, aCtrPt2 );
194 break;
198 bool CoreTextStyle::GetGlyphOutline(sal_GlyphId nId, basegfx::B2DPolyPolygon& rResult, bool) const
200 rResult.clear();
202 CGGlyph nCGGlyph = nId;
203 CTFontRef pCTFont = static_cast<CTFontRef>(CFDictionaryGetValue( mpStyleDict, kCTFontAttributeName ));
205 SAL_WNODEPRECATED_DECLARATIONS_PUSH
206 const CTFontOrientation aFontOrientation = kCTFontDefaultOrientation;
207 SAL_WNODEPRECATED_DECLARATIONS_POP
208 CGRect aCGRect = CTFontGetBoundingRectsForGlyphs(pCTFont, aFontOrientation, &nCGGlyph, nullptr, 1);
210 if (!CGRectIsNull(aCGRect) && CGRectIsEmpty(aCGRect))
212 // CTFontCreatePathForGlyph returns NULL for blank glyphs, but we want
213 // to return true for them.
214 return true;
217 CGPathRef xPath = CTFontCreatePathForGlyph( pCTFont, nCGGlyph, nullptr );
218 if (!xPath)
220 return false;
223 GgoData aGgoData;
224 aGgoData.mpPolyPoly = &rResult;
225 CGPathApply( xPath, static_cast<void*>(&aGgoData), MyCGPathApplierFunc );
226 #if 0 // TODO: does OSX ensure that the last polygon is always closed?
227 const CGPathElement aClosingElement = { kCGPathElementCloseSubpath, NULL };
228 MyCGPathApplierFunc( (void*)&aGgoData, &aClosingElement );
229 #endif
230 CFRelease( xPath );
232 return true;
235 static hb_blob_t* getFontTable(hb_face_t* /*face*/, hb_tag_t nTableTag, void* pUserData)
237 sal_uLong nLength = 0;
238 unsigned char* pBuffer = nullptr;
239 CoreTextFontFace* pFont = static_cast<CoreTextFontFace*>(pUserData);
240 nLength = pFont->GetFontTable(nTableTag, nullptr);
241 if (nLength > 0)
243 pBuffer = new unsigned char[nLength];
244 pFont->GetFontTable(nTableTag, pBuffer);
247 hb_blob_t* pBlob = nullptr;
248 if (pBuffer != nullptr)
249 pBlob = hb_blob_create(reinterpret_cast<const char*>(pBuffer), nLength, HB_MEMORY_MODE_READONLY,
250 pBuffer, [](void* data){ delete[] static_cast<unsigned char*>(data); });
251 return pBlob;
254 hb_font_t* CoreTextStyle::ImplInitHbFont()
256 hb_face_t* pHbFace = hb_face_create_for_tables(getFontTable, const_cast<PhysicalFontFace*>(GetFontFace()), nullptr);
258 return InitHbFont(pHbFace);
261 rtl::Reference<LogicalFontInstance> CoreTextFontFace::CreateFontInstance(const FontSelectPattern& rFSD) const
263 return new CoreTextStyle(*this, rFSD);
266 int CoreTextFontFace::GetFontTable( const char pTagName[5], unsigned char* pResultBuf ) const
268 SAL_WARN_IF( pTagName[4]!='\0', "vcl", "CoreTextFontFace::GetFontTable with invalid tagname!" );
270 const CTFontTableTag nTagCode = (pTagName[0]<<24) + (pTagName[1]<<16) + (pTagName[2]<<8) + (pTagName[3]<<0);
272 return GetFontTable(nTagCode, pResultBuf);
275 int CoreTextFontFace::GetFontTable(uint32_t nTagCode, unsigned char* pResultBuf ) const
277 // get the raw table length
278 CTFontDescriptorRef pFontDesc = reinterpret_cast<CTFontDescriptorRef>( GetFontId());
279 CTFontRef rCTFont = CTFontCreateWithFontDescriptor( pFontDesc, 0.0, nullptr);
280 const uint32_t opts( kCTFontTableOptionNoOptions );
281 CFDataRef pDataRef = CTFontCopyTable( rCTFont, nTagCode, opts);
282 CFRelease( rCTFont);
283 if( !pDataRef)
284 return 0;
286 const CFIndex nByteLength = CFDataGetLength( pDataRef);
288 // get the raw table data if requested
289 if( pResultBuf && (nByteLength > 0))
291 const CFRange aFullRange = CFRangeMake( 0, nByteLength);
292 CFDataGetBytes( pDataRef, aFullRange, reinterpret_cast<UInt8*>(pResultBuf));
295 CFRelease( pDataRef);
297 return static_cast<int>(nByteLength);
300 FontAttributes DevFontFromCTFontDescriptor( CTFontDescriptorRef pFD, bool* bFontEnabled )
302 // all CoreText fonts are device fonts that can rotate just fine
303 FontAttributes rDFA;
304 rDFA.SetQuality( 0 );
306 // reset the font attributes
307 rDFA.SetFamilyType( FAMILY_DONTKNOW );
308 rDFA.SetPitch( PITCH_VARIABLE );
309 rDFA.SetWidthType( WIDTH_NORMAL );
310 rDFA.SetWeight( WEIGHT_NORMAL );
311 rDFA.SetItalic( ITALIC_NONE );
312 rDFA.SetSymbolFlag( false );
314 // get font name
315 #ifdef MACOSX
316 const OUString aUILang = Application::GetSettings().GetUILanguageTag().getLanguage();
317 CFStringRef pUILang = CFStringCreateWithCharacters( kCFAllocatorDefault,
318 reinterpret_cast<UniChar const *>(aUILang.getStr()), aUILang.getLength() );
319 CFStringRef pLang = nullptr;
320 CFStringRef pFamilyName = static_cast<CFStringRef>(
321 CTFontDescriptorCopyLocalizedAttribute( pFD, kCTFontFamilyNameAttribute, &pLang ));
323 if ( !pLang || ( CFStringCompare( pUILang, pLang, 0 ) != kCFCompareEqualTo ))
325 if(pFamilyName)
327 CFRelease( pFamilyName );
329 pFamilyName = static_cast<CFStringRef>(CTFontDescriptorCopyAttribute( pFD, kCTFontFamilyNameAttribute ));
331 #else
332 // No "Application" on iOS. And it is unclear whether this code
333 // snippet will actually ever get invoked on iOS anyway. So just
334 // use the old code that uses a non-localized font name.
335 CFStringRef pFamilyName = (CFStringRef)CTFontDescriptorCopyAttribute( pFD, kCTFontFamilyNameAttribute );
336 #endif
338 rDFA.SetFamilyName( GetOUString( pFamilyName ) );
340 // get font style
341 CFStringRef pStyleName = static_cast<CFStringRef>(CTFontDescriptorCopyAttribute( pFD, kCTFontStyleNameAttribute ));
342 rDFA.SetStyleName( GetOUString( pStyleName ) );
344 // get font-enabled status
345 if( bFontEnabled )
347 int bEnabled = TRUE; // by default (and when we're on macOS < 10.6) it's "enabled"
348 CFNumberRef pEnabled = static_cast<CFNumberRef>(CTFontDescriptorCopyAttribute( pFD, kCTFontEnabledAttribute ));
349 CFNumberGetValue( pEnabled, kCFNumberIntType, &bEnabled );
350 *bFontEnabled = bEnabled;
353 // get font attributes
354 CFDictionaryRef pAttrDict = static_cast<CFDictionaryRef>(CTFontDescriptorCopyAttribute( pFD, kCTFontTraitsAttribute ));
356 if (bFontEnabled && *bFontEnabled)
358 // Ignore font formats not supported.
359 int nFormat;
360 CFNumberRef pFormat = static_cast<CFNumberRef>(CTFontDescriptorCopyAttribute(pFD, kCTFontFormatAttribute));
361 CFNumberGetValue(pFormat, kCFNumberIntType, &nFormat);
362 if (nFormat == kCTFontFormatUnrecognized || nFormat == kCTFontFormatPostScript || nFormat == kCTFontFormatBitmap)
364 SAL_INFO("vcl.fonts", "Ignoring font with unsupported format: " << rDFA.GetFamilyName());
365 *bFontEnabled = false;
367 CFRelease(pFormat);
370 // get symbolic trait
371 // TODO: use other traits such as MonoSpace/Condensed/Expanded or Vertical too
372 SInt64 nSymbolTrait = 0;
373 CFNumberRef pSymbolNum = nullptr;
374 if( CFDictionaryGetValueIfPresent( pAttrDict, kCTFontSymbolicTrait, reinterpret_cast<const void**>(&pSymbolNum) ) )
376 CFNumberGetValue( pSymbolNum, kCFNumberSInt64Type, &nSymbolTrait );
377 rDFA.SetSymbolFlag( (nSymbolTrait & kCTFontClassMaskTrait) == kCTFontSymbolicClass );
380 // get the font weight
381 double fWeight = 0;
382 CFNumberRef pWeightNum = static_cast<CFNumberRef>(CFDictionaryGetValue( pAttrDict, kCTFontWeightTrait ));
383 CFNumberGetValue( pWeightNum, kCFNumberDoubleType, &fWeight );
384 int nInt = WEIGHT_NORMAL;
386 // Special case fixes
388 // tdf#67744: Courier Std Medium is always bold. We get a kCTFontWeightTrait of 0.23 which
389 // surely must be wrong.
390 if (rDFA.GetFamilyName() == "Courier Std" &&
391 (rDFA.GetStyleName() == "Medium" || rDFA.GetStyleName() == "Medium Oblique") &&
392 fWeight > 0.2)
394 fWeight = 0;
397 // tdf#68889: Ditto for Gill Sans MT Pro. Here I can kinda understand it, maybe the
398 // kCTFontWeightTrait is intended to give a subjective "optical" impression of how the font
399 // looks, and Gill Sans MT Pro Medium is kinda heavy. But with the way LibreOffice uses fonts,
400 // we still should think of it as being "medium" weight.
401 if (rDFA.GetFamilyName() == "Gill Sans MT Pro" &&
402 (rDFA.GetStyleName() == "Medium" || rDFA.GetStyleName() == "Medium Italic") &&
403 fWeight > 0.2)
405 fWeight = 0;
408 if( fWeight > 0 )
410 nInt = rint(WEIGHT_NORMAL + fWeight * ((WEIGHT_BLACK - WEIGHT_NORMAL)/0.68));
411 if( nInt > WEIGHT_BLACK )
413 nInt = WEIGHT_BLACK;
416 else if( fWeight < 0 )
418 nInt = rint(WEIGHT_NORMAL + fWeight * ((WEIGHT_NORMAL - WEIGHT_THIN)/0.8));
419 if( nInt < WEIGHT_THIN )
421 nInt = WEIGHT_THIN;
424 rDFA.SetWeight( static_cast<FontWeight>(nInt) );
426 // get the font slant
427 double fSlant = 0;
428 CFNumberRef pSlantNum = static_cast<CFNumberRef>(CFDictionaryGetValue( pAttrDict, kCTFontSlantTrait ));
429 CFNumberGetValue( pSlantNum, kCFNumberDoubleType, &fSlant );
430 if( fSlant >= 0.035 )
432 rDFA.SetItalic( ITALIC_NORMAL );
434 // get width trait
435 double fWidth = 0;
436 CFNumberRef pWidthNum = static_cast<CFNumberRef>(CFDictionaryGetValue( pAttrDict, kCTFontWidthTrait ));
437 CFNumberGetValue( pWidthNum, kCFNumberDoubleType, &fWidth );
438 nInt = WIDTH_NORMAL;
440 if( fWidth > 0 )
442 nInt = rint( WIDTH_NORMAL + fWidth * ((WIDTH_ULTRA_EXPANDED - WIDTH_NORMAL)/0.4));
443 if( nInt > WIDTH_ULTRA_EXPANDED )
445 nInt = WIDTH_ULTRA_EXPANDED;
448 else if( fWidth < 0 )
450 nInt = rint( WIDTH_NORMAL + fWidth * ((WIDTH_NORMAL - WIDTH_ULTRA_CONDENSED)/0.5));
451 if( nInt < WIDTH_ULTRA_CONDENSED )
453 nInt = WIDTH_ULTRA_CONDENSED;
456 rDFA.SetWidthType( static_cast<FontWidth>(nInt) );
458 // release the attribute dict that we had copied
459 CFRelease( pAttrDict );
461 // TODO? also use the HEAD table if available to get more attributes
462 // CFDataRef CTFontCopyTable( CTFontRef, kCTFontTableHead, /*kCTFontTableOptionNoOptions*/kCTFontTableOptionExcludeSynthetic );
464 return rDFA;
467 static void fontEnumCallBack( const void* pValue, void* pContext )
469 CTFontDescriptorRef pFD = static_cast<CTFontDescriptorRef>(pValue);
471 bool bFontEnabled;
472 FontAttributes rDFA = DevFontFromCTFontDescriptor( pFD, &bFontEnabled );
474 if( bFontEnabled)
476 const sal_IntPtr nFontId = reinterpret_cast<sal_IntPtr>(pValue);
477 rtl::Reference<CoreTextFontFace> pFontData = new CoreTextFontFace( rDFA, nFontId );
478 SystemFontList* pFontList = static_cast<SystemFontList*>(pContext);
479 pFontList->AddFont( pFontData.get() );
483 SystemFontList::SystemFontList()
484 : mpCTFontCollection( nullptr )
485 , mpCTFontArray( nullptr )
488 SystemFontList::~SystemFontList()
490 maFontContainer.clear();
492 if( mpCTFontArray )
494 CFRelease( mpCTFontArray );
496 if( mpCTFontCollection )
498 CFRelease( mpCTFontCollection );
502 void SystemFontList::AddFont( CoreTextFontFace* pFontData )
504 sal_IntPtr nFontId = pFontData->GetFontId();
505 maFontContainer[ nFontId ] = pFontData;
508 void SystemFontList::AnnounceFonts( PhysicalFontCollection& rFontCollection ) const
510 for(const auto& rEntry : maFontContainer )
512 rFontCollection.Add( rEntry.second.get() );
516 CoreTextFontFace* SystemFontList::GetFontDataFromId( sal_IntPtr nFontId ) const
518 auto it = maFontContainer.find( nFontId );
519 if( it == maFontContainer.end() )
521 return nullptr;
523 return (*it).second.get();
526 bool SystemFontList::Init()
528 // enumerate available system fonts
529 static const int nMaxDictEntries = 8;
530 CFMutableDictionaryRef pCFDict = CFDictionaryCreateMutable( nullptr,
531 nMaxDictEntries,
532 &kCFTypeDictionaryKeyCallBacks,
533 &kCFTypeDictionaryValueCallBacks );
535 CFDictionaryAddValue( pCFDict, kCTFontCollectionRemoveDuplicatesOption, kCFBooleanTrue );
536 mpCTFontCollection = CTFontCollectionCreateFromAvailableFonts( pCFDict );
537 CFRelease( pCFDict );
538 mpCTFontArray = CTFontCollectionCreateMatchingFontDescriptors( mpCTFontCollection );
540 const int nFontCount = CFArrayGetCount( mpCTFontArray );
541 const CFRange aFullRange = CFRangeMake( 0, nFontCount );
542 CFArrayApplyFunction( mpCTFontArray, aFullRange, fontEnumCallBack, this );
544 return true;
547 SystemFontList* GetCoretextFontList()
549 SystemFontList* pList = new SystemFontList();
550 if( !pList->Init() )
552 delete pList;
553 return nullptr;
556 return pList;
559 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */