Branch data Line data Source code
1 : : /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 : : /*************************************************************************
3 : : *
4 : : * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5 : : *
6 : : * Copyright 2000, 2010 Oracle and/or its affiliates.
7 : : *
8 : : * OpenOffice.org - a multi-platform office productivity suite
9 : : *
10 : : * This file is part of OpenOffice.org.
11 : : *
12 : : * OpenOffice.org is free software: you can redistribute it and/or modify
13 : : * it under the terms of the GNU Lesser General Public License version 3
14 : : * only, as published by the Free Software Foundation.
15 : : *
16 : : * OpenOffice.org is distributed in the hope that it will be useful,
17 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 : : * GNU Lesser General Public License version 3 for more details
20 : : * (a copy is included in the LICENSE file that accompanied this code).
21 : : *
22 : : * You should have received a copy of the GNU Lesser General Public License
23 : : * version 3 along with OpenOffice.org. If not, see
24 : : * <http://www.openoffice.org/license.html>
25 : : * for a copy of the LGPLv3 License.
26 : : *
27 : : ************************************************************************/
28 : :
29 : :
30 : : #include <tools/gen.hxx>
31 : :
32 : : #include <canvas/debug.hxx>
33 : : #include <canvas/verbosetrace.hxx>
34 : : #include <canvas/canvastools.hxx>
35 : :
36 : : #include <rtl/logfile.hxx>
37 : :
38 : : #include <com/sun/star/rendering/XBitmap.hpp>
39 : : #include <com/sun/star/rendering/XCanvas.hpp>
40 : :
41 : : #include <rtl/math.hxx>
42 : :
43 : : #include <vcl/metaact.hxx>
44 : : #include <vcl/bitmapex.hxx>
45 : : #include <vcl/canvastools.hxx>
46 : : #include <vcl/svapp.hxx>
47 : : #include <vcl/outdev.hxx>
48 : : #include <vcl/virdev.hxx>
49 : : #include <vcl/gdimtf.hxx>
50 : : #include <vcl/gradient.hxx>
51 : :
52 : : #include <basegfx/range/b2drange.hxx>
53 : : #include <basegfx/point/b2dpoint.hxx>
54 : : #include <basegfx/vector/b2dsize.hxx>
55 : : #include <basegfx/numeric/ftools.hxx>
56 : : #include <basegfx/matrix/b2dhommatrix.hxx>
57 : : #include <basegfx/tuple/b2dtuple.hxx>
58 : : #include <basegfx/tools/canvastools.hxx>
59 : :
60 : : #include <boost/utility.hpp>
61 : :
62 : : #include "transparencygroupaction.hxx"
63 : : #include "outdevstate.hxx"
64 : : #include "mtftools.hxx"
65 : : #include "cppcanvas/vclfactory.hxx"
66 : :
67 : :
68 : : using namespace ::com::sun::star;
69 : :
70 : : namespace cppcanvas
71 : : {
72 : : namespace internal
73 : : {
74 : : // free support functions
75 : : // ======================
76 : : namespace
77 : : {
78 [ # # ][ # # ]: 0 : class TransparencyGroupAction : public Action, private ::boost::noncopyable
[ # # ][ # # ]
[ # # ][ # # ]
[ # # ]
79 : : {
80 : : public:
81 : : /** Create new transparency group action.
82 : :
83 : : @param rGroupMtf
84 : : Metafile that groups all actions to be rendered
85 : : transparent.
86 : :
87 : : @param rAlphaGradient
88 : : VCL gradient, to be rendered into the action's alpha
89 : : channel.
90 : :
91 : : @param rParms
92 : : Render parameters
93 : :
94 : : @param rDstPoint
95 : : Left, top edge of destination, in current state
96 : : coordinate system
97 : :
98 : : @param rDstSize
99 : : Size of the transparency group object, in current
100 : : state coordinate system.
101 : : */
102 : : TransparencyGroupAction( MtfAutoPtr& rGroupMtf,
103 : : GradientAutoPtr& rAlphaGradient,
104 : : const Renderer::Parameters& rParms,
105 : : const ::basegfx::B2DPoint& rDstPoint,
106 : : const ::basegfx::B2DVector& rDstSize,
107 : : const CanvasSharedPtr& rCanvas,
108 : : const OutDevState& rState );
109 : :
110 : : virtual bool render( const ::basegfx::B2DHomMatrix& rTransformation ) const;
111 : : virtual bool renderSubset( const ::basegfx::B2DHomMatrix& rTransformation,
112 : : const Subset& rSubset ) const;
113 : :
114 : : virtual ::basegfx::B2DRange getBounds( const ::basegfx::B2DHomMatrix& rTransformation ) const;
115 : : virtual ::basegfx::B2DRange getBounds( const ::basegfx::B2DHomMatrix& rTransformation,
116 : : const Subset& rSubset ) const;
117 : :
118 : : virtual sal_Int32 getActionCount() const;
119 : :
120 : : private:
121 : : MtfAutoPtr mpGroupMtf;
122 : : GradientAutoPtr mpAlphaGradient;
123 : :
124 : : const Renderer::Parameters maParms;
125 : :
126 : : const ::basegfx::B2DSize maDstSize;
127 : :
128 : : mutable uno::Reference< rendering::XBitmap > mxBufferBitmap; // contains last rendered version
129 : : mutable ::basegfx::B2DHomMatrix maLastTransformation; // contains last active transformation
130 : : mutable Subset maLastSubset; // contains last effective subset
131 : :
132 : : // transformation for
133 : : // mxBufferBitmap content
134 : : CanvasSharedPtr mpCanvas;
135 : : rendering::RenderState maState;
136 : : const double mnAlpha;
137 : : };
138 : :
139 : :
140 : : /** Setup transformation such that the next render call is
141 : : moved rPoint away, and scaled according to the ratio
142 : : given by src and dst size.
143 : : */
144 : 0 : void implSetupTransform( rendering::RenderState& rRenderState,
145 : : const ::basegfx::B2DPoint& rDstPoint )
146 : : {
147 [ # # ]: 0 : ::basegfx::B2DHomMatrix aLocalTransformation;
148 : :
149 : : aLocalTransformation.translate( rDstPoint.getX(),
150 [ # # ]: 0 : rDstPoint.getY() );
151 : : ::canvas::tools::appendToRenderState( rRenderState,
152 [ # # ][ # # ]: 0 : aLocalTransformation );
153 : 0 : }
154 : :
155 : : SAL_WNODEPRECATED_DECLARATIONS_PUSH
156 : 0 : TransparencyGroupAction::TransparencyGroupAction( MtfAutoPtr& rGroupMtf,
157 : : GradientAutoPtr& rAlphaGradient,
158 : : const Renderer::Parameters& rParms,
159 : : const ::basegfx::B2DPoint& rDstPoint,
160 : : const ::basegfx::B2DVector& rDstSize,
161 : : const CanvasSharedPtr& rCanvas,
162 : : const OutDevState& rState ) :
163 : : mpGroupMtf( rGroupMtf ),
164 : : mpAlphaGradient( rAlphaGradient ),
165 : : maParms( rParms ),
166 : : maDstSize( rDstSize ),
167 : : mxBufferBitmap(),
168 : : maLastTransformation(),
169 : : mpCanvas( rCanvas ),
170 : : maState(),
171 [ # # ][ # # ]: 0 : mnAlpha( 1.0 )
[ # # ][ # # ]
172 : : {
173 [ # # ]: 0 : tools::initRenderState(maState,rState);
174 [ # # ]: 0 : implSetupTransform( maState, rDstPoint );
175 : :
176 : : // correct clip (which is relative to original transform)
177 : : tools::modifyClip( maState,
178 : : rState,
179 : : rCanvas,
180 : : rDstPoint,
181 : : NULL,
182 [ # # ]: 0 : NULL );
183 : :
184 : 0 : maLastSubset.mnSubsetBegin = 0;
185 : 0 : maLastSubset.mnSubsetEnd = -1;
186 : 0 : }
187 : : SAL_WNODEPRECATED_DECLARATIONS_POP
188 : :
189 : : // TODO(P3): The whole float transparency handling is a mess,
190 : : // this should be refactored. What's more, the old idea of
191 : : // having only internal 'metaactions', and not the original
192 : : // GDIMetaFile now looks a lot less attractive. Try to move
193 : : // into the direction of having a direct GDIMetaFile2XCanvas
194 : : // renderer, and maybe a separate metafile XCanvas
195 : : // implementation.
196 : 0 : bool TransparencyGroupAction::renderSubset( const ::basegfx::B2DHomMatrix& rTransformation,
197 : : const Subset& rSubset ) const
198 : : {
199 : : RTL_LOGFILE_CONTEXT( aLog, "::cppcanvas::internal::TransparencyGroupAction::renderSubset()" );
200 : : RTL_LOGFILE_CONTEXT_TRACE1( aLog, "::cppcanvas::internal::TransparencyGroupAction: 0x%X", this );
201 : :
202 : : // determine overall transformation matrix (render, view,
203 : : // and passed transformation)
204 [ # # ]: 0 : ::basegfx::B2DHomMatrix aTransform;
205 [ # # ]: 0 : ::canvas::tools::getRenderStateTransform( aTransform, maState );
206 [ # # ][ # # ]: 0 : aTransform = rTransformation * aTransform;
[ # # ]
207 : :
208 [ # # ]: 0 : ::basegfx::B2DHomMatrix aTotalTransform;
209 [ # # ][ # # ]: 0 : ::canvas::tools::getViewStateTransform( aTotalTransform, mpCanvas->getViewState() );
[ # # ]
210 [ # # ][ # # ]: 0 : aTotalTransform = aTotalTransform * aTransform;
[ # # ]
211 : :
212 : : // since pure translational changes to the transformation
213 : : // does not matter, remove them before comparing
214 [ # # ]: 0 : aTotalTransform.set( 0, 2, 0.0 );
215 [ # # ]: 0 : aTotalTransform.set( 1, 2, 0.0 );
216 : :
217 : : // determine total scaling factor of the
218 : : // transformation matrix - need to make the bitmap
219 : : // large enough
220 : 0 : ::basegfx::B2DTuple aScale;
221 : 0 : ::basegfx::B2DTuple aTranslate;
222 : : double nRotate;
223 : : double nShearX;
224 [ # # ]: 0 : if( !aTotalTransform.decompose( aScale,
225 : : aTranslate,
226 : : nRotate,
227 [ # # ]: 0 : nShearX ) )
228 : : {
229 : : OSL_FAIL( "TransparencyGroupAction::renderSubset(): non-decomposable transformation" );
230 : 0 : return false;
231 : : }
232 : :
233 : : // if there's no buffer bitmap, or as soon as the
234 : : // total transformation changes, we've got to
235 : : // re-render the bitmap
236 [ # # ][ # # ]: 0 : if( !mxBufferBitmap.is() ||
[ # # ][ # # ]
[ # # ]
237 [ # # ]: 0 : aTotalTransform != maLastTransformation ||
238 : : rSubset.mnSubsetBegin != maLastSubset.mnSubsetBegin ||
239 : : rSubset.mnSubsetEnd != maLastSubset.mnSubsetEnd )
240 : : {
241 : : DBG_TESTSOLARMUTEX();
242 : :
243 : : // output size of metafile
244 : 0 : ::Size aOutputSizePixel( ::basegfx::fround( aScale.getX() * maDstSize.getX() ),
245 : 0 : ::basegfx::fround( aScale.getY() * maDstSize.getY() ) );
246 : :
247 : : // pixel size of cache bitmap: round up to nearest int
248 : 0 : ::Size aBitmapSizePixel( static_cast<sal_Int32>( aScale.getX() * maDstSize.getX() )+1,
249 : 0 : static_cast<sal_Int32>( aScale.getY() * maDstSize.getY() )+1 );
250 : :
251 : 0 : ::Point aEmptyPoint;
252 : :
253 : : // render our content into an appropriately sized
254 : : // VirtualDevice with alpha channel
255 : : VirtualDevice aVDev(
256 [ # # ][ # # ]: 0 : *::Application::GetDefaultDevice(), 0, 0 );
257 [ # # ]: 0 : aVDev.SetOutputSizePixel( aBitmapSizePixel );
258 [ # # ]: 0 : aVDev.SetMapMode();
259 : :
260 [ # # ][ # # ]: 0 : if( rSubset.mnSubsetBegin != 0 ||
261 : : rSubset.mnSubsetEnd != -1 )
262 : : {
263 : : // true subset - extract referenced
264 : : // metaactions from mpGroupMtf
265 [ # # ]: 0 : GDIMetaFile aMtf;
266 : : MetaAction* pCurrAct;
267 : : int nCurrActionIndex;
268 : :
269 : : // extract subset actions
270 [ # # ][ # # ]: 0 : for( nCurrActionIndex=0,
271 [ # # ]: 0 : pCurrAct=mpGroupMtf->FirstAction();
272 : : pCurrAct;
273 : 0 : ++nCurrActionIndex, pCurrAct = mpGroupMtf->NextAction() )
274 : : {
275 [ # # # ]: 0 : switch( pCurrAct->GetType() )
276 : : {
277 : : case META_PUSH_ACTION:
278 : : case META_POP_ACTION:
279 : : case META_CLIPREGION_ACTION:
280 : : case META_ISECTRECTCLIPREGION_ACTION:
281 : : case META_ISECTREGIONCLIPREGION_ACTION:
282 : : case META_MOVECLIPREGION_ACTION:
283 : : case META_LINECOLOR_ACTION:
284 : : case META_FILLCOLOR_ACTION:
285 : : case META_TEXTCOLOR_ACTION:
286 : : case META_TEXTFILLCOLOR_ACTION:
287 : : case META_TEXTLINECOLOR_ACTION:
288 : : case META_TEXTALIGN_ACTION:
289 : : case META_FONT_ACTION:
290 : : case META_RASTEROP_ACTION:
291 : : case META_REFPOINT_ACTION:
292 : : case META_LAYOUTMODE_ACTION:
293 : : // state-changing action - copy as-is
294 [ # # ][ # # ]: 0 : aMtf.AddAction( pCurrAct->Clone() );
295 : 0 : break;
296 : :
297 : : case META_GRADIENT_ACTION:
298 : : case META_HATCH_ACTION:
299 : : case META_EPS_ACTION:
300 : : case META_COMMENT_ACTION:
301 : : case META_POINT_ACTION:
302 : : case META_PIXEL_ACTION:
303 : : case META_LINE_ACTION:
304 : : case META_RECT_ACTION:
305 : : case META_ROUNDRECT_ACTION:
306 : : case META_ELLIPSE_ACTION:
307 : : case META_ARC_ACTION:
308 : : case META_PIE_ACTION:
309 : : case META_CHORD_ACTION:
310 : : case META_POLYLINE_ACTION:
311 : : case META_POLYGON_ACTION:
312 : : case META_POLYPOLYGON_ACTION:
313 : : case META_BMP_ACTION:
314 : : case META_BMPSCALE_ACTION:
315 : : case META_BMPSCALEPART_ACTION:
316 : : case META_BMPEX_ACTION:
317 : : case META_BMPEXSCALE_ACTION:
318 : : case META_BMPEXSCALEPART_ACTION:
319 : : case META_MASK_ACTION:
320 : : case META_MASKSCALE_ACTION:
321 : : case META_MASKSCALEPART_ACTION:
322 : : case META_GRADIENTEX_ACTION:
323 : : case META_WALLPAPER_ACTION:
324 : : case META_TRANSPARENT_ACTION:
325 : : case META_FLOATTRANSPARENT_ACTION:
326 : : case META_TEXT_ACTION:
327 : : case META_TEXTARRAY_ACTION:
328 : : case META_TEXTLINE_ACTION:
329 : : case META_TEXTRECT_ACTION:
330 : : case META_STRETCHTEXT_ACTION:
331 : : case META_RENDERGRAPHIC_ACTION:
332 : : // output-generating action - only
333 : : // copy, if we're within the
334 : : // requested subset
335 [ # # ][ # # ]: 0 : if( rSubset.mnSubsetBegin <= nCurrActionIndex &&
336 : : rSubset.mnSubsetEnd > nCurrActionIndex )
337 : : {
338 [ # # ][ # # ]: 0 : aMtf.AddAction( pCurrAct->Clone() );
339 : : }
340 : 0 : break;
341 : :
342 : : default:
343 : : OSL_FAIL( "Unknown meta action type encountered" );
344 : 0 : break;
345 : : }
346 : : }
347 : :
348 : : aVDev.DrawTransparent( aMtf,
349 : : aEmptyPoint,
350 : : aOutputSizePixel,
351 [ # # ][ # # ]: 0 : *mpAlphaGradient );
352 : : }
353 : : else
354 : : {
355 : : // no subsetting - render whole mtf
356 : 0 : aVDev.DrawTransparent( *mpGroupMtf,
357 : : aEmptyPoint,
358 : : aOutputSizePixel,
359 [ # # ]: 0 : *mpAlphaGradient );
360 : : }
361 : :
362 : :
363 : : // update buffered bitmap and transformation
364 [ # # ]: 0 : BitmapSharedPtr aBmp( VCLFactory::getInstance().createBitmap(
365 : : mpCanvas,
366 : : aVDev.GetBitmapEx(
367 : : aEmptyPoint,
368 [ # # ][ # # ]: 0 : aBitmapSizePixel ) ) );
[ # # ]
369 [ # # ][ # # ]: 0 : mxBufferBitmap = aBmp->getUNOBitmap();
370 [ # # ]: 0 : maLastTransformation = aTotalTransform;
371 [ # # ][ # # ]: 0 : maLastSubset = rSubset;
372 : : }
373 : :
374 : : // determine target transformation (we can't simply pass
375 : : // aTotalTransform as assembled above, since we must take
376 : : // the canvas' view state as is, it might contain clipping
377 : : // (which, in turn, is relative to the view
378 : : // transformation))
379 : :
380 : : // given that aTotalTransform is the identity
381 : : // transformation, we could simply render our bitmap
382 : : // as-is. Now, since the mxBufferBitmap content already
383 : : // accounts for scale changes in the overall
384 : : // transformation, we must factor this out
385 : : // before. Generally, the transformation matrix should be
386 : : // structured like this:
387 : : // Translation*Rotation*Shear*Scale. Thus, to neutralize
388 : : // the contained scaling, we've got to right-multiply with
389 : : // the inverse.
390 [ # # ]: 0 : ::basegfx::B2DHomMatrix aScaleCorrection;
391 [ # # ]: 0 : aScaleCorrection.scale( 1/aScale.getX(), 1/aScale.getY() );
392 [ # # ][ # # ]: 0 : aTransform = aTransform * aScaleCorrection;
[ # # ]
393 : :
394 [ # # ]: 0 : rendering::RenderState aLocalState( maState );
395 [ # # ]: 0 : ::canvas::tools::setRenderStateTransform(aLocalState, aTransform);
396 : :
397 : : #if OSL_DEBUG_LEVEL > 2
398 : : aLocalState.Clip.clear();
399 : : aLocalState.DeviceColor =
400 : : ::vcl::unotools::colorToDoubleSequence(
401 : : ::Color( 0x80FF0000 ),
402 : : mpCanvas->getUNOCanvas()->getDevice()->getDeviceColorSpace() );
403 : :
404 : : if( maState.Clip.is() )
405 : : mpCanvas->getUNOCanvas()->fillPolyPolygon( maState.Clip,
406 : : mpCanvas->getViewState(),
407 : : aLocalState );
408 : :
409 : : aLocalState.DeviceColor = maState.DeviceColor;
410 : : #endif
411 : :
412 [ # # ]: 0 : if( ::rtl::math::approxEqual(mnAlpha, 1.0) )
413 : : {
414 : : // no further alpha changes necessary -> draw directly
415 [ # # ][ # # ]: 0 : mpCanvas->getUNOCanvas()->drawBitmap( mxBufferBitmap,
416 : 0 : mpCanvas->getViewState(),
417 [ # # ][ # # ]: 0 : aLocalState );
[ # # ]
418 : : }
419 : : else
420 : : {
421 : : // add alpha modulation value to DeviceColor
422 [ # # ]: 0 : uno::Sequence<rendering::ARGBColor> aCols(1);
423 [ # # ]: 0 : aCols[0] = rendering::ARGBColor( mnAlpha, 1.0, 1.0, 1.0);
424 : : aLocalState.DeviceColor =
425 [ # # ][ # # ]: 0 : mpCanvas->getUNOCanvas()->getDevice()->getDeviceColorSpace()->convertFromARGB(
[ # # ][ # # ]
[ # # ][ # # ]
426 [ # # ][ # # ]: 0 : aCols);
[ # # ]
427 : :
428 [ # # ][ # # ]: 0 : mpCanvas->getUNOCanvas()->drawBitmapModulated( mxBufferBitmap,
429 : 0 : mpCanvas->getViewState(),
430 [ # # ]: 0 : aLocalState );
[ # # # # ]
[ # # ]
431 : : }
432 : :
433 [ # # ][ # # ]: 0 : return true;
[ # # ][ # # ]
434 : : }
435 : :
436 : : // TODO(P3): The whole float transparency handling is a mess,
437 : : // this should be refactored. What's more, the old idea of
438 : : // having only internal 'metaactions', and not the original
439 : : // GDIMetaFile now looks a lot less attractive. Try to move
440 : : // into the direction of having a direct GDIMetaFile2XCanvas
441 : : // renderer, and maybe a separate metafile XCanvas
442 : : // implementation.
443 : 0 : bool TransparencyGroupAction::render( const ::basegfx::B2DHomMatrix& rTransformation ) const
444 : : {
445 : : Subset aSubset;
446 : :
447 : 0 : aSubset.mnSubsetBegin = 0;
448 : 0 : aSubset.mnSubsetEnd = -1;
449 : :
450 [ # # ]: 0 : return renderSubset( rTransformation, aSubset );
451 : : }
452 : :
453 : 0 : ::basegfx::B2DRange TransparencyGroupAction::getBounds( const ::basegfx::B2DHomMatrix& rTransformation ) const
454 : : {
455 [ # # ]: 0 : rendering::RenderState aLocalState( maState );
456 [ # # ]: 0 : ::canvas::tools::prependToRenderState(aLocalState, rTransformation);
457 : :
458 : : return tools::calcDevicePixelBounds(
459 : : ::basegfx::B2DRange( 0,0,
460 : : maDstSize.getX(),
461 : : maDstSize.getY() ),
462 : 0 : mpCanvas->getViewState(),
463 [ # # ][ # # ]: 0 : aLocalState );
[ # # ][ # # ]
[ # # ]
464 : : }
465 : :
466 : 0 : ::basegfx::B2DRange TransparencyGroupAction::getBounds( const ::basegfx::B2DHomMatrix& rTransformation,
467 : : const Subset& rSubset ) const
468 : : {
469 : : // TODO(F3): Currently, the bounds for
470 : : // TransparencyGroupAction subsets equal those of the
471 : : // full set, although this action is able to render
472 : : // true subsets.
473 : :
474 : : // polygon only contains a single action, empty bounds
475 : : // if subset requests different range
476 [ # # ][ # # ]: 0 : if( rSubset.mnSubsetBegin != 0 ||
477 : : rSubset.mnSubsetEnd != 1 )
478 : 0 : return ::basegfx::B2DRange();
479 : :
480 : 0 : return getBounds( rTransformation );
481 : : }
482 : :
483 : 0 : sal_Int32 TransparencyGroupAction::getActionCount() const
484 : : {
485 [ # # ]: 0 : return mpGroupMtf.get() ? mpGroupMtf->GetActionSize() : 0;
486 : : }
487 : :
488 : : }
489 : :
490 : : SAL_WNODEPRECATED_DECLARATIONS_PUSH
491 : 0 : ActionSharedPtr TransparencyGroupActionFactory::createTransparencyGroupAction( MtfAutoPtr& rGroupMtf,
492 : : GradientAutoPtr& rAlphaGradient,
493 : : const Renderer::Parameters& rParms,
494 : : const ::basegfx::B2DPoint& rDstPoint,
495 : : const ::basegfx::B2DVector& rDstSize,
496 : : const CanvasSharedPtr& rCanvas,
497 : : const OutDevState& rState )
498 : : {
499 : : return ActionSharedPtr( new TransparencyGroupAction(rGroupMtf,
500 : : rAlphaGradient,
501 : : rParms,
502 : : rDstPoint,
503 : : rDstSize,
504 : : rCanvas,
505 [ # # ]: 0 : rState ) );
506 : : }
507 : : SAL_WNODEPRECATED_DECLARATIONS_POP
508 : :
509 : : }
510 : : }
511 : :
512 : : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|