From 8f221aacf08661bfc47bfdbc9d39e8631e8f44cc Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Thu, 4 Mar 2021 18:37:28 -0800 Subject: [PATCH] Implement new GetContextInfo API overloads Implements #47880, adding new, more performant overloads for GetContextInfo. - Add helper for only creating a region if it isn't infinite - Start an internal extensions class for easier mapping of System.Drawing concepts to System.Numerics types - Simplify GraphicsContext --- .../ref/System.Drawing.Common.cs | 3 + .../src/System.Drawing.Common.csproj | 1 + .../src/System/Drawing/Graphics.Unix.cs | 12 ++ .../src/System/Drawing/Graphics.Windows.cs | 80 +++++++--- .../src/System/Drawing/Graphics.cs | 33 ++++ .../src/System/Drawing/GraphicsContext.cs | 142 +++-------------- .../src/System/Drawing/NumericsExtensions.cs | 27 ++++ .../tests/Graphics_GetContextTests.Core.cs | 149 ++++++++++++++++++ .../tests/Graphics_GetContextTests.cs | 4 +- .../tests/System.Drawing.Common.Tests.csproj | 3 +- 10 files changed, 312 insertions(+), 142 deletions(-) create mode 100644 src/libraries/System.Drawing.Common/src/System/Drawing/NumericsExtensions.cs create mode 100644 src/libraries/System.Drawing.Common/tests/Graphics_GetContextTests.Core.cs diff --git a/src/libraries/System.Drawing.Common/ref/System.Drawing.Common.cs b/src/libraries/System.Drawing.Common/ref/System.Drawing.Common.cs index 157484d8543ff..b1831da81a241 100644 --- a/src/libraries/System.Drawing.Common/ref/System.Drawing.Common.cs +++ b/src/libraries/System.Drawing.Common/ref/System.Drawing.Common.cs @@ -595,7 +595,10 @@ public void Flush(System.Drawing.Drawing2D.FlushIntention intention) { } public static System.Drawing.Graphics FromHwndInternal(System.IntPtr hwnd) { throw null; } public static System.Drawing.Graphics FromImage(System.Drawing.Image image) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + [Obsolete("Use one of the other overloads.")] public object GetContextInfo() { throw null; } + public void GetContextInfo(out PointF offset) { throw null; } + public void GetContextInfo(out PointF offset, out Region? clip) { throw null; } public static System.IntPtr GetHalftonePalette() { throw null; } public System.IntPtr GetHdc() { throw null; } public System.Drawing.Color GetNearestColor(System.Drawing.Color color) { throw null; } diff --git a/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj b/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj index 153bbc77f97fc..ced08e350ecc3 100644 --- a/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj +++ b/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj @@ -30,6 +30,7 @@ + diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.Unix.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.Unix.cs index 3da1617e5979b..d0b123621b8f9 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.Unix.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.Unix.cs @@ -581,6 +581,18 @@ public object GetContextInfo() throw new NotImplementedException(); } + [EditorBrowsable(EditorBrowsableState.Never)] + public void GetContextInfo(out PointF offset) + { + throw new PlatformNotSupportedException(); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public void GetContextInfo(out PointF offset, out Region? clip) + { + throw new PlatformNotSupportedException(); + } + private void CheckErrorStatus(int status) { Gdip.CheckStatus(status); diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.Windows.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.Windows.cs index f523e1c4e58b5..d414246121164 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.Windows.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.Windows.cs @@ -8,6 +8,7 @@ using System.Drawing.Imaging; using System.Drawing.Internal; using System.Globalization; +using System.Numerics; using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; using Gdip = System.Drawing.SafeNativeMethods.Gdip; @@ -682,40 +683,58 @@ public unsafe void EnumerateMetafile( /// WARNING: This method is for internal FX support only. /// [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use one of the other overloads.")] public object GetContextInfo() { - Region cumulClip = Clip; // current context clip. - Matrix cumulTransform = Transform; // current context transform. - PointF currentOffset = PointF.Empty; // offset of current context. - PointF totalOffset = PointF.Empty; // absolute coord offset of top context. + GetContextInfo(out Matrix3x2 cumulativeTransform, calculateClip: true, out Region? cumulativeClip); + return new object[] { cumulativeClip ?? new Region(), new Matrix(cumulativeTransform) }; + } - if (!cumulTransform.IsIdentity) - { - currentOffset = cumulTransform.Offset; - } + private void GetContextInfo(out Matrix3x2 cumulativeTransform, bool calculateClip, out Region? cumulativeClip) + { + cumulativeClip = calculateClip ? GetRegionIfNotInfinite() : null; // Current context clip. + cumulativeTransform = TransformElements; // Current context transform. + Vector2 currentOffset = default; // Offset of current context. + Vector2 totalOffset = default; // Absolute coordinate offset of top context. GraphicsContext? context = _previousContext; - while (context != null) + if (!cumulativeTransform.IsIdentity) { - if (!context.TransformOffset.IsEmpty) + currentOffset = cumulativeTransform.Translation; + } + + while (context is not null) + { + if (!context.TransformOffset.IsEmpty()) { - cumulTransform.Translate(context.TransformOffset.X, context.TransformOffset.Y); + cumulativeTransform.Translate(context.TransformOffset); } - if (!currentOffset.IsEmpty) + if (!currentOffset.IsEmpty()) { // The location of the GDI+ clip region is relative to the coordinate origin after any translate transform // has been applied. We need to intersect regions using the same coordinate origin relative to the previous // context. - cumulClip.Translate(currentOffset.X, currentOffset.Y); + + // If we don't have a cumulative clip, we're infinite, and translation on infinite regions is a no-op. + cumulativeClip?.Translate(currentOffset.X, currentOffset.Y); totalOffset.X += currentOffset.X; totalOffset.Y += currentOffset.Y; } - if (context.Clip != null) + // Context only stores clips if they are not infinite. Intersecting a clip with an infinite clip is a no-op. + if (calculateClip && context.Clip is not null) { - cumulClip.Intersect(context.Clip); + // Intersecting an infinite clip with another is just a copy of the second clip. + if (cumulativeClip is null) + { + cumulativeClip = context.Clip; + } + else + { + cumulativeClip.Intersect(context.Clip); + } } currentOffset = context.TransformOffset; @@ -732,14 +751,39 @@ public object GetContextInfo() } while (context.IsCumulative); } - if (!totalOffset.IsEmpty) + if (!totalOffset.IsEmpty()) { // We need now to reset the total transform in the region so when calling Region.GetHRgn(Graphics) // the HRegion is properly offset by GDI+ based on the total offset of the graphics object. - cumulClip.Translate(-totalOffset.X, -totalOffset.Y); + + // If we don't have a cumulative clip, we're infinite, and translation on infinite regions is a no-op. + cumulativeClip?.Translate(-totalOffset.X, -totalOffset.Y); } + } + + /// + /// Gets the cumulative offset. + /// + /// The cumulative offset. + [EditorBrowsable(EditorBrowsableState.Never)] + public void GetContextInfo(out PointF offset) + { + GetContextInfo(out Matrix3x2 cumulativeTransform, calculateClip: false, out _); + Vector2 translation = cumulativeTransform.Translation; + offset = new PointF(translation.X, translation.Y); + } - return new object[] { cumulClip, cumulTransform }; + /// + /// Gets the cumulative offset and clip region. + /// + /// The cumulative offset. + /// The cumulative clip region or null if the clip region is infinite. + [EditorBrowsable(EditorBrowsableState.Never)] + public void GetContextInfo(out PointF offset, out Region? clip) + { + GetContextInfo(out Matrix3x2 cumulativeTransform, calculateClip: true, out clip); + Vector2 translation = cumulativeTransform.Translation; + offset = new PointF(translation.X, translation.Y); } public RectangleF VisibleClipBounds diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs index 9ab0ebe423784..4917e8f33d831 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs @@ -2465,5 +2465,38 @@ private void IgnoreMetafileErrors(Image image, ref int errorStatus) if (errorStatus != Gdip.Ok && image.RawFormat.Equals(ImageFormat.Emf)) errorStatus = Gdip.Ok; } + + /// + /// Creates a Region class only if the native region is not infinite. + /// + internal Region? GetRegionIfNotInfinite() + { + Gdip.CheckStatus(Gdip.GdipCreateRegion(out IntPtr regionHandle)); + try + { + Gdip.GdipGetClip(new HandleRef(this, NativeGraphics), new HandleRef(null, regionHandle)); + Gdip.CheckStatus(Gdip.GdipIsInfiniteRegion( + new HandleRef(null, regionHandle), + new HandleRef(this, NativeGraphics), + out int isInfinite)); + + if (isInfinite != 0) + { + // Infinite + return null; + } + + Region region = new Region(regionHandle); + regionHandle = IntPtr.Zero; + return region; + } + finally + { + if (regionHandle != IntPtr.Zero) + { + Gdip.GdipDeleteRegion(new HandleRef(null, regionHandle)); + } + } + } } } diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/GraphicsContext.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/GraphicsContext.cs index 82340aa17020f..c1587d0e383a6 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/GraphicsContext.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/GraphicsContext.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Drawing.Drawing2D; +using System.Numerics; namespace System.Drawing { @@ -10,58 +10,10 @@ namespace System.Drawing /// internal sealed class GraphicsContext : IDisposable { - /// - /// The state that identifies the context. - /// - private int _contextState; - - /// - /// The context's translate transform. - /// - private PointF _transformOffset; - - /// - /// The context's clip region. - /// - private Region? _clipRegion; - - /// - /// The next context up the stack. - /// - private GraphicsContext? _nextContext; - - /// - /// The previous context down the stack. - /// - private GraphicsContext? _prevContext; - - /// - /// Flags that determines whether the context was created for a Graphics.Save() operation. - /// This kind of contexts are cumulative across subsequent Save() calls so the top context - /// info is cumulative. This is not the same for contexts created for a Graphics.BeginContainer() - /// operation, in this case the new context information is reset. See Graphics.BeginContainer() - /// and Graphics.Save() for more information. - /// - private bool _isCumulative; - public GraphicsContext(Graphics g) { - Matrix transform = g.Transform; - if (!transform.IsIdentity) - { - _transformOffset = transform.Offset; - } - transform.Dispose(); - - Region clip = g.Clip; - if (clip.IsInfinite(g)) - { - clip.Dispose(); - } - else - { - _clipRegion = clip; - } + TransformOffset = g.TransformElements.Translation; + Clip = g.GetRegionIfNotInfinite(); } /// @@ -78,100 +30,46 @@ public void Dispose() /// public void Dispose(bool disposing) { - if (_nextContext != null) - { - // Dispose all contexts up the stack since they are relative to this one and its state will be invalid. - _nextContext.Dispose(); - _nextContext = null; - } + // Dispose all contexts up the stack since they are relative to this one and its state will be invalid. + Next?.Dispose(); + Next = null; - if (_clipRegion != null) - { - _clipRegion.Dispose(); - _clipRegion = null; - } + Clip?.Dispose(); + Clip = null; } /// /// The state id representing the GraphicsContext. /// - public int State - { - get - { - return _contextState; - } - set - { - _contextState = value; - } - } + public int State { get; set; } /// /// The translate transform in the GraphicsContext. /// - public PointF TransformOffset - { - get - { - return _transformOffset; - } - } + public Vector2 TransformOffset { get; private set; } /// - /// The clipping region the GraphicsContext. + /// The clipping region the GraphicsContext. /// - public Region? Clip - { - get - { - return _clipRegion; - } - } + public Region? Clip { get; private set; } /// /// The next GraphicsContext object in the stack. /// - public GraphicsContext? Next - { - get - { - return _nextContext; - } - set - { - _nextContext = value; - } - } + public GraphicsContext? Next { get; set; } /// /// The previous GraphicsContext object in the stack. /// - public GraphicsContext? Previous - { - get - { - return _prevContext; - } - set - { - _prevContext = value; - } - } + public GraphicsContext? Previous { get; set; } /// - /// Determines whether this context is cumulative or not. See filed for more info. + /// Flag that determines whether the context was created for a Graphics.Save() operation. + /// This kind of contexts are cumulative across subsequent Save() calls so the top context + /// info is cumulative. This is not the same for contexts created for a Graphics.BeginContainer() + /// operation, in this case the new context information is reset. See Graphics.BeginContainer() + /// and Graphics.Save() for more information. /// - public bool IsCumulative - { - get - { - return _isCumulative; - } - set - { - _isCumulative = value; - } - } + public bool IsCumulative { get; set; } } } diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/NumericsExtensions.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/NumericsExtensions.cs new file mode 100644 index 0000000000000..1f468afdae269 --- /dev/null +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/NumericsExtensions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace System.Drawing +{ + /// + /// Helpers to allow using System.Numerics types like the System.Drawing equivalents. + /// + internal static class NumericsExtensions + { + internal static void Translate(this ref Matrix3x2 matrix, Vector2 offset) + { + // Replicating what Matrix.Translate(float offsetX, float offsetY) does. + matrix.M31 += (offset.X * matrix.M11) + (offset.Y * matrix.M21); + matrix.M32 += (offset.X * matrix.M12) + (offset.Y * matrix.M22); + } + + internal static bool IsEmpty(this Vector2 vector) => vector.X == 0 && vector.Y == 0; + } +} diff --git a/src/libraries/System.Drawing.Common/tests/Graphics_GetContextTests.Core.cs b/src/libraries/System.Drawing.Common/tests/Graphics_GetContextTests.Core.cs new file mode 100644 index 0000000000000..0e09cef317868 --- /dev/null +++ b/src/libraries/System.Drawing.Common/tests/Graphics_GetContextTests.Core.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Drawing.Drawing2D; +using System.Numerics; +using Xunit; + +namespace System.Drawing.Tests +{ + public partial class Graphics_GetContextTests + { + [ConditionalFact(Helpers.IsWindows)] + public void GetContextInfo_New_DefaultGraphics() + { + using (var image = new Bitmap(10, 10)) + using (Graphics graphics = Graphics.FromImage(image)) + { + graphics.GetContextInfo(out PointF offset); + Assert.True(offset.IsEmpty); + + graphics.GetContextInfo(out offset, out Region? clip); + Assert.True(offset.IsEmpty); + Assert.Null(clip); + } + } + + [ConditionalFact(Helpers.IsWindows)] + public void GetContextInfo_New_Clipping() + { + using (var image = new Bitmap(10, 10)) + using (Graphics graphics = Graphics.FromImage(image)) + using (Region initialClip = new Region(new Rectangle(1, 2, 9, 10))) + { + graphics.Clip = initialClip; + + graphics.GetContextInfo(out PointF offset); + Assert.True(offset.IsEmpty); + + graphics.GetContextInfo(out offset, out Region? clip); + Assert.True(offset.IsEmpty); + Assert.NotNull(clip); + Assert.Equal(initialClip.GetBounds(graphics), clip.GetBounds(graphics)); + clip.Dispose(); + } + } + + [ConditionalFact(Helpers.IsWindows)] + public void GetContextInfo_New_Transform() + { + using (var image = new Bitmap(10, 10)) + using (Graphics graphics = Graphics.FromImage(image)) + { + graphics.TransformElements = Matrix3x2.CreateTranslation(1, 2); + + graphics.GetContextInfo(out PointF offset); + Assert.Equal(new PointF(1, 2), offset); + + graphics.GetContextInfo(out offset, out Region? clip); + Assert.Null(clip); + Assert.Equal(new PointF(1, 2), offset); + } + } + + [ConditionalFact(Helpers.IsWindows)] + public void GetContextInfo_New_ClipAndTransform() + { + using (var image = new Bitmap(10, 10)) + using (Graphics graphics = Graphics.FromImage(image)) + using (Region initialClip = new Region(new Rectangle(1, 2, 9, 10))) + { + graphics.Clip = initialClip; + graphics.TransformElements = Matrix3x2.CreateTranslation(1, 2); + + graphics.GetContextInfo(out PointF offset); + Assert.Equal(new PointF(1, 2), offset); + + graphics.GetContextInfo(out offset, out Region? clip); + Assert.NotNull(clip); + Assert.Equal(new RectangleF(0, 0, 9, 10), clip.GetBounds(graphics)); + Assert.Equal(new PointF(1, 2), offset); + clip.Dispose(); + } + } + + [ConditionalFact(Helpers.IsWindows)] + public void GetContextInfo_New_TransformAndClip() + { + using (var image = new Bitmap(10, 10)) + using (Graphics graphics = Graphics.FromImage(image)) + using (Region initialClip = new Region(new Rectangle(1, 2, 9, 10))) + { + graphics.TransformElements = Matrix3x2.CreateTranslation(1, 2); + graphics.Clip = initialClip; + + graphics.GetContextInfo(out PointF offset); + Assert.Equal(new PointF(1, 2), offset); + + graphics.GetContextInfo(out offset, out Region? clip); + Assert.NotNull(clip); + Assert.Equal(new RectangleF(1, 2, 9, 10), clip.GetBounds(graphics)); + Assert.Equal(new PointF(1, 2), offset); + clip.Dispose(); + } + } + + [ConditionalFact(Helpers.IsWindows)] + public void GetContextInfo_New_ClipAndTransformSaveState() + { + using (var image = new Bitmap(10, 10)) + using (Graphics graphics = Graphics.FromImage(image)) + using (Region initialClip = new Region(new Rectangle(1, 2, 9, 10))) + { + graphics.Clip = initialClip; + graphics.TransformElements = Matrix3x2.CreateTranslation(1, 2); + + GraphicsState state = graphics.Save(); + + graphics.GetContextInfo(out PointF offset); + Assert.Equal(new PointF(2, 4), offset); + + graphics.GetContextInfo(out offset, out Region? clip); + Assert.NotNull(clip); + Assert.Equal(new RectangleF(0, 0, 8, 8), clip.GetBounds(graphics)); + Assert.Equal(new PointF(2, 4), offset); + clip.Dispose(); + } + } + + [ConditionalFact(Helpers.IsWindows)] + public void GetContextInfo_New_ClipAndTransformSaveAndRestoreState() + { + using (var image = new Bitmap(10, 10)) + using (Graphics graphics = Graphics.FromImage(image)) + { + graphics.SetClip(new Rectangle(1, 2, 9, 10)); + graphics.TransformElements = Matrix3x2.CreateTranslation(1, 2); + + GraphicsState state = graphics.Save(); + graphics.GetContextInfo(out PointF offset, out Region? clip); + graphics.Restore(state); + + Assert.NotNull(clip); + Assert.Equal(new RectangleF(0, 0, 8, 8), clip.GetBounds(graphics)); + Assert.Equal(new PointF(2, 4), offset); + clip.Dispose(); + } + } + } +} diff --git a/src/libraries/System.Drawing.Common/tests/Graphics_GetContextTests.cs b/src/libraries/System.Drawing.Common/tests/Graphics_GetContextTests.cs index 3eb940d75da69..9dd8d4e389993 100644 --- a/src/libraries/System.Drawing.Common/tests/Graphics_GetContextTests.cs +++ b/src/libraries/System.Drawing.Common/tests/Graphics_GetContextTests.cs @@ -6,7 +6,8 @@ namespace System.Drawing.Tests { - public class Graphics_GetContextTests : DrawingTest +#pragma warning disable CS0618 // Type or member is obsolete + public partial class Graphics_GetContextTests : DrawingTest { [ConditionalFact(Helpers.IsWindows)] public void GetContextInfo_DefaultGraphics() @@ -163,4 +164,5 @@ public void GetContextInfo_ClipAndTransformSaveAndRestoreState() } } } +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj b/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj index baa169e969831..e8f4d68133f0d 100644 --- a/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj +++ b/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj @@ -1,4 +1,4 @@ - + true true @@ -117,6 +117,7 @@ +