bump product version to 4.1.6.2
[LibreOffice.git] / vcl / source / helper / canvastools.cxx
blob9ba85d0585c8ca2b0d0bfa7330606dbce7776f29
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 <rtl/logfile.hxx>
22 #include <cppuhelper/compbase1.hxx>
24 #include <com/sun/star/geometry/RealSize2D.hpp>
25 #include <com/sun/star/geometry/RealPoint2D.hpp>
26 #include <com/sun/star/geometry/RealRectangle2D.hpp>
27 #include <com/sun/star/geometry/IntegerSize2D.hpp>
28 #include <com/sun/star/geometry/IntegerPoint2D.hpp>
29 #include <com/sun/star/geometry/IntegerRectangle2D.hpp>
30 #include <com/sun/star/geometry/RealBezierSegment2D.hpp>
32 #include <com/sun/star/rendering/ColorSpaceType.hpp>
33 #include <com/sun/star/rendering/RenderingIntent.hpp>
34 #include <com/sun/star/rendering/XGraphicDevice.hpp>
35 #include <com/sun/star/rendering/XBitmap.hpp>
36 #include <com/sun/star/rendering/XPolyPolygon2D.hpp>
37 #include <com/sun/star/rendering/IntegerBitmapLayout.hpp>
38 #include <com/sun/star/rendering/XIntegerBitmap.hpp>
39 #include <com/sun/star/rendering/ColorComponentTag.hpp>
41 #include <basegfx/matrix/b2dhommatrix.hxx>
42 #include <basegfx/vector/b2dsize.hxx>
43 #include <basegfx/point/b2dpoint.hxx>
44 #include <basegfx/range/b2drectangle.hxx>
45 #include <basegfx/vector/b2isize.hxx>
46 #include <basegfx/point/b2ipoint.hxx>
47 #include <basegfx/range/b2irectangle.hxx>
49 // #i79917#
50 #include <basegfx/polygon/b2dpolygon.hxx>
51 #include <basegfx/tools/canvastools.hxx>
52 #include <basegfx/polygon/b2dpolypolygon.hxx>
54 #include <tools/poly.hxx>
55 #include <tools/diagnose_ex.h>
56 #include <rtl/uuid.h>
58 #include <vcl/salbtype.hxx>
59 #include <vcl/bmpacc.hxx>
60 #include <vcl/bitmapex.hxx>
62 #include <canvasbitmap.hxx>
63 #include <vcl/canvastools.hxx>
64 #include <boost/unordered_map.hpp>
67 using namespace ::com::sun::star;
69 namespace vcl
71 namespace unotools
73 uno::Reference< rendering::XBitmap > xBitmapFromBitmapEx( const uno::Reference< rendering::XGraphicDevice >& /*xGraphicDevice*/,
74 const ::BitmapEx& inputBitmap )
76 RTL_LOGFILE_CONTEXT( aLog, "::vcl::unotools::xBitmapFromBitmapEx()" );
78 return new vcl::unotools::VclCanvasBitmap( inputBitmap );
81 namespace
83 inline bool operator==( const rendering::IntegerBitmapLayout& rLHS,
84 const rendering::IntegerBitmapLayout& rRHS )
86 return
87 rLHS.ScanLineBytes == rRHS.ScanLineBytes &&
88 rLHS.ScanLineStride == rRHS.ScanLineStride &&
89 rLHS.PlaneStride == rRHS.PlaneStride &&
90 rLHS.ColorSpace == rRHS.ColorSpace &&
91 rLHS.Palette == rRHS.Palette &&
92 rLHS.IsMsbFirst == rRHS.IsMsbFirst;
95 bool readBmp( sal_Int32 nWidth,
96 sal_Int32 nHeight,
97 const rendering::IntegerBitmapLayout& rLayout,
98 const uno::Reference< rendering::XIntegerReadOnlyBitmap >& xInputBitmap,
99 Bitmap::ScopedWriteAccess& rWriteAcc,
100 Bitmap::ScopedWriteAccess& rAlphaAcc )
102 rendering::IntegerBitmapLayout aCurrLayout;
103 geometry::IntegerRectangle2D aRect;
104 uno::Sequence<sal_Int8> aPixelData;
105 uno::Sequence<rendering::RGBColor> aRGBColors;
106 uno::Sequence<rendering::ARGBColor> aARGBColors;
108 for( aRect.Y1=0; aRect.Y1<nHeight; ++aRect.Y1 )
110 aRect.X1 = 0; aRect.X2 = nWidth; aRect.Y2 = aRect.Y1+1;
113 aPixelData = xInputBitmap->getData(aCurrLayout,aRect);
115 catch( rendering::VolatileContentDestroyedException& )
117 // re-read bmp from the start
118 return false;
120 if( !(aCurrLayout == rLayout) )
121 return false; // re-read bmp from the start
123 if( rAlphaAcc.get() )
125 // read ARGB color
126 aARGBColors = rLayout.ColorSpace->convertIntegerToARGB(aPixelData);
128 if( rWriteAcc->HasPalette() )
130 for( sal_Int32 x=0; x<nWidth; ++x )
132 const rendering::ARGBColor& rColor=aARGBColors[x];
133 rWriteAcc->SetPixelIndex( aRect.Y1, x,
134 (sal_uInt8) rWriteAcc->GetBestPaletteIndex(
135 BitmapColor( toByteColor(rColor.Red),
136 toByteColor(rColor.Green),
137 toByteColor(rColor.Blue))) );
138 rAlphaAcc->SetPixel( aRect.Y1, x,
139 BitmapColor( 255 - toByteColor(rColor.Alpha) ));
142 else
144 for( sal_Int32 x=0; x<nWidth; ++x )
146 const rendering::ARGBColor& rColor=aARGBColors[x];
147 rWriteAcc->SetPixel( aRect.Y1, x,
148 BitmapColor( toByteColor(rColor.Red),
149 toByteColor(rColor.Green),
150 toByteColor(rColor.Blue) ));
151 rAlphaAcc->SetPixel( aRect.Y1, x,
152 BitmapColor( 255 - toByteColor(rColor.Alpha) ));
156 else
158 // read RGB color
159 aRGBColors = rLayout.ColorSpace->convertIntegerToRGB(aPixelData);
160 if( rWriteAcc->HasPalette() )
162 for( sal_Int32 x=0; x<nWidth; ++x )
164 const rendering::RGBColor& rColor=aRGBColors[x];
165 rWriteAcc->SetPixelIndex( aRect.Y1, x,
166 (sal_uInt8) rWriteAcc->GetBestPaletteIndex(
167 BitmapColor( toByteColor(rColor.Red),
168 toByteColor(rColor.Green),
169 toByteColor(rColor.Blue))) );
172 else
174 for( sal_Int32 x=0; x<nWidth; ++x )
176 const rendering::RGBColor& rColor=aRGBColors[x];
177 rWriteAcc->SetPixel( aRect.Y1, x,
178 BitmapColor( toByteColor(rColor.Red),
179 toByteColor(rColor.Green),
180 toByteColor(rColor.Blue) ));
186 return true;
190 ::BitmapEx VCL_DLLPUBLIC bitmapExFromXBitmap( const uno::Reference< rendering::XIntegerReadOnlyBitmap >& xInputBitmap )
192 RTL_LOGFILE_CONTEXT( aLog, "::vcl::unotools::bitmapExFromXBitmap()" );
194 if( !xInputBitmap.is() )
195 return ::BitmapEx();
197 // tunnel directly for known implementation
198 VclCanvasBitmap* pImplBitmap = dynamic_cast<VclCanvasBitmap*>(xInputBitmap.get());
199 if( pImplBitmap )
200 return pImplBitmap->getBitmapEx();
202 // retrieve data via UNO interface
204 // volatile bitmaps are a bit more complicated to read
205 // from..
206 uno::Reference<rendering::XVolatileBitmap> xVolatileBitmap(
207 xInputBitmap, uno::UNO_QUERY);
209 // loop a few times, until successfully read (for XVolatileBitmap)
210 for( int i=0; i<10; ++i )
212 sal_Int32 nDepth=0;
213 sal_Int32 nAlphaDepth=0;
214 const rendering::IntegerBitmapLayout aLayout(
215 xInputBitmap->getMemoryLayout());
217 OSL_ENSURE(aLayout.ColorSpace.is(),
218 "Cannot convert image without color space!");
219 if( !aLayout.ColorSpace.is() )
220 return ::BitmapEx();
222 nDepth = aLayout.ColorSpace->getBitsPerPixel();
224 if( xInputBitmap->hasAlpha() )
226 // determine alpha channel depth
227 const uno::Sequence<sal_Int8> aTags(
228 aLayout.ColorSpace->getComponentTags() );
229 const uno::Sequence<sal_Int32> aDepths(
230 aLayout.ColorSpace->getComponentBitCounts() );
231 const sal_Int8* pStart(aTags.getConstArray());
232 const sal_Size nLen(aTags.getLength());
233 const sal_Int8* pEnd(pStart+nLen);
235 const std::ptrdiff_t nAlphaIndex =
236 std::find(pStart,pEnd,
237 rendering::ColorComponentTag::ALPHA) - pStart;
239 if( nAlphaIndex < sal::static_int_cast<std::ptrdiff_t>(nLen) )
241 nAlphaDepth = aLayout.ColorSpace->getComponentBitCounts()[nAlphaIndex] > 1 ? 8 : 1;
242 nDepth -= nAlphaDepth;
246 BitmapPalette aPalette;
247 if( aLayout.Palette.is() )
249 uno::Reference< rendering::XColorSpace > xPaletteColorSpace(
250 aLayout.Palette->getColorSpace());
251 ENSURE_OR_THROW(xPaletteColorSpace.is(),
252 "Palette without color space");
254 const sal_Int32 nEntryCount( aLayout.Palette->getNumberOfEntries() );
255 if( nEntryCount <= 256 )
257 if( nEntryCount <= 2 )
258 nDepth = 1;
259 else
260 nDepth = 8;
262 const sal_uInt16 nPaletteEntries(
263 sal::static_int_cast<sal_uInt16>(
264 std::min(sal_Int32(255), nEntryCount)));
266 // copy palette entries
267 aPalette.SetEntryCount(nPaletteEntries);
268 uno::Reference<rendering::XBitmapPalette> xPalette( aLayout.Palette );
269 uno::Reference<rendering::XColorSpace> xPalColorSpace( xPalette->getColorSpace() );
271 uno::Sequence<double> aPaletteEntry;
272 for( sal_uInt16 j=0; j<nPaletteEntries; ++j )
274 if( !xPalette->getIndex(aPaletteEntry,j) &&
275 nAlphaDepth == 0 )
277 nAlphaDepth = 1;
279 uno::Sequence<rendering::RGBColor> aColors=xPalColorSpace->convertToRGB(aPaletteEntry);
280 ENSURE_OR_THROW(aColors.getLength() == 1,
281 "Palette returned more or less than one entry");
282 const rendering::RGBColor& rColor=aColors[0];
283 aPalette[j] = BitmapColor(toByteColor(rColor.Red),
284 toByteColor(rColor.Green),
285 toByteColor(rColor.Blue));
290 const ::Size aPixelSize(
291 sizeFromIntegerSize2D(xInputBitmap->getSize()));
293 // normalize bitcount
294 nDepth =
295 ( nDepth <= 1 ) ? 1 :
296 ( nDepth <= 4 ) ? 4 :
297 ( nDepth <= 8 ) ? 8 : 24;
299 ::Bitmap aBitmap( aPixelSize,
300 sal::static_int_cast<sal_uInt16>(nDepth),
301 aLayout.Palette.is() ? &aPalette : NULL );
302 ::Bitmap aAlpha;
303 if( nAlphaDepth )
304 aAlpha = ::Bitmap( aPixelSize,
305 sal::static_int_cast<sal_uInt16>(nAlphaDepth),
306 &::Bitmap::GetGreyPalette(
307 sal::static_int_cast<sal_uInt16>(1L << nAlphaDepth)) );
309 { // limit scoped access
310 Bitmap::ScopedWriteAccess pWriteAccess( aBitmap );
311 Bitmap::ScopedWriteAccess pAlphaWriteAccess( nAlphaDepth ? aAlpha.AcquireWriteAccess() : NULL,
312 aAlpha );
314 ENSURE_OR_THROW(pWriteAccess.get() != NULL,
315 "Cannot get write access to bitmap");
317 const sal_Int32 nWidth(aPixelSize.Width());
318 const sal_Int32 nHeight(aPixelSize.Height());
320 if( !readBmp(nWidth,nHeight,aLayout,xInputBitmap,
321 pWriteAccess,pAlphaWriteAccess) )
322 continue;
323 } // limit scoped access
325 if( nAlphaDepth )
326 return ::BitmapEx( aBitmap,
327 AlphaMask( aAlpha ) );
328 else
329 return ::BitmapEx( aBitmap );
332 // failed to read data 10 times - bail out
333 return ::BitmapEx();
337 geometry::RealSize2D size2DFromSize( const Size& rSize )
339 return geometry::RealSize2D( rSize.Width(),
340 rSize.Height() );
343 Size sizeFromRealSize2D( const geometry::RealSize2D& rSize )
345 return Size( static_cast<long>(rSize.Width + .5),
346 static_cast<long>(rSize.Height + .5) );
349 ::Size sizeFromB2DSize( const ::basegfx::B2DVector& rVec )
351 return ::Size( FRound( rVec.getX() ),
352 FRound( rVec.getY() ) );
355 ::Point pointFromB2DPoint( const ::basegfx::B2DPoint& rPoint )
357 return ::Point( FRound( rPoint.getX() ),
358 FRound( rPoint.getY() ) );
361 ::Rectangle rectangleFromB2DRectangle( const ::basegfx::B2DRange& rRect )
363 return ::Rectangle( FRound( rRect.getMinX() ),
364 FRound( rRect.getMinY() ),
365 FRound( rRect.getMaxX() ),
366 FRound( rRect.getMaxY() ) );
369 Point pointFromB2IPoint( const ::basegfx::B2IPoint& rPoint )
371 return ::Point( rPoint.getX(),
372 rPoint.getY() );
375 Rectangle rectangleFromB2IRectangle( const ::basegfx::B2IRange& rRect )
377 return ::Rectangle( rRect.getMinX(),
378 rRect.getMinY(),
379 rRect.getMaxX(),
380 rRect.getMaxY() );
383 ::basegfx::B2DVector b2DSizeFromSize( const ::Size& rSize )
385 return ::basegfx::B2DVector( rSize.Width(),
386 rSize.Height() );
389 ::basegfx::B2DPoint b2DPointFromPoint( const ::Point& rPoint )
391 return ::basegfx::B2DPoint( rPoint.X(),
392 rPoint.Y() );
395 ::basegfx::B2DRange b2DRectangleFromRectangle( const ::Rectangle& rRect )
397 return ::basegfx::B2DRange( rRect.Left(),
398 rRect.Top(),
399 rRect.Right(),
400 rRect.Bottom() );
403 geometry::IntegerSize2D integerSize2DFromSize( const Size& rSize )
405 return geometry::IntegerSize2D( rSize.Width(),
406 rSize.Height() );
409 Size sizeFromIntegerSize2D( const geometry::IntegerSize2D& rSize )
411 return Size( rSize.Width,
412 rSize.Height );
415 Point pointFromIntegerPoint2D( const geometry::IntegerPoint2D& rPoint )
417 return Point( rPoint.X,
418 rPoint.Y );
421 Rectangle rectangleFromIntegerRectangle2D( const geometry::IntegerRectangle2D& rRectangle )
423 return Rectangle( rRectangle.X1, rRectangle.Y1,
424 rRectangle.X2, rRectangle.Y2 );
427 namespace
429 class StandardColorSpace : public cppu::WeakImplHelper1< com::sun::star::rendering::XColorSpace >
431 private:
432 uno::Sequence< sal_Int8 > m_aComponentTags;
434 virtual ::sal_Int8 SAL_CALL getType( ) throw (uno::RuntimeException)
436 return rendering::ColorSpaceType::RGB;
438 virtual uno::Sequence< ::sal_Int8 > SAL_CALL getComponentTags( ) throw (uno::RuntimeException)
440 return m_aComponentTags;
442 virtual ::sal_Int8 SAL_CALL getRenderingIntent( ) throw (uno::RuntimeException)
444 return rendering::RenderingIntent::PERCEPTUAL;
446 virtual uno::Sequence< beans::PropertyValue > SAL_CALL getProperties( ) throw (uno::RuntimeException)
448 return uno::Sequence< beans::PropertyValue >();
450 virtual uno::Sequence< double > SAL_CALL convertColorSpace( const uno::Sequence< double >& deviceColor,
451 const uno::Reference< rendering::XColorSpace >& targetColorSpace ) throw (lang::IllegalArgumentException,
452 uno::RuntimeException)
454 // TODO(P3): if we know anything about target
455 // colorspace, this can be greatly sped up
456 uno::Sequence<rendering::ARGBColor> aIntermediate(
457 convertToARGB(deviceColor));
458 return targetColorSpace->convertFromARGB(aIntermediate);
460 virtual uno::Sequence< rendering::RGBColor > SAL_CALL convertToRGB( const uno::Sequence< double >& deviceColor ) throw (lang::IllegalArgumentException, uno::RuntimeException)
462 const double* pIn( deviceColor.getConstArray() );
463 const sal_Size nLen( deviceColor.getLength() );
464 ENSURE_ARG_OR_THROW2(nLen%4==0,
465 "number of channels no multiple of 4",
466 static_cast<rendering::XColorSpace*>(this), 0);
468 uno::Sequence< rendering::RGBColor > aRes(nLen/4);
469 rendering::RGBColor* pOut( aRes.getArray() );
470 for( sal_Size i=0; i<nLen; i+=4 )
472 *pOut++ = rendering::RGBColor(pIn[0],pIn[1],pIn[2]);
473 pIn += 4;
475 return aRes;
477 virtual uno::Sequence< rendering::ARGBColor > SAL_CALL convertToARGB( const uno::Sequence< double >& deviceColor ) throw (lang::IllegalArgumentException, uno::RuntimeException)
479 const double* pIn( deviceColor.getConstArray() );
480 const sal_Size nLen( deviceColor.getLength() );
481 ENSURE_ARG_OR_THROW2(nLen%4==0,
482 "number of channels no multiple of 4",
483 static_cast<rendering::XColorSpace*>(this), 0);
485 uno::Sequence< rendering::ARGBColor > aRes(nLen/4);
486 rendering::ARGBColor* pOut( aRes.getArray() );
487 for( sal_Size i=0; i<nLen; i+=4 )
489 *pOut++ = rendering::ARGBColor(pIn[3],pIn[0],pIn[1],pIn[2]);
490 pIn += 4;
492 return aRes;
494 virtual uno::Sequence< rendering::ARGBColor > SAL_CALL convertToPARGB( const uno::Sequence< double >& deviceColor ) throw (lang::IllegalArgumentException, uno::RuntimeException)
496 const double* pIn( deviceColor.getConstArray() );
497 const sal_Size nLen( deviceColor.getLength() );
498 ENSURE_ARG_OR_THROW2(nLen%4==0,
499 "number of channels no multiple of 4",
500 static_cast<rendering::XColorSpace*>(this), 0);
502 uno::Sequence< rendering::ARGBColor > aRes(nLen/4);
503 rendering::ARGBColor* pOut( aRes.getArray() );
504 for( sal_Size i=0; i<nLen; i+=4 )
506 *pOut++ = rendering::ARGBColor(pIn[3],pIn[3]*pIn[0],pIn[3]*pIn[1],pIn[3]*pIn[2]);
507 pIn += 4;
509 return aRes;
511 virtual uno::Sequence< double > SAL_CALL convertFromRGB( const uno::Sequence< rendering::RGBColor >& rgbColor ) throw (lang::IllegalArgumentException, uno::RuntimeException)
513 const rendering::RGBColor* pIn( rgbColor.getConstArray() );
514 const sal_Size nLen( rgbColor.getLength() );
516 uno::Sequence< double > aRes(nLen*4);
517 double* pColors=aRes.getArray();
518 for( sal_Size i=0; i<nLen; ++i )
520 *pColors++ = pIn->Red;
521 *pColors++ = pIn->Green;
522 *pColors++ = pIn->Blue;
523 *pColors++ = 1.0;
524 ++pIn;
526 return aRes;
528 virtual uno::Sequence< double > SAL_CALL convertFromARGB( const uno::Sequence< rendering::ARGBColor >& rgbColor ) throw (lang::IllegalArgumentException, uno::RuntimeException)
530 const rendering::ARGBColor* pIn( rgbColor.getConstArray() );
531 const sal_Size nLen( rgbColor.getLength() );
533 uno::Sequence< double > aRes(nLen*4);
534 double* pColors=aRes.getArray();
535 for( sal_Size i=0; i<nLen; ++i )
537 *pColors++ = pIn->Red;
538 *pColors++ = pIn->Green;
539 *pColors++ = pIn->Blue;
540 *pColors++ = pIn->Alpha;
541 ++pIn;
543 return aRes;
545 virtual uno::Sequence< double > SAL_CALL convertFromPARGB( const uno::Sequence< rendering::ARGBColor >& rgbColor ) throw (lang::IllegalArgumentException, uno::RuntimeException)
547 const rendering::ARGBColor* pIn( rgbColor.getConstArray() );
548 const sal_Size nLen( rgbColor.getLength() );
550 uno::Sequence< double > aRes(nLen*4);
551 double* pColors=aRes.getArray();
552 for( sal_Size i=0; i<nLen; ++i )
554 *pColors++ = pIn->Red/pIn->Alpha;
555 *pColors++ = pIn->Green/pIn->Alpha;
556 *pColors++ = pIn->Blue/pIn->Alpha;
557 *pColors++ = pIn->Alpha;
558 ++pIn;
560 return aRes;
563 public:
564 StandardColorSpace() : m_aComponentTags(4)
566 sal_Int8* pTags = m_aComponentTags.getArray();
567 pTags[0] = rendering::ColorComponentTag::RGB_RED;
568 pTags[1] = rendering::ColorComponentTag::RGB_GREEN;
569 pTags[2] = rendering::ColorComponentTag::RGB_BLUE;
570 pTags[3] = rendering::ColorComponentTag::ALPHA;
575 uno::Reference<rendering::XColorSpace> VCL_DLLPUBLIC createStandardColorSpace()
577 return new StandardColorSpace();
581 Color stdColorSpaceSequenceToColor( const uno::Sequence< double >& rColor )
583 ENSURE_ARG_OR_THROW( rColor.getLength() == 4,
584 "color must have 4 channels" );
586 Color aColor;
588 aColor.SetRed ( toByteColor(rColor[0]) );
589 aColor.SetGreen( toByteColor(rColor[1]) );
590 aColor.SetBlue ( toByteColor(rColor[2]) );
591 // VCL's notion of alpha is different from the rest of the world's
592 aColor.SetTransparency( 255 - toByteColor(rColor[3]) );
594 return aColor;
597 uno::Sequence< double > VCL_DLLPUBLIC colorToDoubleSequence(
598 const Color& rColor,
599 const uno::Reference< rendering::XColorSpace >& xColorSpace )
601 uno::Sequence<rendering::ARGBColor> aSeq(1);
602 aSeq[0] = rendering::ARGBColor(
603 1.0-toDoubleColor(rColor.GetTransparency()),
604 toDoubleColor(rColor.GetRed()),
605 toDoubleColor(rColor.GetGreen()),
606 toDoubleColor(rColor.GetBlue()) );
608 return xColorSpace->convertFromARGB(aSeq);
611 Color VCL_DLLPUBLIC doubleSequenceToColor(
612 const uno::Sequence< double > rColor,
613 const uno::Reference< rendering::XColorSpace >& xColorSpace )
615 const rendering::ARGBColor aARGBColor(
616 xColorSpace->convertToARGB(rColor)[0]);
618 return Color( 255-toByteColor(aARGBColor.Alpha),
619 toByteColor(aARGBColor.Red),
620 toByteColor(aARGBColor.Green),
621 toByteColor(aARGBColor.Blue) );
625 } // namespace vcltools
627 } // namespace canvas
629 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */