Bump version to 21.06.18.1
[LibreOffice.git] / cppcanvas / source / mtfrenderer / mtftools.cxx
blobc5d36eb00652462c8d9a2661beb9fe91a437ef8f
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 .
21 #include <tools/diagnose_ex.h>
22 #include <com/sun/star/rendering/XCanvas.hpp>
23 #include <basegfx/utils/canvastools.hxx>
24 #include <basegfx/polygon/b2dpolygontools.hxx>
25 #include <basegfx/polygon/b2dpolygon.hxx>
26 #include <basegfx/range/b2drectangle.hxx>
27 #include <basegfx/vector/b2dvector.hxx>
28 #include <canvas/canvastools.hxx>
29 #include <rtl/math.hxx>
30 #include <vcl/canvastools.hxx>
31 #include <vcl/virdev.hxx>
32 #include <vcl/metric.hxx>
33 #include "mtftools.hxx"
34 #include <outdevstate.hxx>
35 #include <basegfx/matrix/b2dhommatrixtools.hxx>
38 using namespace ::com::sun::star;
40 namespace cppcanvas::tools
42 void initRenderState( rendering::RenderState& renderState,
43 const ::cppcanvas::internal::OutDevState& outdevState )
45 ::canvas::tools::initRenderState( renderState );
46 ::canvas::tools::setRenderStateTransform( renderState,
47 outdevState.transform );
48 renderState.Clip = outdevState.xClipPoly;
51 ::Size getBaselineOffset( const ::cppcanvas::internal::OutDevState& outdevState,
52 const VirtualDevice& rVDev )
54 const ::FontMetric& aMetric = rVDev.GetFontMetric();
56 // calc offset for text output, the XCanvas always renders
57 // baseline offset.
58 switch( outdevState.textReferencePoint )
60 case ALIGN_TOP:
61 return ::Size( 0,
62 aMetric.GetInternalLeading() + aMetric.GetAscent() );
64 default:
65 ENSURE_OR_THROW( false,
66 "tools::getBaselineOffset(): Unexpected TextAlign value" );
67 // FALLTHROUGH intended (to calm compiler warning - case won't happen)
68 case ALIGN_BASELINE:
69 return ::Size( 0, 0 );
71 case ALIGN_BOTTOM:
72 return ::Size( 0,
73 -aMetric.GetDescent() );
78 ::basegfx::B2DHomMatrix& calcLogic2PixelLinearTransform( ::basegfx::B2DHomMatrix& o_rMatrix,
79 const VirtualDevice& rVDev )
81 // select size value in the middle of the available range,
82 // to have headroom both when map mode scales up, and when
83 // it scales down.
84 const ::Size aSizeLogic( 0x00010000L,
85 0x00010000L );
87 const ::Size aSizePixel( rVDev.LogicToPixel( aSizeLogic ) );
89 o_rMatrix = basegfx::utils::createScaleB2DHomMatrix(
90 aSizePixel.Width() / static_cast<double>(aSizeLogic.Width()),
91 aSizePixel.Height() / static_cast<double>(aSizeLogic.Height()) );
93 return o_rMatrix;
96 ::basegfx::B2DHomMatrix& calcLogic2PixelAffineTransform( ::basegfx::B2DHomMatrix& o_rMatrix,
97 const VirtualDevice& rVDev )
99 // retrieves scale
100 calcLogic2PixelLinearTransform(o_rMatrix, rVDev);
102 // translate according to curr map mode/pref map mode offset
103 const ::Point aEmptyPoint;
104 const ::Point& rTranslatedPoint(
105 rVDev.LogicToPixel( aEmptyPoint ));
107 o_rMatrix.translate(rTranslatedPoint.X(),
108 rTranslatedPoint.Y());
110 return o_rMatrix;
113 bool modifyClip( rendering::RenderState& o_rRenderState,
114 const struct ::cppcanvas::internal::OutDevState& rOutdevState,
115 const CanvasSharedPtr& rCanvas,
116 const ::basegfx::B2DPoint& rOffset,
117 const ::basegfx::B2DVector* pScaling,
118 const double* pRotation )
120 const bool bOffsetting( !rOffset.equalZero() );
121 const bool bScaling( pScaling &&
122 !rtl::math::approxEqual(pScaling->getX(), 1.0) &&
123 !rtl::math::approxEqual(pScaling->getY(), 1.0) );
124 const bool bRotation( pRotation &&
125 *pRotation != 0.0 );
127 if( !bOffsetting && !bScaling && !bRotation )
128 return false; // nothing to do
130 if( rOutdevState.clip.count() )
132 // general polygon case
134 ::basegfx::B2DPolyPolygon aLocalClip( rOutdevState.clip );
135 ::basegfx::B2DHomMatrix aTransform;
137 if( bOffsetting )
138 aTransform.translate( -rOffset.getX(),
139 -rOffset.getY() );
140 if( bScaling )
141 aTransform.scale( 1.0/pScaling->getX(), 1.0/pScaling->getY() );
143 if( bRotation )
144 aTransform.rotate( - *pRotation );
146 aLocalClip.transform( aTransform );
148 o_rRenderState.Clip = ::basegfx::unotools::xPolyPolygonFromB2DPolyPolygon(
149 rCanvas->getUNOCanvas()->getDevice(),
150 aLocalClip );
152 return true;
154 else if( !rOutdevState.clipRect.IsEmpty() )
156 // simple rect case
158 const ::tools::Rectangle aLocalClipRect( rOutdevState.clipRect );
160 if( bRotation )
162 // rotation involved - convert to polygon first,
163 // then transform that
164 ::basegfx::B2DPolygon aLocalClip(
165 ::basegfx::utils::createPolygonFromRect(
166 vcl::unotools::b2DRectangleFromRectangle(aLocalClipRect) ) );
167 ::basegfx::B2DHomMatrix aTransform;
169 if( bOffsetting )
170 aTransform.translate( -rOffset.getX(),
171 -rOffset.getY() );
172 if( bScaling )
173 aTransform.scale( 1.0/pScaling->getX(), 1.0/pScaling->getY() );
175 aTransform.rotate( - *pRotation );
177 aLocalClip.transform( aTransform );
179 o_rRenderState.Clip = ::basegfx::unotools::xPolyPolygonFromB2DPolyPolygon(
180 rCanvas->getUNOCanvas()->getDevice(),
181 ::basegfx::B2DPolyPolygon( aLocalClip ) );
183 else if( bScaling )
185 // scale and offset - do it on the fly, have to
186 // convert to float anyway.
187 o_rRenderState.Clip = ::basegfx::unotools::xPolyPolygonFromB2DPolyPolygon(
188 rCanvas->getUNOCanvas()->getDevice(),
189 ::basegfx::B2DPolyPolygon(
190 ::basegfx::utils::createPolygonFromRect(
191 ::basegfx::B2DRectangle(
192 (aLocalClipRect.Left() - rOffset.getX())/pScaling->getX(),
193 (aLocalClipRect.Top() - rOffset.getY())/pScaling->getY(),
194 (aLocalClipRect.Right() - rOffset.getX())/pScaling->getX(),
195 (aLocalClipRect.Bottom() - rOffset.getY())/pScaling->getY() ) ) ) );
197 else
199 // offset only - do it on the fly, have to convert
200 // to float anyway.
201 o_rRenderState.Clip = ::basegfx::unotools::xPolyPolygonFromB2DPolyPolygon(
202 rCanvas->getUNOCanvas()->getDevice(),
203 ::basegfx::B2DPolyPolygon(
204 ::basegfx::utils::createPolygonFromRect(
205 ::basegfx::B2DRectangle( aLocalClipRect.Left() - rOffset.getX(),
206 aLocalClipRect.Top() - rOffset.getY(),
207 aLocalClipRect.Right() - rOffset.getX(),
208 aLocalClipRect.Bottom() - rOffset.getY() ) ) ) );
211 return true;
214 // empty clip, nothing to do
215 return false;
218 // create overline/underline/strikeout line info struct
219 TextLineInfo createTextLineInfo( const ::VirtualDevice& rVDev,
220 const ::cppcanvas::internal::OutDevState& rState )
222 const bool bOldMode( rVDev.IsMapModeEnabled() );
224 // #i68512# Force metric regeneration with mapmode enabled
225 // (prolly OutDev bug)
226 rVDev.GetFontMetric();
228 // will restore map mode below
229 const_cast< ::VirtualDevice& >(rVDev).EnableMapMode( false );
231 const ::FontMetric aMetric = rVDev.GetFontMetric();
233 TextLineInfo aTextInfo(
234 (aMetric.GetDescent() + 2) / 4.0,
235 ((aMetric.GetInternalLeading() + 1.5) / 3.0),
236 (aMetric.GetInternalLeading() / 2.0) - aMetric.GetAscent(),
237 aMetric.GetDescent() / 2.0,
238 (aMetric.GetInternalLeading() - aMetric.GetAscent()) / 3.0,
239 rState.textOverlineStyle,
240 rState.textUnderlineStyle,
241 rState.textStrikeoutStyle );
243 const_cast< ::VirtualDevice& >(rVDev).EnableMapMode( bOldMode );
245 return aTextInfo;
248 namespace
250 void appendWaveline( ::basegfx::B2DPolyPolygon& o_rPoly,
251 const ::basegfx::B2DPoint& rStartPos,
252 const double nStartOffset,
253 const double nWidth,
254 const double nHeight,
255 sal_Int8 nLineStyle)
257 const double x(rStartPos.getX());
258 const double y(rStartPos.getY() + nStartOffset + nHeight);
259 double nWaveWidth = nHeight * 10.6 * 0.25;
260 // Offset for the double line.
261 double nOffset = 0.0;
263 if (nLineStyle == LINESTYLE_DOUBLEWAVE)
264 nOffset = -nHeight * 0.5;
265 else
266 nWaveWidth *= 2.0;
268 basegfx::B2DPolygon aLine;
269 aLine.append(basegfx::B2DPoint(x, y + nOffset));
270 aLine.append(basegfx::B2DPoint(x + nWidth, y + nOffset));
272 o_rPoly.append(::basegfx::utils::createWaveline(aLine, nWaveWidth, nWaveWidth * 0.5));
274 if (nLineStyle == LINESTYLE_DOUBLEWAVE)
276 nOffset = nHeight * 1.2;
278 basegfx::B2DPolygon aLine2;
279 aLine2.append(basegfx::B2DPoint(x, y + nOffset));
280 aLine2.append(basegfx::B2DPoint(x + nWidth, y + nOffset));
281 o_rPoly.append(::basegfx::utils::createWaveline(aLine2, nWaveWidth, nWaveWidth * 0.5));
285 void appendRect( ::basegfx::B2DPolyPolygon& o_rPoly,
286 const ::basegfx::B2DPoint& rStartPos,
287 const double nX1,
288 const double nY1,
289 const double nX2,
290 const double nY2 )
292 const double x( rStartPos.getX() );
293 const double y( rStartPos.getY() );
295 o_rPoly.append(
296 ::basegfx::utils::createPolygonFromRect(
297 ::basegfx::B2DRectangle( x + nX1, y + nY1, x + nX2, y + nY2 ) ) );
300 void appendRect( ::basegfx::B2DPolyPolygon& o_rPoly,
301 const double nX1,
302 const double nY1,
303 const double nX2,
304 const double nY2 )
306 o_rPoly.append(
307 ::basegfx::utils::createPolygonFromRect(
308 ::basegfx::B2DRectangle( nX1, nY1, nX2, nY2 ) ) );
311 bool appendDashes( ::basegfx::B2DPolyPolygon& o_rPoly,
312 const double nX,
313 double nY,
314 const double nLineWidth,
315 double nLineHeight,
316 sal_Int8 nLineStyle,
317 bool bIsOverline)
319 static const int aDottedArray[] = { 1, 1, 0}; // DOTTED LINE
320 static const int aDotDashArray[] = { 1, 1, 4, 1, 0}; // DASHDOT
321 static const int aDashDotDotArray[] = { 1, 1, 1, 1, 4, 1, 0}; // DASHDOTDOT
322 static const int aDashedArray[] = { 5, 2, 0}; // DASHED LINE
323 static const int aLongDashArray[] = { 7, 2, 0}; // LONGDASH
324 const int *pArray = nullptr;
325 bool bIsBold = false;
327 switch(nLineStyle)
329 case LINESTYLE_BOLDDOTTED:
330 bIsBold = true;
331 [[fallthrough]];
332 case LINESTYLE_DOTTED:
333 pArray = aDottedArray;
334 break;
336 case LINESTYLE_BOLDDASH:
337 bIsBold = true;
338 [[fallthrough]];
339 case LINESTYLE_DASH:
340 pArray = aDashedArray;
341 break;
343 case LINESTYLE_BOLDLONGDASH:
344 bIsBold = true;
345 [[fallthrough]];
346 case LINESTYLE_LONGDASH:
347 pArray = aLongDashArray;
348 break;
350 case LINESTYLE_BOLDDASHDOT:
351 bIsBold = true;
352 [[fallthrough]];
353 case LINESTYLE_DASHDOT:
354 pArray = aDotDashArray;
355 break;
356 case LINESTYLE_BOLDDASHDOTDOT:
357 bIsBold = true;
358 [[fallthrough]];
359 case LINESTYLE_DASHDOTDOT:
360 pArray = aDashDotDotArray;
361 break;
364 if (!pArray)
365 return false;
367 if (bIsBold)
369 if (bIsOverline)
370 nY -= nLineHeight;
372 nLineHeight *= 2;
375 const double nEnd = nX + nLineWidth;
376 sal_Int32 nIndex = 0;
377 bool bAppend = true;
378 double nX1 = nX;
380 while(nX1 < nEnd)
382 if (pArray[nIndex] == 0)
383 nIndex = 0;
385 const double nX2 = std::min(nEnd, nX1 + pArray[nIndex] * nLineHeight);
387 if (bAppend)
388 appendRect(o_rPoly, nX1, nY, nX2, nY + nLineHeight);
390 nX1 = nX2;
392 ++nIndex;
394 bAppend = !bAppend;
396 return true;
399 // create line actions for text such as underline and
400 // strikeout
401 void createOverlinePolyPolygon(::basegfx::B2DPolyPolygon& rTextLinesPolyPoly,
402 const ::basegfx::B2DPoint& rStartPos,
403 const double& rLineWidth,
404 const TextLineInfo& rTextLineInfo)
406 switch( rTextLineInfo.mnOverlineStyle )
408 case LINESTYLE_NONE: // nothing to do
409 case LINESTYLE_DONTKNOW:
410 break;
412 case LINESTYLE_DOUBLEWAVE:
413 case LINESTYLE_SMALLWAVE:
414 case LINESTYLE_BOLDWAVE:
415 case LINESTYLE_WAVE:
416 appendWaveline(
417 rTextLinesPolyPoly,
418 rStartPos,
419 rTextLineInfo.mnOverlineOffset,
420 rLineWidth,
421 rTextLineInfo.mnOverlineHeight,
422 rTextLineInfo.mnOverlineStyle);
424 break;
425 case LINESTYLE_SINGLE:
426 appendRect(
427 rTextLinesPolyPoly,
428 rStartPos,
430 rTextLineInfo.mnOverlineOffset,
431 rLineWidth,
432 rTextLineInfo.mnOverlineOffset + rTextLineInfo.mnOverlineHeight );
433 break;
434 case LINESTYLE_BOLD:
435 appendRect(
436 rTextLinesPolyPoly,
437 rStartPos,
439 rTextLineInfo.mnOverlineOffset - rTextLineInfo.mnOverlineHeight,
440 rLineWidth,
441 rTextLineInfo.mnOverlineOffset + rTextLineInfo.mnOverlineHeight );
442 break;
444 case LINESTYLE_DOUBLE:
445 appendRect(
446 rTextLinesPolyPoly,
447 rStartPos,
449 rTextLineInfo.mnOverlineOffset - rTextLineInfo.mnOverlineHeight * 2.0 ,
450 rLineWidth,
451 rTextLineInfo.mnOverlineOffset - rTextLineInfo.mnOverlineHeight );
453 appendRect(
454 rTextLinesPolyPoly,
455 rStartPos,
457 rTextLineInfo.mnOverlineOffset + rTextLineInfo.mnOverlineHeight,
458 rLineWidth,
459 rTextLineInfo.mnOverlineOffset + rTextLineInfo.mnOverlineHeight * 2.0 );
460 break;
462 default:
463 if (!appendDashes(
464 rTextLinesPolyPoly,
465 rStartPos.getX(),
466 rStartPos.getY() + rTextLineInfo.mnOverlineOffset,
467 rLineWidth,
468 rTextLineInfo.mnOverlineHeight,
469 rTextLineInfo.mnOverlineStyle,
470 true))
472 ENSURE_OR_THROW( false,
473 "::cppcanvas::internal::createTextLinesPolyPolygon(): Unexpected overline case" );
478 void createUnderlinePolyPolygon(::basegfx::B2DPolyPolygon& rTextLinesPolyPoly,
479 const ::basegfx::B2DPoint& rStartPos,
480 const double& rLineWidth,
481 const TextLineInfo& rTextLineInfo )
484 switch( rTextLineInfo.mnUnderlineStyle )
486 case LINESTYLE_NONE: // nothing to do
487 case LINESTYLE_DONTKNOW:
488 break;
490 case LINESTYLE_DOUBLEWAVE:
491 case LINESTYLE_SMALLWAVE:
492 case LINESTYLE_BOLDWAVE:
493 case LINESTYLE_WAVE:
494 appendWaveline(
495 rTextLinesPolyPoly,
496 rStartPos,
497 rTextLineInfo.mnUnderlineOffset,
498 rLineWidth,
499 rTextLineInfo.mnLineHeight,
500 rTextLineInfo.mnUnderlineStyle);
501 break;
502 case LINESTYLE_SINGLE:
503 appendRect(
504 rTextLinesPolyPoly,
505 rStartPos,
507 rTextLineInfo.mnUnderlineOffset,
508 rLineWidth,
509 rTextLineInfo.mnUnderlineOffset + rTextLineInfo.mnLineHeight );
510 break;
512 case LINESTYLE_BOLD:
513 appendRect(
514 rTextLinesPolyPoly,
515 rStartPos,
517 rTextLineInfo.mnUnderlineOffset,
518 rLineWidth,
519 rTextLineInfo.mnUnderlineOffset + 2*rTextLineInfo.mnLineHeight );
520 break;
522 case LINESTYLE_DOUBLE:
523 appendRect(
524 rTextLinesPolyPoly,
525 rStartPos,
527 rTextLineInfo.mnUnderlineOffset - rTextLineInfo.mnLineHeight,
528 rLineWidth,
529 rTextLineInfo.mnUnderlineOffset );
531 appendRect(
532 rTextLinesPolyPoly,
533 rStartPos,
535 rTextLineInfo.mnUnderlineOffset + 2*rTextLineInfo.mnLineHeight,
536 rLineWidth,
537 rTextLineInfo.mnUnderlineOffset + 3*rTextLineInfo.mnLineHeight );
538 break;
540 default:
541 if (!appendDashes(
542 rTextLinesPolyPoly,
543 rStartPos.getX(),
544 rStartPos.getY() + rTextLineInfo.mnUnderlineOffset,
545 rLineWidth,
546 rTextLineInfo.mnLineHeight,
547 rTextLineInfo.mnUnderlineStyle,
548 false))
550 ENSURE_OR_THROW( false,
551 "::cppcanvas::internal::createTextLinesPolyPolygon(): Unexpected underline case" );
556 void createStrikeoutPolyPolygon(::basegfx::B2DPolyPolygon& rTextLinesPolyPoly,
557 const ::basegfx::B2DPoint& rStartPos,
558 const double& rLineWidth,
559 const TextLineInfo& rTextLineInfo)
561 switch( rTextLineInfo.mnStrikeoutStyle )
563 case STRIKEOUT_NONE: // nothing to do
564 case STRIKEOUT_DONTKNOW:
565 break;
567 case STRIKEOUT_SLASH: // TODO(Q1): we should handle this in the text layer
568 case STRIKEOUT_X:
569 break;
571 case STRIKEOUT_SINGLE:
572 appendRect(
573 rTextLinesPolyPoly,
574 rStartPos,
576 rTextLineInfo.mnStrikeoutOffset,
577 rLineWidth,
578 rTextLineInfo.mnStrikeoutOffset + rTextLineInfo.mnLineHeight );
579 break;
581 case STRIKEOUT_BOLD:
582 appendRect(
583 rTextLinesPolyPoly,
584 rStartPos,
586 rTextLineInfo.mnStrikeoutOffset,
587 rLineWidth,
588 rTextLineInfo.mnStrikeoutOffset + 2*rTextLineInfo.mnLineHeight );
589 break;
591 case STRIKEOUT_DOUBLE:
592 appendRect(
593 rTextLinesPolyPoly,
594 rStartPos,
596 rTextLineInfo.mnStrikeoutOffset - rTextLineInfo.mnLineHeight,
597 rLineWidth,
598 rTextLineInfo.mnStrikeoutOffset );
600 appendRect(
601 rTextLinesPolyPoly,
602 rStartPos,
604 rTextLineInfo.mnStrikeoutOffset + 2*rTextLineInfo.mnLineHeight,
605 rLineWidth,
606 rTextLineInfo.mnStrikeoutOffset + 3*rTextLineInfo.mnLineHeight );
607 break;
609 default:
610 ENSURE_OR_THROW( false,
611 "::cppcanvas::internal::createTextLinesPolyPolygon(): Unexpected strikeout case" );
616 ::basegfx::B2DPolyPolygon createTextLinesPolyPolygon( const ::basegfx::B2DPoint& rStartPos,
617 const double& rLineWidth,
618 const TextLineInfo& rTextLineInfo )
620 // fill the polypolygon with all text lines
621 ::basegfx::B2DPolyPolygon aTextLinesPolyPoly;
623 createOverlinePolyPolygon(aTextLinesPolyPoly, rStartPos, rLineWidth, rTextLineInfo);
624 createUnderlinePolyPolygon(aTextLinesPolyPoly, rStartPos, rLineWidth, rTextLineInfo);
625 createStrikeoutPolyPolygon(aTextLinesPolyPoly, rStartPos, rLineWidth, rTextLineInfo);
626 return aTextLinesPolyPoly;
629 ::basegfx::B2DRange calcDevicePixelBounds( const ::basegfx::B2DRange& rBounds,
630 const rendering::ViewState& viewState,
631 const rendering::RenderState& renderState )
633 ::basegfx::B2DHomMatrix aTransform;
634 ::canvas::tools::mergeViewAndRenderTransform( aTransform,
635 viewState,
636 renderState );
638 ::basegfx::B2DRange aTransformedBounds;
639 return ::canvas::tools::calcTransformedRectBounds( aTransformedBounds,
640 rBounds,
641 aTransform );
644 // create line actions for text such as underline and
645 // strikeout
646 ::basegfx::B2DPolyPolygon createTextLinesPolyPolygon( const double& rStartOffset,
647 const double& rLineWidth,
648 const TextLineInfo& rTextLineInfo )
650 return createTextLinesPolyPolygon(
651 ::basegfx::B2DPoint( rStartOffset,
652 0.0 ),
653 rLineWidth,
654 rTextLineInfo );
657 void createTextLinesPolyPolygon( const double& rStartOffset,
658 const double& rLineWidth,
659 const TextLineInfo& rTextLineInfo,
660 ::basegfx::B2DPolyPolygon& rOverlinePolyPoly,
661 ::basegfx::B2DPolyPolygon& rUnderlinePolyPoly,
662 ::basegfx::B2DPolyPolygon& rStrikeoutPolyPoly )
664 ::basegfx::B2DPoint aStartPos(rStartOffset, 0.0);
666 createOverlinePolyPolygon(rOverlinePolyPoly, aStartPos, rLineWidth, rTextLineInfo);
667 createUnderlinePolyPolygon(rUnderlinePolyPoly, aStartPos, rLineWidth, rTextLineInfo);
668 createStrikeoutPolyPolygon(rStrikeoutPolyPoly, aStartPos, rLineWidth, rTextLineInfo);
672 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */