diff options
Diffstat (limited to '')
-rw-r--r-- | vcl/source/outdev/text.cxx | 2532 |
1 files changed, 2532 insertions, 0 deletions
diff --git a/vcl/source/outdev/text.cxx b/vcl/source/outdev/text.cxx new file mode 100644 index 000000000..5d8595abe --- /dev/null +++ b/vcl/source/outdev/text.cxx @@ -0,0 +1,2532 @@ +/* -*- 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 <memory> +#include <basegfx/matrix/b2dhommatrix.hxx> + +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <com/sun/star/linguistic2/LinguServiceManager.hpp> + +#include <comphelper/processfactory.hxx> +#include <osl/file.h> +#include <rtl/ustrbuf.hxx> +#include <sal/log.hxx> +#include <tools/lineend.hxx> +#include <tools/debug.hxx> +#include <vcl/ctrl.hxx> +#include <vcl/gdimtf.hxx> +#include <vcl/metaact.hxx> +#include <vcl/metric.hxx> +#include <vcl/textrectinfo.hxx> +#include <vcl/virdev.hxx> +#include <vcl/sysdata.hxx> +#include <vcl/unohelp.hxx> + +#include <ImplLayoutArgs.hxx> +#include <ImplOutDevData.hxx> +#include <drawmode.hxx> +#include <salgdi.hxx> +#include <svdata.hxx> +#include <textlayout.hxx> +#include <textlineinfo.hxx> +#include <impglyphitem.hxx> +#include <optional> +#include <TextLayoutCache.hxx> +#include <font/PhysicalFontFace.hxx> + +#define TEXT_DRAW_ELLIPSIS (DrawTextFlags::EndEllipsis | DrawTextFlags::PathEllipsis | DrawTextFlags::NewsEllipsis) + +void OutputDevice::SetLayoutMode( vcl::text::ComplexTextLayoutFlags nTextLayoutMode ) +{ + if( mpMetaFile ) + mpMetaFile->AddAction( new MetaLayoutModeAction( nTextLayoutMode ) ); + + mnTextLayoutMode = nTextLayoutMode; + + if( mpAlphaVDev ) + mpAlphaVDev->SetLayoutMode( nTextLayoutMode ); +} + +void OutputDevice::SetDigitLanguage( LanguageType eTextLanguage ) +{ + if( mpMetaFile ) + mpMetaFile->AddAction( new MetaTextLanguageAction( eTextLanguage ) ); + + meTextLanguage = eTextLanguage; + + if( mpAlphaVDev ) + mpAlphaVDev->SetDigitLanguage( eTextLanguage ); +} + +ImplMultiTextLineInfo::ImplMultiTextLineInfo() +{ +} + +ImplMultiTextLineInfo::~ImplMultiTextLineInfo() +{ +} + +void ImplMultiTextLineInfo::AddLine( const ImplTextLineInfo& rLine ) +{ + mvLines.push_back(rLine); +} + +void ImplMultiTextLineInfo::Clear() +{ + mvLines.clear(); +} + +void OutputDevice::ImplInitTextColor() +{ + DBG_TESTSOLARMUTEX(); + + if ( mbInitTextColor ) + { + mpGraphics->SetTextColor( GetTextColor() ); + mbInitTextColor = false; + } +} + +void OutputDevice::ImplDrawTextRect( tools::Long nBaseX, tools::Long nBaseY, + tools::Long nDistX, tools::Long nDistY, tools::Long nWidth, tools::Long nHeight ) +{ + tools::Long nX = nDistX; + tools::Long nY = nDistY; + + Degree10 nOrientation = mpFontInstance->mnOrientation; + if ( nOrientation ) + { + // Rotate rect without rounding problems for 90 degree rotations + if ( !(nOrientation % 900_deg10) ) + { + if ( nOrientation == 900_deg10 ) + { + tools::Long nTemp = nX; + nX = nY; + nY = -nTemp; + nTemp = nWidth; + nWidth = nHeight; + nHeight = nTemp; + nY -= nHeight; + } + else if ( nOrientation == 1800_deg10 ) + { + nX = -nX; + nY = -nY; + nX -= nWidth; + nY -= nHeight; + } + else /* ( nOrientation == 2700 ) */ + { + tools::Long nTemp = nX; + nX = -nY; + nY = nTemp; + nTemp = nWidth; + nWidth = nHeight; + nHeight = nTemp; + nX -= nWidth; + } + } + else + { + nX += nBaseX; + nY += nBaseY; + // inflate because polygons are drawn smaller + tools::Rectangle aRect( Point( nX, nY ), Size( nWidth+1, nHeight+1 ) ); + tools::Polygon aPoly( aRect ); + aPoly.Rotate( Point( nBaseX, nBaseY ), mpFontInstance->mnOrientation ); + ImplDrawPolygon( aPoly ); + return; + } + } + + nX += nBaseX; + nY += nBaseY; + mpGraphics->DrawRect( nX, nY, nWidth, nHeight, *this ); // original code + +} + +void OutputDevice::ImplDrawTextBackground( const SalLayout& rSalLayout ) +{ + const tools::Long nWidth = rSalLayout.GetTextWidth(); + const DevicePoint aBase = rSalLayout.DrawBase(); + const tools::Long nX = aBase.getX(); + const tools::Long nY = aBase.getY(); + + if ( mbLineColor || mbInitLineColor ) + { + mpGraphics->SetLineColor(); + mbInitLineColor = true; + } + mpGraphics->SetFillColor( GetTextFillColor() ); + mbInitFillColor = true; + + ImplDrawTextRect( nX, nY, 0, -(mpFontInstance->mxFontMetric->GetAscent() + mnEmphasisAscent), + nWidth, + mpFontInstance->mnLineHeight+mnEmphasisAscent+mnEmphasisDescent ); +} + +tools::Rectangle OutputDevice::ImplGetTextBoundRect( const SalLayout& rSalLayout ) const +{ + DevicePoint aPoint = rSalLayout.GetDrawPosition(); + tools::Long nX = aPoint.getX(); + tools::Long nY = aPoint.getY(); + + tools::Long nWidth = rSalLayout.GetTextWidth(); + tools::Long nHeight = mpFontInstance->mnLineHeight + mnEmphasisAscent + mnEmphasisDescent; + + nY -= mpFontInstance->mxFontMetric->GetAscent() + mnEmphasisAscent; + + if ( mpFontInstance->mnOrientation ) + { + tools::Long nBaseX = nX, nBaseY = nY; + if ( !(mpFontInstance->mnOrientation % 900_deg10) ) + { + tools::Long nX2 = nX+nWidth; + tools::Long nY2 = nY+nHeight; + + Point aBasePt( nBaseX, nBaseY ); + aBasePt.RotateAround( nX, nY, mpFontInstance->mnOrientation ); + aBasePt.RotateAround( nX2, nY2, mpFontInstance->mnOrientation ); + nWidth = nX2-nX; + nHeight = nY2-nY; + } + else + { + // inflate by +1+1 because polygons are drawn smaller + tools::Rectangle aRect( Point( nX, nY ), Size( nWidth+1, nHeight+1 ) ); + tools::Polygon aPoly( aRect ); + aPoly.Rotate( Point( nBaseX, nBaseY ), mpFontInstance->mnOrientation ); + return aPoly.GetBoundRect(); + } + } + + return tools::Rectangle( Point( nX, nY ), Size( nWidth, nHeight ) ); +} + +bool OutputDevice::ImplDrawRotateText( SalLayout& rSalLayout ) +{ + tools::Long nX = rSalLayout.DrawBase().getX(); + tools::Long nY = rSalLayout.DrawBase().getY(); + + tools::Rectangle aBoundRect; + rSalLayout.DrawBase() = DevicePoint( 0, 0 ); + rSalLayout.DrawOffset() = Point( 0, 0 ); + if (!rSalLayout.GetBoundRect(aBoundRect)) + { + // guess vertical text extents if GetBoundRect failed + tools::Long nRight = rSalLayout.GetTextWidth(); + tools::Long nTop = mpFontInstance->mxFontMetric->GetAscent() + mnEmphasisAscent; + tools::Long nHeight = mpFontInstance->mnLineHeight + mnEmphasisAscent + mnEmphasisDescent; + aBoundRect = tools::Rectangle( 0, -nTop, nRight, nHeight - nTop ); + } + + // cache virtual device for rotation + if (!mpOutDevData->mpRotateDev) + mpOutDevData->mpRotateDev = VclPtr<VirtualDevice>::Create(*this); + VirtualDevice* pVDev = mpOutDevData->mpRotateDev; + + // size it accordingly + if( !pVDev->SetOutputSizePixel( aBoundRect.GetSize() ) ) + return false; + + const vcl::font::FontSelectPattern& rPattern = mpFontInstance->GetFontSelectPattern(); + vcl::Font aFont( GetFont() ); + aFont.SetOrientation( 0_deg10 ); + aFont.SetFontSize( Size( rPattern.mnWidth, rPattern.mnHeight ) ); + pVDev->SetFont( aFont ); + pVDev->SetTextColor( COL_BLACK ); + pVDev->SetTextFillColor(); + if (!pVDev->InitFont()) + return false; + pVDev->ImplInitTextColor(); + + // draw text into upper left corner + rSalLayout.DrawBase().adjustX(-aBoundRect.Left()); + rSalLayout.DrawBase().adjustY(-aBoundRect.Top()); + rSalLayout.DrawText( *pVDev->mpGraphics ); + + Bitmap aBmp = pVDev->GetBitmap( Point(), aBoundRect.GetSize() ); + if ( aBmp.IsEmpty() || !aBmp.Rotate( mpFontInstance->mnOwnOrientation, COL_WHITE ) ) + return false; + + // calculate rotation offset + tools::Polygon aPoly( aBoundRect ); + aPoly.Rotate( Point(), mpFontInstance->mnOwnOrientation ); + Point aPoint = aPoly.GetBoundRect().TopLeft(); + aPoint += Point( nX, nY ); + + // mask output with text colored bitmap + GDIMetaFile* pOldMetaFile = mpMetaFile; + tools::Long nOldOffX = mnOutOffX; + tools::Long nOldOffY = mnOutOffY; + bool bOldMap = mbMap; + + mnOutOffX = 0; + mnOutOffY = 0; + mpMetaFile = nullptr; + EnableMapMode( false ); + + DrawMask( aPoint, aBmp, GetTextColor() ); + + EnableMapMode( bOldMap ); + mnOutOffX = nOldOffX; + mnOutOffY = nOldOffY; + mpMetaFile = pOldMetaFile; + + return true; +} + +void OutputDevice::ImplDrawTextDirect( SalLayout& rSalLayout, + bool bTextLines) +{ + if( mpFontInstance->mnOwnOrientation ) + if( ImplDrawRotateText( rSalLayout ) ) + return; + + auto nOldX = rSalLayout.DrawBase().getX(); + if( HasMirroredGraphics() ) + { + tools::Long w = IsVirtual() ? mnOutWidth : mpGraphics->GetGraphicsWidth(); + auto x = rSalLayout.DrawBase().getX(); + rSalLayout.DrawBase().setX( w - 1 - x ); + if( !IsRTLEnabled() ) + { + OutputDevice *pOutDevRef = this; + // mirror this window back + tools::Long devX = w-pOutDevRef->mnOutWidth-pOutDevRef->mnOutOffX; // re-mirrored mnOutOffX + rSalLayout.DrawBase().setX( devX + ( pOutDevRef->mnOutWidth - 1 - (rSalLayout.DrawBase().getX() - devX) ) ) ; + } + } + else if( IsRTLEnabled() ) + { + OutputDevice *pOutDevRef = this; + + // mirror this window back + tools::Long devX = pOutDevRef->mnOutOffX; // re-mirrored mnOutOffX + rSalLayout.DrawBase().setX( pOutDevRef->mnOutWidth - 1 - (rSalLayout.DrawBase().getX() - devX) + devX ); + } + + rSalLayout.DrawText( *mpGraphics ); + rSalLayout.DrawBase().setX( nOldX ); + + if( bTextLines ) + ImplDrawTextLines( rSalLayout, + maFont.GetStrikeout(), maFont.GetUnderline(), maFont.GetOverline(), + maFont.IsWordLineMode(), maFont.IsUnderlineAbove() ); + + // emphasis marks + if( maFont.GetEmphasisMark() & FontEmphasisMark::Style ) + ImplDrawEmphasisMarks( rSalLayout ); +} + +void OutputDevice::ImplDrawSpecialText( SalLayout& rSalLayout ) +{ + Color aOldColor = GetTextColor(); + Color aOldTextLineColor = GetTextLineColor(); + Color aOldOverlineColor = GetOverlineColor(); + FontRelief eRelief = maFont.GetRelief(); + + DevicePoint aOrigPos = rSalLayout.DrawBase(); + if ( eRelief != FontRelief::NONE ) + { + Color aReliefColor( COL_LIGHTGRAY ); + Color aTextColor( aOldColor ); + + Color aTextLineColor( aOldTextLineColor ); + Color aOverlineColor( aOldOverlineColor ); + + // we don't have an automatic color, so black is always drawn on white + if ( aTextColor == COL_BLACK ) + aTextColor = COL_WHITE; + if ( aTextLineColor == COL_BLACK ) + aTextLineColor = COL_WHITE; + if ( aOverlineColor == COL_BLACK ) + aOverlineColor = COL_WHITE; + + // relief-color is black for white text, in all other cases + // we set this to LightGray + // coverity[copy_paste_error: FALSE] - this is intentional + if ( aTextColor == COL_WHITE ) + aReliefColor = COL_BLACK; + SetTextLineColor( aReliefColor ); + SetOverlineColor( aReliefColor ); + SetTextColor( aReliefColor ); + ImplInitTextColor(); + + // calculate offset - for high resolution printers the offset + // should be greater so that the effect is visible + tools::Long nOff = 1; + nOff += mnDPIX/300; + + if ( eRelief == FontRelief::Engraved ) + nOff = -nOff; + rSalLayout.DrawOffset() += Point( nOff, nOff); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawOffset() -= Point( nOff, nOff); + + SetTextLineColor( aTextLineColor ); + SetOverlineColor( aOverlineColor ); + SetTextColor( aTextColor ); + ImplInitTextColor(); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + + SetTextLineColor( aOldTextLineColor ); + SetOverlineColor( aOldOverlineColor ); + + if ( aTextColor != aOldColor ) + { + SetTextColor( aOldColor ); + ImplInitTextColor(); + } + } + else + { + if ( maFont.IsShadow() ) + { + tools::Long nOff = 1 + ((mpFontInstance->mnLineHeight-24)/24); + if ( maFont.IsOutline() ) + nOff++; + SetTextLineColor(); + SetOverlineColor(); + if ( (GetTextColor() == COL_BLACK) + || (GetTextColor().GetLuminance() < 8) ) + SetTextColor( COL_LIGHTGRAY ); + else + SetTextColor( COL_BLACK ); + ImplInitTextColor(); + rSalLayout.DrawBase() += DevicePoint( nOff, nOff ); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() -= DevicePoint( nOff, nOff ); + SetTextColor( aOldColor ); + SetTextLineColor( aOldTextLineColor ); + SetOverlineColor( aOldOverlineColor ); + ImplInitTextColor(); + + if ( !maFont.IsOutline() ) + ImplDrawTextDirect( rSalLayout, mbTextLines ); + } + + if ( maFont.IsOutline() ) + { + rSalLayout.DrawBase() = aOrigPos + DevicePoint(-1,-1); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() = aOrigPos + DevicePoint(+1,+1); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() = aOrigPos + DevicePoint(-1,+0); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() = aOrigPos + DevicePoint(-1,+1); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() = aOrigPos + DevicePoint(+0,+1); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() = aOrigPos + DevicePoint(+0,-1); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() = aOrigPos + DevicePoint(+1,-1); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() = aOrigPos + DevicePoint(+1,+0); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + rSalLayout.DrawBase() = aOrigPos; + + SetTextColor( COL_WHITE ); + SetTextLineColor( COL_WHITE ); + SetOverlineColor( COL_WHITE ); + ImplInitTextColor(); + ImplDrawTextDirect( rSalLayout, mbTextLines ); + SetTextColor( aOldColor ); + SetTextLineColor( aOldTextLineColor ); + SetOverlineColor( aOldOverlineColor ); + ImplInitTextColor(); + } + } +} + +void OutputDevice::ImplDrawText( SalLayout& rSalLayout ) +{ + + if( mbInitClipRegion ) + InitClipRegion(); + if( mbOutputClipped ) + return; + if( mbInitTextColor ) + ImplInitTextColor(); + + rSalLayout.DrawBase() += DevicePoint(mnTextOffX, mnTextOffY); + + if( IsTextFillColor() ) + ImplDrawTextBackground( rSalLayout ); + + if( mbTextSpecial ) + ImplDrawSpecialText( rSalLayout ); + else + ImplDrawTextDirect( rSalLayout, mbTextLines ); +} + +tools::Long OutputDevice::ImplGetTextLines( const tools::Rectangle& rRect, const tools::Long nTextHeight, + ImplMultiTextLineInfo& rLineInfo, + tools::Long nWidth, const OUString& rStr, + DrawTextFlags nStyle, const vcl::ITextLayout& _rLayout ) +{ + SAL_WARN_IF( nWidth <= 0, "vcl", "ImplGetTextLines: nWidth <= 0!" ); + + if ( nWidth <= 0 ) + nWidth = 1; + + rLineInfo.Clear(); + if (rStr.isEmpty()) + return 0; + + const bool bClipping = (nStyle & DrawTextFlags::Clip) && !(nStyle & DrawTextFlags::EndEllipsis); + + tools::Long nMaxLineWidth = 0; + const bool bHyphenate = (nStyle & DrawTextFlags::WordBreakHyphenation) == DrawTextFlags::WordBreakHyphenation; + css::uno::Reference< css::linguistic2::XHyphenator > xHyph; + if (bHyphenate) + { + // get service provider + css::uno::Reference<css::uno::XComponentContext> xContext(comphelper::getProcessComponentContext()); + css::uno::Reference<css::linguistic2::XLinguServiceManager2> xLinguMgr = css::linguistic2::LinguServiceManager::create(xContext); + xHyph = xLinguMgr->getHyphenator(); + } + + css::uno::Reference<css::i18n::XBreakIterator> xBI; + sal_Int32 nPos = 0; + sal_Int32 nLen = rStr.getLength(); + sal_Int32 nCurrentTextY = 0; + while ( nPos < nLen ) + { + sal_Int32 nBreakPos = nPos; + + while ( ( nBreakPos < nLen ) && ( rStr[ nBreakPos ] != '\r' ) && ( rStr[ nBreakPos ] != '\n' ) ) + nBreakPos++; + + tools::Long nLineWidth = _rLayout.GetTextWidth( rStr, nPos, nBreakPos-nPos ); + if ( ( nLineWidth > nWidth ) && ( nStyle & DrawTextFlags::WordBreak ) ) + { + if ( !xBI.is() ) + xBI = vcl::unohelper::CreateBreakIterator(); + + if ( xBI.is() ) + { + nBreakPos = ImplBreakLinesWithIterator(nWidth, rStr, _rLayout, xHyph, xBI, bHyphenate, nPos, nBreakPos); + nLineWidth = _rLayout.GetTextWidth(rStr, nPos, nBreakPos - nPos); + } + else + // fallback to something really simple + nBreakPos = ImplBreakLinesSimple(nWidth, rStr, _rLayout, nPos, nBreakPos, nLineWidth); + } + + if ( nLineWidth > nMaxLineWidth ) + nMaxLineWidth = nLineWidth; + + rLineInfo.AddLine( ImplTextLineInfo( nLineWidth, nPos, nBreakPos-nPos ) ); + + if ( nBreakPos == nPos ) + nBreakPos++; + nPos = nBreakPos; + + if ( nPos < nLen && ( ( rStr[ nPos ] == '\r' ) || ( rStr[ nPos ] == '\n' ) ) ) + { + nPos++; + // CR/LF? + if ( ( nPos < nLen ) && ( rStr[ nPos ] == '\n' ) && ( rStr[ nPos-1 ] == '\r' ) ) + nPos++; + } + nCurrentTextY += nTextHeight; + if (bClipping && nCurrentTextY > rRect.GetHeight()) + break; + } + +#ifdef DBG_UTIL + for ( sal_Int32 nL = 0; nL < rLineInfo.Count(); nL++ ) + { + ImplTextLineInfo& rLine = rLineInfo.GetLine( nL ); + OUString aLine = rStr.copy( rLine.GetIndex(), rLine.GetLen() ); + SAL_WARN_IF( aLine.indexOf( '\r' ) != -1, "vcl", "ImplGetTextLines - Found CR!" ); + SAL_WARN_IF( aLine.indexOf( '\n' ) != -1, "vcl", "ImplGetTextLines - Found LF!" ); + } +#endif + + return nMaxLineWidth; +} + +sal_Int32 OutputDevice::ImplBreakLinesWithIterator(const tools::Long nWidth, const OUString& rStr, const vcl::ITextLayout& _rLayout, + const css::uno::Reference< css::linguistic2::XHyphenator >& xHyph, + const css::uno::Reference<css::i18n::XBreakIterator>& xBI, + const bool bHyphenate, + const sal_Int32 nPos, sal_Int32 nBreakPos) +{ + const css::lang::Locale& rDefLocale(Application::GetSettings().GetUILanguageTag().getLocale()); + sal_Int32 nSoftBreak = _rLayout.GetTextBreak( rStr, nWidth, nPos, nBreakPos - nPos ); + if (nSoftBreak == -1) + { + nSoftBreak = nPos; + } + SAL_WARN_IF( nSoftBreak >= nBreakPos, "vcl", "Break?!" ); + css::i18n::LineBreakHyphenationOptions aHyphOptions( xHyph, css::uno::Sequence <css::beans::PropertyValue>(), 1 ); + css::i18n::LineBreakUserOptions aUserOptions; + css::i18n::LineBreakResults aLBR = xBI->getLineBreak( rStr, nSoftBreak, rDefLocale, nPos, aHyphOptions, aUserOptions ); + nBreakPos = aLBR.breakIndex; + if ( nBreakPos <= nPos ) + nBreakPos = nSoftBreak; + if ( !bHyphenate ) + return nBreakPos; + + // Whether hyphen or not: Put the word after the hyphen through + // word boundary. + + // nMaxBreakPos the last char that fits into the line + // nBreakPos is the word's start + + // We run into a problem if the doc is so narrow, that a word + // is broken into more than two lines ... + if ( !xHyph.is() ) + return nBreakPos; + + css::i18n::Boundary aBoundary = xBI->getWordBoundary( rStr, nBreakPos, rDefLocale, css::i18n::WordType::DICTIONARY_WORD, true ); + sal_Int32 nWordStart = nPos; + sal_Int32 nWordEnd = aBoundary.endPos; + SAL_WARN_IF( nWordEnd <= nWordStart, "vcl", "ImpBreakLine: Start >= End?" ); + + sal_Int32 nWordLen = nWordEnd - nWordStart; + if ( ( nWordEnd < nSoftBreak ) || ( nWordLen <= 3 ) ) + return nBreakPos; + + // #104415# May happen, because getLineBreak may differ from getWordBoundary with DICTIONARY_WORD + // SAL_WARN_IF( nWordEnd < nMaxBreakPos, "vcl", "Hyph: Break?" ); + OUString aWord = rStr.copy( nWordStart, nWordLen ); + sal_Int32 nMinTrail = nWordEnd-nSoftBreak+1; //+1: Before the "broken off" char + css::uno::Reference< css::linguistic2::XHyphenatedWord > xHyphWord; + if (xHyph.is()) + xHyphWord = xHyph->hyphenate( aWord, rDefLocale, aWord.getLength() - nMinTrail, css::uno::Sequence< css::beans::PropertyValue >() ); + if (!xHyphWord.is()) + return nBreakPos; + + bool bAlternate = xHyphWord->isAlternativeSpelling(); + sal_Int32 _nWordLen = 1 + xHyphWord->getHyphenPos(); + + if ( ( _nWordLen < 2 ) || ( (nWordStart+_nWordLen) < 2 ) ) + return nBreakPos; + + if ( bAlternate ) + { + nBreakPos = nWordStart + _nWordLen; + return nBreakPos; + } + + + OUString aAlt( xHyphWord->getHyphenatedWord() ); + + // We can have two cases: + // 1) "packen" turns into "pak-ken" + // 2) "Schiffahrt" turns into "Schiff-fahrt" + + // In case 1 we need to replace a char + // In case 2 we add a char + + // Correct recognition is made harder by words such as + // "Schiffahrtsbrennesseln", as the Hyphenator splits all + // positions of the word and comes up with "Schifffahrtsbrennnesseln" + // Thus, we cannot infer the aWord from the AlternativeWord's + // index. + // TODO: The whole junk will be made easier by a function in + // the Hyphenator, as soon as AMA adds it. + sal_Int32 nAltStart = _nWordLen - 1; + sal_Int32 nTxtStart = nAltStart - (aAlt.getLength() - aWord.getLength()); + sal_Int32 nTxtEnd = nTxtStart; + sal_Int32 nAltEnd = nAltStart; + + // The area between nStart and nEnd is the difference + // between AlternativeString and OriginalString + while( nTxtEnd < aWord.getLength() && nAltEnd < aAlt.getLength() && + aWord[nTxtEnd] != aAlt[nAltEnd] ) + { + ++nTxtEnd; + ++nAltEnd; + } + + // If a char was added, we notice it now: + if( nAltEnd > nTxtEnd && nAltStart == nAltEnd && + aWord[ nTxtEnd ] == aAlt[nAltEnd] ) + { + ++nAltEnd; + ++nTxtStart; + ++nTxtEnd; + } + + SAL_WARN_IF( ( nAltEnd - nAltStart ) != 1, "vcl", "Alternate: Wrong assumption!" ); + + sal_Unicode cAlternateReplChar = 0; + if ( nTxtEnd > nTxtStart ) + cAlternateReplChar = aAlt[ nAltStart ]; + + nBreakPos = nWordStart + nTxtStart; + if ( cAlternateReplChar ) + nBreakPos++; + return nBreakPos; +} + +sal_Int32 OutputDevice::ImplBreakLinesSimple( const tools::Long nWidth, const OUString& rStr, + const vcl::ITextLayout& _rLayout, const sal_Int32 nPos, sal_Int32 nBreakPos, tools::Long& nLineWidth ) +{ + sal_Int32 nSpacePos = rStr.getLength(); + tools::Long nW = 0; + do + { + nSpacePos = rStr.lastIndexOf( ' ', nSpacePos ); + if( nSpacePos != -1 ) + { + if( nSpacePos > nPos ) + nSpacePos--; + nW = _rLayout.GetTextWidth( rStr, nPos, nSpacePos-nPos ); + } + } while( nW > nWidth ); + + if( nSpacePos != -1 ) + { + nBreakPos = nSpacePos; + nLineWidth = _rLayout.GetTextWidth( rStr, nPos, nBreakPos-nPos ); + if( nBreakPos < rStr.getLength()-1 ) + nBreakPos++; + } + return nBreakPos; +} + + +void OutputDevice::SetTextColor( const Color& rColor ) +{ + + Color aColor(vcl::drawmode::GetTextColor(rColor, GetDrawMode(), GetSettings().GetStyleSettings())); + + if ( mpMetaFile ) + mpMetaFile->AddAction( new MetaTextColorAction( aColor ) ); + + if ( maTextColor != aColor ) + { + maTextColor = aColor; + mbInitTextColor = true; + } + + if( mpAlphaVDev ) + mpAlphaVDev->SetTextColor( COL_BLACK ); +} + +void OutputDevice::SetTextFillColor() +{ + + if ( mpMetaFile ) + mpMetaFile->AddAction( new MetaTextFillColorAction( Color(), false ) ); + + if ( maFont.GetColor() != COL_TRANSPARENT ) { + maFont.SetFillColor( COL_TRANSPARENT ); + } + if ( !maFont.IsTransparent() ) + maFont.SetTransparent( true ); + + if( mpAlphaVDev ) + mpAlphaVDev->SetTextFillColor(); +} + +void OutputDevice::SetTextFillColor( const Color& rColor ) +{ + Color aColor(vcl::drawmode::GetFillColor(rColor, GetDrawMode(), GetSettings().GetStyleSettings())); + + if ( mpMetaFile ) + mpMetaFile->AddAction( new MetaTextFillColorAction( aColor, true ) ); + + if ( maFont.GetFillColor() != aColor ) + maFont.SetFillColor( aColor ); + if ( maFont.IsTransparent() != rColor.IsTransparent() ) + maFont.SetTransparent( rColor.IsTransparent() ); + + if( mpAlphaVDev ) + mpAlphaVDev->SetTextFillColor( COL_BLACK ); +} + +Color OutputDevice::GetTextFillColor() const +{ + if ( maFont.IsTransparent() ) + return COL_TRANSPARENT; + else + return maFont.GetFillColor(); +} + +void OutputDevice::SetTextAlign( TextAlign eAlign ) +{ + + if ( mpMetaFile ) + mpMetaFile->AddAction( new MetaTextAlignAction( eAlign ) ); + + if ( maFont.GetAlignment() != eAlign ) + { + maFont.SetAlignment( eAlign ); + mbNewFont = true; + } + + if( mpAlphaVDev ) + mpAlphaVDev->SetTextAlign( eAlign ); +} + +vcl::Region OutputDevice::GetOutputBoundsClipRegion() const +{ + return GetClipRegion(); +} + +const SalLayoutFlags eDefaultLayout = SalLayoutFlags::NONE; + +void OutputDevice::DrawText( const Point& rStartPt, const OUString& rStr, + sal_Int32 nIndex, sal_Int32 nLen, + std::vector< tools::Rectangle >* pVector, OUString* pDisplayText, + const SalLayoutGlyphs* pLayoutCache + ) +{ + assert(!is_double_buffered_window()); + + if( (nLen < 0) || (nIndex + nLen >= rStr.getLength())) + { + nLen = rStr.getLength() - nIndex; + } + + if (mpOutDevData->mpRecordLayout) + { + pVector = &mpOutDevData->mpRecordLayout->m_aUnicodeBoundRects; + pDisplayText = &mpOutDevData->mpRecordLayout->m_aDisplayText; + } + +#if OSL_DEBUG_LEVEL > 2 + SAL_INFO("vcl.gdi", "OutputDevice::DrawText(\"" << rStr << "\")"); +#endif + + if ( mpMetaFile ) + mpMetaFile->AddAction( new MetaTextAction( rStartPt, rStr, nIndex, nLen ) ); + if( pVector ) + { + vcl::Region aClip(GetOutputBoundsClipRegion()); + + if (mpOutDevData->mpRecordLayout) + { + mpOutDevData->mpRecordLayout->m_aLineIndices.push_back( mpOutDevData->mpRecordLayout->m_aDisplayText.getLength() ); + aClip.Intersect( mpOutDevData->maRecordRect ); + } + if( ! aClip.IsNull() ) + { + std::vector< tools::Rectangle > aTmp; + GetGlyphBoundRects( rStartPt, rStr, nIndex, nLen, aTmp ); + + bool bInserted = false; + for( std::vector< tools::Rectangle >::const_iterator it = aTmp.begin(); it != aTmp.end(); ++it, nIndex++ ) + { + bool bAppend = false; + + if( aClip.Overlaps( *it ) ) + bAppend = true; + else if( rStr[ nIndex ] == ' ' && bInserted ) + { + std::vector< tools::Rectangle >::const_iterator next = it; + ++next; + if( next != aTmp.end() && aClip.Overlaps( *next ) ) + bAppend = true; + } + + if( bAppend ) + { + pVector->push_back( *it ); + if( pDisplayText ) + *pDisplayText += OUStringChar(rStr[ nIndex ]); + bInserted = true; + } + } + } + else + { + GetGlyphBoundRects( rStartPt, rStr, nIndex, nLen, *pVector ); + if( pDisplayText ) + *pDisplayText += rStr.subView( nIndex, nLen ); + } + } + + if ( !IsDeviceOutputNecessary() || pVector ) + return; + + if(mpFontInstance) + // do not use cache with modified string + if(mpFontInstance->mpConversion) + pLayoutCache = nullptr; + + std::unique_ptr<SalLayout> pSalLayout = ImplLayout(rStr, nIndex, nLen, rStartPt, 0, {}, eDefaultLayout, nullptr, pLayoutCache); + if(pSalLayout) + { + ImplDrawText( *pSalLayout ); + } + + if( mpAlphaVDev ) + mpAlphaVDev->DrawText( rStartPt, rStr, nIndex, nLen, pVector, pDisplayText ); +} + +tools::Long OutputDevice::GetTextWidth( const OUString& rStr, sal_Int32 nIndex, sal_Int32 nLen, + vcl::text::TextLayoutCache const*const pLayoutCache, + SalLayoutGlyphs const*const pSalLayoutCache) const +{ + + tools::Long nWidth = GetTextArray( rStr, nullptr, nIndex, + nLen, pLayoutCache, pSalLayoutCache ); + + return nWidth; +} + +tools::Long OutputDevice::GetTextHeight() const +{ + if (!InitFont()) + return 0; + + tools::Long nHeight = mpFontInstance->mnLineHeight + mnEmphasisAscent + mnEmphasisDescent; + + if ( mbMap ) + nHeight = ImplDevicePixelToLogicHeight( nHeight ); + + return nHeight; +} + +float OutputDevice::approximate_char_width() const +{ + //note pango uses "The quick brown fox jumps over the lazy dog." for english + //and has a bunch of per-language strings which corresponds somewhat with + //makeRepresentativeText in include/svtools/sampletext.hxx + return GetTextWidth("aemnnxEM") / 8.0; +} + +float OutputDevice::approximate_digit_width() const +{ + return GetTextWidth("0123456789") / 10.0; +} + +void OutputDevice::DrawTextArray( const Point& rStartPt, const OUString& rStr, + o3tl::span<const sal_Int32> pDXAry, + sal_Int32 nIndex, sal_Int32 nLen, SalLayoutFlags flags, + const SalLayoutGlyphs* pSalLayoutCache ) +{ + assert(!is_double_buffered_window()); + + if( nLen < 0 || nIndex + nLen >= rStr.getLength() ) + { + nLen = rStr.getLength() - nIndex; + } + if ( mpMetaFile ) + mpMetaFile->AddAction( new MetaTextArrayAction( rStartPt, rStr, pDXAry, nIndex, nLen ) ); + + if ( !IsDeviceOutputNecessary() ) + return; + if( !mpGraphics && !AcquireGraphics() ) + return; + assert(mpGraphics); + if( mbInitClipRegion ) + InitClipRegion(); + if( mbOutputClipped ) + return; + + std::unique_ptr<SalLayout> pSalLayout = ImplLayout(rStr, nIndex, nLen, rStartPt, 0, pDXAry, flags, nullptr, pSalLayoutCache); + if( pSalLayout ) + { + ImplDrawText( *pSalLayout ); + } + + if( mpAlphaVDev ) + mpAlphaVDev->DrawTextArray( rStartPt, rStr, pDXAry, nIndex, nLen, flags ); +} + +tools::Long OutputDevice::GetTextArray( const OUString& rStr, std::vector<sal_Int32>* pDXAry, + sal_Int32 nIndex, sal_Int32 nLen, + vcl::text::TextLayoutCache const*const pLayoutCache, + SalLayoutGlyphs const*const pSalLayoutCache) const +{ + if( nIndex >= rStr.getLength() ) + return 0; // TODO: this looks like a buggy caller? + + if( nLen < 0 || nIndex + nLen >= rStr.getLength() ) + { + nLen = rStr.getLength() - nIndex; + } + + // do layout + std::unique_ptr<SalLayout> pSalLayout = ImplLayout(rStr, nIndex, nLen, + Point(0,0), 0, {}, eDefaultLayout, pLayoutCache, pSalLayoutCache); + if( !pSalLayout ) + { + // The caller expects this to init the elements of pDXAry. + // Adapting all the callers to check that GetTextArray succeeded seems + // too much work. + // Init here to 0 only in the (rare) error case, so that any missing + // element init in the happy case will still be found by tools, + // and hope that is sufficient. + if (pDXAry) + std::fill(pDXAry->begin(), pDXAry->end(), 0); + return 0; + } + +#if VCL_FLOAT_DEVICE_PIXEL + std::unique_ptr<std::vector<DeviceCoordinate>> xDXPixelArray; + if(pDXAry) + { + xDXPixelArray.reset(new std::vector<DeviceCoordinate>(nLen)); + } + std::vector<DeviceCoordinate>* pDXPixelArray = xDXPixelArray.get(); + DeviceCoordinate nWidth = pSalLayout->FillDXArray(pDXPixelArray); + + // convert virtual char widths to virtual absolute positions + if( pDXPixelArray ) + { + for( int i = 1; i < nLen; ++i ) + { + (*pDXPixelArray)[i] += (*pDXPixelArray)[i - 1]; + } + } + if( mbMap ) + { + if( pDXPixelArray ) + { + for( int i = 0; i < nLen; ++i ) + { + (*pDXPixelArray)[i] = ImplDevicePixelToLogicWidth((*pDXPixelArray)[i]); + } + } + nWidth = ImplDevicePixelToLogicWidth( nWidth ); + } + if(pDXAry) + { + pDXAry->resize(nLen); + for( int i = 0; i < nLen; ++i ) + { + (*pDXAry)[i] = basegfx::fround((*pDXPixelArray)[i]); + } + } + return basegfx::fround(nWidth); + +#else /* ! VCL_FLOAT_DEVICE_PIXEL */ + + tools::Long nWidth = pSalLayout->FillDXArray( pDXAry ); + + // convert virtual char widths to virtual absolute positions + if( pDXAry ) + for( int i = 1; i < nLen; ++i ) + (*pDXAry)[ i ] += (*pDXAry)[ i-1 ]; + + // convert from font units to logical units + if( mbMap ) + { + if( pDXAry ) + for( int i = 0; i < nLen; ++i ) + (*pDXAry)[i] = ImplDevicePixelToLogicWidth( (*pDXAry)[i] ); + nWidth = ImplDevicePixelToLogicWidth( nWidth ); + } + + return nWidth; +#endif /* VCL_FLOAT_DEVICE_PIXEL */ +} + +void OutputDevice::GetCaretPositions( const OUString& rStr, sal_Int32* pCaretXArray, + sal_Int32 nIndex, sal_Int32 nLen, + const SalLayoutGlyphs* pGlyphs ) const +{ + + if( nIndex >= rStr.getLength() ) + return; + if( nIndex+nLen >= rStr.getLength() ) + nLen = rStr.getLength() - nIndex; + + // layout complex text + std::unique_ptr<SalLayout> pSalLayout = ImplLayout(rStr, nIndex, nLen, Point(0, 0), 0, {}, + eDefaultLayout, nullptr, pGlyphs); + if( !pSalLayout ) + { + std::fill(pCaretXArray, pCaretXArray + nLen * 2, -1); + return; + } + + pSalLayout->GetCaretPositions( 2*nLen, pCaretXArray ); + tools::Long nWidth = pSalLayout->GetTextWidth(); + + // fixup unknown caret positions + int i; + for( i = 0; i < 2 * nLen; ++i ) + if( pCaretXArray[ i ] >= 0 ) + break; + tools::Long nXPos = (i < 2 * nLen) ? pCaretXArray[i] : -1; + for( i = 0; i < 2 * nLen; ++i ) + { + if( pCaretXArray[ i ] >= 0 ) + nXPos = pCaretXArray[ i ]; + else + pCaretXArray[ i ] = nXPos; + } + + // handle window mirroring + if( IsRTLEnabled() ) + { + for( i = 0; i < 2 * nLen; ++i ) + pCaretXArray[i] = nWidth - pCaretXArray[i] - 1; + } + + // convert from font units to logical units + if( mbMap ) + { + for( i = 0; i < 2*nLen; ++i ) + pCaretXArray[i] = ImplDevicePixelToLogicWidth( pCaretXArray[i] ); + } +} + +void OutputDevice::DrawStretchText( const Point& rStartPt, sal_uLong nWidth, + const OUString& rStr, + sal_Int32 nIndex, sal_Int32 nLen) +{ + assert(!is_double_buffered_window()); + + if( (nLen < 0) || (nIndex + nLen >= rStr.getLength())) + { + nLen = rStr.getLength() - nIndex; + } + + if ( mpMetaFile ) + mpMetaFile->AddAction( new MetaStretchTextAction( rStartPt, nWidth, rStr, nIndex, nLen ) ); + + if ( !IsDeviceOutputNecessary() ) + return; + + std::unique_ptr<SalLayout> pSalLayout = ImplLayout(rStr, nIndex, nLen, rStartPt, nWidth); + if( pSalLayout ) + { + ImplDrawText( *pSalLayout ); + } + + if( mpAlphaVDev ) + mpAlphaVDev->DrawStretchText( rStartPt, nWidth, rStr, nIndex, nLen ); +} + +vcl::text::ImplLayoutArgs OutputDevice::ImplPrepareLayoutArgs( OUString& rStr, + const sal_Int32 nMinIndex, const sal_Int32 nLen, + DeviceCoordinate nPixelWidth, + SalLayoutFlags nLayoutFlags, + vcl::text::TextLayoutCache const*const pLayoutCache) const +{ + assert(nMinIndex >= 0); + assert(nLen >= 0); + + // get string length for calculating extents + sal_Int32 nEndIndex = rStr.getLength(); + if( nMinIndex + nLen < nEndIndex ) + nEndIndex = nMinIndex + nLen; + + // don't bother if there is nothing to do + if( nEndIndex < nMinIndex ) + nEndIndex = nMinIndex; + + nLayoutFlags |= GetBiDiLayoutFlags( rStr, nMinIndex, nEndIndex ); + + if( !maFont.IsKerning() ) + nLayoutFlags |= SalLayoutFlags::DisableKerning; + if( maFont.GetKerning() & FontKerning::Asian ) + nLayoutFlags |= SalLayoutFlags::KerningAsian; + if( maFont.IsVertical() ) + nLayoutFlags |= SalLayoutFlags::Vertical; + + if( meTextLanguage ) //TODO: (mnTextLayoutMode & vcl::text::ComplexTextLayoutFlags::SubstituteDigits) + { + // disable character localization when no digits used + const sal_Unicode* pBase = rStr.getStr(); + const sal_Unicode* pStr = pBase + nMinIndex; + const sal_Unicode* pEnd = pBase + nEndIndex; + std::optional<OUStringBuffer> xTmpStr; + for( ; pStr < pEnd; ++pStr ) + { + // TODO: are there non-digit localizations? + if( (*pStr >= '0') && (*pStr <= '9') ) + { + // translate characters to local preference + sal_UCS4 cChar = GetLocalizedChar( *pStr, meTextLanguage ); + if( cChar != *pStr ) + { + if (!xTmpStr) + xTmpStr = OUStringBuffer(rStr); + // TODO: are the localized digit surrogates? + (*xTmpStr)[pStr - pBase] = cChar; + } + } + } + if (xTmpStr) + rStr = (*xTmpStr).makeStringAndClear(); + } + + // right align for RTL text, DRAWPOS_REVERSED, RTL window style + bool bRightAlign = bool(mnTextLayoutMode & vcl::text::ComplexTextLayoutFlags::BiDiRtl); + if( mnTextLayoutMode & vcl::text::ComplexTextLayoutFlags::TextOriginLeft ) + bRightAlign = false; + else if ( mnTextLayoutMode & vcl::text::ComplexTextLayoutFlags::TextOriginRight ) + bRightAlign = true; + // SSA: hack for western office, ie text get right aligned + // for debugging purposes of mirrored UI + bool bRTLWindow = IsRTLEnabled(); + bRightAlign ^= bRTLWindow; + if( bRightAlign ) + nLayoutFlags |= SalLayoutFlags::RightAlign; + + // set layout options + vcl::text::ImplLayoutArgs aLayoutArgs(rStr, nMinIndex, nEndIndex, nLayoutFlags, maFont.GetLanguageTag(), pLayoutCache); + + Degree10 nOrientation = mpFontInstance ? mpFontInstance->mnOrientation : 0_deg10; + aLayoutArgs.SetOrientation( nOrientation ); + + aLayoutArgs.SetLayoutWidth( nPixelWidth ); + + return aLayoutArgs; +} + +SalLayoutFlags OutputDevice::GetBiDiLayoutFlags( std::u16string_view rStr, + const sal_Int32 nMinIndex, + const sal_Int32 nEndIndex ) const +{ + SalLayoutFlags nLayoutFlags = SalLayoutFlags::NONE; + if( mnTextLayoutMode & vcl::text::ComplexTextLayoutFlags::BiDiRtl ) + nLayoutFlags |= SalLayoutFlags::BiDiRtl; + if( mnTextLayoutMode & vcl::text::ComplexTextLayoutFlags::BiDiStrong ) + nLayoutFlags |= SalLayoutFlags::BiDiStrong; + else if( !(mnTextLayoutMode & vcl::text::ComplexTextLayoutFlags::BiDiRtl) ) + { + // Disable Bidi if no RTL hint and only known LTR codes used. + bool bAllLtr = true; + for (sal_Int32 i = nMinIndex; i < nEndIndex; i++) + { + // [0x0000, 0x052F] are Latin, Greek and Cyrillic. + // [0x0370, 0x03FF] has a few holes as if Unicode 10.0.0, but + // hopefully no RTL character will be encoded there. + if (rStr[i] > 0x052F) + { + bAllLtr = false; + break; + } + } + if (bAllLtr) + nLayoutFlags |= SalLayoutFlags::BiDiStrong; + } + return nLayoutFlags; +} + +static OutputDevice::FontMappingUseData* fontMappingUseData = nullptr; + +static inline bool IsTrackingFontMappingUse() +{ + return fontMappingUseData != nullptr; +} + +static void TrackFontMappingUse( const vcl::Font& originalFont, const SalLayout* salLayout) +{ + assert(fontMappingUseData); + OUString originalName = originalFont.GetStyleName().isEmpty() + ? originalFont.GetFamilyName() + : originalFont.GetFamilyName() + "/" + originalFont.GetStyleName(); + std::vector<OUString> usedFontNames; + SalLayoutGlyphs glyphs = salLayout->GetGlyphs(); // includes all font fallbacks + int level = 0; + while( const SalLayoutGlyphsImpl* impl = glyphs.Impl(level++)) + { + const vcl::font::PhysicalFontFace* face = impl->GetFont()->GetFontFace(); + OUString name = face->GetStyleName().isEmpty() + ? face->GetFamilyName() + : face->GetFamilyName() + "/" + face->GetStyleName(); + usedFontNames.push_back( name ); + } + for( OutputDevice::FontMappingUseItem& item : *fontMappingUseData ) + { + if( item.mOriginalFont == originalName && item.mUsedFonts == usedFontNames ) + { + ++item.mCount; + return; + } + } + fontMappingUseData->push_back( { originalName, usedFontNames, 1 } ); +} + +void OutputDevice::StartTrackingFontMappingUse() +{ + delete fontMappingUseData; + fontMappingUseData = new FontMappingUseData; +} + +OutputDevice::FontMappingUseData OutputDevice::FinishTrackingFontMappingUse() +{ + if(!fontMappingUseData) + return {}; + FontMappingUseData ret = std::move( *fontMappingUseData ); + delete fontMappingUseData; + fontMappingUseData = nullptr; + return ret; +} + +std::unique_ptr<SalLayout> OutputDevice::ImplLayout(const OUString& rOrigStr, + sal_Int32 nMinIndex, sal_Int32 nLen, + const Point& rLogicalPos, tools::Long nLogicalWidth, + o3tl::span<const sal_Int32> pDXArray, SalLayoutFlags flags, + vcl::text::TextLayoutCache const* pLayoutCache, + const SalLayoutGlyphs* pGlyphs) const +{ + if (pGlyphs && !pGlyphs->IsValid()) + { + SAL_WARN("vcl", "Trying to setup invalid cached glyphs - falling back to relayout!"); + pGlyphs = nullptr; + } +#ifdef DBG_UTIL + if (pGlyphs) + { + for( int level = 0;; ++level ) + { + SalLayoutGlyphsImpl* glyphsImpl = pGlyphs->Impl(level); + if(glyphsImpl == nullptr) + break; + // It is allowed to reuse only glyphs created with SalLayoutFlags::GlyphItemsOnly. + // If the glyphs have already been used, the AdjustLayout() call below might have + // altered them (MultiSalLayout::ImplAdjustMultiLayout() drops glyphs that need + // fallback from the base layout, but then GenericSalLayout::LayoutText() + // would not know to call SetNeedFallback()). + assert(glyphsImpl->GetFlags() & SalLayoutFlags::GlyphItemsOnly); + } + } +#endif + + if (!InitFont()) + return nullptr; + + // check string index and length + if( -1 == nLen || nMinIndex + nLen > rOrigStr.getLength() ) + { + const sal_Int32 nNewLen = rOrigStr.getLength() - nMinIndex; + if( nNewLen <= 0 ) + return nullptr; + nLen = nNewLen; + } + + OUString aStr = rOrigStr; + + // convert from logical units to physical units + // recode string if needed + if( mpFontInstance->mpConversion ) { + mpFontInstance->mpConversion->RecodeString( aStr, 0, aStr.getLength() ); + pLayoutCache = nullptr; // don't use cache with modified string! + pGlyphs = nullptr; + } + + DeviceCoordinate nPixelWidth = static_cast<DeviceCoordinate>(nLogicalWidth); + if( nLogicalWidth && mbMap ) + { + nPixelWidth = LogicWidthToDeviceCoordinate( nLogicalWidth ); + } + + vcl::text::ImplLayoutArgs aLayoutArgs = ImplPrepareLayoutArgs( aStr, nMinIndex, nLen, + nPixelWidth, flags, pLayoutCache); + + bool bTextRenderModeForResolutionIndependentLayout(false); + DeviceCoordinate nEndGlyphCoord(0); + std::unique_ptr<DeviceCoordinate[]> xDXPixelArray; + std::unique_ptr<double[]> xNaturalDXPixelArray; + if( !pDXArray.empty() ) + { + DeviceCoordinate* pDXPixelArray(nullptr); + if (mbMap) + { + if (GetTextRenderModeForResolutionIndependentLayout()) + { + bTextRenderModeForResolutionIndependentLayout = true; + + // convert from logical units to font units using a temporary array + xNaturalDXPixelArray.reset(new double[nLen]); + + for (int i = 0; i < nLen; ++i) + xNaturalDXPixelArray[i] = ImplLogicWidthToDeviceFontWidth(pDXArray[i]); + + aLayoutArgs.SetAltNaturalDXArray(xNaturalDXPixelArray.get()); + nEndGlyphCoord = std::lround(xNaturalDXPixelArray[nLen - 1]); + } + else + { + // convert from logical units to font units using a temporary array + xDXPixelArray.reset(new DeviceCoordinate[nLen]); + pDXPixelArray = xDXPixelArray.get(); + // using base position for better rounding a.k.a. "dancing characters" + DeviceCoordinate nPixelXOfs2 = LogicWidthToDeviceCoordinate(rLogicalPos.X() * 2); + for( int i = 0; i < nLen; ++i ) + { + pDXPixelArray[i] = (LogicWidthToDeviceCoordinate((rLogicalPos.X() + pDXArray[i]) * 2) - nPixelXOfs2) / 2; + } + + aLayoutArgs.SetDXArray(pDXPixelArray); + nEndGlyphCoord = pDXPixelArray[nLen - 1]; + } + + } + else + { +#if VCL_FLOAT_DEVICE_PIXEL + xDXPixelArray.reset(new DeviceCoordinate[nLen]); + pDXPixelArray = xDXPixelArray.get(); + for( int i = 0; i < nLen; ++i ) + { + pDXPixelArray[i] = pDXArray[i]; + } +#else /* !VCL_FLOAT_DEVICE_PIXEL */ + pDXPixelArray = const_cast<DeviceCoordinate*>(pDXArray.data()); +#endif /* !VCL_FLOAT_DEVICE_PIXEL */ + + aLayoutArgs.SetDXArray(pDXPixelArray); + nEndGlyphCoord = pDXPixelArray[nLen - 1]; + } + } + + // get matching layout object for base font + std::unique_ptr<SalLayout> pSalLayout = mpGraphics->GetTextLayout(0); + + // layout text + if( pSalLayout && !pSalLayout->LayoutText( aLayoutArgs, pGlyphs ? pGlyphs->Impl(0) : nullptr ) ) + { + pSalLayout.reset(); + } + + if( !pSalLayout ) + return nullptr; + + pSalLayout->SetTextRenderModeForResolutionIndependentLayout(bTextRenderModeForResolutionIndependentLayout); + + // do glyph fallback if needed + // #105768# avoid fallback for very small font sizes + if (aLayoutArgs.HasFallbackRun() && mpFontInstance->GetFontSelectPattern().mnHeight >= 3) + pSalLayout = ImplGlyphFallbackLayout(std::move(pSalLayout), aLayoutArgs, pGlyphs); + + if (flags & SalLayoutFlags::GlyphItemsOnly) + // Return glyph items only after fallback handling. Otherwise they may + // contain invalid glyph IDs. + return pSalLayout; + + // position, justify, etc. the layout + pSalLayout->AdjustLayout( aLayoutArgs ); + + if (bTextRenderModeForResolutionIndependentLayout) + pSalLayout->DrawBase() = ImplLogicToDeviceFontCoordinate(rLogicalPos); + else + { + Point aDevicePos = ImplLogicToDevicePixel(rLogicalPos); + pSalLayout->DrawBase() = DevicePoint(aDevicePos.X(), aDevicePos.Y()); + } + + // adjust to right alignment if necessary + if( aLayoutArgs.mnFlags & SalLayoutFlags::RightAlign ) + { + DeviceCoordinate nRTLOffset; + if (!pDXArray.empty()) + nRTLOffset = nEndGlyphCoord; + else if( nPixelWidth ) + nRTLOffset = nPixelWidth; + else + nRTLOffset = pSalLayout->GetTextWidth(); + pSalLayout->DrawOffset().setX( 1 - nRTLOffset ); + } + + if(IsTrackingFontMappingUse()) + TrackFontMappingUse(GetFont(), pSalLayout.get()); + + return pSalLayout; +} + +std::shared_ptr<const vcl::text::TextLayoutCache> OutputDevice::CreateTextLayoutCache( + OUString const& rString) +{ + return vcl::text::TextLayoutCache::Create(rString); +} + +bool OutputDevice::GetTextIsRTL( const OUString& rString, sal_Int32 nIndex, sal_Int32 nLen ) const +{ + OUString aStr( rString ); + vcl::text::ImplLayoutArgs aArgs = ImplPrepareLayoutArgs(aStr, nIndex, nLen, 0); + bool bRTL = false; + int nCharPos = -1; + if (!aArgs.GetNextPos(&nCharPos, &bRTL)) + return false; + return (nCharPos != nIndex); +} + +sal_Int32 OutputDevice::GetTextBreak( const OUString& rStr, tools::Long nTextWidth, + sal_Int32 nIndex, sal_Int32 nLen, + tools::Long nCharExtra, + vcl::text::TextLayoutCache const*const pLayoutCache, + const SalLayoutGlyphs* pGlyphs) const +{ + std::unique_ptr<SalLayout> pSalLayout = ImplLayout( rStr, nIndex, nLen, + Point(0,0), 0, {}, eDefaultLayout, pLayoutCache, pGlyphs); + sal_Int32 nRetVal = -1; + if( pSalLayout ) + { + // convert logical widths into layout units + // NOTE: be very careful to avoid rounding errors for nCharExtra case + // problem with rounding errors especially for small nCharExtras + // TODO: remove when layout units have subpixel granularity + tools::Long nSubPixelFactor = 64; + nTextWidth *= nSubPixelFactor; + DeviceCoordinate nTextPixelWidth = LogicWidthToDeviceCoordinate( nTextWidth ); + DeviceCoordinate nExtraPixelWidth = 0; + if( nCharExtra != 0 ) + { + nCharExtra *= nSubPixelFactor; + nExtraPixelWidth = LogicWidthToDeviceCoordinate( nCharExtra ); + } + nRetVal = pSalLayout->GetTextBreak( nTextPixelWidth, nExtraPixelWidth, nSubPixelFactor ); + } + + return nRetVal; +} + +sal_Int32 OutputDevice::GetTextBreak( const OUString& rStr, tools::Long nTextWidth, + sal_Unicode nHyphenChar, sal_Int32& rHyphenPos, + sal_Int32 nIndex, sal_Int32 nLen, + tools::Long nCharExtra, + vcl::text::TextLayoutCache const*const pLayoutCache, + const SalLayoutGlyphs* pGlyphs) const +{ + rHyphenPos = -1; + + std::unique_ptr<SalLayout> pSalLayout = ImplLayout( rStr, nIndex, nLen, + Point(0,0), 0, {}, eDefaultLayout, pLayoutCache, pGlyphs); + sal_Int32 nRetVal = -1; + if( pSalLayout ) + { + // convert logical widths into layout units + // NOTE: be very careful to avoid rounding errors for nCharExtra case + // problem with rounding errors especially for small nCharExtras + // TODO: remove when layout units have subpixel granularity + tools::Long nSubPixelFactor = 64; + + nTextWidth *= nSubPixelFactor; + DeviceCoordinate nTextPixelWidth = LogicWidthToDeviceCoordinate( nTextWidth ); + DeviceCoordinate nExtraPixelWidth = 0; + if( nCharExtra != 0 ) + { + nCharExtra *= nSubPixelFactor; + nExtraPixelWidth = LogicWidthToDeviceCoordinate( nCharExtra ); + } + + // calculate un-hyphenated break position + nRetVal = pSalLayout->GetTextBreak( nTextPixelWidth, nExtraPixelWidth, nSubPixelFactor ); + + // calculate hyphenated break position + OUString aHyphenStr(nHyphenChar); + std::unique_ptr<SalLayout> pHyphenLayout = ImplLayout( aHyphenStr, 0, 1 ); + if( pHyphenLayout ) + { + // calculate subpixel width of hyphenation character + tools::Long nHyphenPixelWidth = pHyphenLayout->GetTextWidth() * nSubPixelFactor; + + // calculate hyphenated break position + nTextPixelWidth -= nHyphenPixelWidth; + if( nExtraPixelWidth > 0 ) + nTextPixelWidth -= nExtraPixelWidth; + + rHyphenPos = pSalLayout->GetTextBreak(nTextPixelWidth, nExtraPixelWidth, nSubPixelFactor); + + if( rHyphenPos > nRetVal ) + rHyphenPos = nRetVal; + } + } + + return nRetVal; +} + +void OutputDevice::ImplDrawText( OutputDevice& rTargetDevice, const tools::Rectangle& rRect, + const OUString& rOrigStr, DrawTextFlags nStyle, + std::vector< tools::Rectangle >* pVector, OUString* pDisplayText, + vcl::ITextLayout& _rLayout ) +{ + + Color aOldTextColor; + Color aOldTextFillColor; + bool bRestoreFillColor = false; + if ( (nStyle & DrawTextFlags::Disable) && ! pVector ) + { + bool bHighContrastBlack = false; + bool bHighContrastWhite = false; + const StyleSettings& rStyleSettings( rTargetDevice.GetSettings().GetStyleSettings() ); + if( rStyleSettings.GetHighContrastMode() ) + { + Color aCol; + if( rTargetDevice.IsBackground() ) + aCol = rTargetDevice.GetBackground().GetColor(); + else + // best guess is the face color here + // but it may be totally wrong. the background color + // was typically already reset + aCol = rStyleSettings.GetFaceColor(); + + bHighContrastBlack = aCol.IsDark(); + bHighContrastWhite = aCol.IsBright(); + } + + aOldTextColor = rTargetDevice.GetTextColor(); + if ( rTargetDevice.IsTextFillColor() ) + { + bRestoreFillColor = true; + aOldTextFillColor = rTargetDevice.GetTextFillColor(); + } + if( bHighContrastBlack ) + rTargetDevice.SetTextColor( COL_GREEN ); + else if( bHighContrastWhite ) + rTargetDevice.SetTextColor( COL_LIGHTGREEN ); + else + { + // draw disabled text always without shadow + // as it fits better with native look + rTargetDevice.SetTextColor( rTargetDevice.GetSettings().GetStyleSettings().GetDisableColor() ); + } + } + + tools::Long nWidth = rRect.GetWidth(); + tools::Long nHeight = rRect.GetHeight(); + + if ( ((nWidth <= 0) || (nHeight <= 0)) && (nStyle & DrawTextFlags::Clip) ) + return; + + Point aPos = rRect.TopLeft(); + + tools::Long nTextHeight = rTargetDevice.GetTextHeight(); + TextAlign eAlign = rTargetDevice.GetTextAlign(); + sal_Int32 nMnemonicPos = -1; + + OUString aStr = rOrigStr; + if ( nStyle & DrawTextFlags::Mnemonic ) + aStr = GetNonMnemonicString( aStr, nMnemonicPos ); + + const bool bDrawMnemonics = !(rTargetDevice.GetSettings().GetStyleSettings().GetOptions() & StyleSettingsOptions::NoMnemonics) && !pVector; + + // We treat multiline text differently + if ( nStyle & DrawTextFlags::MultiLine ) + { + + ImplMultiTextLineInfo aMultiLineInfo; + sal_Int32 i; + sal_Int32 nFormatLines; + + if ( nTextHeight ) + { + tools::Long nMaxTextWidth = ImplGetTextLines( rRect, nTextHeight, aMultiLineInfo, nWidth, aStr, nStyle, _rLayout ); + sal_Int32 nLines = static_cast<sal_Int32>(nHeight/nTextHeight); + OUString aLastLine; + nFormatLines = aMultiLineInfo.Count(); + if (nLines <= 0) + nLines = 1; + if ( nFormatLines > nLines ) + { + if ( nStyle & DrawTextFlags::EndEllipsis ) + { + // Create last line and shorten it + nFormatLines = nLines-1; + + ImplTextLineInfo& rLineInfo = aMultiLineInfo.GetLine( nFormatLines ); + aLastLine = convertLineEnd(aStr.copy(rLineInfo.GetIndex()), LINEEND_LF); + // Replace all LineFeeds with Spaces + OUStringBuffer aLastLineBuffer(aLastLine); + sal_Int32 nLastLineLen = aLastLineBuffer.getLength(); + for ( i = 0; i < nLastLineLen; i++ ) + { + if ( aLastLineBuffer[ i ] == '\n' ) + aLastLineBuffer[ i ] = ' '; + } + aLastLine = aLastLineBuffer.makeStringAndClear(); + aLastLine = ImplGetEllipsisString( rTargetDevice, aLastLine, nWidth, nStyle, _rLayout ); + nStyle &= ~DrawTextFlags(DrawTextFlags::VCenter | DrawTextFlags::Bottom); + nStyle |= DrawTextFlags::Top; + } + } + else + { + if ( nMaxTextWidth <= nWidth ) + nStyle &= ~DrawTextFlags::Clip; + } + + // Do we need to clip the height? + if ( nFormatLines*nTextHeight > nHeight ) + nStyle |= DrawTextFlags::Clip; + + // Set clipping + if ( nStyle & DrawTextFlags::Clip ) + { + rTargetDevice.Push( vcl::PushFlags::CLIPREGION ); + rTargetDevice.IntersectClipRegion( rRect ); + } + + // Vertical alignment + if ( nStyle & DrawTextFlags::Bottom ) + aPos.AdjustY(nHeight-(nFormatLines*nTextHeight) ); + else if ( nStyle & DrawTextFlags::VCenter ) + aPos.AdjustY((nHeight-(nFormatLines*nTextHeight))/2 ); + + // Font alignment + if ( eAlign == ALIGN_BOTTOM ) + aPos.AdjustY(nTextHeight ); + else if ( eAlign == ALIGN_BASELINE ) + aPos.AdjustY(rTargetDevice.GetFontMetric().GetAscent() ); + + // Output all lines except for the last one + for ( i = 0; i < nFormatLines; i++ ) + { + ImplTextLineInfo& rLineInfo = aMultiLineInfo.GetLine( i ); + if ( nStyle & DrawTextFlags::Right ) + aPos.AdjustX(nWidth-rLineInfo.GetWidth() ); + else if ( nStyle & DrawTextFlags::Center ) + aPos.AdjustX((nWidth-rLineInfo.GetWidth())/2 ); + sal_Int32 nIndex = rLineInfo.GetIndex(); + sal_Int32 nLineLen = rLineInfo.GetLen(); + _rLayout.DrawText( aPos, aStr, nIndex, nLineLen, pVector, pDisplayText ); + if ( bDrawMnemonics ) + { + if ( (nMnemonicPos >= nIndex) && (nMnemonicPos < nIndex+nLineLen) ) + { + tools::Long nMnemonicX; + tools::Long nMnemonicY; + DeviceCoordinate nMnemonicWidth; + + std::unique_ptr<sal_Int32[]> const pCaretXArray(new sal_Int32[2 * nLineLen]); + /*sal_Bool bRet =*/ _rLayout.GetCaretPositions( aStr, pCaretXArray.get(), + nIndex, nLineLen ); + sal_Int32 lc_x1 = pCaretXArray[2*(nMnemonicPos - nIndex)]; + sal_Int32 lc_x2 = pCaretXArray[2*(nMnemonicPos - nIndex)+1]; + nMnemonicWidth = rTargetDevice.LogicWidthToDeviceCoordinate( std::abs(lc_x1 - lc_x2) ); + + Point aTempPos = rTargetDevice.LogicToPixel( aPos ); + nMnemonicX = rTargetDevice.GetOutOffXPixel() + aTempPos.X() + rTargetDevice.ImplLogicWidthToDevicePixel( std::min( lc_x1, lc_x2 ) ); + nMnemonicY = rTargetDevice.GetOutOffYPixel() + aTempPos.Y() + rTargetDevice.ImplLogicWidthToDevicePixel( rTargetDevice.GetFontMetric().GetAscent() ); + rTargetDevice.ImplDrawMnemonicLine( nMnemonicX, nMnemonicY, nMnemonicWidth ); + } + } + aPos.AdjustY(nTextHeight ); + aPos.setX( rRect.Left() ); + } + + // If there still is a last line, we output it left-aligned as the line would be clipped + if ( !aLastLine.isEmpty() ) + _rLayout.DrawText( aPos, aLastLine, 0, aLastLine.getLength(), pVector, pDisplayText ); + + // Reset clipping + if ( nStyle & DrawTextFlags::Clip ) + rTargetDevice.Pop(); + } + } + else + { + tools::Long nTextWidth = _rLayout.GetTextWidth( aStr, 0, -1 ); + + // Clip text if needed + if ( nTextWidth > nWidth ) + { + if ( nStyle & TEXT_DRAW_ELLIPSIS ) + { + aStr = ImplGetEllipsisString( rTargetDevice, aStr, nWidth, nStyle, _rLayout ); + nStyle &= ~DrawTextFlags(DrawTextFlags::Center | DrawTextFlags::Right); + nStyle |= DrawTextFlags::Left; + nTextWidth = _rLayout.GetTextWidth( aStr, 0, aStr.getLength() ); + } + } + else + { + if ( nTextHeight <= nHeight ) + nStyle &= ~DrawTextFlags::Clip; + } + + // horizontal text alignment + if ( nStyle & DrawTextFlags::Right ) + aPos.AdjustX(nWidth-nTextWidth ); + else if ( nStyle & DrawTextFlags::Center ) + aPos.AdjustX((nWidth-nTextWidth)/2 ); + + // vertical font alignment + if ( eAlign == ALIGN_BOTTOM ) + aPos.AdjustY(nTextHeight ); + else if ( eAlign == ALIGN_BASELINE ) + aPos.AdjustY(rTargetDevice.GetFontMetric().GetAscent() ); + + if ( nStyle & DrawTextFlags::Bottom ) + aPos.AdjustY(nHeight-nTextHeight ); + else if ( nStyle & DrawTextFlags::VCenter ) + aPos.AdjustY((nHeight-nTextHeight)/2 ); + + tools::Long nMnemonicX = 0; + tools::Long nMnemonicY = 0; + DeviceCoordinate nMnemonicWidth = 0; + if (nMnemonicPos != -1 && nMnemonicPos < aStr.getLength()) + { + std::unique_ptr<sal_Int32[]> const pCaretXArray(new sal_Int32[2 * aStr.getLength()]); + /*sal_Bool bRet =*/ _rLayout.GetCaretPositions( aStr, pCaretXArray.get(), 0, aStr.getLength() ); + sal_Int32 lc_x1 = pCaretXArray[2*nMnemonicPos]; + sal_Int32 lc_x2 = pCaretXArray[2*nMnemonicPos+1]; + nMnemonicWidth = rTargetDevice.LogicWidthToDeviceCoordinate( std::abs(lc_x1 - lc_x2) ); + + Point aTempPos = rTargetDevice.LogicToPixel( aPos ); + nMnemonicX = rTargetDevice.GetOutOffXPixel() + aTempPos.X() + rTargetDevice.ImplLogicWidthToDevicePixel( std::min(lc_x1, lc_x2) ); + nMnemonicY = rTargetDevice.GetOutOffYPixel() + aTempPos.Y() + rTargetDevice.ImplLogicWidthToDevicePixel( rTargetDevice.GetFontMetric().GetAscent() ); + } + + if ( nStyle & DrawTextFlags::Clip ) + { + rTargetDevice.Push( vcl::PushFlags::CLIPREGION ); + rTargetDevice.IntersectClipRegion( rRect ); + _rLayout.DrawText( aPos, aStr, 0, aStr.getLength(), pVector, pDisplayText ); + if ( bDrawMnemonics && nMnemonicPos != -1 ) + rTargetDevice.ImplDrawMnemonicLine( nMnemonicX, nMnemonicY, nMnemonicWidth ); + rTargetDevice.Pop(); + } + else + { + _rLayout.DrawText( aPos, aStr, 0, aStr.getLength(), pVector, pDisplayText ); + if ( bDrawMnemonics && nMnemonicPos != -1 ) + rTargetDevice.ImplDrawMnemonicLine( nMnemonicX, nMnemonicY, nMnemonicWidth ); + } + } + + if ( nStyle & DrawTextFlags::Disable && !pVector ) + { + rTargetDevice.SetTextColor( aOldTextColor ); + if ( bRestoreFillColor ) + rTargetDevice.SetTextFillColor( aOldTextFillColor ); + } +} + +void OutputDevice::AddTextRectActions( const tools::Rectangle& rRect, + const OUString& rOrigStr, + DrawTextFlags nStyle, + GDIMetaFile& rMtf ) +{ + + if ( rOrigStr.isEmpty() || rRect.IsEmpty() ) + return; + + // we need a graphics + if( !mpGraphics && !AcquireGraphics() ) + return; + assert(mpGraphics); + if( mbInitClipRegion ) + InitClipRegion(); + + // temporarily swap in passed mtf for action generation, and + // disable output generation. + const bool bOutputEnabled( IsOutputEnabled() ); + GDIMetaFile* pMtf = mpMetaFile; + + mpMetaFile = &rMtf; + EnableOutput( false ); + + // #i47157# Factored out to ImplDrawTextRect(), to be shared + // between us and DrawText() + vcl::DefaultTextLayout aLayout( *this ); + ImplDrawText( *this, rRect, rOrigStr, nStyle, nullptr, nullptr, aLayout ); + + // and restore again + EnableOutput( bOutputEnabled ); + mpMetaFile = pMtf; +} + +void OutputDevice::DrawText( const tools::Rectangle& rRect, const OUString& rOrigStr, DrawTextFlags nStyle, + std::vector< tools::Rectangle >* pVector, OUString* pDisplayText, + vcl::ITextLayout* _pTextLayout ) +{ + assert(!is_double_buffered_window()); + + if (mpOutDevData->mpRecordLayout) + { + pVector = &mpOutDevData->mpRecordLayout->m_aUnicodeBoundRects; + pDisplayText = &mpOutDevData->mpRecordLayout->m_aDisplayText; + } + + bool bDecomposeTextRectAction = ( _pTextLayout != nullptr ) && _pTextLayout->DecomposeTextRectAction(); + if ( mpMetaFile && !bDecomposeTextRectAction ) + mpMetaFile->AddAction( new MetaTextRectAction( rRect, rOrigStr, nStyle ) ); + + if ( ( !IsDeviceOutputNecessary() && !pVector && !bDecomposeTextRectAction ) || rOrigStr.isEmpty() || rRect.IsEmpty() ) + return; + + // we need a graphics + if( !mpGraphics && !AcquireGraphics() ) + return; + assert(mpGraphics); + if( mbInitClipRegion ) + InitClipRegion(); + if (mbOutputClipped && !bDecomposeTextRectAction && !pDisplayText) + return; + + // temporarily disable mtf action generation (ImplDrawText _does_ + // create MetaActionType::TEXTs otherwise) + GDIMetaFile* pMtf = mpMetaFile; + if ( !bDecomposeTextRectAction ) + mpMetaFile = nullptr; + + // #i47157# Factored out to ImplDrawText(), to be used also + // from AddTextRectActions() + vcl::DefaultTextLayout aDefaultLayout( *this ); + ImplDrawText( *this, rRect, rOrigStr, nStyle, pVector, pDisplayText, _pTextLayout ? *_pTextLayout : aDefaultLayout ); + + // and enable again + mpMetaFile = pMtf; + + if( mpAlphaVDev ) + mpAlphaVDev->DrawText( rRect, rOrigStr, nStyle, pVector, pDisplayText ); +} + +tools::Rectangle OutputDevice::GetTextRect( const tools::Rectangle& rRect, + const OUString& rStr, DrawTextFlags nStyle, + TextRectInfo* pInfo, + const vcl::ITextLayout* _pTextLayout ) const +{ + + tools::Rectangle aRect = rRect; + sal_Int32 nLines; + tools::Long nWidth = rRect.GetWidth(); + tools::Long nMaxWidth; + tools::Long nTextHeight = GetTextHeight(); + + OUString aStr = rStr; + if ( nStyle & DrawTextFlags::Mnemonic ) + aStr = GetNonMnemonicString( aStr ); + + if ( nStyle & DrawTextFlags::MultiLine ) + { + ImplMultiTextLineInfo aMultiLineInfo; + sal_Int32 nFormatLines; + sal_Int32 i; + + nMaxWidth = 0; + vcl::DefaultTextLayout aDefaultLayout( *const_cast< OutputDevice* >( this ) ); + ImplGetTextLines( rRect, nTextHeight, aMultiLineInfo, nWidth, aStr, nStyle, _pTextLayout ? *_pTextLayout : aDefaultLayout ); + nFormatLines = aMultiLineInfo.Count(); + if ( !nTextHeight ) + nTextHeight = 1; + nLines = static_cast<sal_uInt16>(aRect.GetHeight()/nTextHeight); + if ( pInfo ) + pInfo->mnLineCount = nFormatLines; + if ( !nLines ) + nLines = 1; + if ( nFormatLines <= nLines ) + nLines = nFormatLines; + else + { + if ( !(nStyle & DrawTextFlags::EndEllipsis) ) + nLines = nFormatLines; + else + { + if ( pInfo ) + pInfo->mbEllipsis = true; + nMaxWidth = nWidth; + } + } + if ( pInfo ) + { + bool bMaxWidth = nMaxWidth == 0; + pInfo->mnMaxWidth = 0; + for ( i = 0; i < nLines; i++ ) + { + ImplTextLineInfo& rLineInfo = aMultiLineInfo.GetLine( i ); + if ( bMaxWidth && (rLineInfo.GetWidth() > nMaxWidth) ) + nMaxWidth = rLineInfo.GetWidth(); + if ( rLineInfo.GetWidth() > pInfo->mnMaxWidth ) + pInfo->mnMaxWidth = rLineInfo.GetWidth(); + } + } + else if ( !nMaxWidth ) + { + for ( i = 0; i < nLines; i++ ) + { + ImplTextLineInfo& rLineInfo = aMultiLineInfo.GetLine( i ); + if ( rLineInfo.GetWidth() > nMaxWidth ) + nMaxWidth = rLineInfo.GetWidth(); + } + } + } + else + { + nLines = 1; + nMaxWidth = _pTextLayout ? _pTextLayout->GetTextWidth( aStr, 0, aStr.getLength() ) : GetTextWidth( aStr ); + + if ( pInfo ) + { + pInfo->mnLineCount = 1; + pInfo->mnMaxWidth = nMaxWidth; + } + + if ( (nMaxWidth > nWidth) && (nStyle & TEXT_DRAW_ELLIPSIS) ) + { + if ( pInfo ) + pInfo->mbEllipsis = true; + nMaxWidth = nWidth; + } + } + + if ( nStyle & DrawTextFlags::Right ) + aRect.SetLeft( aRect.Right()-nMaxWidth+1 ); + else if ( nStyle & DrawTextFlags::Center ) + { + aRect.AdjustLeft((nWidth-nMaxWidth)/2 ); + aRect.SetRight( aRect.Left()+nMaxWidth-1 ); + } + else + aRect.SetRight( aRect.Left()+nMaxWidth-1 ); + + if ( nStyle & DrawTextFlags::Bottom ) + aRect.SetTop( aRect.Bottom()-(nTextHeight*nLines)+1 ); + else if ( nStyle & DrawTextFlags::VCenter ) + { + aRect.AdjustTop((aRect.GetHeight()-(nTextHeight*nLines))/2 ); + aRect.SetBottom( aRect.Top()+(nTextHeight*nLines)-1 ); + } + else + aRect.SetBottom( aRect.Top()+(nTextHeight*nLines)-1 ); + + // #99188# get rid of rounding problems when using this rect later + if (nStyle & DrawTextFlags::Right) + aRect.AdjustLeft( -1 ); + else + aRect.AdjustRight( 1 ); + return aRect; +} + +static bool ImplIsCharIn( sal_Unicode c, const char* pStr ) +{ + while ( *pStr ) + { + if ( *pStr == c ) + return true; + pStr++; + } + + return false; +} + +OUString OutputDevice::GetEllipsisString( const OUString& rOrigStr, tools::Long nMaxWidth, + DrawTextFlags nStyle ) const +{ + vcl::DefaultTextLayout aTextLayout( *const_cast< OutputDevice* >( this ) ); + return ImplGetEllipsisString( *this, rOrigStr, nMaxWidth, nStyle, aTextLayout ); +} + +OUString OutputDevice::ImplGetEllipsisString( const OutputDevice& rTargetDevice, const OUString& rOrigStr, tools::Long nMaxWidth, + DrawTextFlags nStyle, const vcl::ITextLayout& _rLayout ) +{ + OUString aStr = rOrigStr; + sal_Int32 nIndex = _rLayout.GetTextBreak( aStr, nMaxWidth, 0, aStr.getLength() ); + + if ( nIndex != -1 ) + { + if( (nStyle & DrawTextFlags::CenterEllipsis) == DrawTextFlags::CenterEllipsis ) + { + OUStringBuffer aTmpStr( aStr ); + // speed it up by removing all but 1.33x as many as the break pos. + sal_Int32 nEraseChars = std::max<sal_Int32>(4, aStr.getLength() - (nIndex*4)/3); + while( nEraseChars < aStr.getLength() && _rLayout.GetTextWidth( aTmpStr.toString(), 0, aTmpStr.getLength() ) > nMaxWidth ) + { + aTmpStr = aStr; + sal_Int32 i = (aTmpStr.getLength() - nEraseChars)/2; + aTmpStr.remove(i, nEraseChars++); + aTmpStr.insert(i, "..."); + } + aStr = aTmpStr.makeStringAndClear(); + } + else if ( nStyle & DrawTextFlags::EndEllipsis ) + { + aStr = aStr.copy(0, nIndex); + if ( nIndex > 1 ) + { + aStr += "..."; + while ( !aStr.isEmpty() && (_rLayout.GetTextWidth( aStr, 0, aStr.getLength() ) > nMaxWidth) ) + { + if ( (nIndex > 1) || (nIndex == aStr.getLength()) ) + nIndex--; + aStr = aStr.replaceAt( nIndex, 1, u""); + } + } + + if ( aStr.isEmpty() && (nStyle & DrawTextFlags::Clip) ) + aStr += OUStringChar(rOrigStr[ 0 ]); + } + else if ( nStyle & DrawTextFlags::PathEllipsis ) + { + OUString aPath( rOrigStr ); + OUString aAbbreviatedPath; + osl_abbreviateSystemPath( aPath.pData, &aAbbreviatedPath.pData, nIndex, nullptr ); + aStr = aAbbreviatedPath; + } + else if ( nStyle & DrawTextFlags::NewsEllipsis ) + { + static char const pSepChars[] = "."; + // Determine last section + sal_Int32 nLastContent = aStr.getLength(); + while ( nLastContent ) + { + nLastContent--; + if ( ImplIsCharIn( aStr[ nLastContent ], pSepChars ) ) + break; + } + while ( nLastContent && + ImplIsCharIn( aStr[ nLastContent-1 ], pSepChars ) ) + nLastContent--; + + OUString aLastStr = aStr.copy(nLastContent); + OUString aTempLastStr1 = "..." + aLastStr; + if ( _rLayout.GetTextWidth( aTempLastStr1, 0, aTempLastStr1.getLength() ) > nMaxWidth ) + aStr = OutputDevice::ImplGetEllipsisString( rTargetDevice, aStr, nMaxWidth, nStyle | DrawTextFlags::EndEllipsis, _rLayout ); + else + { + sal_Int32 nFirstContent = 0; + while ( nFirstContent < nLastContent ) + { + nFirstContent++; + if ( ImplIsCharIn( aStr[ nFirstContent ], pSepChars ) ) + break; + } + while ( (nFirstContent < nLastContent) && + ImplIsCharIn( aStr[ nFirstContent ], pSepChars ) ) + nFirstContent++; + // MEM continue here + if ( nFirstContent >= nLastContent ) + aStr = OutputDevice::ImplGetEllipsisString( rTargetDevice, aStr, nMaxWidth, nStyle | DrawTextFlags::EndEllipsis, _rLayout ); + else + { + if ( nFirstContent > 4 ) + nFirstContent = 4; + OUString aFirstStr = OUString::Concat(aStr.subView( 0, nFirstContent )) + "..."; + OUString aTempStr = aFirstStr + aLastStr; + if ( _rLayout.GetTextWidth( aTempStr, 0, aTempStr.getLength() ) > nMaxWidth ) + aStr = OutputDevice::ImplGetEllipsisString( rTargetDevice, aStr, nMaxWidth, nStyle | DrawTextFlags::EndEllipsis, _rLayout ); + else + { + do + { + aStr = aTempStr; + if( nLastContent > aStr.getLength() ) + nLastContent = aStr.getLength(); + while ( nFirstContent < nLastContent ) + { + nLastContent--; + if ( ImplIsCharIn( aStr[ nLastContent ], pSepChars ) ) + break; + + } + while ( (nFirstContent < nLastContent) && + ImplIsCharIn( aStr[ nLastContent-1 ], pSepChars ) ) + nLastContent--; + + if ( nFirstContent < nLastContent ) + { + std::u16string_view aTempLastStr = aStr.subView( nLastContent ); + aTempStr = aFirstStr + aTempLastStr; + + if ( _rLayout.GetTextWidth( aTempStr, 0, aTempStr.getLength() ) > nMaxWidth ) + break; + } + } + while ( nFirstContent < nLastContent ); + } + } + } + } + } + + return aStr; +} + +void OutputDevice::DrawCtrlText( const Point& rPos, const OUString& rStr, + sal_Int32 nIndex, sal_Int32 nLen, + DrawTextFlags nStyle, std::vector< tools::Rectangle >* pVector, OUString* pDisplayText, + const SalLayoutGlyphs* pGlyphs ) +{ + assert(!is_double_buffered_window()); + + if( (nLen < 0) || (nIndex + nLen >= rStr.getLength())) + { + nLen = rStr.getLength() - nIndex; + } + + if ( !IsDeviceOutputNecessary() || (nIndex >= rStr.getLength()) ) + return; + + // better get graphics here because ImplDrawMnemonicLine() will not + // we need a graphics + if( !mpGraphics && !AcquireGraphics() ) + return; + assert(mpGraphics); + if( mbInitClipRegion ) + InitClipRegion(); + if ( mbOutputClipped ) + return; + + if( nIndex >= rStr.getLength() ) + return; + + if( (nLen < 0) || (nIndex + nLen >= rStr.getLength())) + { + nLen = rStr.getLength() - nIndex; + } + OUString aStr = rStr; + sal_Int32 nMnemonicPos = -1; + + tools::Long nMnemonicX = 0; + tools::Long nMnemonicY = 0; + tools::Long nMnemonicWidth = 0; + if ( (nStyle & DrawTextFlags::Mnemonic) && nLen > 1 ) + { + aStr = GetNonMnemonicString( aStr, nMnemonicPos ); + if ( nMnemonicPos != -1 ) + { + if( nMnemonicPos < nIndex ) + { + --nIndex; + } + else + { + if( nMnemonicPos < (nIndex+nLen) ) + --nLen; + SAL_WARN_IF( nMnemonicPos >= (nIndex+nLen), "vcl", "Mnemonic underline marker after last character" ); + } + bool bInvalidPos = false; + + if( nMnemonicPos >= nLen ) + { + // may occur in BiDi-Strings: the '~' is sometimes found behind the last char + // due to some strange BiDi text editors + // -> place the underline behind the string to indicate a failure + bInvalidPos = true; + nMnemonicPos = nLen-1; + } + + std::unique_ptr<sal_Int32[]> const pCaretXArray(new sal_Int32[2 * nLen]); + /*sal_Bool bRet =*/ GetCaretPositions( aStr, pCaretXArray.get(), nIndex, nLen, pGlyphs ); + sal_Int32 lc_x1 = pCaretXArray[ 2*(nMnemonicPos - nIndex) ]; + sal_Int32 lc_x2 = pCaretXArray[ 2*(nMnemonicPos - nIndex)+1 ]; + nMnemonicWidth = ::abs(static_cast<int>(lc_x1 - lc_x2)); + + Point aTempPos( std::min(lc_x1,lc_x2), GetFontMetric().GetAscent() ); + if( bInvalidPos ) // #106952#, place behind the (last) character + aTempPos = Point( std::max(lc_x1,lc_x2), GetFontMetric().GetAscent() ); + + aTempPos += rPos; + aTempPos = LogicToPixel( aTempPos ); + nMnemonicX = mnOutOffX + aTempPos.X(); + nMnemonicY = mnOutOffY + aTempPos.Y(); + } + } + + bool autoacc = ImplGetSVData()->maNWFData.mbAutoAccel; + + if ( nStyle & DrawTextFlags::Disable && ! pVector ) + { + Color aOldTextColor; + Color aOldTextFillColor; + bool bRestoreFillColor; + bool bHighContrastBlack = false; + bool bHighContrastWhite = false; + const StyleSettings& rStyleSettings( GetSettings().GetStyleSettings() ); + if( rStyleSettings.GetHighContrastMode() ) + { + if( IsBackground() ) + { + Wallpaper aWall = GetBackground(); + Color aCol = aWall.GetColor(); + bHighContrastBlack = aCol.IsDark(); + bHighContrastWhite = aCol.IsBright(); + } + } + + aOldTextColor = GetTextColor(); + if ( IsTextFillColor() ) + { + bRestoreFillColor = true; + aOldTextFillColor = GetTextFillColor(); + } + else + bRestoreFillColor = false; + + if( bHighContrastBlack ) + SetTextColor( COL_GREEN ); + else if( bHighContrastWhite ) + SetTextColor( COL_LIGHTGREEN ); + else + SetTextColor( GetSettings().GetStyleSettings().GetDisableColor() ); + + DrawText( rPos, aStr, nIndex, nLen, pVector, pDisplayText ); + if (!(GetSettings().GetStyleSettings().GetOptions() & StyleSettingsOptions::NoMnemonics) + && (!autoacc || !(nStyle & DrawTextFlags::HideMnemonic)) ) + { + if ( nMnemonicPos != -1 ) + ImplDrawMnemonicLine( nMnemonicX, nMnemonicY, nMnemonicWidth ); + } + SetTextColor( aOldTextColor ); + if ( bRestoreFillColor ) + SetTextFillColor( aOldTextFillColor ); + } + else + { + DrawText( rPos, aStr, nIndex, nLen, pVector, pDisplayText, pGlyphs ); + if ( !(GetSettings().GetStyleSettings().GetOptions() & StyleSettingsOptions::NoMnemonics) && !pVector + && (!autoacc || !(nStyle & DrawTextFlags::HideMnemonic)) ) + { + if ( nMnemonicPos != -1 ) + ImplDrawMnemonicLine( nMnemonicX, nMnemonicY, nMnemonicWidth ); + } + } + + if( mpAlphaVDev ) + mpAlphaVDev->DrawCtrlText( rPos, rStr, nIndex, nLen, nStyle, pVector, pDisplayText ); +} + +tools::Long OutputDevice::GetCtrlTextWidth( const OUString& rStr, const SalLayoutGlyphs* pGlyphs ) const +{ + sal_Int32 nLen = rStr.getLength(); + sal_Int32 nIndex = 0; + + sal_Int32 nMnemonicPos; + OUString aStr = GetNonMnemonicString( rStr, nMnemonicPos ); + if ( nMnemonicPos != -1 ) + { + if ( nMnemonicPos < nIndex ) + nIndex--; + else if (static_cast<sal_uLong>(nMnemonicPos) < static_cast<sal_uLong>(nIndex+nLen)) + nLen--; + } + return GetTextWidth( aStr, nIndex, nLen, nullptr, pGlyphs ); +} + +OUString OutputDevice::GetNonMnemonicString( const OUString& rStr, sal_Int32& rMnemonicPos ) +{ + OUString aStr = rStr; + sal_Int32 nLen = aStr.getLength(); + sal_Int32 i = 0; + + rMnemonicPos = -1; + while ( i < nLen ) + { + if ( aStr[ i ] == '~' ) + { + if ( nLen <= i+1 ) + break; + + if ( aStr[ i+1 ] != '~' ) + { + if ( rMnemonicPos == -1 ) + rMnemonicPos = i; + aStr = aStr.replaceAt( i, 1, u"" ); + nLen--; + } + else + { + aStr = aStr.replaceAt( i, 1, u"" ); + nLen--; + i++; + } + } + else + i++; + } + + return aStr; +} + +bool OutputDevice::GetTextBoundRect( tools::Rectangle& rRect, + const OUString& rStr, sal_Int32 nBase, + sal_Int32 nIndex, sal_Int32 nLen, + sal_uLong nLayoutWidth, o3tl::span<const sal_Int32> pDXAry, + const SalLayoutGlyphs* pGlyphs ) const +{ + bool bRet = false; + rRect.SetEmpty(); + + std::unique_ptr<SalLayout> pSalLayout; + const Point aPoint; + // calculate offset when nBase!=nIndex + tools::Long nXOffset = 0; + if( nBase != nIndex ) + { + sal_Int32 nStart = std::min( nBase, nIndex ); + sal_Int32 nOfsLen = std::max( nBase, nIndex ) - nStart; + pSalLayout = ImplLayout( rStr, nStart, nOfsLen, aPoint, nLayoutWidth, pDXAry ); + if( pSalLayout ) + { + nXOffset = pSalLayout->GetTextWidth(); + // TODO: fix offset calculation for Bidi case + if( nBase < nIndex) + nXOffset = -nXOffset; + } + } + + pSalLayout = ImplLayout(rStr, nIndex, nLen, aPoint, nLayoutWidth, pDXAry, eDefaultLayout, + nullptr, pGlyphs); + if( pSalLayout ) + { + tools::Rectangle aPixelRect; + bRet = pSalLayout->GetBoundRect(aPixelRect); + + if( bRet ) + { + Point aRotatedOfs( mnTextOffX, mnTextOffY ); + DevicePoint aPos = pSalLayout->GetDrawPosition(DevicePoint(nXOffset, 0)); + aRotatedOfs -= Point(aPos.getX(), aPos.getY()); + aPixelRect += aRotatedOfs; + rRect = PixelToLogic( aPixelRect ); + if( mbMap ) + rRect += Point( maMapRes.mnMapOfsX, maMapRes.mnMapOfsY ); + } + } + + return bRet; +} + +bool OutputDevice::GetTextOutlines( basegfx::B2DPolyPolygonVector& rVector, + const OUString& rStr, sal_Int32 nBase, + sal_Int32 nIndex, sal_Int32 nLen, + sal_uLong nLayoutWidth, o3tl::span<const sal_Int32> pDXArray ) const +{ + if (!InitFont()) + return false; + + bool bRet = false; + rVector.clear(); + if( nLen < 0 ) + { + nLen = rStr.getLength() - nIndex; + } + rVector.reserve( nLen ); + + // we want to get the Rectangle in logical units, so to + // avoid rounding errors we just size the font in logical units + bool bOldMap = mbMap; + if( bOldMap ) + { + const_cast<OutputDevice&>(*this).mbMap = false; + const_cast<OutputDevice&>(*this).mbNewFont = true; + } + + std::unique_ptr<SalLayout> pSalLayout; + + // calculate offset when nBase!=nIndex + tools::Long nXOffset = 0; + if( nBase != nIndex ) + { + sal_Int32 nStart = std::min( nBase, nIndex ); + sal_Int32 nOfsLen = std::max( nBase, nIndex ) - nStart; + pSalLayout = ImplLayout( rStr, nStart, nOfsLen, Point(0,0), nLayoutWidth, pDXArray ); + if( pSalLayout ) + { + nXOffset = pSalLayout->GetTextWidth(); + pSalLayout.reset(); + // TODO: fix offset calculation for Bidi case + if( nBase > nIndex) + nXOffset = -nXOffset; + } + } + + pSalLayout = ImplLayout( rStr, nIndex, nLen, Point(0,0), nLayoutWidth, pDXArray ); + if( pSalLayout ) + { + bRet = pSalLayout->GetOutline(rVector); + if( bRet ) + { + // transform polygon to pixel units + basegfx::B2DHomMatrix aMatrix; + + if( nXOffset | mnTextOffX | mnTextOffY ) + { + DevicePoint aRotatedOfs(mnTextOffX, mnTextOffY); + aRotatedOfs -= pSalLayout->GetDrawPosition(DevicePoint(nXOffset, 0)); + aMatrix.translate( aRotatedOfs.getX(), aRotatedOfs.getY() ); + } + + if( !aMatrix.isIdentity() ) + { + for (auto & elem : rVector) + elem.transform( aMatrix ); + } + } + + pSalLayout.reset(); + } + + if( bOldMap ) + { + // restore original font size and map mode + const_cast<OutputDevice&>(*this).mbMap = bOldMap; + const_cast<OutputDevice&>(*this).mbNewFont = true; + } + + return bRet; +} + +bool OutputDevice::GetTextOutlines( PolyPolyVector& rResultVector, + const OUString& rStr, sal_Int32 nBase, + sal_Int32 nIndex, sal_Int32 nLen, + sal_uLong nLayoutWidth, o3tl::span<const sal_Int32> pDXArray ) const +{ + rResultVector.clear(); + + // get the basegfx polypolygon vector + basegfx::B2DPolyPolygonVector aB2DPolyPolyVector; + if( !GetTextOutlines( aB2DPolyPolyVector, rStr, nBase, nIndex, nLen, + nLayoutWidth, pDXArray ) ) + return false; + + // convert to a tool polypolygon vector + rResultVector.reserve( aB2DPolyPolyVector.size() ); + for (auto const& elem : aB2DPolyPolyVector) + rResultVector.emplace_back(elem); // #i76339# + + return true; +} + +bool OutputDevice::GetTextOutline( tools::PolyPolygon& rPolyPoly, const OUString& rStr ) const +{ + rPolyPoly.Clear(); + + // get the basegfx polypolygon vector + basegfx::B2DPolyPolygonVector aB2DPolyPolyVector; + if( !GetTextOutlines( aB2DPolyPolyVector, rStr, 0/*nBase*/, 0/*nIndex*/, /*nLen*/-1, + /*nLayoutWidth*/0, /*pDXArray*/{} ) ) + return false; + + // convert and merge into a tool polypolygon + for (auto const& elem : aB2DPolyPolyVector) + for(auto const& rB2DPolygon : elem) + rPolyPoly.Insert(tools::Polygon(rB2DPolygon)); // #i76339# + + return true; +} + +void OutputDevice::SetSystemTextColor(SystemTextColorFlags nFlags, bool bEnabled) +{ + if (nFlags & SystemTextColorFlags::Mono) + { + SetTextColor(COL_BLACK); + } + else + { + if (!bEnabled) + { + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + SetTextColor(rStyleSettings.GetDisableColor()); + } + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |