From 2a79137e5625486381f5b7a1347d3420860b9dad Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 Feb 2017 20:32:06 +0100 Subject: [PATCH] Make cairo render tests pass. --- src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj | 2 - src/Gtk/Avalonia.Cairo/CairoPlatform.cs | 24 +++++- .../Avalonia.Cairo/Media/DrawingContext.cs | 61 ++++++++++----- .../Avalonia.Cairo/Media/ImageBrushImpl.cs | 55 ++++++++++++-- .../Media/LinearGradientBrushImpl.cs | 2 +- .../Media/RadialGradientBrushImpl.cs | 7 +- .../Media/SolidColorBrushImpl.cs | 2 +- .../Media/StreamGeometryImpl.cs | 2 +- src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs | 55 -------------- .../Avalonia.Cairo/Media/VisualBrushImpl.cs | 14 ---- src/Gtk/Avalonia.Cairo/RenderTarget.cs | 4 +- ...alonia.Cairo.RenderTests.v3.ncrunchproject | 71 ------------------ .../Media/VisualBrushTests.cs | 6 ++ .../Shapes/EllipseTests.cs | 5 +- .../Geometry_Clip_Clips_Path.expected.png | Bin 373 -> 692 bytes .../RadialGradientBrush_RedBlue.expected.png | Bin 0 -> 7985 bytes 16 files changed, 134 insertions(+), 176 deletions(-) delete mode 100644 src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs delete mode 100644 src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs create mode 100644 tests/TestFiles/Cairo/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png diff --git a/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj b/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj index 3ecd91eee2..1220a7fcdc 100644 --- a/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj +++ b/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj @@ -61,7 +61,6 @@ - @@ -71,7 +70,6 @@ - diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs index ef237cb35e..a9491cd46d 100644 --- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs +++ b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs @@ -26,14 +26,22 @@ namespace Avalonia.Cairo { using System.IO; using global::Cairo; + using Rendering; - public class CairoPlatform : IPlatformRenderInterface + public class CairoPlatform : IPlatformRenderInterface, IRendererFactory { private static readonly CairoPlatform s_instance = new CairoPlatform(); private static readonly Pango.Context s_pangoContext = CreatePangoContext(); - public static void Initialize() => AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); + public static bool UseImmediateRenderer { get; set; } + + public static void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(s_instance) + .Bind().ToConstant(s_instance); + } public IBitmapImpl CreateBitmap(int width, int height) { @@ -53,6 +61,18 @@ namespace Avalonia.Cairo return new FormattedTextImpl(s_pangoContext, text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight, constraint); } + public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) + { + if (UseImmediateRenderer) + { + return new ImmediateRenderer(root, renderLoop); + } + else + { + return new DeferredRenderer(root, renderLoop); + } + } + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { var accessor = surfaces?.OfType>().FirstOrDefault(); diff --git a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs index ee01d5ecb0..c671d5fa08 100644 --- a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; -using System.Runtime.InteropServices; using Avalonia.Cairo.Media.Imaging; using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; namespace Avalonia.Cairo.Media { - using Avalonia.Media.Imaging; - using Platform; using Cairo = global::Cairo; /// @@ -20,32 +19,30 @@ namespace Avalonia.Cairo.Media /// public class DrawingContext : IDrawingContextImpl, IDisposable { - /// - /// The cairo context. - /// private readonly Cairo.Context _context; - + private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly Stack _maskStack = new Stack(); /// /// Initializes a new instance of the class. /// /// The target surface. - public DrawingContext(Cairo.Surface surface) + public DrawingContext(Cairo.Surface surface, IVisualBrushRenderer visualBrushRenderer) { _context = new Cairo.Context(surface); + _visualBrushRenderer = visualBrushRenderer; } /// /// Initializes a new instance of the class. /// /// The GDK drawable. - public DrawingContext(Gdk.Drawable drawable) + public DrawingContext(Gdk.Drawable drawable, IVisualBrushRenderer visualBrushRenderer) { _context = Gdk.CairoHelper.Create(drawable); + _visualBrushRenderer = visualBrushRenderer; } - private Matrix _transform = Matrix.Identity; /// /// Gets the current transform of the drawing context. @@ -120,7 +117,9 @@ namespace Avalonia.Cairo.Media public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { - throw new NotImplementedException(); + PushOpacityMask(opacityMask, opacityMaskRect); + DrawImage(source, 1, new Rect(0, 0, source.PixelWidth, source.PixelHeight), destRect); + PopOpacityMask(); } /// @@ -299,11 +298,11 @@ namespace Avalonia.Cairo.Media private BrushImpl CreateBrushImpl(IBrush brush, Size destinationSize) { - var solid = brush as SolidColorBrush; - var linearGradientBrush = brush as LinearGradientBrush; - var radialGradientBrush = brush as RadialGradientBrush; - var imageBrush = brush as ImageBrush; - var visualBrush = brush as VisualBrush; + var solid = brush as ISolidColorBrush; + var linearGradientBrush = brush as ILinearGradientBrush; + var radialGradientBrush = brush as IRadialGradientBrush; + var imageBrush = brush as IImageBrush; + var visualBrush = brush as IVisualBrush; BrushImpl impl = null; if (solid != null) @@ -320,7 +319,35 @@ namespace Avalonia.Cairo.Media } else if (imageBrush != null) { - impl = new ImageBrushImpl(imageBrush, destinationSize); + impl = new ImageBrushImpl(imageBrush, (BitmapImpl)imageBrush.Source.PlatformImpl, destinationSize); + } + else if (visualBrush != null) + { + if (_visualBrushRenderer != null) + { + var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + + if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) + { + using (var intermediate = new Cairo.ImageSurface(Cairo.Format.ARGB32, (int)intermediateSize.Width, (int)intermediateSize.Height)) + { + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) + { + ctx.Clear(Colors.Transparent); + _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + } + + return new ImageBrushImpl( + visualBrush, + new RenderTargetBitmapImpl(intermediate), + destinationSize); + } + } + } + else + { + throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + } } else { diff --git a/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs index 486ad50b1f..4e037ce210 100644 --- a/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs @@ -1,14 +1,59 @@ using System; +using Avalonia.Cairo.Media.Imaging; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Utilities; +using Gdk; using global::Cairo; namespace Avalonia.Cairo.Media { public class ImageBrushImpl : BrushImpl { - public ImageBrushImpl(Avalonia.Media.ImageBrush brush, Size destinationSize) - { - this.PlatformBrush = TileBrushes.CreateTileBrush(brush, destinationSize); - } - } + public ImageBrushImpl( + ITileBrush brush, + IBitmapImpl bitmap, + Size targetSize) + { + var calc = new TileBrushCalculator(brush, new Size(bitmap.PixelWidth, bitmap.PixelHeight), targetSize); + + using (var intermediate = new ImageSurface(Format.ARGB32, (int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height)) + { + using (var context = new RenderTarget(intermediate).CreateDrawingContext(null)) + { + var rect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); + + context.Clear(Colors.Transparent); + context.PushClip(calc.IntermediateClip); + context.Transform = calc.IntermediateTransform; + context.DrawImage(bitmap, 1, rect, rect); + context.PopClip(); + } + + var result = new SurfacePattern(intermediate); + + if ((brush.TileMode & TileMode.FlipXY) != 0) + { + // TODO: Currently always FlipXY as that's all cairo supports natively. + // Support separate FlipX and FlipY by drawing flipped images to intermediate + // surface. + result.Extend = Extend.Reflect; + } + else + { + result.Extend = Extend.Repeat; + } + + if (brush.TileMode != TileMode.None) + { + var matrix = result.Matrix; + matrix.InitTranslate(-calc.DestinationRect.X, -calc.DestinationRect.Y); + result.Matrix = matrix; + } + + PlatformBrush = result; + } + } + } } diff --git a/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs index c809e5d2da..5354d4899a 100644 --- a/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs @@ -5,7 +5,7 @@ namespace Avalonia.Cairo { public class LinearGradientBrushImpl : BrushImpl { - public LinearGradientBrushImpl(Avalonia.Media.LinearGradientBrush brush, Size destinationSize) + public LinearGradientBrushImpl(Avalonia.Media.ILinearGradientBrush brush, Size destinationSize) { var start = brush.StartPoint.ToPixels(destinationSize); var end = brush.EndPoint.ToPixels(destinationSize); diff --git a/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs index 3fcb9c8244..51d028b935 100644 --- a/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs @@ -5,13 +5,14 @@ namespace Avalonia.Cairo { public class RadialGradientBrushImpl : BrushImpl { - public RadialGradientBrushImpl(Avalonia.Media.RadialGradientBrush brush, Size destinationSize) + public RadialGradientBrushImpl(Avalonia.Media.IRadialGradientBrush brush, Size destinationSize) { var center = brush.Center.ToPixels(destinationSize); var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize); - var radius = brush.Radius; + var radius = brush.Radius * Math.Min(destinationSize.Width, destinationSize.Height); - this.PlatformBrush = new RadialGradient(center.X, center.Y, radius, gradientOrigin.X, gradientOrigin.Y, radius); + this.PlatformBrush = new RadialGradient(center.X, center.Y, 1, gradientOrigin.X, gradientOrigin.Y, radius); + this.PlatformBrush.Matrix = Matrix.Identity.ToCairo(); foreach (var stop in brush.GradientStops) { diff --git a/src/Gtk/Avalonia.Cairo/Media/SolidColorBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/SolidColorBrushImpl.cs index 421e44710d..86f8aa7f25 100644 --- a/src/Gtk/Avalonia.Cairo/Media/SolidColorBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/SolidColorBrushImpl.cs @@ -5,7 +5,7 @@ namespace Avalonia.Cairo { public class SolidColorBrushImpl : BrushImpl { - public SolidColorBrushImpl(Avalonia.Media.SolidColorBrush brush, double opacityOverride = 1.0f) + public SolidColorBrushImpl(Avalonia.Media.ISolidColorBrush brush, double opacityOverride = 1.0f) { var color = brush?.Color.ToCairo() ?? new Color(); diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs index 31cba39276..7d59988918 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Cairo.Media public Rect GetRenderBounds(double strokeThickness) { // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); } public IStreamGeometryContextImpl Open() diff --git a/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs b/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs deleted file mode 100644 index ee80b9fdf3..0000000000 --- a/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Cairo; -using Avalonia.Cairo.Media.Imaging; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.RenderHelpers; - -namespace Avalonia.Cairo.Media -{ - internal static class TileBrushes - { - public static SurfacePattern CreateTileBrush(TileBrush brush, Size targetSize) - { - throw new NotImplementedException(); - //// var helper = new TileBrushImplHelper(brush, targetSize); - //// if (!helper.IsValid) - //// return null; - - ////using (var intermediate = new ImageSurface(Format.ARGB32, (int)helper.IntermediateSize.Width, (int)helper.IntermediateSize.Height)) - //// using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) - //// { - //// helper.DrawIntermediate(new Avalonia.Media.DrawingContext(ctx)); - - //// var result = new SurfacePattern(intermediate); - - //// if ((brush.TileMode & TileMode.FlipXY) != 0) - //// { - //// // TODO: Currently always FlipXY as that's all cairo supports natively. - //// // Support separate FlipX and FlipY by drawing flipped images to intermediate - //// // surface. - //// result.Extend = Extend.Reflect; - //// } - //// else - //// { - //// result.Extend = Extend.Repeat; - //// } - - //// if (brush.TileMode != TileMode.None) - //// { - //// var matrix = result.Matrix; - //// matrix.InitTranslate(-helper.DestinationRect.X, -helper.DestinationRect.Y); - //// result.Matrix = matrix; - //// } - - //// return result; - //// } - } - - } -} diff --git a/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs deleted file mode 100644 index 7c0e59e4d9..0000000000 --- a/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using global::Cairo; - -namespace Avalonia.Cairo.Media -{ - public class VisualBrushImpl : BrushImpl - { - public VisualBrushImpl(Avalonia.Media.VisualBrush brush, Size destinationSize) - { - this.PlatformBrush = TileBrushes.CreateTileBrush(brush, destinationSize); - } - } -} - diff --git a/src/Gtk/Avalonia.Cairo/RenderTarget.cs b/src/Gtk/Avalonia.Cairo/RenderTarget.cs index dec8977d39..b18c07377b 100644 --- a/src/Gtk/Avalonia.Cairo/RenderTarget.cs +++ b/src/Gtk/Avalonia.Cairo/RenderTarget.cs @@ -47,9 +47,9 @@ namespace Avalonia.Cairo public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { if (_drawableAccessor != null) - return new Media.DrawingContext(_drawableAccessor()); + return new Media.DrawingContext(_drawableAccessor(), visualBrushRenderer); if (_surface != null) - return new Media.DrawingContext(_surface); + return new Media.DrawingContext(_surface, visualBrushRenderer); throw new InvalidOperationException("Unspecified render target"); } diff --git a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v3.ncrunchproject b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v3.ncrunchproject index 5c2560547a..101c806e63 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v3.ncrunchproject +++ b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v3.ncrunchproject @@ -3,77 +3,6 @@ AbnormalReferenceResolution - - - Avalonia.Cairo.RenderTests.Controls.BorderTests - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_Fill_NoTile - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_FlipXY_TopLeftDest - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_Alignment_BottomRight - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_Alignment_Center - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_BottomRightQuarterDest - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_BottomRightQuarterSource - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_Tile_BottomRightQuarterSource_CenterQuarterDest - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_Uniform_NoTile - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_UniformToFill_NoTile - - - Avalonia.Cairo.RenderTests.Controls.ImageTests - - - Avalonia.Cairo.RenderTests.GeometryClippingTests - - - Avalonia.Cairo.RenderTests.Media.LinearGradientBrushTests.LinearGradientBrush_RedBlue_Vertical_Fill - - - Avalonia.Cairo.RenderTests.Media.VisualBrushTests - - - Avalonia.Cairo.RenderTests.OpacityMaskTests - - - Avalonia.Cairo.RenderTests.Shapes.EllipseTests - - - Avalonia.Cairo.RenderTests.Shapes.LineTests - - - Avalonia.Cairo.RenderTests.Shapes.PathTests - - - Avalonia.Cairo.RenderTests.Shapes.RectangleTests - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_Alignment_TopLeft - - - Avalonia.Cairo.RenderTests.Media.LinearGradientBrushTests.LinearGradientBrush_RedBlue_Horizontal_Fill - - - Avalonia.Cairo.RenderTests.Media.RadialGradientBrushTests.RadialGradientBrush_RedBlue - - True \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index 9ff5cb6354..9ec901535a 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -427,7 +427,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media CompareImages(); } +#if AVALONIA_CAIRO + [Fact(Skip = "Font scaling currently broken on cairo")] +#elif AVALONIA_SKIA_SKIP_FAIL + [Fact(Skip = "FIXME")] +#else [Fact] +#endif public async Task VisualBrush_InTree_Visual() { Border source; diff --git a/tests/Avalonia.RenderTests/Shapes/EllipseTests.cs b/tests/Avalonia.RenderTests/Shapes/EllipseTests.cs index 67cc50cd58..9687e817e3 100644 --- a/tests/Avalonia.RenderTests/Shapes/EllipseTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/EllipseTests.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Media; @@ -22,7 +23,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes } [Fact] - public void Circle_1px_Stroke() + public async Task Circle_1px_Stroke() { Decorator target = new Decorator { @@ -36,7 +37,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes } }; - RenderToFile(target); + await RenderToFile(target); CompareImages(); } } diff --git a/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png b/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png index 892899507b7d71439ee220baf272495473ad7394..1218293ab32a1440368054970ed91ce105d6b96d 100644 GIT binary patch delta 668 zcmV;N0%QI40<;B?B!32COGiWi{{a60|De66lK=n$X-PyuRCt{2+A&V#KoCID@69<4 zDZyM~u0Ti%LP9%aBr9#o88+Ml3=)W#vv2^40JUTWx81h8s%&?E$r6xI{ZtswKqJBf za7`_#UroaeaPK|HT57@vA{>C~7{GqdlNJ~bTc!@Cjb^=NCSclVmd|BoU;tNF{MP`p+%i)z zfVVgPTwWq)XY0I|8FPIl!r}cLd3;39&)2nO=2#&S4j&)L?d`hmW!6sLy1V0F*TJV= z7F%Xa^t0T*J+<#;wmOk=qG@lLF-^RjXqsDQ%+g;jG=I%4vo;wl7n;_VS$iav15Ino z%rR1%i*|8Q7#Pe$E9ytG-4wKMehL-U?(vBl85 z=UQ}KXx=j|zEd>sc~(&y&3l&B6wtiqSXClCd=&!Rj3U5Mv~Jq~0000GZx^prw85kH?(j9#r85lP9bN@+X1@buyJR*x382Ao@Fyrz3 z6)6l1j9i{Bjv*CsZ?7KYWisS(xv1MXNu%}L0%48Dpcf2poR?&n>PQRo-T3eMze^;$ z0b~pad}!~xRvh#%@@1^<*B>7LB3GV`3D^F5sy=Pq>ZxU^`y;}&pPs7QxGwD0)Ro82 zX}!&#S@otT=zsDm_VXHF?Uz-3*uFGw=I#i7Z(F;lS5tR3mu`AD&FVnL%kXP+bW0mN zUWPxra(MC7$eC3&TZFWWUwxc-YR#FI%r07+jh6EnM6H}^S=w^ZYwekp$~_@@seZCa z`R1!tN_%Xl2LJZ@cExd<*6wxltj_HATEBXE-{!v3wJVgrZq##>J(V0QedS=pb@sm- pBd+_c(QYVZg!l$THvF9Sl=buR*U>XxgiZ$uc)I$ztaD0e0svO1nA-pV diff --git a/tests/TestFiles/Cairo/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png b/tests/TestFiles/Cairo/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..871a90caf38e6f022065c5fcb3c93e7b8dcac3ba GIT binary patch literal 7985 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 V(|qmP*cv#8iID}q!oZF4{{SsbnGpa0 literal 0 HcmV?d00001