1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
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>
22 #include <osl/diagnose.h>
27 #include <o3tl/make_shared.hxx>
28 #include <tools/color.hxx>
29 #include <vcl/bitmap.hxx>
30 #include <vcl/BitmapAccessMode.hxx>
31 #include <vcl/BitmapBuffer.hxx>
32 #include <vcl/BitmapColor.hxx>
33 #include <vcl/BitmapPalette.hxx>
34 #include <vcl/ColorMask.hxx>
35 #include <vcl/Scanline.hxx>
37 #include <bitmap/bmpfast.hxx>
38 #include <quartz/salbmp.h>
39 #include <quartz/utils.h>
40 #include <bitmap/ScanlineTools.hxx>
43 #include <osx/saldata.hxx>
45 #include "saldatabasic.hxx"
48 const unsigned long k32BitRedColorMask
= 0x00ff0000;
49 const unsigned long k32BitGreenColorMask
= 0x0000ff00;
50 const unsigned long k32BitBlueColorMask
= 0x000000ff;
52 QuartzSalBitmap::QuartzSalBitmap()
53 : mxCachedImage( nullptr )
61 QuartzSalBitmap::~QuartzSalBitmap()
66 bool QuartzSalBitmap::Create( const Size
& rSize
, vcl::PixelFormat ePixelFormat
, const BitmapPalette
& rBitmapPalette
)
68 if (ePixelFormat
== vcl::PixelFormat::INVALID
)
71 maPalette
= rBitmapPalette
;
72 mnBits
= vcl::pixelFormatBitCount(ePixelFormat
);
73 mnWidth
= rSize
.Width();
74 mnHeight
= rSize
.Height();
75 return AllocateUserData();
78 bool QuartzSalBitmap::Create( const SalBitmap
& rSalBmp
)
80 vcl::PixelFormat ePixelFormat
= vcl::bitDepthToPixelFormat(rSalBmp
.GetBitCount());
81 return Create( rSalBmp
, ePixelFormat
);
84 bool QuartzSalBitmap::Create( const SalBitmap
& rSalBmp
, SalGraphics
* pGraphics
)
86 vcl::PixelFormat ePixelFormat
= vcl::PixelFormat::INVALID
;
88 ePixelFormat
= vcl::bitDepthToPixelFormat(pGraphics
->GetBitCount());
90 ePixelFormat
= vcl::bitDepthToPixelFormat(rSalBmp
.GetBitCount());
92 return Create( rSalBmp
, ePixelFormat
);
95 bool QuartzSalBitmap::Create( const SalBitmap
& rSalBmp
, vcl::PixelFormat eNewPixelFormat
)
97 const QuartzSalBitmap
& rSourceBitmap
= static_cast<const QuartzSalBitmap
&>(rSalBmp
);
99 if (eNewPixelFormat
!= vcl::PixelFormat::INVALID
&& rSourceBitmap
.m_pUserBuffer
)
101 mnBits
= vcl::pixelFormatBitCount(eNewPixelFormat
);
102 mnWidth
= rSourceBitmap
.mnWidth
;
103 mnHeight
= rSourceBitmap
.mnHeight
;
104 maPalette
= rSourceBitmap
.maPalette
;
106 if( AllocateUserData() )
108 ConvertBitmapData( mnWidth
, mnHeight
, mnBits
, mnBytesPerRow
, maPalette
,
109 m_pUserBuffer
.get(), rSourceBitmap
.mnBits
,
110 rSourceBitmap
.mnBytesPerRow
, rSourceBitmap
.maPalette
,
111 rSourceBitmap
.m_pUserBuffer
.get() );
118 bool QuartzSalBitmap::Create( const css::uno::Reference
< css::rendering::XBitmapCanvas
>& /*xBitmapCanvas*/,
119 Size
& /*rSize*/, bool /*bMask*/ )
124 void QuartzSalBitmap::Destroy()
129 void QuartzSalBitmap::doDestroy()
132 m_pUserBuffer
.reset();
135 void QuartzSalBitmap::DestroyContext()
139 CGImageRelease( mxCachedImage
);
140 mxCachedImage
= nullptr;
143 if (maGraphicContext
.isSet())
145 CGContextRelease(maGraphicContext
.get());
146 maGraphicContext
.set(nullptr);
147 m_pContextBuffer
.reset();
151 bool QuartzSalBitmap::CreateContext()
155 // prepare graphics context
156 // convert image from user input if available
157 const bool bSkipConversion
= !m_pUserBuffer
;
158 if( bSkipConversion
)
161 // default to RGBA color space
162 CGColorSpaceRef aCGColorSpace
= GetSalData()->mxRGBSpace
;
163 CGBitmapInfo aCGBmpInfo
= kCGImageAlphaNoneSkipFirst
;
165 // convert data into something accepted by CGBitmapContextCreate()
166 size_t bitsPerComponent
= 8;
167 sal_uInt32 nContextBytesPerRow
= mnBytesPerRow
;
170 // no conversion needed for truecolor
171 m_pContextBuffer
= m_pUserBuffer
;
173 else if( mnBits
== 8 && maPalette
.IsGreyPalette8Bit() )
175 // no conversion needed for grayscale
176 m_pContextBuffer
= m_pUserBuffer
;
177 aCGColorSpace
= GetSalData()->mxGraySpace
;
178 aCGBmpInfo
= kCGImageAlphaNone
;
179 bitsPerComponent
= mnBits
;
181 // TODO: is special handling for 1bit input buffers worth it?
184 // convert user data to 32 bit
185 nContextBytesPerRow
= mnWidth
<< 2;
188 m_pContextBuffer
= o3tl::make_shared_array
<sal_uInt8
>(mnHeight
* nContextBytesPerRow
);
190 if( !bSkipConversion
)
192 ConvertBitmapData( mnWidth
, mnHeight
,
193 32, nContextBytesPerRow
, maPalette
, m_pContextBuffer
.get(),
194 mnBits
, mnBytesPerRow
, maPalette
, m_pUserBuffer
.get() );
197 catch( const std::bad_alloc
& )
199 maGraphicContext
.set(nullptr);
203 if (m_pContextBuffer
)
205 maGraphicContext
.set(CGBitmapContextCreate(m_pContextBuffer
.get(), mnWidth
, mnHeight
,
206 bitsPerComponent
, nContextBytesPerRow
,
207 aCGColorSpace
, aCGBmpInfo
));
210 if (!maGraphicContext
.isSet())
211 m_pContextBuffer
.reset();
213 return maGraphicContext
.isSet();
216 bool QuartzSalBitmap::AllocateUserData()
220 if( mnWidth
&& mnHeight
)
226 case 1: mnBytesPerRow
= (mnWidth
+ 7) >> 3; break;
227 case 4: mnBytesPerRow
= (mnWidth
+ 1) >> 1; break;
228 case 8: mnBytesPerRow
= mnWidth
; break;
229 case 24: mnBytesPerRow
= (mnWidth
<< 1) + mnWidth
; break;
230 case 32: mnBytesPerRow
= mnWidth
<< 2; break;
232 assert(false && "vcl::QuartzSalBitmap::AllocateUserData(), illegal bitcount!");
237 if (mnBytesPerRow
!= 0 &&
238 mnBytesPerRow
<= std::numeric_limits
<sal_uInt32
>::max() / mnHeight
)
242 m_pUserBuffer
= o3tl::make_shared_array
<sal_uInt8
>(mnBytesPerRow
* mnHeight
);
245 catch (std::bad_alloc
&) {}
249 SAL_WARN( "vcl.quartz", "bad_alloc: " << mnWidth
<< "x" << mnHeight
<< " (" << mnBytesPerRow
* mnHeight
<< " bytes)");
250 m_pUserBuffer
.reset();
254 return bool(m_pUserBuffer
);
257 void QuartzSalBitmap::ConvertBitmapData( sal_uInt32 nWidth
, sal_uInt32 nHeight
,
258 sal_uInt16 nDestBits
, sal_uInt32 nDestBytesPerRow
,
259 const BitmapPalette
& rDestPalette
, sal_uInt8
* pDestData
,
260 sal_uInt16 nSrcBits
, sal_uInt32 nSrcBytesPerRow
,
261 const BitmapPalette
& rSrcPalette
, sal_uInt8
* pSrcData
)
264 if( (nDestBytesPerRow
== nSrcBytesPerRow
) &&
265 (nDestBits
== nSrcBits
) && ((nSrcBits
!= 8) || (rDestPalette
.operator==( rSrcPalette
))) )
267 // simple case, same format, so just copy
268 memcpy( pDestData
, pSrcData
, nHeight
* nDestBytesPerRow
);
272 // try accelerated conversion if possible
273 // TODO: are other truecolor conversions except BGR->ARGB worth it?
274 bool bConverted
= false;
275 if( (nSrcBits
== 24) && (nDestBits
== 32) )
277 // TODO: extend bmpfast.cxx with a method that can be directly used here
278 BitmapBuffer aSrcBuf
;
279 aSrcBuf
.mnFormat
= ScanlineFormat::N24BitTcBgr
;
280 aSrcBuf
.mpBits
= pSrcData
;
281 aSrcBuf
.mnBitCount
= nSrcBits
;
282 aSrcBuf
.mnScanlineSize
= nSrcBytesPerRow
;
283 BitmapBuffer aDstBuf
;
284 aDstBuf
.mnFormat
= ScanlineFormat::N32BitTcArgb
;
285 aDstBuf
.mpBits
= pDestData
;
286 aDstBuf
.mnBitCount
= nDestBits
;
287 aDstBuf
.mnScanlineSize
= nDestBytesPerRow
;
289 aSrcBuf
.mnWidth
= aDstBuf
.mnWidth
= nWidth
;
290 aSrcBuf
.mnHeight
= aDstBuf
.mnHeight
= nHeight
;
292 SalTwoRect
aTwoRects(0, 0, mnWidth
, mnHeight
, 0, 0, mnWidth
, mnHeight
);
293 bConverted
= ::ImplFastBitmapConversion( aDstBuf
, aSrcBuf
, aTwoRects
);
298 // TODO: this implementation is for clarity, not for speed
300 auto pTarget
= vcl::bitmap::getScanlineTransformer(nDestBits
, rDestPalette
);
301 auto pSource
= vcl::bitmap::getScanlineTransformer(nSrcBits
, rSrcPalette
);
303 if (pTarget
&& pSource
)
305 sal_uInt32 nY
= nHeight
;
308 pTarget
->startLine(pDestData
);
309 pSource
->startLine(pSrcData
);
311 sal_uInt32 nX
= nWidth
;
314 pTarget
->writePixel(pSource
->readPixel());
316 pSrcData
+= nSrcBytesPerRow
;
317 pDestData
+= nDestBytesPerRow
;
323 Size
QuartzSalBitmap::GetSize() const
325 return Size( mnWidth
, mnHeight
);
328 sal_uInt16
QuartzSalBitmap::GetBitCount() const
344 pal_entry
const aImplSalSysPalEntryAry
[ 16 ] =
353 { 0x80, 0x80, 0x80 },
354 { 0xC0, 0xC0, 0xC0 },
364 static const BitmapPalette
& GetDefaultPalette( int mnBits
, bool bMonochrome
)
367 return Bitmap::GetGreyPalette( 1U << mnBits
);
369 // at this point we should provide some kind of default palette
370 // since all other platforms do so, too.
371 static bool bDefPalInit
= false;
372 static BitmapPalette aDefPalette256
;
373 static BitmapPalette aDefPalette16
;
374 static BitmapPalette aDefPalette2
;
378 aDefPalette256
.SetEntryCount( 256 );
379 aDefPalette16
.SetEntryCount( 16 );
380 aDefPalette2
.SetEntryCount( 2 );
384 for( i
= 0; i
< 16; i
++ )
387 aDefPalette256
[i
] = BitmapColor( aImplSalSysPalEntryAry
[i
].mnRed
,
388 aImplSalSysPalEntryAry
[i
].mnGreen
,
389 aImplSalSysPalEntryAry
[i
].mnBlue
);
392 aDefPalette2
[0] = BitmapColor( 0, 0, 0 );
393 aDefPalette2
[1] = BitmapColor( 0xff, 0xff, 0xff );
395 // own palette (6/6/6)
396 const int DITHER_PAL_STEPS
= 6;
397 const sal_uInt8 DITHER_PAL_DELTA
= 51;
399 sal_uInt8 nRed
, nGreen
, nBlue
;
400 for( nB
=0, nBlue
=0; nB
< DITHER_PAL_STEPS
; nB
++, nBlue
+= DITHER_PAL_DELTA
)
402 for( nG
=0, nGreen
=0; nG
< DITHER_PAL_STEPS
; nG
++, nGreen
+= DITHER_PAL_DELTA
)
404 for( nR
=0, nRed
=0; nR
< DITHER_PAL_STEPS
; nR
++, nRed
+= DITHER_PAL_DELTA
)
406 aDefPalette256
[ i
] = BitmapColor( nRed
, nGreen
, nBlue
);
413 // now fill in appropriate palette
416 case 1: return aDefPalette2
;
417 case 4: return aDefPalette16
;
418 case 8: return aDefPalette256
;
422 const static BitmapPalette aEmptyPalette
;
423 return aEmptyPalette
;
426 BitmapBuffer
* QuartzSalBitmap::AcquireBuffer( BitmapAccessMode
/*nMode*/ )
428 // TODO: AllocateUserData();
432 BitmapBuffer
* pBuffer
= new BitmapBuffer
;
433 pBuffer
->mnWidth
= mnWidth
;
434 pBuffer
->mnHeight
= mnHeight
;
435 pBuffer
->maPalette
= maPalette
;
436 pBuffer
->mnScanlineSize
= mnBytesPerRow
;
437 pBuffer
->mpBits
= m_pUserBuffer
.get();
438 pBuffer
->mnBitCount
= mnBits
;
442 pBuffer
->mnFormat
= ScanlineFormat::N1BitMsbPal
;
445 pBuffer
->mnFormat
= ScanlineFormat::N8BitPal
;
448 pBuffer
->mnFormat
= ScanlineFormat::N24BitTcBgr
;
452 pBuffer
->mnFormat
= ScanlineFormat::N32BitTcArgb
;
453 ColorMaskElement
aRedMask(k32BitRedColorMask
);
454 aRedMask
.CalcMaskShift();
455 ColorMaskElement
aGreenMask(k32BitGreenColorMask
);
456 aGreenMask
.CalcMaskShift();
457 ColorMaskElement
aBlueMask(k32BitBlueColorMask
);
458 aBlueMask
.CalcMaskShift();
459 pBuffer
->maColorMask
= ColorMask(aRedMask
, aGreenMask
, aBlueMask
);
462 default: assert(false);
465 // some BitmapBuffer users depend on a complete palette
466 if( (mnBits
<= 8) && !maPalette
)
467 pBuffer
->maPalette
= GetDefaultPalette( mnBits
, true );
472 void QuartzSalBitmap::ReleaseBuffer( BitmapBuffer
* pBuffer
, BitmapAccessMode nMode
)
474 // invalidate graphic context if we have different data
475 if( nMode
== BitmapAccessMode::Write
)
477 maPalette
= pBuffer
->maPalette
;
478 if (maGraphicContext
.isSet())
482 InvalidateChecksum();
488 CGImageRef
QuartzSalBitmap::CreateCroppedImage( int nX
, int nY
, int nNewWidth
, int nNewHeight
) const
492 if (!maGraphicContext
.isSet())
494 if( !const_cast<QuartzSalBitmap
*>(this)->CreateContext() )
499 mxCachedImage
= CGBitmapContextCreateImage(maGraphicContext
.get());
502 CGImageRef xCroppedImage
= nullptr;
503 // short circuit if there is nothing to crop
504 if( !nX
&& !nY
&& (mnWidth
== nNewWidth
) && (mnHeight
== nNewHeight
) )
506 xCroppedImage
= mxCachedImage
;
507 CFRetain( xCroppedImage
);
511 nY
= mnHeight
- (nY
+ nNewHeight
); // adjust for y-mirrored context
512 const CGRect aCropRect
= { { static_cast<CGFloat
>(nX
), static_cast<CGFloat
>(nY
) }, { static_cast<CGFloat
>(nNewWidth
), static_cast<CGFloat
>(nNewHeight
) } };
513 xCroppedImage
= CGImageCreateWithImageInRect( mxCachedImage
, aCropRect
);
516 return xCroppedImage
;
519 static void CFRTLFree(void* /*info*/, const void* data
, size_t /*size*/)
521 std::free( const_cast<void*>(data
) );
524 CGImageRef
QuartzSalBitmap::CreateWithMask( const QuartzSalBitmap
& rMask
,
525 int nX
, int nY
, int nWidth
, int nHeight
) const
527 CGImageRef
xImage( CreateCroppedImage( nX
, nY
, nWidth
, nHeight
) );
531 CGImageRef xMask
= rMask
.CreateCroppedImage( nX
, nY
, nWidth
, nHeight
);
535 // CGImageCreateWithMask() only likes masks or greyscale images => convert if needed
536 // TODO: isolate in an extra method?
537 if( !CGImageIsMask(xMask
) || rMask
.GetBitCount() != 8)//(CGImageGetColorSpace(xMask) != GetSalData()->mxGraySpace) )
539 const CGRect xImageRect
=CGRectMake( 0, 0, nWidth
, nHeight
);//the rect has no offset
541 // create the alpha mask image fitting our image
542 // TODO: is caching the full mask or the subimage mask worth it?
543 int nMaskBytesPerRow
= ((nWidth
+ 3) & ~3);
544 void* pMaskMem
= std::malloc( nMaskBytesPerRow
* nHeight
);
545 CGContextRef xMaskContext
= CGBitmapContextCreate( pMaskMem
,
546 nWidth
, nHeight
, 8, nMaskBytesPerRow
, GetSalData()->mxGraySpace
, kCGImageAlphaNone
);
547 CGContextDrawImage( xMaskContext
, xImageRect
, xMask
);
549 CGDataProviderRef
xDataProvider( CGDataProviderCreateWithData( nullptr,
550 pMaskMem
, nHeight
* nMaskBytesPerRow
, &CFRTLFree
) );
552 static const CGFloat
* pDecode
= nullptr;
553 xMask
= CGImageMaskCreate( nWidth
, nHeight
, 8, 8, nMaskBytesPerRow
, xDataProvider
, pDecode
, false );
554 CFRelease( xDataProvider
);
555 CFRelease( xMaskContext
);
561 // combine image and alpha mask
562 CGImageRef xMaskedImage
= CGImageCreateWithMask( xImage
, xMask
);
568 /** creates an image from the given rectangle, replacing all black pixels
569 with nMaskColor and make all other full transparent */
570 CGImageRef
QuartzSalBitmap::CreateColorMask( int nX
, int nY
, int nWidth
,
571 int nHeight
, Color nMaskColor
) const
573 CGImageRef xMask
= nullptr;
574 if (m_pUserBuffer
&& (nX
+ nWidth
<= mnWidth
) && (nY
+ nHeight
<= mnHeight
))
576 const sal_uInt32 nDestBytesPerRow
= nWidth
<< 2;
577 std::unique_ptr
<sal_uInt32
[]> pMaskBuffer(new (std::nothrow
) sal_uInt32
[ nHeight
* nDestBytesPerRow
/ 4] );
578 sal_uInt32
* pDest
= pMaskBuffer
.get();
580 auto pSourcePixels
= vcl::bitmap::getScanlineTransformer(mnBits
, maPalette
);
582 if( pMaskBuffer
&& pSourcePixels
)
585 reinterpret_cast<sal_uInt8
*>(&nColor
)[0] = 0xff;
586 reinterpret_cast<sal_uInt8
*>(&nColor
)[1] = nMaskColor
.GetRed();
587 reinterpret_cast<sal_uInt8
*>(&nColor
)[2] = nMaskColor
.GetGreen();
588 reinterpret_cast<sal_uInt8
*>(&nColor
)[3] = nMaskColor
.GetBlue();
590 sal_uInt8
* pSource
= m_pUserBuffer
.get();
591 // First to nY on y-axis, as that is our starting point (sub-image)
593 pSource
+= nY
* mnBytesPerRow
;
598 pSourcePixels
->startLine( pSource
);
599 pSourcePixels
->skipPixel(nX
); // Skip on x axis to nX
600 sal_uInt32 x
= nWidth
;
603 *pDest
++ = (pSourcePixels
->readPixel() == 0) ? nColor
: 0;
605 pSource
+= mnBytesPerRow
;
608 CGDataProviderRef
xDataProvider( CGDataProviderCreateWithData(nullptr, pMaskBuffer
.release(), nHeight
* nDestBytesPerRow
, &CFRTLFree
) );
609 xMask
= CGImageCreate(nWidth
, nHeight
, 8, 32, nDestBytesPerRow
, GetSalData()->mxRGBSpace
, kCGImageAlphaPremultipliedFirst
, xDataProvider
, nullptr, true, kCGRenderingIntentDefault
);
610 CFRelease(xDataProvider
);
616 /** QuartzSalBitmap::GetSystemData Get platform native image data from existing image
618 * @param rData struct BitmapSystemData, defined in vcl/inc/bitmap.hxx
619 * @return true if successful
621 bool QuartzSalBitmap::GetSystemData( BitmapSystemData
& rData
)
625 if (!maGraphicContext
.isSet())
628 if (maGraphicContext
.isSet())
632 if ((CGBitmapContextGetBitsPerPixel(maGraphicContext
.get()) == 32) &&
633 (CGBitmapContextGetBitmapInfo(maGraphicContext
.get()) & kCGBitmapByteOrderMask
) != kCGBitmapByteOrder32Host
)
636 * We need to hack things because VCL does not use kCGBitmapByteOrder32Host, while Cairo requires it.
638 * Not sure what the above comment means. We don't use Cairo on macOS or iOS.
640 * This whole if statement was originally (before 2011) inside #ifdef CAIRO. Did we use Cairo on Mac back then?
641 * Anyway, nowadays (since many years, I think) we don't, so should this if statement be dropped? Fun.
644 CGImageRef xImage
= CGBitmapContextCreateImage(maGraphicContext
.get());
646 // re-create the context with single change: include kCGBitmapByteOrder32Host flag.
647 CGContextHolder
aGraphicContextNew(CGBitmapContextCreate(CGBitmapContextGetData(maGraphicContext
.get()),
648 CGBitmapContextGetWidth(maGraphicContext
.get()),
649 CGBitmapContextGetHeight(maGraphicContext
.get()),
650 CGBitmapContextGetBitsPerComponent(maGraphicContext
.get()),
651 CGBitmapContextGetBytesPerRow(maGraphicContext
.get()),
652 CGBitmapContextGetColorSpace(maGraphicContext
.get()),
653 CGBitmapContextGetBitmapInfo(maGraphicContext
.get()) | kCGBitmapByteOrder32Host
));
654 CFRelease(maGraphicContext
.get());
656 // Needs to be flipped
657 aGraphicContextNew
.saveState();
658 CGContextTranslateCTM (aGraphicContextNew
.get(), 0, CGBitmapContextGetHeight(aGraphicContextNew
.get()));
659 CGContextScaleCTM (aGraphicContextNew
.get(), 1.0, -1.0);
661 CGContextDrawImage(aGraphicContextNew
.get(), CGRectMake( 0, 0, CGImageGetWidth(xImage
), CGImageGetHeight(xImage
)), xImage
);
664 CGContextRestoreGState( aGraphicContextNew
.get() );
665 CGImageRelease( xImage
);
666 maGraphicContext
= aGraphicContextNew
;
669 rData
.mnWidth
= mnWidth
;
670 rData
.mnHeight
= mnHeight
;
676 bool QuartzSalBitmap::ScalingSupported() const
681 bool QuartzSalBitmap::Scale( const double& /*rScaleX*/, const double& /*rScaleY*/, BmpScaleFlag
/*nScaleFlag*/ )
686 bool QuartzSalBitmap::Replace( const Color
& /*rSearchColor*/, const Color
& /*rReplaceColor*/, sal_uInt8
/*nTol*/ )
691 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */