/************************************************************** * * 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 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * *************************************************************/ #include "ctfonts.hxx" // ======================================================================= class CTLayout : public SalLayout { public: explicit CTLayout( const CTTextStyle* ); virtual ~CTLayout( void ); virtual bool LayoutText( ImplLayoutArgs& ); virtual void AdjustLayout( ImplLayoutArgs& ); virtual void DrawText( SalGraphics& ) const; virtual int GetNextGlyphs( int nLen, sal_GlyphId* pOutGlyphIds, Point& rPos, int&, sal_Int32* pGlyphAdvances, int* pCharIndexes ) const; virtual long GetTextWidth() const; virtual long FillDXArray( sal_Int32* pDXArray ) const; virtual int GetTextBreak( long nMaxWidth, long nCharExtra, int nFactor ) const; virtual void GetCaretPositions( int nArraySize, sal_Int32* pCaretXArray ) const; virtual bool GetGlyphOutlines( SalGraphics&, PolyPolyVector& ) const; virtual bool GetBoundRect( SalGraphics&, Rectangle& ) const; const ImplFontData* GetFallbackFontData( sal_GlyphId ) const; virtual void InitFont( void) const; virtual void MoveGlyph( int nStart, long nNewXPos ); virtual void DropGlyph( int nStart ); virtual void Simplify( bool bIsBase ); private: // CoreText specific objects CFMutableDictionaryRef mpStyleDict; CFAttributedStringRef mpAttrString; CTLineRef mpCTLine; int mnCharCount; // ==mnEndCharPos-mnMinCharPos int mnTrailingSpaceCount; double mfTrailingSpaceWidth; // preserves the width of stripped-off trailing space // to prevent overflows // font requests get size limited by downscaling huge fonts // in these cases the font scale becomes something bigger than 1.0 float mfFontScale; // TODO: does CoreText have a font size limit? CGFloat mfFontRotation; // text direction angle (in radians) CGFloat mfFontStretch; // <1.0: font gets squeezed, >1.0: font gets stretched // cached details about the resulting layout // mutable members since these details are all lazy initialized mutable double mfCachedWidth; // cached value of resulting typographical width // x-offset relative to layout origin // currently only used in RTL-layouts mutable long mnBaseAdv; }; // ======================================================================= CTLayout::CTLayout( const CTTextStyle* pTextStyle ) : mpStyleDict( pTextStyle->GetStyleDict() ) , mpAttrString( NULL ) , mpCTLine( NULL ) , mnCharCount( 0 ) , mnTrailingSpaceCount( 0 ) , mfTrailingSpaceWidth( 0.0 ) , mfFontScale( pTextStyle->mfFontScale ) , mfFontRotation( pTextStyle->mfFontRotation ) , mfFontStretch( pTextStyle->mfFontStretch ) , mfCachedWidth( -1 ) , mnBaseAdv( 0 ) { CFRetain( mpStyleDict ); } // ----------------------------------------------------------------------- CTLayout::~CTLayout() { if( mpCTLine ) CFRelease( mpCTLine ); if( mpAttrString ) CFRelease( mpAttrString ); CFRelease( mpStyleDict ); } // ----------------------------------------------------------------------- bool CTLayout::LayoutText( ImplLayoutArgs& rArgs ) { // release an eventual older layout if( mpAttrString ) CFRelease( mpAttrString ); mpAttrString = NULL; if( mpCTLine ) CFRelease( mpCTLine ); mpCTLine = NULL; // initialize the new layout SalLayout::AdjustLayout( rArgs ); mnCharCount = mnEndCharPos - mnMinCharPos; // short circuit if there is nothing to do if( mnCharCount <= 0 ) return false; // prepare the string to be layouted by CoreText CFStringRef aCFText = CFStringCreateWithCharactersNoCopy( NULL, rArgs.mpStr + mnMinCharPos, mnCharCount, kCFAllocatorNull ); // #i124375# force soft-hyphen visibility to meet the expectations of Writer+EditEngine if( CFStringFind( aCFText, (CFStringRef)@"\u00AD", 0).length > 0 ) { NSString* pDashStr = [(NSString*)aCFText stringByReplacingOccurrencesOfString: @"\u00AD" withString: @"-"]; aCFText = CFStringCreateCopy( NULL, (CFStringRef)pDashStr ); } // create the CoreText line layout using the requested text style mpAttrString = CFAttributedStringCreate( NULL, aCFText, mpStyleDict ); mpCTLine = CTLineCreateWithAttributedString( mpAttrString ); CFRelease( aCFText); // get info about trailing whitespace to prepare for text justification in AdjustLayout() mnTrailingSpaceCount = 0; for( int i = mnEndCharPos; --i >= mnMinCharPos; ++mnTrailingSpaceCount ) if( !IsSpacingGlyph( rArgs.mpStr[i] | GF_ISCHAR ) && (rArgs.mpStr[i] != 0x00A0) ) break; return true; } // ----------------------------------------------------------------------- void CTLayout::AdjustLayout( ImplLayoutArgs& rArgs ) { if( !mpCTLine) return; int nPixelWidth = rArgs.mnLayoutWidth; if( rArgs.mpDXArray ) { // for now we are only interested in the layout width // TODO: use all mpDXArray elements for layouting nPixelWidth = rArgs.mpDXArray[ mnCharCount-1 ]; } else if( !nPixelWidth ) // short-circuit if there is nothing to adjust return; // short-circuit when justifying an all-whitespace string if( mnTrailingSpaceCount >= mnCharCount) { mfCachedWidth = nPixelWidth / mfFontScale; return; } // return early if there is nothing to do if( nPixelWidth <= 0 ) return; // HACK: justification requests which change the width by just one pixel are probably // #i86038# introduced by lossy conversions between integer based coordinate system const int nOrigWidth = GetTextWidth(); if( (nOrigWidth >= nPixelWidth-1) && (nOrigWidth <= nPixelWidth+1) ) return; // if the text to be justified has whitespace in it then // - Writer goes crazy with its HalfSpace magic // - CoreText handles spaces specially (in particular at the text end) if( mnTrailingSpaceCount ) { int nTrailingSpaceWidth = 0; if( rArgs.mpDXArray) { const int nFullPixWidth = nPixelWidth; nPixelWidth = rArgs.mpDXArray[ mnCharCount-1-mnTrailingSpaceCount ]; nTrailingSpaceWidth = nFullPixWidth - nPixelWidth; mfTrailingSpaceWidth = nTrailingSpaceWidth; } else { if( mfTrailingSpaceWidth <= 0.0 ) mfTrailingSpaceWidth = CTLineGetTrailingWhitespaceWidth( mpCTLine ); nTrailingSpaceWidth = rint( mfTrailingSpaceWidth ); nPixelWidth -= nTrailingSpaceWidth; } if( nPixelWidth <= 0 ) return; // recreate the CoreText line layout without trailing spaces CFRelease( mpCTLine ); CFStringRef aCFText = CFStringCreateWithCharactersNoCopy( NULL, rArgs.mpStr + mnMinCharPos, mnCharCount - mnTrailingSpaceCount, kCFAllocatorNull ); CFAttributedStringRef pAttrStr = CFAttributedStringCreate( NULL, aCFText, mpStyleDict ); mpCTLine = CTLineCreateWithAttributedString( pAttrStr ); CFRelease( aCFText); CFRelease( pAttrStr ); // in RTL-layouts trailing spaces are leftmost // TODO: use BiDi-algorithm to thoroughly check this assumption if( rArgs.mnFlags & SAL_LAYOUT_BIDI_RTL) mnBaseAdv = nTrailingSpaceWidth; } const double fAdjustedWidth = nPixelWidth / mfFontScale; CTLineRef pNewCTLine = CTLineCreateJustifiedLine( mpCTLine, 1.0, fAdjustedWidth ); if( !pNewCTLine ) { // CTLineCreateJustifiedLine can and does fail // handle failure by keeping the unjustified layout // TODO: a better solution such as // - forcing glyph overlap // - changing the font size // - changing the CTM matrix return; } CFRelease( mpCTLine ); mpCTLine = pNewCTLine; mfCachedWidth = fAdjustedWidth + mfTrailingSpaceWidth; } // ----------------------------------------------------------------------- void CTLayout::DrawText( SalGraphics& rGraphics ) const { AquaSalGraphics& rAquaGraphics = static_cast(rGraphics); // short circuit if there is nothing to do if( (mnCharCount <= 0) || !rAquaGraphics.CheckContext() ) return; // the view is vertically flipped => flipped glyphs // so apply a temporary transformation that it flips back // also compensate if the font was size limited CGContextSaveGState( rAquaGraphics.mrContext ); CGContextScaleCTM( rAquaGraphics.mrContext, +mfFontScale, -mfFontScale ); CGContextSetShouldAntialias( rAquaGraphics.mrContext, !rAquaGraphics.mbNonAntialiasedText ); // set the text transformation (e.g. position) const Point aVclPos = GetDrawPosition( Point(mnBaseAdv,0) ); CGPoint aTextPos = { +aVclPos.X()/mfFontScale, -aVclPos.Y()/mfFontScale }; if( mfFontRotation != 0.0 ) { CGContextRotateCTM( rAquaGraphics.mrContext, +mfFontRotation ); const CGAffineTransform aInvMatrix = CGAffineTransformMakeRotation( -mfFontRotation ); aTextPos = CGPointApplyAffineTransform( aTextPos, aInvMatrix ); } CGContextSetTextPosition( rAquaGraphics.mrContext, aTextPos.x, aTextPos.y ); // request an update of the to-be-changed window area if( rAquaGraphics.IsWindowGraphics() ) { const CGRect aInkRect = CTLineGetImageBounds( mpCTLine, rAquaGraphics.mrContext ); const CGRect aRefreshRect = CGContextConvertRectToDeviceSpace( rAquaGraphics.mrContext, aInkRect ); rAquaGraphics.RefreshRect( aRefreshRect ); } // set the text color as fill color (see kCTForegroundColorFromContextAttributeName) CGContextSetFillColor( rAquaGraphics.mrContext, rAquaGraphics.maTextColor.AsArray() ); // draw the text CTLineDraw( mpCTLine, rAquaGraphics.mrContext ); // restore the original graphic context transformations CGContextRestoreGState( rAquaGraphics.mrContext ); } // ----------------------------------------------------------------------- int CTLayout::GetNextGlyphs( int nLen, sal_GlyphId* pOutGlyphIds, Point& rPos, int& nStart, sal_Int32* pGlyphAdvances, int* pCharIndexes ) const { if( !mpCTLine ) return 0; if( nStart < 0 ) // first glyph requested? nStart = 0; nLen = 1; // TODO: handle nLen>1 below // prepare to iterate over the glyph runs int nCount = 0; int nSubIndex = nStart; const DynCoreTextSyms& rCT = DynCoreTextSyms::get(); typedef std::vector CGGlyphVector; typedef std::vector CGPointVector; typedef std::vector CGSizeVector; typedef std::vector CFIndexVector; CGGlyphVector aCGGlyphVec; CGPointVector aCGPointVec; CGSizeVector aCGSizeVec; CFIndexVector aCFIndexVec; // TODO: iterate over cached layout CFArrayRef aGlyphRuns = rCT.LineGetGlyphRuns( mpCTLine ); const int nRunCount = CFArrayGetCount( aGlyphRuns ); for( int nRunIndex = 0; nRunIndex < nRunCount; ++nRunIndex ) { CTRunRef pGlyphRun = (CTRunRef)CFArrayGetValueAtIndex( aGlyphRuns, nRunIndex ); const CFIndex nGlyphsInRun = rCT.RunGetGlyphCount( pGlyphRun ); // skip to the first glyph run of interest if( nSubIndex >= nGlyphsInRun ) { nSubIndex -= nGlyphsInRun; continue; } const CFRange aFullRange = CFRangeMake( 0, nGlyphsInRun ); // get glyph run details const CGGlyph* pCGGlyphIdx = rCT.RunGetGlyphsPtr( pGlyphRun ); if( !pCGGlyphIdx ) { aCGGlyphVec.reserve( nGlyphsInRun ); CTRunGetGlyphs( pGlyphRun, aFullRange, &aCGGlyphVec[0] ); pCGGlyphIdx = &aCGGlyphVec[0]; } const CGPoint* pCGGlyphPos = rCT.RunGetPositionsPtr( pGlyphRun ); if( !pCGGlyphPos ) { aCGPointVec.reserve( nGlyphsInRun ); CTRunGetPositions( pGlyphRun, aFullRange, &aCGPointVec[0] ); pCGGlyphPos = &aCGPointVec[0]; } const CGSize* pCGGlyphAdvs = NULL; if( pGlyphAdvances) { pCGGlyphAdvs = rCT.RunGetAdvancesPtr( pGlyphRun ); if( !pCGGlyphAdvs) { aCGSizeVec.reserve( nGlyphsInRun ); CTRunGetAdvances( pGlyphRun, aFullRange, &aCGSizeVec[0] ); pCGGlyphAdvs = &aCGSizeVec[0]; } } const CFIndex* pCGGlyphStrIdx = NULL; if( pCharIndexes) { pCGGlyphStrIdx = rCT.RunGetStringIndicesPtr( pGlyphRun ); if( !pCGGlyphStrIdx) { aCFIndexVec.reserve( nGlyphsInRun ); CTRunGetStringIndices( pGlyphRun, aFullRange, &aCFIndexVec[0] ); pCGGlyphStrIdx = &aCFIndexVec[0]; } } // get the details for each interesting glyph // TODO: handle nLen>1 for(; (--nLen >= 0) && (nSubIndex < nGlyphsInRun); ++nSubIndex, ++nStart ) { // convert glyph details for VCL *(pOutGlyphIds++) = pCGGlyphIdx[ nSubIndex ]; if( pGlyphAdvances ) *(pGlyphAdvances++) = mfFontStretch * pCGGlyphAdvs[ nSubIndex ].width; if( pCharIndexes ) *(pCharIndexes++) = pCGGlyphStrIdx[ nSubIndex] + mnMinCharPos; if( !nCount++ ) { const CGPoint& rCurPos = pCGGlyphPos[ nSubIndex ]; rPos = GetDrawPosition( Point( mfFontScale * mfFontStretch * rCurPos.x, mfFontScale * rCurPos.y) ); } } nSubIndex = 0; // prepare for the next glyph run break; // TODO: handle nLen>1 } return nCount; } // ----------------------------------------------------------------------- long CTLayout::GetTextWidth() const { if( (mnCharCount <= 0) || !mpCTLine ) return 0; if( mfCachedWidth < 0.0 ) mfCachedWidth = CTLineGetTypographicBounds( mpCTLine, NULL, NULL, NULL ); const long nScaledWidth = lrint( mfFontScale * mfCachedWidth ); return nScaledWidth; } // ----------------------------------------------------------------------- long CTLayout::FillDXArray( sal_Int32* pDXArray ) const { // short circuit requests which don't need full details if( !pDXArray ) return GetTextWidth(); long nPixWidth = GetTextWidth(); if( pDXArray ) { // prepare the sub-pixel accurate logical-width array ::std::vector aWidthVector( mnCharCount ); if( mnTrailingSpaceCount && (mfTrailingSpaceWidth > 0.0) ) { const double fOneWidth = mfTrailingSpaceWidth / mnTrailingSpaceCount; for( int i = 1; i <= mnTrailingSpaceCount; ++i) aWidthVector[ mnCharCount - i ] = fOneWidth; } // measure advances in each glyph run CFArrayRef aGlyphRuns = CTLineGetGlyphRuns( mpCTLine ); const int nRunCount = CFArrayGetCount( aGlyphRuns ); typedef std::vector CGSizeVector; CGSizeVector aSizeVec; typedef std::vector CFIndexVector; CFIndexVector aIndexVec; for( int nRunIndex = 0; nRunIndex < nRunCount; ++nRunIndex ) { CTRunRef pGlyphRun = (CTRunRef)CFArrayGetValueAtIndex( aGlyphRuns, nRunIndex ); const CFIndex nGlyphCount = CTRunGetGlyphCount( pGlyphRun ); const CFRange aFullRange = CFRangeMake( 0, nGlyphCount ); aSizeVec.resize( nGlyphCount ); aIndexVec.resize( nGlyphCount ); CTRunGetAdvances( pGlyphRun, aFullRange, &aSizeVec[0] ); CTRunGetStringIndices( pGlyphRun, aFullRange, &aIndexVec[0] ); for( int i = 0; i != nGlyphCount; ++i ) { const int nRelIdx = aIndexVec[i]; aWidthVector[nRelIdx] += aSizeVec[i].width; } } // convert the sub-pixel accurate array into classic pDXArray integers float fWidthSum = 0.0; sal_Int32 nOldDX = 0; for( int i = 0; i < mnCharCount; ++i) { const sal_Int32 nNewDX = rint( fWidthSum += aWidthVector[i]); pDXArray[i] = nNewDX - nOldDX; nOldDX = nNewDX; } } return nPixWidth; } // ----------------------------------------------------------------------- int CTLayout::GetTextBreak( long nMaxWidth, long nCharExtra, int nFactor ) const { if( !mpCTLine ) return STRING_LEN; CTTypesetterRef aCTTypeSetter = CTTypesetterCreateWithAttributedString( mpAttrString ); CFIndex nBestGuess = (nCharExtra >= 0) ? 0 : mnCharCount; for( int i = 1; i <= mnCharCount; i *= 2 ) { // guess the target width considering char-extra expansion/condensation const long nTargetWidth = nMaxWidth - nBestGuess * nCharExtra; const double fCTMaxWidth = nTargetWidth / (nFactor * mfFontScale); // calculate the breaking index for the guessed target width const CFIndex nNewIndex = CTTypesetterSuggestClusterBreak( aCTTypeSetter, 0, fCTMaxWidth ); if( nNewIndex >= mnCharCount ) { CFRelease( aCTTypeSetter ); return STRING_LEN; } // check if the original extra-width guess was good if( !nCharExtra ) nBestGuess = nNewIndex; if( nBestGuess == nNewIndex ) break; // prepare another round for a different number of characters CFIndex nNewGuess = (nNewIndex + nBestGuess + 1) / 2; if( nNewGuess == nBestGuess ) nNewGuess += (nNewIndex > nBestGuess) ? +1 : -1; nBestGuess = nNewGuess; } // suggest the best fitting cluster break as breaking position CFRelease( aCTTypeSetter ); const int nIndex = nBestGuess + mnMinCharPos; return nIndex; } // ----------------------------------------------------------------------- void CTLayout::GetCaretPositions( int nMaxIndex, sal_Int32* pCaretXArray ) const { DBG_ASSERT( ((nMaxIndex>0)&&!(nMaxIndex&1)), "CTLayout::GetCaretPositions() : invalid number of caret pairs requested"); // initialize the caret positions for( int i = 0; i < nMaxIndex; ++i ) pCaretXArray[ i ] = -1; const DynCoreTextSyms& rCT = DynCoreTextSyms::get(); for( int n = 0; n <= mnCharCount; ++n ) { // measure the characters cursor position CGFloat fPos2 = -1; const CGFloat fPos1 = rCT.LineGetOffsetForStringIndex( mpCTLine, n, &fPos2 ); (void)fPos2; // TODO: split cursor at line direction change // update previous trailing position if( n > 0 ) pCaretXArray[ 2*n-1 ] = lrint( fPos1 * mfFontScale ); // update current leading position if( 2*n >= nMaxIndex ) break; pCaretXArray[ 2*n+0 ] = lrint( fPos1 * mfFontScale ); } } // ----------------------------------------------------------------------- bool CTLayout::GetBoundRect( SalGraphics& rGraphics, Rectangle& rVCLRect ) const { AquaSalGraphics& rAquaGraphics = static_cast(rGraphics); CGRect aMacRect = CTLineGetImageBounds( mpCTLine, rAquaGraphics.mrContext ); CGPoint aMacPos = CGContextGetTextPosition( rAquaGraphics.mrContext ); aMacRect.origin.x -= aMacPos.x; aMacRect.origin.y -= aMacPos.y; const Point aPos = GetDrawPosition( Point(mnBaseAdv, 0) ); // CoreText top-bottom are vertically flipped from a VCL aspect rVCLRect.Left() = aPos.X() + mfFontScale * aMacRect.origin.x; rVCLRect.Right() = aPos.X() + mfFontScale * (aMacRect.origin.x + aMacRect.size.width); rVCLRect.Bottom() = aPos.Y() - mfFontScale * aMacRect.origin.y; rVCLRect.Top() = aPos.Y() - mfFontScale * (aMacRect.origin.y + aMacRect.size.height); return true; } // ======================================================================= // glyph fallback is supported directly by Aqua // so methods used only by MultiSalLayout can be dummy implemented bool CTLayout::GetGlyphOutlines( SalGraphics&, PolyPolyVector& ) const { return false; } void CTLayout::InitFont() const {} void CTLayout::MoveGlyph( int /*nStart*/, long /*nNewXPos*/ ) {} void CTLayout::DropGlyph( int /*nStart*/ ) {} void CTLayout::Simplify( bool /*bIsBase*/ ) {} // get the ImplFontData for a glyph fallback font // for a glyphid that was returned by CTLayout::GetNextGlyphs() const ImplFontData* CTLayout::GetFallbackFontData( sal_GlyphId /*aGlyphId*/ ) const { #if 0 // check if any fallback fonts were needed if( !mpFallbackInfo ) return NULL; // check if the current glyph needs a fallback font int nFallbackLevel = (aGlyphId & GF_FONTMASK) >> GF_FONTSHIFT; if( !nFallbackLevel ) return NULL; pFallbackFont = mpFallbackInfo->GetFallbackFontData( nFallbackLevel ); #else // let CoreText's font cascading handle glyph fallback const ImplFontData* pFallbackFont = NULL; #endif return pFallbackFont; } // ======================================================================= SalLayout* CTTextStyle::GetTextLayout( void ) const { return new CTLayout( this); } // =======================================================================