Version 24.2.2.2, tag libreoffice-24.2.2.2
[LibreOffice.git] / svx / source / customshapes / EnhancedCustomShapeFontWork.cxx
blob2e19aa344ec44e9df58fef00246e054fb05c4e20
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include "EnhancedCustomShapeFontWork.hxx"
21 #include <svl/itemset.hxx>
22 #include <svx/compatflags.hxx>
23 #include <svx/svddef.hxx>
24 #include <svx/svdopath.hxx>
25 #include <vcl/kernarray.hxx>
26 #include <vcl/metric.hxx>
27 #include <svx/sdasitm.hxx>
28 #include <svx/sdtfsitm.hxx>
29 #include <vcl/virdev.hxx>
30 #include <svx/svditer.hxx>
31 #include <editeng/eeitem.hxx>
32 #include <editeng/frmdiritem.hxx>
33 #include <editeng/fontitem.hxx>
34 #include <editeng/postitem.hxx>
35 #include <editeng/wghtitem.hxx>
36 #include <editeng/fhgtitem.hxx>
37 #include <editeng/charscaleitem.hxx>
38 #include <svx/svdoashp.hxx>
39 #include <svx/sdshitm.hxx>
40 #include <svx/svdmodel.hxx>
41 #include <editeng/outlobj.hxx>
42 #include <editeng/editobj.hxx>
43 #include <o3tl/numeric.hxx>
44 #include <vector>
45 #include <numeric>
46 #include <algorithm>
47 #include <comphelper/processfactory.hxx>
48 #include <com/sun/star/i18n/BreakIterator.hpp>
49 #include <com/sun/star/i18n/ScriptType.hpp>
50 #include <basegfx/polygon/b2dpolypolygontools.hxx>
51 #include <basegfx/polygon/b2dpolygontools.hxx>
52 #include <sal/log.hxx>
53 #include <rtl/math.hxx>
54 #include <unotools/configmgr.hxx>
55 #include <comphelper/string.hxx>
57 using namespace com::sun::star;
58 using namespace com::sun::star::uno;
60 namespace {
62 struct FWCharacterData // representing a single character
64 std::vector< tools::PolyPolygon > vOutlines;
65 tools::Rectangle aBoundRect;
67 struct FWParagraphData // representing a single paragraph
69 OUString aString;
70 std::vector< FWCharacterData > vCharacters;
71 tools::Rectangle aBoundRect;
72 SvxFrameDirection nFrameDirection;
74 struct FWTextArea // representing multiple concluding paragraphs
76 std::vector< FWParagraphData > vParagraphs;
77 tools::Rectangle aBoundRect;
78 sal_Int32 nHAlignMove = 0;
80 struct FWData // representing the whole text
82 std::vector< FWTextArea > vTextAreas;
83 double fHorizontalTextScaling;
84 double fVerticalTextScaling;
85 sal_uInt32 nMaxParagraphsPerTextArea;
86 sal_Int32 nSingleLineHeight;
87 bool bSingleLineMode;
88 bool bScaleX;
93 static bool InitializeFontWorkData(
94 const SdrObjCustomShape& rSdrObjCustomShape,
95 const sal_uInt16 nOutlinesCount2d,
96 FWData& rFWData)
98 bool bNoErr = false;
99 bool bSingleLineMode = false;
100 sal_uInt16 nTextAreaCount = nOutlinesCount2d;
101 if ( nOutlinesCount2d & 1 )
102 bSingleLineMode = true;
103 else
104 nTextAreaCount >>= 1;
106 const SdrCustomShapeGeometryItem& rGeometryItem( rSdrObjCustomShape.GetMergedItem( SDRATTR_CUSTOMSHAPE_GEOMETRY ) );
107 const css::uno::Any* pAny = rGeometryItem.GetPropertyValueByName( "TextPath", "ScaleX" );
108 if (pAny)
109 *pAny >>= rFWData.bScaleX;
110 else
111 rFWData.bScaleX = false;
113 if ( nTextAreaCount )
115 rFWData.bSingleLineMode = bSingleLineMode;
117 // setting the strings
118 OutlinerParaObject* pParaObj(rSdrObjCustomShape.GetOutlinerParaObject());
120 if ( pParaObj )
122 const EditTextObject& rTextObj = pParaObj->GetTextObject();
123 sal_Int32 nParagraphsCount = rTextObj.GetParagraphCount();
125 // Collect all the lines from all paragraphs
126 std::vector<int> aLineParaID; // which para this line is in
127 std::vector<int> aLineStart; // where this line start in that para
128 std::vector<int> aLineLength;
129 std::vector<OUString> aParaText;
130 for (sal_Int32 nPara = 0; nPara < nParagraphsCount; ++nPara)
132 aParaText.push_back(rTextObj.GetText(nPara));
133 sal_Int32 nPos = 0;
134 sal_Int32 nPrevPos = 0;
137 // search line break.
138 if (!rSdrObjCustomShape.getSdrModelFromSdrObject().GetCompatibilityFlag(
139 SdrCompatibilityFlag::LegacyFontwork))
140 nPos = aParaText[nPara].indexOf(sal_Unicode(u'\1'), nPrevPos);
141 else
142 nPos = -1; // tdf#148000: ignore line breaks in legacy fontworks
144 aLineParaID.push_back(nPara);
145 aLineStart.push_back(nPrevPos);
146 aLineLength.push_back((nPos >= 0 ? nPos : aParaText[nPara].getLength())
147 - nPrevPos);
148 nPrevPos = nPos + 1;
149 } while (nPos >= 0);
152 sal_Int32 nLinesLeft = aLineParaID.size();
154 rFWData.nMaxParagraphsPerTextArea = ((nLinesLeft - 1) / nTextAreaCount) + 1;
155 sal_Int32 nLine = 0;
156 while (nLinesLeft && nTextAreaCount)
158 FWTextArea aTextArea;
159 sal_Int32 nLinesInPara = ((nLinesLeft - 1) / nTextAreaCount) + 1;
160 for (sal_Int32 i = 0; i < nLinesInPara; ++i, ++nLine)
162 FWParagraphData aParagraphData;
163 aParagraphData.aString = aParaText[aLineParaID[nLine]].subView(
164 aLineStart[nLine], aLineLength[nLine]);
166 // retrieving some paragraph attributes
167 const SfxItemSet& rParaSet = rTextObj.GetParaAttribs(aLineParaID[nLine]);
168 aParagraphData.nFrameDirection = rParaSet.Get(EE_PARA_WRITINGDIR).GetValue();
169 aTextArea.vParagraphs.push_back(aParagraphData);
171 rFWData.vTextAreas.push_back(aTextArea);
172 nLinesLeft -= nLinesInPara;
173 nTextAreaCount--;
176 bNoErr = true;
179 return bNoErr;
182 static double GetLength( const tools::Polygon& rPolygon )
184 double fLength = 0;
185 if ( rPolygon.GetSize() > 1 )
187 sal_uInt16 nCount = rPolygon.GetSize();
188 while( --nCount )
189 fLength += rPolygon.CalcDistance( nCount, nCount - 1 );
191 return fLength;
195 /* CalculateHorizontalScalingFactor returns the horizontal scaling factor for
196 the whole text object, so that each text will match its corresponding 2d Outline */
197 static void CalculateHorizontalScalingFactor(
198 const SdrObjCustomShape& rSdrObjCustomShape,
199 FWData& rFWData,
200 const tools::PolyPolygon& rOutline2d)
202 double fScalingFactor = 1.0;
203 rFWData.fVerticalTextScaling = 1.0;
205 sal_uInt16 i = 0;
206 bool bSingleLineMode = false;
207 sal_uInt16 nOutlinesCount2d = rOutline2d.Count();
209 vcl::Font aFont;
210 const SvxFontItem& rFontItem( rSdrObjCustomShape.GetMergedItem( EE_CHAR_FONTINFO ) );
211 const SvxFontHeightItem& rFontHeight( rSdrObjCustomShape.GetMergedItem( EE_CHAR_FONTHEIGHT ) );
212 sal_Int32 nFontSize = rFontHeight.GetHeight();
214 if (rFWData.bScaleX)
215 aFont.SetFontHeight( nFontSize );
216 else
217 aFont.SetFontHeight( rSdrObjCustomShape.GetLogicRect().GetHeight() / rFWData.nMaxParagraphsPerTextArea );
219 aFont.SetAlignment( ALIGN_TOP );
220 aFont.SetFamilyName( rFontItem.GetFamilyName() );
221 aFont.SetFamily( rFontItem.GetFamily() );
222 aFont.SetStyleName( rFontItem.GetStyleName() );
223 const SvxPostureItem& rPostureItem = rSdrObjCustomShape.GetMergedItem( EE_CHAR_ITALIC );
224 aFont.SetItalic( rPostureItem.GetPosture() );
226 const SvxWeightItem& rWeightItem = rSdrObjCustomShape.GetMergedItem( EE_CHAR_WEIGHT );
227 aFont.SetWeight( rWeightItem.GetWeight() );
228 aFont.SetOrientation( 0_deg10 );
229 // initializing virtual device
231 ScopedVclPtrInstance< VirtualDevice > pVirDev(DeviceFormat::WITHOUT_ALPHA);
232 pVirDev->SetMapMode(MapMode(MapUnit::Map100thMM));
233 pVirDev->SetFont( aFont );
234 pVirDev->SetAntialiasing( AntialiasingFlags::DisableText );
236 if ( nOutlinesCount2d & 1 )
237 bSingleLineMode = true;
239 // In case of rFWData.bScaleX == true it loops with reduced font size until the current run
240 // results in a fScalingFactor >=1.0. The fact, that case rFWData.bScaleX == true keeps font
241 // size if possible, is not done here with scaling factor 1 but is done in method
242 // FitTextOutlinesToShapeOutlines()
245 i = 0;
246 bool bScalingFactorDefined = false; // New calculation for each font size
247 for( const auto& rTextArea : rFWData.vTextAreas )
249 // calculating the width of the corresponding 2d text area
250 double fWidth = GetLength( rOutline2d.GetObject( i++ ) );
251 if ( !bSingleLineMode )
253 fWidth += GetLength( rOutline2d.GetObject( i++ ) );
254 fWidth /= 2.0;
257 for( const auto& rParagraph : rTextArea.vParagraphs )
259 double fTextWidth = pVirDev->GetTextWidth( rParagraph.aString );
260 if ( fTextWidth > 0.0 )
262 double fScale = fWidth / fTextWidth;
263 if ( !bScalingFactorDefined )
265 fScalingFactor = fScale;
266 bScalingFactorDefined = true;
268 else if (fScale < fScalingFactor)
270 fScalingFactor = fScale;
276 if (fScalingFactor < 1.0)
278 nFontSize--;
279 aFont.SetFontHeight( nFontSize );
280 pVirDev->SetFont( aFont );
283 while (rFWData.bScaleX && fScalingFactor < 1.0 && nFontSize > 1 );
285 if (nFontSize > 1)
286 rFWData.fVerticalTextScaling = static_cast<double>(nFontSize) / rFontHeight.GetHeight();
288 rFWData.fHorizontalTextScaling = fScalingFactor;
291 static void GetTextAreaOutline(
292 const FWData& rFWData,
293 const SdrObjCustomShape& rSdrObjCustomShape,
294 FWTextArea& rTextArea,
295 bool bSameLetterHeights)
297 bool bIsVertical(rSdrObjCustomShape.IsVerticalWriting());
298 sal_Int32 nVerticalOffset = rFWData.nMaxParagraphsPerTextArea > rTextArea.vParagraphs.size()
299 ? rFWData.nSingleLineHeight / 2 : 0;
301 for( auto& rParagraph : rTextArea.vParagraphs )
303 const OUString& rText = rParagraph.aString;
304 if ( !rText.isEmpty() )
306 // generating vcl/font
307 sal_uInt16 nScriptType = i18n::ScriptType::LATIN;
308 Reference< i18n::XBreakIterator > xBI( EnhancedCustomShapeFontWork::GetBreakIterator() );
309 if ( xBI.is() )
311 nScriptType = xBI->getScriptType( rText, 0 );
312 if( i18n::ScriptType::WEAK == nScriptType )
314 sal_Int32 nChg = xBI->endOfScript( rText, 0, nScriptType );
315 if (nChg < rText.getLength() && nChg >= 0)
316 nScriptType = xBI->getScriptType( rText, nChg );
317 else
318 nScriptType = i18n::ScriptType::LATIN;
321 sal_uInt16 nFntItm = EE_CHAR_FONTINFO;
322 if ( nScriptType == i18n::ScriptType::COMPLEX )
323 nFntItm = EE_CHAR_FONTINFO_CTL;
324 else if ( nScriptType == i18n::ScriptType::ASIAN )
325 nFntItm = EE_CHAR_FONTINFO_CJK;
326 const SvxFontItem& rFontItem = static_cast<const SvxFontItem&>(rSdrObjCustomShape.GetMergedItem( nFntItm ));
327 vcl::Font aFont;
329 aFont.SetFontHeight( rFWData.nSingleLineHeight );
331 aFont.SetAlignment( ALIGN_TOP );
333 aFont.SetFamilyName( rFontItem.GetFamilyName() );
334 aFont.SetFamily( rFontItem.GetFamily() );
335 aFont.SetStyleName( rFontItem.GetStyleName() );
336 aFont.SetOrientation( 0_deg10 );
338 const SvxPostureItem& rPostureItem = rSdrObjCustomShape.GetMergedItem( EE_CHAR_ITALIC );
339 aFont.SetItalic( rPostureItem.GetPosture() );
341 const SvxWeightItem& rWeightItem = rSdrObjCustomShape.GetMergedItem( EE_CHAR_WEIGHT );
342 aFont.SetWeight( rWeightItem.GetWeight() );
344 // initializing virtual device
345 ScopedVclPtrInstance< VirtualDevice > pVirDev(DeviceFormat::WITHOUT_ALPHA);
346 pVirDev->SetMapMode(MapMode(MapUnit::Map100thMM));
347 pVirDev->SetFont( aFont );
348 pVirDev->SetAntialiasing( AntialiasingFlags::DisableText );
350 pVirDev->EnableRTL();
351 if ( rParagraph.nFrameDirection == SvxFrameDirection::Horizontal_RL_TB )
352 pVirDev->SetLayoutMode( vcl::text::ComplexTextLayoutFlags::BiDiRtl );
354 const SvxCharScaleWidthItem& rCharScaleWidthItem = rSdrObjCustomShape.GetMergedItem( EE_CHAR_FONTWIDTH );
355 sal_uInt16 nCharScaleWidth = rCharScaleWidthItem.GetValue();
356 sal_Int32 nWidth = 0;
358 // VERTICAL
359 if ( bIsVertical )
361 // vertical _> each single character needs to be rotated by 90
362 sal_Int32 i;
363 sal_Int32 nHeight = 0;
364 tools::Rectangle aSingleCharacterUnion;
365 for ( i = 0; i < rText.getLength(); i++ )
367 FWCharacterData aCharacterData;
368 OUString aCharText( rText[ i ] );
369 if ( pVirDev->GetTextOutlines( aCharacterData.vOutlines, aCharText, 0, 0, -1, nWidth, {} ) )
371 sal_Int32 nTextWidth = pVirDev->GetTextWidth( aCharText);
372 if ( aCharacterData.vOutlines.empty() )
374 nHeight += rFWData.nSingleLineHeight;
376 else
378 for ( auto& rOutline : aCharacterData.vOutlines )
380 // rotating
381 rOutline.Rotate( Point( nTextWidth / 2, rFWData.nSingleLineHeight / 2 ), 900_deg10 );
382 aCharacterData.aBoundRect.Union( rOutline.GetBoundRect() );
384 for ( auto& rOutline : aCharacterData.vOutlines )
386 sal_Int32 nM = - aCharacterData.aBoundRect.Left() + nHeight;
387 rOutline.Move( nM, 0 );
388 aCharacterData.aBoundRect.Move( nM, 0 );
390 nHeight += aCharacterData.aBoundRect.GetWidth() + ( rFWData.nSingleLineHeight / 5 );
391 aSingleCharacterUnion.Union( aCharacterData.aBoundRect );
394 rParagraph.vCharacters.push_back( aCharacterData );
396 for ( auto& rCharacter : rParagraph.vCharacters )
398 for ( auto& rOutline : rCharacter.vOutlines )
400 rOutline.Move( ( aSingleCharacterUnion.GetWidth() - rCharacter.aBoundRect.GetWidth() ) / 2, 0 );
404 else
406 KernArray aDXArry;
407 if ( ( nCharScaleWidth != 100 ) && nCharScaleWidth )
408 { // applying character spacing
409 pVirDev->GetTextArray( rText, &aDXArry);
410 FontMetric aFontMetric( pVirDev->GetFontMetric() );
411 aFont.SetAverageFontWidth( static_cast<sal_Int32>( static_cast<double>(aFontMetric.GetAverageFontWidth()) * ( double(100) / static_cast<double>(nCharScaleWidth) ) ) );
412 pVirDev->SetFont( aFont );
414 FWCharacterData aCharacterData;
415 if ( pVirDev->GetTextOutlines( aCharacterData.vOutlines, rText, 0, 0, -1, nWidth, aDXArry ) )
417 rParagraph.vCharacters.push_back( aCharacterData );
419 else
421 // GetTextOutlines failed what usually means that it is
422 // not implemented. To make FontWork not fail (it is
423 // dependent of graphic content to get a Range) create
424 // a rectangle substitution for now
425 pVirDev->GetTextArray( rText, &aDXArry);
426 aCharacterData.vOutlines.clear();
428 if(aDXArry.size())
430 for(size_t a(0); a < aDXArry.size(); a++)
432 const basegfx::B2DPolygon aPolygon(
433 basegfx::utils::createPolygonFromRect(
434 basegfx::B2DRange(
435 0 == a ? 0 : aDXArry[a - 1],
437 aDXArry[a],
438 aFont.GetFontHeight()
439 )));
440 aCharacterData.vOutlines.push_back(tools::PolyPolygon(tools::Polygon(aPolygon)));
443 else
445 const basegfx::B2DPolygon aPolygon(
446 basegfx::utils::createPolygonFromRect(
447 basegfx::B2DRange(
450 aDXArry.empty() ? 10 : aDXArry.back(),
451 aFont.GetFontHeight()
452 )));
453 aCharacterData.vOutlines.push_back(tools::PolyPolygon(tools::Polygon(aPolygon)));
457 rParagraph.vCharacters.push_back( aCharacterData );
461 // vertical alignment
462 for ( auto& rCharacter : rParagraph.vCharacters )
464 for( tools::PolyPolygon& rPolyPoly : rCharacter.vOutlines )
466 if ( nVerticalOffset )
467 rPolyPoly.Move( 0, nVerticalOffset );
469 // retrieving the boundrect for the paragraph
470 tools::Rectangle aBoundRect( rPolyPoly.GetBoundRect() );
471 rParagraph.aBoundRect.Union( aBoundRect );
475 // updating the boundrect for the text area by merging the current paragraph boundrect
476 if ( rParagraph.aBoundRect.IsEmpty() )
478 if ( rTextArea.aBoundRect.IsEmpty() )
479 rTextArea.aBoundRect = tools::Rectangle( Point( 0, 0 ), Size( 1, rFWData.nSingleLineHeight ) );
480 else
481 rTextArea.aBoundRect.AdjustBottom(rFWData.nSingleLineHeight );
483 else
485 tools::Rectangle& rParagraphBoundRect = rParagraph.aBoundRect;
486 rTextArea.aBoundRect.Union( rParagraphBoundRect );
488 if ( bSameLetterHeights )
490 for ( auto& rCharacter : rParagraph.vCharacters )
492 for( auto& rOutline : rCharacter.vOutlines )
494 tools::Rectangle aPolyPolyBoundRect( rOutline.GetBoundRect() );
495 if (aPolyPolyBoundRect.GetHeight() != rParagraphBoundRect.GetHeight() && aPolyPolyBoundRect.GetHeight())
496 rOutline.Scale( 1.0, static_cast<double>(rParagraphBoundRect.GetHeight()) / aPolyPolyBoundRect.GetHeight() );
497 aPolyPolyBoundRect = rOutline.GetBoundRect();
498 sal_Int32 nMove = aPolyPolyBoundRect.Top() - rParagraphBoundRect.Top();
499 if ( nMove )
500 rOutline.Move( 0, -nMove );
505 if ( bIsVertical )
506 nVerticalOffset -= rFWData.nSingleLineHeight;
507 else
508 nVerticalOffset += rFWData.nSingleLineHeight;
512 static bool GetFontWorkOutline(
513 FWData& rFWData,
514 const SdrObjCustomShape& rSdrObjCustomShape)
516 SdrTextHorzAdjust eHorzAdjust(rSdrObjCustomShape.GetMergedItem( SDRATTR_TEXT_HORZADJUST ).GetValue());
517 drawing::TextFitToSizeType const eFTS(rSdrObjCustomShape.GetMergedItem( SDRATTR_TEXT_FITTOSIZE ).GetValue());
519 bool bSameLetterHeights = false;
520 const SdrCustomShapeGeometryItem& rGeometryItem(rSdrObjCustomShape.GetMergedItem( SDRATTR_CUSTOMSHAPE_GEOMETRY ));
521 const css::uno::Any* pAny = rGeometryItem.GetPropertyValueByName( "TextPath", "SameLetterHeights" );
522 if ( pAny )
523 *pAny >>= bSameLetterHeights;
525 const SvxFontHeightItem& rFontHeight( rSdrObjCustomShape.GetMergedItem( EE_CHAR_FONTHEIGHT ) );
526 if (rFWData.bScaleX)
527 rFWData.nSingleLineHeight = rFWData.fVerticalTextScaling * rFontHeight.GetHeight();
528 else
529 rFWData.nSingleLineHeight = static_cast<sal_Int32>( ( static_cast<double>( rSdrObjCustomShape.GetLogicRect().GetHeight() )
530 / rFWData.nMaxParagraphsPerTextArea ) * rFWData.fHorizontalTextScaling );
532 if (rFWData.nSingleLineHeight == SAL_MIN_INT32)
533 return false;
535 for ( auto& rTextArea : rFWData.vTextAreas )
537 GetTextAreaOutline(
538 rFWData,
539 rSdrObjCustomShape,
540 rTextArea,
541 bSameLetterHeights);
543 if (eFTS == drawing::TextFitToSizeType_ALLLINES ||
544 // tdf#97630 interpret PROPORTIONAL same as ALLLINES so we don't
545 // need another ODF attribute!
546 eFTS == drawing::TextFitToSizeType_PROPORTIONAL)
548 for ( auto& rParagraph : rTextArea.vParagraphs )
550 sal_Int32 nParaWidth = rParagraph.aBoundRect.GetWidth();
551 if ( nParaWidth )
553 double fScale = static_cast<double>(rTextArea.aBoundRect.GetWidth()) / nParaWidth;
555 for ( auto& rCharacter : rParagraph.vCharacters )
557 for( auto& rOutline : rCharacter.vOutlines )
559 rOutline.Scale( fScale, 1.0 );
565 else if (rFWData.bScaleX)
567 const SdrTextVertAdjust nVertJustify = rSdrObjCustomShape.GetMergedItem( SDRATTR_TEXT_VERTADJUST ).GetValue();
568 double fFactor = nVertJustify == SdrTextVertAdjust::SDRTEXTVERTADJUST_BOTTOM ? -0.5 : ( nVertJustify == SdrTextVertAdjust::SDRTEXTVERTADJUST_TOP ? 0.5 : 0 );
570 for ( auto& rParagraph : rTextArea.vParagraphs )
572 sal_Int32 nHorzDiff = 0;
573 sal_Int32 nVertDiff = static_cast<double>( rFWData.nSingleLineHeight ) * fFactor * ( rTextArea.vParagraphs.size() - 1 );
574 rTextArea.nHAlignMove = nVertDiff;
576 if ( eHorzAdjust == SDRTEXTHORZADJUST_CENTER )
577 nHorzDiff = ( rFWData.fHorizontalTextScaling * rTextArea.aBoundRect.GetWidth() - rParagraph.aBoundRect.GetWidth() ) / 2;
578 else if ( eHorzAdjust == SDRTEXTHORZADJUST_RIGHT )
579 nHorzDiff = ( rFWData.fHorizontalTextScaling * rTextArea.aBoundRect.GetWidth() - rParagraph.aBoundRect.GetWidth() );
581 if (nHorzDiff || nVertDiff)
583 for ( auto& rCharacter : rParagraph.vCharacters )
585 for( auto& rOutline : rCharacter.vOutlines )
587 rOutline.Move( nHorzDiff, nVertDiff );
593 else
595 switch( eHorzAdjust )
597 case SDRTEXTHORZADJUST_RIGHT :
598 case SDRTEXTHORZADJUST_CENTER:
600 for ( auto& rParagraph : rTextArea.vParagraphs )
602 sal_Int32 nHorzDiff = 0;
603 if ( eHorzAdjust == SDRTEXTHORZADJUST_CENTER )
604 nHorzDiff = ( rTextArea.aBoundRect.GetWidth() - rParagraph.aBoundRect.GetWidth() ) / 2;
605 else if ( eHorzAdjust == SDRTEXTHORZADJUST_RIGHT )
606 nHorzDiff = ( rTextArea.aBoundRect.GetWidth() - rParagraph.aBoundRect.GetWidth() );
607 if ( nHorzDiff )
609 for ( auto& rCharacter : rParagraph.vCharacters )
611 for( auto& rOutline : rCharacter.vOutlines )
613 rOutline.Move( nHorzDiff, 0 );
619 break;
620 default:
621 case SDRTEXTHORZADJUST_BLOCK : break; // don't know
622 case SDRTEXTHORZADJUST_LEFT : break; // already left aligned -> nothing to do
627 return true;
630 static basegfx::B2DPolyPolygon GetOutlinesFromShape2d( const SdrObject* pShape2d )
632 basegfx::B2DPolyPolygon aOutlines2d;
634 SdrObjListIter aObjListIter( *pShape2d, SdrIterMode::DeepWithGroups );
635 while( aObjListIter.IsMore() )
637 SdrObject* pPartObj = aObjListIter.Next();
638 if ( auto pPathObj = dynamic_cast<const SdrPathObj*>( pPartObj))
640 basegfx::B2DPolyPolygon aCandidate(pPathObj->GetPathPoly());
641 if(aCandidate.areControlPointsUsed())
643 aCandidate = basegfx::utils::adaptiveSubdivideByAngle(aCandidate);
645 aOutlines2d.append(aCandidate);
649 return aOutlines2d;
652 static void CalcDistances( const tools::Polygon& rPoly, std::vector< double >& rDistances )
654 sal_uInt16 i, nCount = rPoly.GetSize();
655 if ( nCount <= 1 )
656 return;
658 for ( i = 0; i < nCount; i++ )
660 double fDistance = i ? rPoly.CalcDistance( i, i - 1 ) : 0.0;
661 rDistances.push_back( fDistance );
663 std::partial_sum( rDistances.begin(), rDistances.end(), rDistances.begin() );
664 double fLength = rDistances[ rDistances.size() - 1 ];
665 if ( fLength > 0.0 )
667 for ( auto& rDistance : rDistances )
668 rDistance /= fLength;
672 static void InsertMissingOutlinePoints( const std::vector< double >& rDistances,
673 const tools::Rectangle& rTextAreaBoundRect, tools::Polygon& rPoly )
675 sal_uInt16 nSize = rPoly.GetSize();
676 if (nSize == 0)
677 return;
679 tools::Long nTextWidth = rTextAreaBoundRect.GetWidth();
681 if (nTextWidth == 0)
682 throw o3tl::divide_by_zero();
684 double fLastDistance = 0.0;
685 for (sal_uInt16 i = 0; i < nSize; ++i)
687 Point& rPoint = rPoly[ i ];
688 double fDistance = static_cast<double>( rPoint.X() - rTextAreaBoundRect.Left() ) / static_cast<double>(nTextWidth);
689 if ( i )
691 if ( fDistance > fLastDistance )
693 std::vector< double >::const_iterator aIter = std::upper_bound( rDistances.begin(), rDistances.end(), fLastDistance );
694 if ( aIter != rDistances.end() && ( *aIter > fLastDistance ) && ( *aIter < fDistance ) )
696 Point& rPt0 = rPoly[ i - 1 ];
697 sal_Int32 fX = rPoint.X() - rPt0.X();
698 sal_Int32 fY = rPoint.Y() - rPt0.Y();
699 double fd = ( 1.0 / ( fDistance - fLastDistance ) ) * ( *aIter - fLastDistance );
700 rPoly.Insert( i, Point( static_cast<sal_Int32>( rPt0.X() + fX * fd ), static_cast<sal_Int32>( rPt0.Y() + fY * fd ) ) );
701 fDistance = *aIter;
704 else if ( fDistance < fLastDistance )
706 std::vector< double >::const_iterator aIter = std::lower_bound( rDistances.begin(), rDistances.end(), fLastDistance );
707 if ( aIter != rDistances.begin() )
709 --aIter;
710 if ( ( *aIter > fDistance ) && ( *aIter < fLastDistance ) )
712 Point& rPt0 = rPoly[ i - 1 ];
713 sal_Int32 fX = rPoint.X() - rPt0.X();
714 sal_Int32 fY = rPoint.Y() - rPt0.Y();
715 double fd = ( 1.0 / ( fDistance - fLastDistance ) ) * ( *aIter - fLastDistance );
716 rPoly.Insert( i, Point( static_cast<sal_Int32>( rPt0.X() + fX * fd ), static_cast<sal_Int32>( rPt0.Y() + fY * fd ) ) );
717 fDistance = *aIter;
722 fLastDistance = fDistance;
726 //only 2 types used: 'const tools::Polygon&' and 'const std::vector<Point>&'
727 template <class T>
728 static void GetPoint( T rPoly, const std::vector< double >& rDistances, const double& fX, double& fx1, double& fy1 )
730 fy1 = fx1 = 0.0;
731 if (rPoly.size() <= 1)
732 return;
734 std::vector< double >::const_iterator aIter = std::lower_bound( rDistances.begin(), rDistances.end(), fX );
735 sal_uInt16 nIdx = sal::static_int_cast<sal_uInt16>( std::distance( rDistances.begin(), aIter ) );
736 if ( aIter == rDistances.end() )
737 nIdx--;
738 const Point& rPt = rPoly[ nIdx ];
739 fx1 = rPt.X();
740 fy1 = rPt.Y();
741 if ( !nIdx || ( aIter == rDistances.end() ) || rtl::math::approxEqual( *aIter, fX ) )
742 return;
744 nIdx = sal::static_int_cast<sal_uInt16>( std::distance( rDistances.begin(), aIter ) );
745 double fDist0 = *( aIter - 1 );
746 double fd = ( 1.0 / ( *aIter - fDist0 ) ) * ( fX - fDist0 );
747 const Point& rPt2 = rPoly[ nIdx - 1 ];
748 double fWidth = rPt.X() - rPt2.X();
749 double fHeight= rPt.Y() - rPt2.Y();
750 fWidth *= fd;
751 fHeight*= fd;
752 fx1 = rPt2.X() + fWidth;
753 fy1 = rPt2.Y() + fHeight;
756 static void FitTextOutlinesToShapeOutlines(const tools::PolyPolygon& aOutlines2d, FWData& rFWData,
757 SdrTextHorzAdjust eHorzAdjust, bool bPPFontwork)
759 sal_uInt16 nOutline2dIdx = 0;
760 for( auto& rTextArea : rFWData.vTextAreas )
762 tools::Rectangle rTextAreaBoundRect = rTextArea.aBoundRect;
763 sal_Int32 nLeft = rTextAreaBoundRect.Left();
764 sal_Int32 nTop = rTextAreaBoundRect.Top();
765 sal_Int32 nWidth = rTextAreaBoundRect.GetWidth();
766 sal_Int32 nHeight= rTextAreaBoundRect.GetHeight();
768 if (rFWData.bScaleX)
770 nWidth *= rFWData.fHorizontalTextScaling;
773 if ( rFWData.bSingleLineMode && nHeight && nWidth )
775 if ( nOutline2dIdx >= aOutlines2d.Count() )
776 break;
777 const tools::Polygon& rOutlinePoly( aOutlines2d[ nOutline2dIdx++ ] );
778 const sal_uInt16 nPointCount = rOutlinePoly.GetSize();
779 if ( nPointCount > 1 )
781 std::vector< double > vDistances;
782 vDistances.reserve( nPointCount );
783 CalcDistances( rOutlinePoly, vDistances );
785 if ( !vDistances.empty() )
787 // horizontal alignment: how much we have to move text to the right.
788 int nAdjust = -1;
789 switch (eHorzAdjust)
791 case SDRTEXTHORZADJUST_RIGHT:
792 nAdjust = 2; // 2 half of the possible
793 break;
794 case SDRTEXTHORZADJUST_CENTER:
795 nAdjust = 1; // 1 half of the possible
796 break;
797 case SDRTEXTHORZADJUST_BLOCK:
798 nAdjust = -1; // don't know what it is, so don't even align
799 break;
800 case SDRTEXTHORZADJUST_LEFT:
801 nAdjust = 0; // no need to move
802 break;
805 if (bPPFontwork && rTextArea.vParagraphs.size() > 1 && nAdjust >= 0)
807 // If we have multiple lines of text to fit to the outline (curve)
808 // then we have to be able to calculate outer versions of the outline
809 // where we can fit the next lines of texts
810 // those outer lines will be wider (or shorter) as the original outline
811 // and probably will looks different as the original outline.
813 // for example if we have an outline like this:
814 // <____>
815 // then the middle part will have the same normals, so distances there,
816 // will not change for an outer outline
817 // while the points near the edge will have different normals,
818 // distances around there will increase for an outer (wider) outline
820 //Normal vectors for every rOutlinePoly point. 1024 long
821 std::vector<Point> vNorm;
822 //wider curve path points, for current paragraph (rOutlinePoly + vNorm*line)
823 std::vector<Point> vCurOutline;
824 //distances between points of this wider curve
825 std::vector<double> vCurDistances;
827 vCurDistances.reserve(nPointCount);
828 vCurOutline.reserve(nPointCount);
829 vNorm.reserve(nPointCount);
831 // Calculate Normal vectors, and allocate curve data
832 sal_uInt16 i;
833 for (i = 0; i < nPointCount; i++)
835 //Normal vector for a point will be calculated from its neighbour points
836 //except if it is in the start/end of the vector
837 sal_uInt16 nPointIdx1 = i == 0 ? i : i - 1;
838 sal_uInt16 nPointIdx2 = i == nPointCount - 1 ? i : i + 1;
840 Point aPoint = rOutlinePoly.GetPoint(nPointIdx2)
841 - rOutlinePoly.GetPoint(nPointIdx1);
843 double fLen = sqrt(aPoint.X() * aPoint.X() + aPoint.Y() * aPoint.Y());
845 if (fLen > 0)
847 //Rotate by 90 degree, and divide by length, to get normal vector
848 vNorm.emplace_back(aPoint.getY() * 1024 / fLen,
849 -aPoint.getX() * 1024 / fLen);
851 else
853 vNorm.emplace_back(0, 0);
855 vCurOutline.emplace_back(Point());
856 vCurDistances.push_back(0);
860 for( auto& rParagraph : rTextArea.vParagraphs )
862 //calculate the actual outline length, and its align adjustments
863 double fAdjust;
864 double fCurWidth;
866 // distance between the original and the current curve
867 double fCurvesDist = rTextArea.aBoundRect.GetHeight() / 2.0
868 + rTextArea.aBoundRect.Top()
869 - rParagraph.aBoundRect.Center().Y();
870 // vertical alignment adjust
871 fCurvesDist -= rTextArea.nHAlignMove;
873 for (i = 0; i < nPointCount; i++)
875 vCurOutline[i]
876 = rOutlinePoly.GetPoint(i) + vNorm[i] * fCurvesDist / 1024.0;
877 if (i > 0)
879 //calculate distances between points on the outer outline
880 const double fDx = vCurOutline[i].X() - vCurOutline[i - 1].X();
881 const double fDy = vCurOutline[i].Y() - vCurOutline[i - 1].Y();
882 vCurDistances[i] = sqrt(fDx * fDx + fDy * fDy);
884 else
885 vCurDistances[i] = 0;
887 std::partial_sum(vCurDistances.begin(), vCurDistances.end(),
888 vCurDistances.begin());
889 fCurWidth = vCurDistances[vCurDistances.size() - 1];
890 if (fCurWidth > 0.0)
892 for (auto& rDistance : vCurDistances)
893 rDistance /= fCurWidth;
896 // if the current outline is longer then the text to fit in,
897 // then we have to divide the bonus space between the
898 // before-/after- text area.
899 // fAdjust means how much space we put before the text.
900 if (fCurWidth > rParagraph.aBoundRect.GetWidth())
902 fAdjust
903 = nAdjust * (fCurWidth - rParagraph.aBoundRect.GetWidth()) / 2;
905 else
906 fAdjust = -1; // we need to shrink the text to fit the curve
908 for ( auto& rCharacter : rParagraph.vCharacters )
910 for (tools::PolyPolygon& rPolyPoly : rCharacter.vOutlines)
912 tools::Rectangle aBoundRect(rPolyPoly.GetBoundRect());
913 double fx1 = aBoundRect.Left() - nLeft;
914 double fx2 = aBoundRect.Right() - nLeft;
916 double fParaRectWidth = rParagraph.aBoundRect.GetWidth();
917 // Undo Horizontal alignment, hacked into poly coords,
918 // so we can calculate it the right way
919 double fHA = (rFWData.fHorizontalTextScaling
920 * rTextArea.aBoundRect.GetWidth()
921 - rParagraph.aBoundRect.GetWidth())
922 * nAdjust / 2;
924 fx1 -= fHA;
925 fx2 -= fHA;
927 double fy1, fy2;
928 double fM1 = fx1 / fParaRectWidth;
929 double fM2 = fx2 / fParaRectWidth;
931 // if fAdjust<0, then it means, the text was longer, as
932 // the current outline, so we will skip the text scaling, and
933 // the text horizontal alignment adjustment
934 // so the text will be rendered just as long as the curve is.
935 if (fAdjust >= 0)
937 fM1 = (fM1 * fParaRectWidth + fAdjust) / fCurWidth;
938 fM2 = (fM2 * fParaRectWidth + fAdjust) / fCurWidth;
940 // 0 <= fM1,fM2 <= 1 should be true, but rounding errors can
941 // make a small mistake.
942 // make sure they are >0 because GetPoint() need that
943 if (fM1 < 0) fM1 = 0;
944 if (fM2 < 0) fM2 = 0;
946 GetPoint(vCurOutline, vCurDistances, fM1, fx1, fy1);
947 GetPoint(vCurOutline, vCurDistances, fM2, fx2, fy2);
949 double fvx = fy2 - fy1;
950 double fvy = - ( fx2 - fx1 );
951 fx1 = fx1 + ( ( fx2 - fx1 ) * 0.5 );
952 fy1 = fy1 + ( ( fy2 - fy1 ) * 0.5 );
954 double fAngle = atan2( -fvx, -fvy );
955 double fL = hypot( fvx, fvy );
956 if (fL == 0.0)
958 SAL_WARN("svx", "FitTextOutlinesToShapeOutlines div-by-zero, abandon fit");
959 break;
961 fvx = fvx / fL;
962 fvy = fvy / fL;
963 // Undo Vertical alignment hacked into poly coords
964 // We already calculated the right alignment into the curve
965 fL = rTextArea.nHAlignMove;
966 fvx *= fL;
967 fvy *= fL;
968 rPolyPoly.Rotate( Point( aBoundRect.Center().X(), rParagraph.aBoundRect.Center().Y() ), sin( fAngle ), cos( fAngle ) );
969 rPolyPoly.Move( static_cast<sal_Int32>( ( fx1 + fvx )- aBoundRect.Center().X() ), static_cast<sal_Int32>( ( fy1 + fvy ) - rParagraph.aBoundRect.Center().Y() ) );
974 else
976 // Fallback / old way to handle multiple lines:
977 // Every text lines use the same original outline (curve),
978 // it just scale character coordinates to fit to the right text line
979 // (curve), resulting wider/thinner space between characters
980 for (auto& rParagraph : rTextArea.vParagraphs)
982 for (auto& rCharacter : rParagraph.vCharacters)
984 for (tools::PolyPolygon& rPolyPoly : rCharacter.vOutlines)
986 tools::Rectangle aBoundRect(rPolyPoly.GetBoundRect());
987 double fx1 = aBoundRect.Left() - nLeft;
988 double fx2 = aBoundRect.Right() - nLeft;
989 double fy1, fy2;
990 double fM1 = fx1 / static_cast<double>(nWidth);
991 double fM2 = fx2 / static_cast<double>(nWidth);
993 GetPoint(rOutlinePoly, vDistances, fM1, fx1, fy1);
994 GetPoint(rOutlinePoly, vDistances, fM2, fx2, fy2);
996 double fvx = fy2 - fy1;
997 double fvy = -(fx2 - fx1);
998 fx1 = fx1 + ((fx2 - fx1) * 0.5);
999 fy1 = fy1 + ((fy2 - fy1) * 0.5);
1001 double fAngle = atan2(-fvx, -fvy);
1002 double fL = hypot(fvx, fvy);
1003 if (fL == 0.0)
1005 SAL_WARN("svx", "FitTextOutlinesToShapeOutlines div-by-zero, abandon fit");
1006 break;
1008 fvx = fvx / fL;
1009 fvy = fvy / fL;
1010 fL = rTextArea.aBoundRect.GetHeight() / 2.0 + rTextArea.aBoundRect.Top() - rParagraph.aBoundRect.Center().Y();
1011 fvx *= fL;
1012 fvy *= fL;
1013 rPolyPoly.Rotate( Point( aBoundRect.Center().X(), rParagraph.aBoundRect.Center().Y() ), sin( fAngle ), cos( fAngle ) );
1014 rPolyPoly.Move( static_cast<sal_Int32>( ( fx1 + fvx )- aBoundRect.Center().X() ), static_cast<sal_Int32>( ( fy1 + fvy ) - rParagraph.aBoundRect.Center().Y() ) );
1023 else
1025 if ( ( nOutline2dIdx + 1 ) >= aOutlines2d.Count() )
1026 break;
1027 const tools::Polygon& rOutlinePoly( aOutlines2d[ nOutline2dIdx++ ] );
1028 const tools::Polygon& rOutlinePoly2( aOutlines2d[ nOutline2dIdx++ ] );
1029 const sal_uInt16 nPointCount = rOutlinePoly.GetSize();
1030 const sal_uInt16 nPointCount2 = rOutlinePoly2.GetSize();
1031 if ( ( nPointCount > 1 ) && ( nPointCount2 > 1 ) )
1033 std::vector< double > vDistances;
1034 vDistances.reserve( nPointCount );
1035 std::vector< double > vDistances2;
1036 vDistances2.reserve( nPointCount2 );
1037 CalcDistances( rOutlinePoly, vDistances );
1038 CalcDistances( rOutlinePoly2, vDistances2 );
1039 for( auto& rParagraph : rTextArea.vParagraphs )
1041 for ( auto& rCharacter : rParagraph.vCharacters )
1043 for( tools::PolyPolygon& rPolyPoly : rCharacter.vOutlines )
1045 sal_uInt16 i, nPolyCount = rPolyPoly.Count();
1046 for ( i = 0; i < nPolyCount; i++ )
1048 // #i35928#
1049 basegfx::B2DPolygon aCandidate(rPolyPoly[ i ].getB2DPolygon());
1051 if(aCandidate.areControlPointsUsed())
1053 aCandidate = basegfx::utils::adaptiveSubdivideByAngle(aCandidate);
1056 // create local polygon copy to work on
1057 tools::Polygon aLocalPoly(aCandidate);
1059 InsertMissingOutlinePoints( vDistances, rTextAreaBoundRect, aLocalPoly );
1060 InsertMissingOutlinePoints( vDistances2, rTextAreaBoundRect, aLocalPoly );
1062 sal_uInt16 _nPointCount = aLocalPoly.GetSize();
1063 if (_nPointCount)
1065 if (!nWidth || !nHeight)
1066 throw o3tl::divide_by_zero();
1067 for (sal_uInt16 j = 0; j < _nPointCount; ++j)
1069 Point& rPoint = aLocalPoly[ j ];
1070 rPoint.AdjustX( -nLeft );
1071 rPoint.AdjustY( -nTop );
1072 double fX = static_cast<double>(rPoint.X()) / static_cast<double>(nWidth);
1073 double fY = static_cast<double>(rPoint.Y()) / static_cast<double>(nHeight);
1075 double fx1, fy1, fx2, fy2;
1076 GetPoint( rOutlinePoly, vDistances, fX, fx1, fy1 );
1077 GetPoint( rOutlinePoly2, vDistances2, fX, fx2, fy2 );
1078 double fWidth = fx2 - fx1;
1079 double fHeight= fy2 - fy1;
1080 rPoint.setX( static_cast<sal_Int32>( fx1 + fWidth * fY ) );
1081 rPoint.setY( static_cast<sal_Int32>( fy1 + fHeight* fY ) );
1085 // write back polygon
1086 rPolyPoly[ i ] = aLocalPoly;
1096 static rtl::Reference<SdrObject> CreateSdrObjectFromParagraphOutlines(
1097 const FWData& rFWData,
1098 const SdrObjCustomShape& rSdrObjCustomShape)
1100 rtl::Reference<SdrObject> pRet;
1101 basegfx::B2DPolyPolygon aPolyPoly;
1102 if ( !rFWData.vTextAreas.empty() )
1104 for ( const auto& rTextArea : rFWData.vTextAreas )
1106 for ( const auto& rParagraph : rTextArea.vParagraphs )
1108 for ( const auto& rCharacter : rParagraph.vCharacters )
1110 for( const auto& rOutline : rCharacter.vOutlines )
1112 aPolyPoly.append( rOutline.getB2DPolyPolygon() );
1118 pRet = new SdrPathObj(
1119 rSdrObjCustomShape.getSdrModelFromSdrObject(),
1120 SdrObjKind::Polygon,
1121 std::move(aPolyPoly));
1123 SfxItemSet aSet(rSdrObjCustomShape.GetMergedItemSet());
1124 aSet.ClearItem( SDRATTR_TEXTDIRECTION ); //SJ: vertical writing is not required, by removing this item no outliner is created
1125 aSet.Put(makeSdrShadowItem(false)); // #i37011# NO shadow for FontWork geometry
1126 pRet->SetMergedItemSet( aSet ); // * otherwise we would crash, because the outliner tries to create a Paraobject, but there is no model
1129 return pRet;
1132 Reference < i18n::XBreakIterator > EnhancedCustomShapeFontWork::mxBreakIterator;
1134 Reference < i18n::XBreakIterator > const & EnhancedCustomShapeFontWork::GetBreakIterator()
1136 if ( !mxBreakIterator.is() )
1138 Reference< uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext();
1139 mxBreakIterator = i18n::BreakIterator::create(xContext);
1141 return mxBreakIterator;
1144 rtl::Reference<SdrObject> EnhancedCustomShapeFontWork::CreateFontWork(
1145 const SdrObject* pShape2d,
1146 const SdrObjCustomShape& rSdrObjCustomShape)
1148 rtl::Reference<SdrObject> pRet;
1150 // calculating scaling factor is too slow
1151 if (utl::ConfigManager::IsFuzzing())
1152 return pRet;
1154 tools::PolyPolygon aOutlines2d( GetOutlinesFromShape2d( pShape2d ) );
1155 sal_uInt16 nOutlinesCount2d = aOutlines2d.Count();
1156 if ( nOutlinesCount2d )
1158 FWData aFWData;
1160 if(InitializeFontWorkData(rSdrObjCustomShape, nOutlinesCount2d, aFWData))
1162 /* retrieves the horizontal scaling factor that has to be used
1163 to fit each paragraph text into its corresponding 2d outline */
1164 CalculateHorizontalScalingFactor(
1165 rSdrObjCustomShape,
1166 aFWData,
1167 aOutlines2d);
1169 /* retrieving the Outlines for the each Paragraph. */
1170 if(!GetFontWorkOutline(
1171 aFWData,
1172 rSdrObjCustomShape))
1174 return nullptr;
1177 SdrTextHorzAdjust eHorzAdjust(
1178 rSdrObjCustomShape.GetMergedItem(SDRATTR_TEXT_HORZADJUST).GetValue());
1179 bool bPPFontwork = !rSdrObjCustomShape.getSdrModelFromSdrObject().GetCompatibilityFlag(
1180 SdrCompatibilityFlag::LegacyFontwork);
1181 FitTextOutlinesToShapeOutlines( aOutlines2d, aFWData, eHorzAdjust, bPPFontwork );
1183 pRet = CreateSdrObjectFromParagraphOutlines(
1184 aFWData,
1185 rSdrObjCustomShape);
1188 return pRet;
1191 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */