1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
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 <sal/config.h>
26 #include <basegfx/matrix/b2dhommatrix.hxx>
27 #include <basegfx/matrix/b2dhommatrixtools.hxx>
28 #include <basegfx/numeric/ftools.hxx>
29 #include <basegfx/point/b2dpoint.hxx>
30 #include <basegfx/polygon/b2dpolygontools.hxx>
31 #include <basegfx/range/b2drectangle.hxx>
32 #include <basegfx/utils/canvastools.hxx>
33 #include <basegfx/utils/keystoplerp.hxx>
34 #include <basegfx/utils/lerp.hxx>
35 #include <basegfx/utils/tools.hxx>
36 #include <com/sun/star/rendering/TexturingMode.hpp>
37 #include <rtl/math.hxx>
38 #include <comphelper/diagnose_ex.hxx>
40 #include <parametricpolypolygon.hxx>
42 #include "dx_canvashelper.hxx"
43 #include "dx_impltools.hxx"
44 #include "dx_spritecanvas.hxx"
47 using namespace ::com::sun::star
;
53 typedef std::shared_ptr
< Gdiplus::PathGradientBrush
> PathGradientBrushSharedPtr
;
55 bool fillLinearGradient( GraphicsSharedPtr
const & rGraphics
,
56 const ::canvas::ParametricPolyPolygon::Values
& /*rValues*/,
57 const std::vector
< Gdiplus::Color
>& rColors
,
58 const std::vector
< Gdiplus::REAL
>& rStops
,
59 const GraphicsPathSharedPtr
& rFillPath
,
60 const rendering::Texture
& texture
)
62 // setup a linear gradient with given colors
65 Gdiplus::LinearGradientBrush
aBrush(
73 aBrush
.SetInterpolationColors(rColors
.data(),
77 // render background color, as LinearGradientBrush does not
78 // properly support the WrapModeClamp repeat mode
79 Gdiplus::SolidBrush
aBackgroundBrush( rColors
[0] );
80 rGraphics
->FillPath( &aBackgroundBrush
, rFillPath
.get() );
82 // TODO(F2): This does not yet support other repeat modes
83 // except clamp, and probably also no multi-texturing
85 // calculate parallelogram of gradient in object space, extend
86 // top and bottom of it such that they cover the whole fill
88 ::basegfx::B2DHomMatrix aTextureTransform
;
89 ::basegfx::unotools::homMatrixFromAffineMatrix( aTextureTransform
,
90 texture
.AffineTransform
);
92 ::basegfx::B2DPoint
aLeftTop( 0.0, 0.0 );
93 ::basegfx::B2DPoint
aLeftBottom( 0.0, 1.0 );
94 ::basegfx::B2DPoint
aRightTop( 1.0, 0.0 );
95 ::basegfx::B2DPoint
aRightBottom( 1.0, 1.0 );
97 aLeftTop
*= aTextureTransform
;
98 aLeftBottom
*= aTextureTransform
;
99 aRightTop
*= aTextureTransform
;
100 aRightBottom
*= aTextureTransform
;
102 Gdiplus::RectF aBounds
;
103 rFillPath
->GetBounds( &aBounds
);
105 // now, we potentially have to enlarge our gradient area
106 // atop and below the transformed [0,1]x[0,1] unit rect,
107 // for the gradient to fill the complete bound rect.
108 ::basegfx::utils::infiniteLineFromParallelogram( aLeftTop
,
112 tools::b2dRangeFromGdiPlusRectF( aBounds
) );
114 // calc length of bound rect diagonal
115 const double nDiagonalLength(
116 hypot( aBounds
.Width
,
119 // generate a path which covers the 'right' side of the
120 // gradient, extending two times the bound rect diagonal to
121 // the right (and thus covering the whole half plane 'right'
122 // of the gradient). Take the middle of the gradient as the
123 // 'left' side of the polygon, to not fall victim to rounding
124 // errors at the edge.
125 ::basegfx::B2DVector
aDirection( aLeftTop
- aLeftBottom
);
126 aDirection
= ::basegfx::getNormalizedPerpendicular( aDirection
);
127 aDirection
*= nDiagonalLength
;
129 const ::basegfx::B2DPoint
aHalfPlaneLeftTop( (aLeftTop
+ aRightTop
) * 0.5 );
130 const ::basegfx::B2DPoint
aHalfPlaneLeftBottom( (aLeftBottom
+ aRightBottom
) * 0.5 );
131 const ::basegfx::B2DPoint
aHalfPlaneRightTop( aRightTop
+ aDirection
);
132 const ::basegfx::B2DPoint
aHalfPlaneRightBottom( aRightBottom
+ aDirection
);
134 Gdiplus::GraphicsPath aSolidFillPath
;
135 aSolidFillPath
.AddLine( static_cast<Gdiplus::REAL
>(aHalfPlaneLeftTop
.getX()),
136 static_cast<Gdiplus::REAL
>(aHalfPlaneLeftTop
.getY()),
137 static_cast<Gdiplus::REAL
>(aHalfPlaneRightTop
.getX()),
138 static_cast<Gdiplus::REAL
>(aHalfPlaneRightTop
.getY()) );
139 aSolidFillPath
.AddLine( static_cast<Gdiplus::REAL
>(aHalfPlaneRightBottom
.getX()),
140 static_cast<Gdiplus::REAL
>(aHalfPlaneRightBottom
.getY()),
141 static_cast<Gdiplus::REAL
>(aHalfPlaneLeftBottom
.getX()),
142 static_cast<Gdiplus::REAL
>(aHalfPlaneLeftBottom
.getY()) );
143 aSolidFillPath
.CloseFigure();
145 // limit output to fill path, we've just generated a path that
146 // might be substantially larger
147 if( Gdiplus::Ok
!= rGraphics
->SetClip( rFillPath
.get(),
148 Gdiplus::CombineModeIntersect
) )
153 Gdiplus::SolidBrush
aBackgroundBrush2( rColors
.back() );
154 rGraphics
->FillPath( &aBackgroundBrush2
, &aSolidFillPath
);
156 // generate clip polygon from the extended parallelogram
157 // (exploit the feature that distinct lines in a figure are
158 // automatically closed by a straight line)
159 Gdiplus::GraphicsPath aClipPath
;
160 aClipPath
.AddLine( static_cast<Gdiplus::REAL
>(aLeftTop
.getX()),
161 static_cast<Gdiplus::REAL
>(aLeftTop
.getY()),
162 static_cast<Gdiplus::REAL
>(aRightTop
.getX()),
163 static_cast<Gdiplus::REAL
>(aRightTop
.getY()) );
164 aClipPath
.AddLine( static_cast<Gdiplus::REAL
>(aRightBottom
.getX()),
165 static_cast<Gdiplus::REAL
>(aRightBottom
.getY()),
166 static_cast<Gdiplus::REAL
>(aLeftBottom
.getX()),
167 static_cast<Gdiplus::REAL
>(aLeftBottom
.getY()) );
168 aClipPath
.CloseFigure();
170 // limit output to a _single_ strip of the gradient (have to
171 // clip here, since GDI+ wrapmode clamp does not work here)
172 if( Gdiplus::Ok
!= rGraphics
->SetClip( &aClipPath
,
173 Gdiplus::CombineModeIntersect
) )
178 // now, finally, output the gradient
179 Gdiplus::Matrix aMatrix
;
180 tools::gdiPlusMatrixFromAffineMatrix2D( aMatrix
,
181 texture
.AffineTransform
);
182 aBrush
.SetTransform( &aMatrix
);
184 rGraphics
->FillRectangle( &aBrush
, aBounds
);
189 int numColorSteps( const Gdiplus::Color
& rColor1
, const Gdiplus::Color
& rColor2
)
192 std::abs( rColor1
.GetRed() - rColor2
.GetRed() ),
194 std::abs( rColor1
.GetGreen() - rColor2
.GetGreen() ),
195 std::abs( rColor1
.GetBlue() - rColor2
.GetBlue() ) ) );
198 bool fillPolygonalGradient( const ::canvas::ParametricPolyPolygon::Values
& rValues
,
199 const std::vector
< Gdiplus::Color
>& rColors
,
200 const std::vector
< Gdiplus::REAL
>& rStops
,
201 GraphicsSharedPtr
const & rGraphics
,
202 const GraphicsPathSharedPtr
& rPath
,
203 const rendering::ViewState
& viewState
,
204 const rendering::RenderState
& renderState
,
205 const rendering::Texture
& texture
)
207 // copy original fill path object, might have to change it
209 GraphicsPathSharedPtr
pFillPath( rPath
);
210 const ::basegfx::B2DPolygon
& rGradientPoly( rValues
.maGradientPoly
);
212 PathGradientBrushSharedPtr pGradientBrush
;
214 // fill background uniformly with end color
215 Gdiplus::SolidBrush
aBackgroundBrush( rColors
[0] );
216 rGraphics
->FillPath( &aBackgroundBrush
, pFillPath
.get() );
218 Gdiplus::Matrix aMatrix
;
219 // scale focus according to aspect ratio: for wider-than-tall
220 // bounds (nAspectRatio > 1.0), the focus must have non-zero
221 // width. Specifically, a bound rect twice as wide as tall has
222 // a focus of half its width.
223 if( !::rtl::math::approxEqual(rValues
.mnAspectRatio
,
228 // And here comes the greatest shortcoming of the GDI+
229 // gradients ever: SetFocusScales completely ignores
230 // transformations, both when set at the PathGradientBrush
231 // and for the world coordinate system. Thus, to correctly
232 // display anisotrophic path gradients, we have to render
233 // them by hand. WTF.
235 // TODO(F2): This does not yet support other repeat modes
236 // except clamp, and probably also no multi-texturing
238 // limit output to to-be-filled polygon
239 if( Gdiplus::Ok
!= rGraphics
->SetClip( pFillPath
.get(),
240 Gdiplus::CombineModeIntersect
) )
245 // disable anti-aliasing, if any
246 const Gdiplus::SmoothingMode
eOldAAMode( rGraphics
->GetSmoothingMode() );
247 rGraphics
->SetSmoothingMode( Gdiplus::SmoothingModeHighSpeed
);
250 // determine number of steps to use
253 // TODO(Q2): Unify step calculations with VCL canvas
255 for( size_t i
=0; i
<rColors
.size()-1; ++i
)
256 nColorSteps
+= numColorSteps(rColors
[i
],rColors
[i
+1]);
257 ::basegfx::B2DHomMatrix aTotalTransform
;
258 const int nStepCount
=
259 ::canvas::tools::calcGradientStepCount(aTotalTransform
,
265 ::basegfx::B2DHomMatrix aTextureTransform
;
266 ::basegfx::unotools::homMatrixFromAffineMatrix( aTextureTransform
,
267 texture
.AffineTransform
);
268 // determine overall transformation for inner polygon (might
269 // have to be prefixed by anisotrophic scaling)
270 ::basegfx::B2DHomMatrix aInnerPolygonTransformMatrix
;
272 // For performance reasons, we create a temporary VCL polygon
273 // here, keep it all the way and only change the vertex values
274 // in the loop below (as ::Polygon is a pimpl class, creating
275 // one every loop turn would really stress the mem allocator)
276 ::basegfx::B2DPolygon
aOuterPoly( rGradientPoly
);
277 ::basegfx::B2DPolygon aInnerPoly
;
279 // subdivide polygon _before_ rendering, would otherwise have
280 // to be performed on every loop turn.
281 if( aOuterPoly
.areControlPointsUsed() )
282 aOuterPoly
= ::basegfx::utils::adaptiveSubdivideByAngle(aOuterPoly
);
284 aInnerPoly
= aOuterPoly
;
285 aOuterPoly
.transform(aTextureTransform
);
288 // apply scaling (possibly anisotrophic) to inner polygon
291 // scale inner polygon according to aspect ratio: for
292 // wider-than-tall bounds (nAspectRatio > 1.0), the inner
293 // polygon, representing the gradient focus, must have
294 // non-zero width. Specifically, a bound rect twice as wide as
295 // tall has a focus polygon of half its width.
296 const double nAspectRatio( rValues
.mnAspectRatio
);
297 if( nAspectRatio
> 1.0 )
299 // width > height case
300 aInnerPolygonTransformMatrix
.scale( 1.0 - 1.0/nAspectRatio
,
303 else if( nAspectRatio
< 1.0 )
305 // width < height case
306 aInnerPolygonTransformMatrix
.scale( 0.0,
307 1.0 - nAspectRatio
);
312 aInnerPolygonTransformMatrix
.scale( 0.0, 0.0 );
315 // and finally, add texture transform to it.
316 aInnerPolygonTransformMatrix
*= aTextureTransform
;
318 // apply final matrix to polygon
319 aInnerPoly
.transform( aInnerPolygonTransformMatrix
);
321 Gdiplus::GraphicsPath aCurrPath
;
322 Gdiplus::SolidBrush
aFillBrush( rColors
[0] );
323 const sal_uInt32
nNumPoints( aOuterPoly
.count() );
324 basegfx::utils::KeyStopLerp
aLerper(rValues
.maStops
);
325 for( int i
=1; i
<nStepCount
; ++i
)
327 std::ptrdiff_t nIndex
;
329 const double fT( i
/double(nStepCount
) );
330 std::tie(nIndex
,fAlpha
)=aLerper
.lerp(fT
);
332 const Gdiplus::Color
aFillColor(
333 static_cast<BYTE
>( basegfx::utils::lerp(rColors
[nIndex
].GetRed(),rColors
[nIndex
+1].GetRed(),fAlpha
) ),
334 static_cast<BYTE
>( basegfx::utils::lerp(rColors
[nIndex
].GetGreen(),rColors
[nIndex
+1].GetGreen(),fAlpha
) ),
335 static_cast<BYTE
>( basegfx::utils::lerp(rColors
[nIndex
].GetBlue(),rColors
[nIndex
+1].GetBlue(),fAlpha
) ) );
337 aFillBrush
.SetColor( aFillColor
);
338 aCurrPath
.Reset(); aCurrPath
.StartFigure();
339 for( unsigned int p
=1; p
<nNumPoints
; ++p
)
341 const ::basegfx::B2DPoint
& rOuterPoint1( aOuterPoly
.getB2DPoint(p
-1) );
342 const ::basegfx::B2DPoint
& rInnerPoint1( aInnerPoly
.getB2DPoint(p
-1) );
343 const ::basegfx::B2DPoint
& rOuterPoint2( aOuterPoly
.getB2DPoint(p
) );
344 const ::basegfx::B2DPoint
& rInnerPoint2( aInnerPoly
.getB2DPoint(p
) );
347 Gdiplus::REAL(fT
*rInnerPoint1
.getX() + (1-fT
)*rOuterPoint1
.getX()),
348 Gdiplus::REAL(fT
*rInnerPoint1
.getY() + (1-fT
)*rOuterPoint1
.getY()),
349 Gdiplus::REAL(fT
*rInnerPoint2
.getX() + (1-fT
)*rOuterPoint2
.getX()),
350 Gdiplus::REAL(fT
*rInnerPoint2
.getY() + (1-fT
)*rOuterPoint2
.getY()));
352 aCurrPath
.CloseFigure();
354 rGraphics
->FillPath( &aFillBrush
, &aCurrPath
);
357 // reset to old anti-alias mode
358 rGraphics
->SetSmoothingMode( eOldAAMode
);
364 // We're generating a PathGradientBrush from scratch here,
365 // and put in a transformed GraphicsPath (transformed with
366 // the texture transform). This is because the
367 // straight-forward approach to store a Brush pointer at
368 // this class and set a texture transform via
369 // PathGradientBrush::SetTransform() is spoiled by MS: it
370 // seems that _either_ the texture transform, _or_ the
371 // transform at the Graphics can be set, but not both. If
372 // one sets both, only the translational components of the
373 // texture is respected.
375 tools::gdiPlusMatrixFromAffineMatrix2D( aMatrix
,
376 texture
.AffineTransform
);
377 GraphicsPathSharedPtr
pGradientPath(
378 tools::graphicsPathFromB2DPolygon( rValues
.maGradientPoly
));
379 pGradientPath
->Transform( &aMatrix
);
382 = std::make_shared
<Gdiplus::PathGradientBrush
>( pGradientPath
.get() );
383 pGradientBrush
->SetInterpolationColors( rColors
.data(),
387 // explicitly setup center point. Since the center of GDI+
388 // gradients are by default the _centroid_ of the path
389 // (i.e. the weighted sum of edge points), it will not
390 // necessarily coincide with our notion of center.
391 Gdiplus::PointF
aCenterPoint(0, 0);
392 aMatrix
.TransformPoints( &aCenterPoint
);
393 pGradientBrush
->SetCenterPoint( aCenterPoint
);
395 const bool bTileX( texture
.RepeatModeX
!= rendering::TexturingMode::CLAMP
);
396 const bool bTileY( texture
.RepeatModeY
!= rendering::TexturingMode::CLAMP
);
398 if( bTileX
&& bTileY
)
399 pGradientBrush
->SetWrapMode( Gdiplus::WrapModeTile
);
402 OSL_ENSURE( bTileY
== bTileX
,
403 "ParametricPolyPolygon::fillPolygonalGradient(): Cannot have repeat x and repeat y differ!" );
405 pGradientBrush
->SetWrapMode( Gdiplus::WrapModeClamp
);
408 // render actual gradient
409 rGraphics
->FillPath( pGradientBrush
.get(), pFillPath
.get() );
412 #if OSL_DEBUG_LEVEL > 0
413 Gdiplus::Pen
aPen( Gdiplus::Color( 255, 255, 0, 0 ),
416 rGraphics
->DrawRectangle( &aPen
,
417 Gdiplus::RectF( 0.0f
, 0.0f
,
424 bool fillGradient( const ::canvas::ParametricPolyPolygon::Values
& rValues
,
425 const std::vector
< Gdiplus::Color
>& rColors
,
426 const std::vector
< Gdiplus::REAL
>& rStops
,
427 GraphicsSharedPtr
const & rGraphics
,
428 const GraphicsPathSharedPtr
& rPath
,
429 const rendering::ViewState
& viewState
,
430 const rendering::RenderState
& renderState
,
431 const rendering::Texture
& texture
)
433 switch( rValues
.meType
)
435 case ::canvas::ParametricPolyPolygon::GradientType::Linear
:
436 fillLinearGradient( rGraphics
,
444 case ::canvas::ParametricPolyPolygon::GradientType::Elliptical
:
445 case ::canvas::ParametricPolyPolygon::GradientType::Rectangular
:
446 fillPolygonalGradient( rValues
,
457 ENSURE_OR_THROW( false,
458 "CanvasHelper::fillGradient(): Unexpected case" );
464 void fillBitmap( const uno::Reference
< rendering::XBitmap
>& xBitmap
,
465 GraphicsSharedPtr
const & rGraphics
,
466 const GraphicsPathSharedPtr
& rPath
,
467 const rendering::Texture
& rTexture
)
469 OSL_ENSURE( rTexture
.RepeatModeX
==
470 rTexture
.RepeatModeY
,
471 "CanvasHelper::fillBitmap(): GDI+ cannot handle differing X/Y repeat mode." );
473 const bool bClamp( rTexture
.RepeatModeX
== rendering::TexturingMode::NONE
&&
474 rTexture
.RepeatModeY
== rendering::TexturingMode::NONE
);
476 const geometry::IntegerSize2D
aBmpSize( xBitmap
->getSize() );
477 ENSURE_ARG_OR_THROW( aBmpSize
.Width
!= 0 &&
478 aBmpSize
.Height
!= 0,
479 "CanvasHelper::fillBitmap(): zero-sized texture bitmap" );
481 // TODO(P3): Detect case that path is rectangle and
482 // bitmap is just scaled into that. Then, we can
483 // render directly, without generating a temporary
484 // GDI+ bitmap (this is significant, because drawing
485 // layer presents background object bitmap in that
487 BitmapSharedPtr
pBitmap(
488 tools::bitmapFromXBitmap( xBitmap
) );
490 TextureBrushSharedPtr pBrush
;
491 if( ::rtl::math::approxEqual( rTexture
.Alpha
,
494 pBrush
= std::make_shared
<Gdiplus::TextureBrush
>(
496 bClamp
? Gdiplus::WrapModeClamp
: Gdiplus::WrapModeTile
);
500 Gdiplus::ImageAttributes aImgAttr
;
502 tools::setModulateImageAttributes( aImgAttr
,
508 Gdiplus::Rect
aRect(0,0,
511 pBrush
= std::make_shared
<Gdiplus::TextureBrush
>(
517 bClamp
? Gdiplus::WrapModeClamp
: Gdiplus::WrapModeTile
);
520 Gdiplus::Matrix aTextureTransform
;
521 tools::gdiPlusMatrixFromAffineMatrix2D( aTextureTransform
,
522 rTexture
.AffineTransform
);
524 // scale down bitmap to [0,1]x[0,1] rect, as required
525 // from the XCanvas interface.
526 pBrush
->MultiplyTransform( &aTextureTransform
);
527 pBrush
->ScaleTransform( static_cast< Gdiplus::REAL
>(1.0/aBmpSize
.Width
),
528 static_cast< Gdiplus::REAL
>(1.0/aBmpSize
.Height
) );
530 // TODO(F1): FillRule
532 Gdiplus::Ok
== rGraphics
->FillPath( pBrush
.get(),
534 "CanvasHelper::fillTexturedPolyPolygon(): GDI+ call failed" );
539 uno::Reference
< rendering::XCachedPrimitive
> CanvasHelper::fillTexturedPolyPolygon( const rendering::XCanvas
* /*pCanvas*/,
540 const uno::Reference
< rendering::XPolyPolygon2D
>& xPolyPolygon
,
541 const rendering::ViewState
& viewState
,
542 const rendering::RenderState
& renderState
,
543 const uno::Sequence
< rendering::Texture
>& textures
)
545 ENSURE_OR_THROW( xPolyPolygon
.is(),
546 "CanvasHelper::fillTexturedPolyPolygon: polygon is NULL");
547 ENSURE_OR_THROW( textures
.getLength(),
548 "CanvasHelper::fillTexturedPolyPolygon: empty texture sequence");
552 GraphicsSharedPtr
pGraphics( mpGraphicsProvider
->getGraphics() );
554 setupGraphicsState( pGraphics
, viewState
, renderState
);
556 // TODO(F1): Multi-texturing
557 if( textures
[0].Gradient
.is() )
559 // try to cast XParametricPolyPolygon2D reference to
560 // our implementation class.
561 ::canvas::ParametricPolyPolygon
* pGradient
=
562 dynamic_cast< ::canvas::ParametricPolyPolygon
* >( textures
[0].Gradient
.get() );
566 const ::canvas::ParametricPolyPolygon::Values
& rValues(
567 pGradient
->getValues() );
569 OSL_ASSERT(rValues
.maColors
.getLength() == rValues
.maStops
.getLength()
570 && rValues
.maColors
.getLength() > 1);
572 std::vector
< Gdiplus::Color
> aColors(rValues
.maColors
.getLength());
573 std::transform(&rValues
.maColors
[0],
574 &rValues
.maColors
[0]+rValues
.maColors
.getLength(),
576 [](const uno::Sequence
< double >& aDoubleSequence
) { return tools::sequenceToArgb(aDoubleSequence
); } );
577 std::vector
< Gdiplus::REAL
> aStops
;
578 comphelper::sequenceToContainer(aStops
,rValues
.maStops
);
580 // TODO(E1): Return value
581 // TODO(F1): FillRule
582 fillGradient( rValues
,
586 tools::graphicsPathFromXPolyPolygon2D( xPolyPolygon
),
592 else if( textures
[0].Bitmap
.is() )
594 // TODO(E1): Return value
595 // TODO(F1): FillRule
596 fillBitmap( textures
[0].Bitmap
,
598 tools::graphicsPathFromXPolyPolygon2D( xPolyPolygon
),
603 // TODO(P1): Provide caching here.
604 return uno::Reference
< rendering::XCachedPrimitive
>(nullptr);
608 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */