// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
#if NET9_0_OR_GREATER
using System.Drawing.Imaging.Effects;
#endif
using System.Numerics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace System.Drawing;

/// <summary>
///  Encapsulates a GDI+ drawing surface.
/// </summary>
public sealed unsafe partial class Graphics : MarshalByRefObject, IDisposable, IDeviceContext, IGraphics
{
    /// <summary>
    ///  The context state previous to the current Graphics context (the head of the stack).
    ///  We don't keep a GraphicsContext for the current context since it is available at any time from GDI+ and
    ///  we don't want to keep track of changes in it.
    /// </summary>
    private GraphicsContext? _previousContext;

    private static readonly Lock s_syncObject = new();

    // Object reference used for printing; it could point to a PrintPreviewGraphics to obtain the VisibleClipBounds, or
    // a DeviceContext holding a printer DC.
    private object? _printingHelper;

    // GDI+'s preferred HPALETTE.
    private static HPALETTE s_halftonePalette;

    // pointer back to the Image backing a specific graphic object
    private Image? _backingImage;

    /// <summary>
    ///  Handle to native DC - obtained from the GDI+ graphics object. We need to cache it to implement
    ///  IDeviceContext interface.
    /// </summary>
    private HDC _nativeHdc;

    public delegate bool DrawImageAbort(IntPtr callbackdata);

    /// <summary>
    /// Callback for EnumerateMetafile methods.
    /// This method can then call Metafile.PlayRecord to play the record that was just enumerated.
    /// </summary>
    /// <param name="recordType">if >= MinRecordType, it's an EMF+ record</param>
    /// <param name="flags">always 0 for EMF records</param>
    /// <param name="dataSize">size of the data, or 0 if no data</param>
    /// <param name="data">pointer to the data, or NULL if no data (UINT32 aligned)</param>
    /// <param name="callbackData">pointer to callbackData, if any</param>
    /// <returns>False to abort enumerating, true to continue.</returns>
    public delegate bool EnumerateMetafileProc(
        EmfPlusRecordType recordType,
        int flags,
        int dataSize,
        IntPtr data,
        PlayRecordCallback? callbackData);

    /// <summary>
    ///  Constructor to initialize this object from a native GDI+ Graphics pointer.
    /// </summary>
    private Graphics(GpGraphics* gdipNativeGraphics)
    {
        if (gdipNativeGraphics is null)
            throw new ArgumentNullException(nameof(gdipNativeGraphics));

        NativeGraphics = gdipNativeGraphics;
    }

    /// <summary>
    ///  Creates a new instance of the <see cref='Graphics'/> class from the specified handle to a device context.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public static Graphics FromHdc(IntPtr hdc)
    {
        return hdc == 0 ? throw new ArgumentNullException(nameof(hdc)) : FromHdcInternal(hdc);
    }

    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public static Graphics FromHdcInternal(IntPtr hdc)
    {
        GpGraphics* nativeGraphics;
        Gdip.CheckStatus(PInvokeGdiPlus.GdipCreateFromHDC((HDC)hdc, &nativeGraphics));
        return new Graphics(nativeGraphics);
    }

    /// <summary>
    ///  Creates a new instance of the Graphics class from the specified handle to a device context and handle to a device.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public static Graphics FromHdc(IntPtr hdc, IntPtr hdevice)
    {
        GpGraphics* nativeGraphics;
        Gdip.CheckStatus(PInvokeGdiPlus.GdipCreateFromHDC2((HDC)hdc, (HANDLE)hdevice, &nativeGraphics));
        return new Graphics(nativeGraphics);
    }

    /// <summary>
    ///  Creates a new instance of the <see cref='Graphics'/> class from a window handle.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public static Graphics FromHwnd(IntPtr hwnd) => FromHwndInternal(hwnd);

    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public static Graphics FromHwndInternal(IntPtr hwnd)
    {
        GpGraphics* nativeGraphics;

        // This is one of the few places we need to manually ensure GDI+ is initialized. Other calls to PInvoke will do
        // this automatically, PInvokeCore cannot and as such needs to be manually initialized if we've never called
        // another PInvoke method.
        GdiPlusInitialization.EnsureInitialized();
        Gdip.CheckStatus(PInvokeGdiPlus.GdipCreateFromHWND((HWND)hwnd, &nativeGraphics));
        return new Graphics(nativeGraphics);
    }

    /// <summary>
    ///  Creates an instance of the <see cref='Graphics'/> class from an existing <see cref='Image'/>.
    /// </summary>
    public static Graphics FromImage(Image image)
    {
        ArgumentNullException.ThrowIfNull(image);

        if ((image.PixelFormat & PixelFormat.Indexed) != 0)
            throw new ArgumentException(SR.GdiplusCannotCreateGraphicsFromIndexedPixelFormat, nameof(image));

        GpGraphics* nativeGraphics;
        Gdip.CheckStatus(PInvokeGdiPlus.GdipGetImageGraphicsContext(image.Pointer(), &nativeGraphics));
        GC.KeepAlive(image);

        return new Graphics(nativeGraphics) { _backingImage = image };
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    public void ReleaseHdcInternal(IntPtr hdc)
    {
        CheckStatus(!Gdip.Initialized ? Status.Ok : PInvokeGdiPlus.GdipReleaseDC(NativeGraphics, (HDC)hdc));
        _nativeHdc = HDC.Null;
    }

    /// <summary>
    ///  Deletes this <see cref='Graphics'/>, and frees the memory allocated for it.
    /// </summary>
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            while (_previousContext is not null)
            {
                // Dispose entire stack.
                GraphicsContext? context = _previousContext.Previous;
                _previousContext.Dispose();
                _previousContext = context;
            }

            if (PrintingHelper is HdcHandle printerDC)
            {
                printerDC.Dispose();
                _printingHelper = null;
            }
        }

        if (!_nativeHdc.IsNull)
        {
            ReleaseHdc();
        }

        if (NativeGraphics is not null)
        {
            Status status = !Gdip.Initialized ? Status.Ok : PInvokeGdiPlus.GdipDeleteGraphics(NativeGraphics);
            NativeGraphics = null;
            Debug.Assert(status == Status.Ok, $"GDI+ returned an error status: {status}");
        }
    }

    ~Graphics() => Dispose(disposing: false);

    /// <summary>
    ///  Handle to native GDI+ graphics object. This object is created on demand.
    /// </summary>
    internal GpGraphics* NativeGraphics { get; private set; }

    nint IPointer<GpGraphics>.Pointer => (nint)NativeGraphics;

    public Region Clip
    {
        get
        {
            Region region = new();
            CheckStatus(PInvokeGdiPlus.GdipGetClip(NativeGraphics, region.NativeRegion));
            return region;
        }
        set => SetClip(value, Drawing2D.CombineMode.Replace);
    }

    public RectangleF ClipBounds
    {
        get
        {
            RectF rect;
            CheckStatus(PInvokeGdiPlus.GdipGetClipBounds(NativeGraphics, &rect));
            return rect;
        }
    }

    /// <summary>
    /// Gets or sets the <see cref='Drawing2D.CompositingMode'/> associated with this <see cref='Graphics'/>.
    /// </summary>
    public Drawing2D.CompositingMode CompositingMode
    {
        get
        {
            GdiPlus.CompositingMode mode;
            CheckStatus(PInvokeGdiPlus.GdipGetCompositingMode(NativeGraphics, &mode));
            return (Drawing2D.CompositingMode)mode;
        }
        set
        {
            if (value is < Drawing2D.CompositingMode.SourceOver or > Drawing2D.CompositingMode.SourceCopy)
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(Drawing2D.CompositingMode));

            CheckStatus(PInvokeGdiPlus.GdipSetCompositingMode(NativeGraphics, (GdiPlus.CompositingMode)value));
        }
    }

    public Drawing2D.CompositingQuality CompositingQuality
    {
        get
        {
            GdiPlus.CompositingQuality quality;
            CheckStatus(PInvokeGdiPlus.GdipGetCompositingQuality(NativeGraphics, &quality));
            return (Drawing2D.CompositingQuality)quality;
        }
        set
        {
            if (value is < Drawing2D.CompositingQuality.Invalid or > Drawing2D.CompositingQuality.AssumeLinear)
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(Drawing2D.CompositingQuality));

            CheckStatus(PInvokeGdiPlus.GdipSetCompositingQuality(NativeGraphics, (GdiPlus.CompositingQuality)value));
        }
    }

    public float DpiX
    {
        get
        {
            float dpi;
            CheckStatus(PInvokeGdiPlus.GdipGetDpiX(NativeGraphics, &dpi));
            return dpi;
        }
    }

    public float DpiY
    {
        get
        {
            float dpi;
            CheckStatus(PInvokeGdiPlus.GdipGetDpiY(NativeGraphics, &dpi));
            return dpi;
        }
    }

    /// <summary>
    /// Gets or sets the interpolation mode associated with this Graphics.
    /// </summary>
    public Drawing2D.InterpolationMode InterpolationMode
    {
        get
        {
            GdiPlus.InterpolationMode mode;
            CheckStatus(PInvokeGdiPlus.GdipGetInterpolationMode(NativeGraphics, &mode));
            return (Drawing2D.InterpolationMode)mode;
        }
        set
        {
            if (value is < Drawing2D.InterpolationMode.Invalid or > Drawing2D.InterpolationMode.HighQualityBicubic)
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(Drawing2D.InterpolationMode));

            CheckStatus(PInvokeGdiPlus.GdipSetInterpolationMode(NativeGraphics, (GdiPlus.InterpolationMode)value));
        }
    }

    public bool IsClipEmpty
    {
        get
        {
            BOOL isEmpty;
            CheckStatus(PInvokeGdiPlus.GdipIsClipEmpty(NativeGraphics, &isEmpty));
            return isEmpty;
        }
    }

    public bool IsVisibleClipEmpty
    {
        get
        {
            BOOL isEmpty;
            CheckStatus(PInvokeGdiPlus.GdipIsVisibleClipEmpty(NativeGraphics, &isEmpty));
            return isEmpty;
        }
    }

    public float PageScale
    {
        get
        {
            float scale;
            CheckStatus(PInvokeGdiPlus.GdipGetPageScale(NativeGraphics, &scale));
            return scale;
        }
        set => CheckStatus(PInvokeGdiPlus.GdipSetPageScale(NativeGraphics, value));
    }

    public GraphicsUnit PageUnit
    {
        get
        {
            Unit unit;
            CheckStatus(PInvokeGdiPlus.GdipGetPageUnit(NativeGraphics, &unit));
            return (GraphicsUnit)unit;
        }
        set
        {
            if (value is < GraphicsUnit.World or > GraphicsUnit.Millimeter)
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(GraphicsUnit));

            CheckStatus(PInvokeGdiPlus.GdipSetPageUnit(NativeGraphics, (Unit)value));
        }
    }

    public PixelOffsetMode PixelOffsetMode
    {
        get
        {
            GdiPlus.PixelOffsetMode mode;
            CheckStatus(PInvokeGdiPlus.GdipGetPixelOffsetMode(NativeGraphics, &mode));
            return (PixelOffsetMode)mode;
        }
        set
        {
            if (value is < PixelOffsetMode.Invalid or > PixelOffsetMode.Half)
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(PixelOffsetMode));

            CheckStatus(PInvokeGdiPlus.GdipSetPixelOffsetMode(NativeGraphics, (GdiPlus.PixelOffsetMode)value));
        }
    }

    public Point RenderingOrigin
    {
        get
        {
            int x, y;
            CheckStatus(PInvokeGdiPlus.GdipGetRenderingOrigin(NativeGraphics, &x, &y));
            return new Point(x, y);
        }
        set => CheckStatus(PInvokeGdiPlus.GdipSetRenderingOrigin(NativeGraphics, value.X, value.Y));
    }

    public Drawing2D.SmoothingMode SmoothingMode
    {
        get
        {
            GdiPlus.SmoothingMode mode;
            CheckStatus(PInvokeGdiPlus.GdipGetSmoothingMode(NativeGraphics, &mode));
            return (Drawing2D.SmoothingMode)mode;
        }
        set
        {
            if (value is < Drawing2D.SmoothingMode.Invalid or > Drawing2D.SmoothingMode.AntiAlias)
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(Drawing2D.SmoothingMode));

            CheckStatus(PInvokeGdiPlus.GdipSetSmoothingMode(NativeGraphics, (GdiPlus.SmoothingMode)value));
        }
    }

    public int TextContrast
    {
        get
        {
            uint textContrast;
            CheckStatus(PInvokeGdiPlus.GdipGetTextContrast(NativeGraphics, &textContrast));
            return (int)textContrast;
        }
        set => CheckStatus(PInvokeGdiPlus.GdipSetTextContrast(NativeGraphics, (uint)value));
    }

    /// <summary>
    ///  Gets or sets the rendering mode for text associated with this <see cref='Graphics'/>.
    /// </summary>
    public TextRenderingHint TextRenderingHint
    {
        get
        {
            GdiPlus.TextRenderingHint hint;
            CheckStatus(PInvokeGdiPlus.GdipGetTextRenderingHint(NativeGraphics, &hint));
            return (TextRenderingHint)hint;
        }
        set
        {
            if (value is < TextRenderingHint.SystemDefault or > TextRenderingHint.ClearTypeGridFit)
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(TextRenderingHint));

            CheckStatus(PInvokeGdiPlus.GdipSetTextRenderingHint(NativeGraphics, (GdiPlus.TextRenderingHint)value));
        }
    }

    /// <summary>
    ///  Gets or sets the world transform for this <see cref='Graphics'/>.
    /// </summary>
    public Matrix Transform
    {
        get
        {
            Matrix matrix = new();
            CheckStatus(PInvokeGdiPlus.GdipGetWorldTransform(NativeGraphics, matrix.NativeMatrix));
            return matrix;
        }
        set
        {
            CheckStatus(PInvokeGdiPlus.GdipSetWorldTransform(NativeGraphics, value.NativeMatrix));
            GC.KeepAlive(value);
        }
    }

    /// <summary>
    ///  Gets or sets the world transform elements for this <see cref="Graphics"/>.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This is a more performant alternative to <see cref="Transform"/> that does not need disposal.
    ///  </para>
    /// </remarks>
    public Matrix3x2 TransformElements
    {
        get
        {
            GdiPlus.Matrix* nativeMatrix;
            CheckStatus(PInvokeGdiPlus.GdipCreateMatrix(&nativeMatrix));

            try
            {
                CheckStatus(PInvokeGdiPlus.GdipGetWorldTransform(NativeGraphics, nativeMatrix));

                Matrix3x2 matrix = default;
                CheckStatus(PInvokeGdiPlus.GdipGetMatrixElements(nativeMatrix, (float*)&matrix));
                return matrix;
            }
            finally
            {
                if (nativeMatrix is not null)
                {
                    PInvokeGdiPlus.GdipDeleteMatrix(nativeMatrix);
                }
            }
        }
        set
        {
            GdiPlus.Matrix* nativeMatrix = Matrix.CreateNativeHandle(value);

            try
            {
                CheckStatus(PInvokeGdiPlus.GdipSetWorldTransform(NativeGraphics, nativeMatrix));
            }
            finally
            {
                if (nativeMatrix is not null)
                {
                    PInvokeGdiPlus.GdipDeleteMatrix(nativeMatrix);
                }
            }
        }
    }

    HDC IHdcContext.GetHdc() => (HDC)GetHdc();

    public IntPtr GetHdc()
    {
        HDC hdc;
        CheckStatus(PInvokeGdiPlus.GdipGetDC(NativeGraphics, &hdc));

        // Need to cache the hdc to be able to release with a call to IDeviceContext.ReleaseHdc().
        _nativeHdc = hdc;
        return _nativeHdc;
    }

    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public void ReleaseHdc(IntPtr hdc) => ReleaseHdcInternal(hdc);

    public void ReleaseHdc() => ReleaseHdcInternal(_nativeHdc);

    /// <summary>
    ///  Forces immediate execution of all operations currently on the stack.
    /// </summary>
    public void Flush() => Flush(Drawing2D.FlushIntention.Flush);

    /// <summary>
    ///  Forces execution of all operations currently on the stack.
    /// </summary>
    public void Flush(Drawing2D.FlushIntention intention) =>
        CheckStatus(PInvokeGdiPlus.GdipFlush(NativeGraphics, (GdiPlus.FlushIntention)intention));

    public void SetClip(Graphics g) => SetClip(g, Drawing2D.CombineMode.Replace);

    public void SetClip(Graphics g, Drawing2D.CombineMode combineMode)
    {
        ArgumentNullException.ThrowIfNull(g);

        CheckStatus(PInvokeGdiPlus.GdipSetClipGraphics(NativeGraphics, g.NativeGraphics, (GdiPlus.CombineMode)combineMode));
        GC.KeepAlive(g);
    }

    public void SetClip(Rectangle rect) => SetClip(rect, Drawing2D.CombineMode.Replace);

    public void SetClip(Rectangle rect, Drawing2D.CombineMode combineMode) => SetClip((RectangleF)rect, combineMode);

    public void SetClip(RectangleF rect) => SetClip(rect, Drawing2D.CombineMode.Replace);

    public void SetClip(RectangleF rect, Drawing2D.CombineMode combineMode) =>
        CheckStatus(PInvokeGdiPlus.GdipSetClipRect(NativeGraphics, rect.X, rect.Y, rect.Width, rect.Height, (GdiPlus.CombineMode)combineMode));

    public void SetClip(GraphicsPath path) => SetClip(path, Drawing2D.CombineMode.Replace);

    public void SetClip(GraphicsPath path, Drawing2D.CombineMode combineMode)
    {
        ArgumentNullException.ThrowIfNull(path);
        CheckStatus(PInvokeGdiPlus.GdipSetClipPath(NativeGraphics, path._nativePath, (GdiPlus.CombineMode)combineMode));
        GC.KeepAlive(path);
    }

    public void SetClip(Region region, Drawing2D.CombineMode combineMode)
    {
        ArgumentNullException.ThrowIfNull(region);
        CheckStatus(PInvokeGdiPlus.GdipSetClipRegion(NativeGraphics, region.NativeRegion, (GdiPlus.CombineMode)combineMode));
        GC.KeepAlive(region);
    }

    public void IntersectClip(Rectangle rect) => IntersectClip((RectangleF)rect);

    public void IntersectClip(RectangleF rect) =>
        CheckStatus(PInvokeGdiPlus.GdipSetClipRect(
            NativeGraphics,
            rect.X, rect.Y, rect.Width, rect.Height,
            GdiPlus.CombineMode.CombineModeIntersect));

    public void IntersectClip(Region region)
    {
        ArgumentNullException.ThrowIfNull(region);
        CheckStatus(PInvokeGdiPlus.GdipSetClipRegion(NativeGraphics, region.NativeRegion, GdiPlus.CombineMode.CombineModeIntersect));
        GC.KeepAlive(region);
    }

    public void ExcludeClip(Rectangle rect) =>
        CheckStatus(PInvokeGdiPlus.GdipSetClipRect(
            NativeGraphics,
            rect.X, rect.Y, rect.Width, rect.Height,
            GdiPlus.CombineMode.CombineModeExclude));

    public void ExcludeClip(Region region)
    {
        ArgumentNullException.ThrowIfNull(region);
        CheckStatus(PInvokeGdiPlus.GdipSetClipRegion(NativeGraphics, region.NativeRegion, GdiPlus.CombineMode.CombineModeExclude));
        GC.KeepAlive(region);
    }

    public void ResetClip() => CheckStatus(PInvokeGdiPlus.GdipResetClip(NativeGraphics));

    public void TranslateClip(float dx, float dy) => CheckStatus(PInvokeGdiPlus.GdipTranslateClip(NativeGraphics, dx, dy));

    public void TranslateClip(int dx, int dy) => CheckStatus(PInvokeGdiPlus.GdipTranslateClip(NativeGraphics, dx, dy));

    public bool IsVisible(int x, int y) => IsVisible((float)x, y);

    public bool IsVisible(Point point) => IsVisible(point.X, point.Y);

    public bool IsVisible(float x, float y)
    {
        BOOL isVisible;
        CheckStatus(PInvokeGdiPlus.GdipIsVisiblePoint(NativeGraphics, x, y, &isVisible));
        return isVisible;
    }

    public bool IsVisible(PointF point) => IsVisible(point.X, point.Y);

    public bool IsVisible(int x, int y, int width, int height) => IsVisible((float)x, y, width, height);

    public bool IsVisible(Rectangle rect) => IsVisible((float)rect.X, rect.Y, rect.Width, rect.Height);

    public bool IsVisible(float x, float y, float width, float height)
    {
        BOOL isVisible;
        CheckStatus(PInvokeGdiPlus.GdipIsVisibleRect(NativeGraphics, x, y, width, height, &isVisible));
        return isVisible;
    }

    public bool IsVisible(RectangleF rect) => IsVisible(rect.X, rect.Y, rect.Width, rect.Height);

    /// <summary>
    ///  Resets the world transform to identity.
    /// </summary>
    public void ResetTransform() => CheckStatus(PInvokeGdiPlus.GdipResetWorldTransform(NativeGraphics));

    /// <summary>
    ///  Multiplies the <see cref='Matrix'/> that represents the world transform and <paramref name="matrix"/>.
    /// </summary>
    public void MultiplyTransform(Matrix matrix) => MultiplyTransform(matrix, MatrixOrder.Prepend);

    /// <summary>
    ///  Multiplies the <see cref='Matrix'/> that represents the world transform and <paramref name="matrix"/>.
    /// </summary>
    public void MultiplyTransform(Matrix matrix, MatrixOrder order)
    {
        ArgumentNullException.ThrowIfNull(matrix);
        CheckStatus(PInvokeGdiPlus.GdipMultiplyWorldTransform(NativeGraphics, matrix.NativeMatrix, (GdiPlus.MatrixOrder)order));
        GC.KeepAlive(matrix);
    }

    public void TranslateTransform(float dx, float dy) => TranslateTransform(dx, dy, MatrixOrder.Prepend);

    public void TranslateTransform(float dx, float dy, MatrixOrder order) =>
        CheckStatus(PInvokeGdiPlus.GdipTranslateWorldTransform(NativeGraphics, dx, dy, (GdiPlus.MatrixOrder)order));

    public void ScaleTransform(float sx, float sy) => ScaleTransform(sx, sy, MatrixOrder.Prepend);

    public void ScaleTransform(float sx, float sy, MatrixOrder order) =>
        CheckStatus(PInvokeGdiPlus.GdipScaleWorldTransform(NativeGraphics, sx, sy, (GdiPlus.MatrixOrder)order));

    public void RotateTransform(float angle) => RotateTransform(angle, MatrixOrder.Prepend);

    public void RotateTransform(float angle, MatrixOrder order) =>
        CheckStatus(PInvokeGdiPlus.GdipRotateWorldTransform(NativeGraphics, angle, (GdiPlus.MatrixOrder)order));

    /// <summary>
    ///  Draws an arc from the specified ellipse.
    /// </summary>
    public void DrawArc(Pen pen, float x, float y, float width, float height, float startAngle, float sweepAngle)
    {
        ArgumentNullException.ThrowIfNull(pen);

        CheckErrorStatus(PInvokeGdiPlus.GdipDrawArc(
            NativeGraphics,
            pen.NativePen,
            x, y, width, height,
            startAngle,
            sweepAngle));

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws an arc from the specified ellipse.
    /// </summary>
    public void DrawArc(Pen pen, RectangleF rect, float startAngle, float sweepAngle) =>
        DrawArc(pen, rect.X, rect.Y, rect.Width, rect.Height, startAngle, sweepAngle);

    /// <summary>
    /// Draws an arc from the specified ellipse.
    /// </summary>
    public void DrawArc(Pen pen, int x, int y, int width, int height, int startAngle, int sweepAngle)
        => DrawArc(pen, (float)x, y, width, height, startAngle, sweepAngle);

    /// <summary>
    ///  Draws an arc from the specified ellipse.
    /// </summary>
    public void DrawArc(Pen pen, Rectangle rect, float startAngle, float sweepAngle) =>
        DrawArc(pen, rect.X, rect.Y, rect.Width, rect.Height, startAngle, sweepAngle);

    /// <summary>
    ///  Draws a cubic Bezier curve defined by four ordered pairs that represent points.
    /// </summary>
    public void DrawBezier(Pen pen, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4)
    {
        ArgumentNullException.ThrowIfNull(pen);

        CheckErrorStatus(PInvokeGdiPlus.GdipDrawBezier(
            NativeGraphics,
            pen.NativePen,
            x1, y1, x2, y2, x3, y3, x4, y4));

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws a cubic Bezier curve defined by four points.
    /// </summary>
    public void DrawBezier(Pen pen, PointF pt1, PointF pt2, PointF pt3, PointF pt4) =>
        DrawBezier(pen, pt1.X, pt1.Y, pt2.X, pt2.Y, pt3.X, pt3.Y, pt4.X, pt4.Y);

    /// <summary>
    ///  Draws a cubic Bezier curve defined by four points.
    /// </summary>
    public void DrawBezier(Pen pen, Point pt1, Point pt2, Point pt3, Point pt4) =>
        DrawBezier(pen, pt1.X, pt1.Y, pt2.X, pt2.Y, pt3.X, pt3.Y, pt4.X, pt4.Y);

    /// <summary>
    ///  Draws the outline of a rectangle specified by <paramref name="rect"/>.
    /// </summary>
    /// <param name="pen">A Pen that determines the color, width, and style of the rectangle.</param>
    /// <param name="rect">A Rectangle structure that represents the rectangle to draw.</param>
    public void DrawRectangle(Pen pen, RectangleF rect) => DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);

    /// <summary>
    ///  Draws the outline of a rectangle specified by <paramref name="rect"/>.
    /// </summary>
    public void DrawRectangle(Pen pen, Rectangle rect) => DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);

#if NET9_0_OR_GREATER
    /// <inheritdoc cref="DrawRoundedRectangle(Pen, RectangleF, SizeF)"/>
    public void DrawRoundedRectangle(Pen pen, Rectangle rect, Size radius) =>
        DrawRoundedRectangle(pen, (RectangleF)rect, radius);

    /// <summary>
    ///  Draws the outline of the specified rounded rectangle.
    /// </summary>
    /// <param name="pen">The <see cref="Pen"/> to draw the outline with.</param>
    /// <param name="rect">The bounds of the rounded rectangle.</param>
    /// <param name="radius">The radius width and height used to round the corners of the rectangle.</param>
    public void DrawRoundedRectangle(Pen pen, RectangleF rect, SizeF radius)
    {
        using GraphicsPath path = new();
        path.AddRoundedRectangle(rect, radius);
        DrawPath(pen, path);
    }
#endif

    /// <summary>
    ///  Draws the outline of the specified rectangle.
    /// </summary>
    public void DrawRectangle(Pen pen, float x, float y, float width, float height)
    {
        ArgumentNullException.ThrowIfNull(pen);
        CheckErrorStatus(PInvokeGdiPlus.GdipDrawRectangle(NativeGraphics, pen.NativePen, x, y, width, height));
        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws the outline of the specified rectangle.
    /// </summary>
    public void DrawRectangle(Pen pen, int x, int y, int width, int height)
        => DrawRectangle(pen, (float)x, y, width, height);

    /// <inheritdoc cref="DrawRectangles(Pen, Rectangle[])"/>
    public void DrawRectangles(Pen pen, params RectangleF[] rects) => DrawRectangles(pen, rects.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawRectangles(Pen, Rectangle[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawRectangles(Pen pen, params ReadOnlySpan<RectangleF> rects)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (RectangleF* r = rects)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawRectangles(NativeGraphics, pen.NativePen, (RectF*)r, rects.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws the outlines of a series of rectangles.
    /// </summary>
    /// <param name="pen"><see cref="Pen"/> that determines the color, width, and style of the outlines of the rectangles.</param>
    /// <param name="rects">An array of <see cref="Rectangle"/> structures that represents the rectangles to draw.</param>
    public void DrawRectangles(Pen pen, params Rectangle[] rects) => DrawRectangles(pen, rects.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawRectangles(Pen, Rectangle[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawRectangles(Pen pen, params ReadOnlySpan<Rectangle> rects)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Rectangle* r = rects)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawRectanglesI(NativeGraphics, pen.NativePen, (Rect*)r, rects.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws the outline of an ellipse defined by a bounding rectangle.
    /// </summary>
    public void DrawEllipse(Pen pen, RectangleF rect) => DrawEllipse(pen, rect.X, rect.Y, rect.Width, rect.Height);

    /// <summary>
    ///  Draws the outline of an ellipse defined by a bounding rectangle.
    /// </summary>
    public void DrawEllipse(Pen pen, float x, float y, float width, float height)
    {
        ArgumentNullException.ThrowIfNull(pen);
        CheckErrorStatus(PInvokeGdiPlus.GdipDrawEllipse(NativeGraphics, pen.NativePen, x, y, width, height));
        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws the outline of an ellipse specified by a bounding rectangle.
    /// </summary>
    public void DrawEllipse(Pen pen, Rectangle rect) => DrawEllipse(pen, (float)rect.X, rect.Y, rect.Width, rect.Height);

    /// <summary>
    ///  Draws the outline of an ellipse defined by a bounding rectangle.
    /// </summary>
    public void DrawEllipse(Pen pen, int x, int y, int width, int height) => DrawEllipse(pen, (float)x, y, width, height);

    /// <summary>
    ///  Draws the outline of a pie section defined by an ellipse and two radial lines.
    /// </summary>
    public void DrawPie(Pen pen, RectangleF rect, float startAngle, float sweepAngle) =>
        DrawPie(pen, rect.X, rect.Y, rect.Width, rect.Height, startAngle, sweepAngle);

    /// <summary>
    ///  Draws the outline of a pie section defined by an ellipse and two radial lines.
    /// </summary>
    public void DrawPie(Pen pen, float x, float y, float width, float height, float startAngle, float sweepAngle)
    {
        ArgumentNullException.ThrowIfNull(pen);
        CheckErrorStatus(PInvokeGdiPlus.GdipDrawPie(NativeGraphics, pen.NativePen, x, y, width, height, startAngle, sweepAngle));
        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws the outline of a pie section defined by an ellipse and two radial lines.
    /// </summary>
    public void DrawPie(Pen pen, Rectangle rect, float startAngle, float sweepAngle) =>
        DrawPie(pen, rect.X, rect.Y, rect.Width, rect.Height, startAngle, sweepAngle);

    /// <summary>
    ///  Draws the outline of a pie section defined by an ellipse and two radial lines.
    /// </summary>
    public void DrawPie(Pen pen, int x, int y, int width, int height, int startAngle, int sweepAngle) =>
        DrawPie(pen, (float)x, y, width, height, startAngle, sweepAngle);

    /// <inheritdoc cref="DrawPolygon(Pen, Point[])"/>
    public void DrawPolygon(Pen pen, params PointF[] points) => DrawPolygon(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawPolygon(Pen, Point[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawPolygon(Pen pen, params ReadOnlySpan<PointF> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawPolygon(NativeGraphics, pen.NativePen, (GdiPlus.PointF*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws the outline of a polygon defined by an array of points.
    /// </summary>
    /// <param name="pen">The <see cref="Pen"/> to draw the outline with.</param>
    /// <param name="points">An array of <see cref="Point"/> structures that represent the vertices of the polygon.</param>
    public void DrawPolygon(Pen pen, params Point[] points) => DrawPolygon(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawPolygon(Pen, Point[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawPolygon(Pen pen, params ReadOnlySpan<Point> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawPolygonI(NativeGraphics, pen.NativePen, (GdiPlus.Point*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws the lines and curves defined by a <see cref='GraphicsPath'/>.
    /// </summary>
    public void DrawPath(Pen pen, GraphicsPath path)
    {
        ArgumentNullException.ThrowIfNull(pen);
        ArgumentNullException.ThrowIfNull(path);

        CheckErrorStatus(PInvokeGdiPlus.GdipDrawPath(NativeGraphics, pen.NativePen, path._nativePath));

        GC.KeepAlive(pen);
        GC.KeepAlive(path);
    }

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
    public void DrawCurve(Pen pen, params PointF[] points) => DrawCurve(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawCurve(Pen pen, params ReadOnlySpan<PointF> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawCurve(NativeGraphics, pen.NativePen, (GdiPlus.PointF*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
    public void DrawCurve(Pen pen, PointF[] points, float tension) =>
        DrawCurve(pen, points.OrThrowIfNull().AsSpan(), tension);

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawCurve(Pen pen, ReadOnlySpan<PointF> points, float tension)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawCurve2(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.PointF*)p, points.Length,
                tension));
        }

        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
    public void DrawCurve(Pen pen, PointF[] points, int offset, int numberOfSegments) =>
        DrawCurve(pen, points, offset, numberOfSegments, 0.5f);

#if NET9_0_OR_GREATER
    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
    public void DrawCurve(Pen pen, ReadOnlySpan<PointF> points, int offset, int numberOfSegments) =>
        DrawCurve(pen, points, offset, numberOfSegments, 0.5f);
#endif

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
    public void DrawCurve(Pen pen, PointF[] points, int offset, int numberOfSegments, float tension) =>
        DrawCurve(pen, points.OrThrowIfNull().AsSpan(), offset, numberOfSegments, tension);

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawCurve(Pen pen, ReadOnlySpan<PointF> points, int offset, int numberOfSegments, float tension)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawCurve3(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.PointF*)p, points.Length,
                offset,
                numberOfSegments,
                tension));
        }

        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
    public void DrawCurve(Pen pen, params Point[] points) => DrawCurve(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawCurve(Pen pen, params ReadOnlySpan<Point> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawCurveI(NativeGraphics, pen.NativePen, (GdiPlus.Point*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
    public void DrawCurve(Pen pen, Point[] points, float tension) =>
        DrawCurve(pen, points.OrThrowIfNull().AsSpan(), tension);

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawCurve(Pen pen, ReadOnlySpan<Point> points, float tension)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawCurve2I(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.Point*)p, points.Length,
                tension));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws a curve defined by an array of points.
    /// </summary>
    /// <param name="pen">The <see cref="Pen"/> to draw the curve with.</param>
    /// <param name="points">An array of points that define the curve.</param>
    /// <param name="offset">The index of the first point in the array to draw.</param>
    /// <param name="numberOfSegments">The number of segments to draw.</param>
    /// <param name="tension">A value greater than, or equal to zero that specifies the tension of the curve.</param>
    public void DrawCurve(Pen pen, Point[] points, int offset, int numberOfSegments, float tension) =>
        DrawCurve(pen, points.OrThrowIfNull().AsSpan(), offset, numberOfSegments, tension);

    /// <inheritdoc cref="DrawCurve(Pen, Point[], int, int, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawCurve(Pen pen, ReadOnlySpan<Point> points, int offset, int numberOfSegments, float tension)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawCurve3I(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.Point*)p, points.Length,
                offset,
                numberOfSegments,
                tension));
        }

        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawClosedCurve(Pen, PointF[], float, FillMode)"/>
    public void DrawClosedCurve(Pen pen, params PointF[] points) =>
        DrawClosedCurve(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawClosedCurve(Pen, PointF[], float, FillMode)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawClosedCurve(Pen pen, params ReadOnlySpan<PointF> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawClosedCurve(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.PointF*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws a closed curve defined by an array of points.
    /// </summary>
    /// <param name="pen">The <see cref="Pen"/> to draw the closed curve with.</param>
    /// <param name="points">An array of points that define the closed curve.</param>
    /// <param name="tension">A value greater than, or equal to zero that specifies the tension of the curve.</param>
    /// <param name="fillmode">A <see cref="FillMode"/> enumeration that specifies the fill mode of the curve.</param>
    public void DrawClosedCurve(Pen pen, PointF[] points, float tension, FillMode fillmode) =>
        DrawClosedCurve(pen, points.OrThrowIfNull().AsSpan(), tension, fillmode);

    /// <inheritdoc cref="DrawClosedCurve(Pen, PointF[], float, FillMode)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawClosedCurve(Pen pen, ReadOnlySpan<PointF> points, float tension, FillMode fillmode)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawClosedCurve2(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.PointF*)p, points.Length,
                tension));
        }

        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawClosedCurve(Pen, PointF[], float, FillMode)"/>
    public void DrawClosedCurve(Pen pen, params Point[] points) => DrawClosedCurve(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawClosedCurve(Pen, PointF[], float, FillMode)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawClosedCurve(Pen pen, params ReadOnlySpan<Point> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawClosedCurveI(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.Point*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawClosedCurve(Pen, PointF[], float, FillMode)"/>

    public void DrawClosedCurve(Pen pen, Point[] points, float tension, FillMode fillmode) =>
        DrawClosedCurve(pen, points.OrThrowIfNull().AsSpan(), tension, fillmode);

    /// <inheritdoc cref="DrawClosedCurve(Pen, PointF[], float, FillMode)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawClosedCurve(Pen pen, ReadOnlySpan<Point> points, float tension, FillMode fillmode)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawClosedCurve2I(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.Point*)p, points.Length,
                tension));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Fills the entire drawing surface with the specified color.
    /// </summary>
    public void Clear(Color color) => CheckStatus(PInvokeGdiPlus.GdipGraphicsClear(NativeGraphics, (uint)color.ToArgb()));

#if NET9_0_OR_GREATER
    /// <inheritdoc cref="FillRoundedRectangle(Brush, RectangleF, SizeF)"/>/>
    public void FillRoundedRectangle(Brush brush, Rectangle rect, Size radius) =>
        FillRoundedRectangle(brush, (RectangleF)rect, radius);

    /// <summary>
    ///  Fills the interior of a rounded rectangle with a <see cref='Brush'/>.
    /// </summary>
    /// <param name="brush">The <see cref="Brush"/> to fill the rounded rectangle with.</param>
    /// <param name="rect">The bounds of the rounded rectangle.</param>
    /// <param name="radius">The radius width and height used to round the corners of the rectangle.</param>
    public void FillRoundedRectangle(Brush brush, RectangleF rect, SizeF radius)
    {
        using GraphicsPath path = new();
        path.AddRoundedRectangle(rect, radius);
        FillPath(brush, path);
    }
#endif

    /// <summary>
    ///  Fills the interior of a rectangle with a <see cref='Brush'/>.
    /// </summary>
    public void FillRectangle(Brush brush, RectangleF rect) => FillRectangle(brush, rect.X, rect.Y, rect.Width, rect.Height);

    /// <summary>
    ///  Fills the interior of a rectangle with a <see cref='Brush'/>.
    /// </summary>
    public void FillRectangle(Brush brush, float x, float y, float width, float height)
    {
        ArgumentNullException.ThrowIfNull(brush);

        CheckErrorStatus(PInvokeGdiPlus.GdipFillRectangle(
            NativeGraphics,
            brush.NativeBrush,
            x, y, width, height));

        GC.KeepAlive(brush);
    }

    /// <summary>
    ///  Fills the interior of a rectangle with a <see cref='Brush'/>.
    /// </summary>
    public void FillRectangle(Brush brush, Rectangle rect) => FillRectangle(brush, (float)rect.X, rect.Y, rect.Width, rect.Height);

    /// <summary>
    ///  Fills the interior of a rectangle with a <see cref='Brush'/>.
    /// </summary>
    public void FillRectangle(Brush brush, int x, int y, int width, int height) => FillRectangle(brush, (float)x, y, width, height);

    /// <summary>
    ///  Fills the interiors of a series of rectangles with a <see cref='Brush'/>.
    /// </summary>
    /// <param name="brush">The <see cref="Brush"/> to fill the rectangles with.</param>
    /// <param name="rects">An array of rectangles to fill.</param>
    public void FillRectangles(Brush brush, params RectangleF[] rects)
    {
        ArgumentNullException.ThrowIfNull(rects);
        FillRectangles(brush, rects.AsSpan());
    }

    /// <inheritdoc cref="FillRectangles(Brush, RectangleF[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void FillRectangles(Brush brush, params ReadOnlySpan<RectangleF> rects)
    {
        ArgumentNullException.ThrowIfNull(brush);

        fixed (RectangleF* r = rects)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipFillRectangles(NativeGraphics, brush.NativeBrush, (RectF*)r, rects.Length));
        }

        GC.KeepAlive(brush);
    }

    /// <inheritdoc cref="FillRectangles(Brush, RectangleF[])"/>
    public void FillRectangles(Brush brush, params Rectangle[] rects) =>
        FillRectangles(brush, rects.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="FillRectangles(Brush, RectangleF[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void FillRectangles(Brush brush, params ReadOnlySpan<Rectangle> rects)
    {
        ArgumentNullException.ThrowIfNull(brush);

        fixed (Rectangle* r = rects)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipFillRectanglesI(NativeGraphics, brush.NativeBrush, (Rect*)r, rects.Length));
        }

        GC.KeepAlive(brush);
    }

    /// <inheritdoc cref="FillPolygon(Brush, Point[], FillMode)"/>
    public void FillPolygon(Brush brush, params PointF[] points) => FillPolygon(brush, points, FillMode.Alternate);

#if NET9_0_OR_GREATER
    /// <inheritdoc cref="FillPolygon(Brush, Point[], FillMode)"/>
    public void FillPolygon(Brush brush, params ReadOnlySpan<PointF> points) => FillPolygon(brush, points, FillMode.Alternate);
#endif

    /// <inheritdoc cref="FillPolygon(Brush, Point[], FillMode)"/>
    public void FillPolygon(Brush brush, PointF[] points, FillMode fillMode) =>
        FillPolygon(brush, points.OrThrowIfNull().AsSpan(), fillMode);

    /// <inheritdoc cref="FillPolygon(Brush, Point[], FillMode)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void FillPolygon(Brush brush, ReadOnlySpan<PointF> points, FillMode fillMode)
    {
        ArgumentNullException.ThrowIfNull(brush);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipFillPolygon(
                NativeGraphics,
                brush.NativeBrush,
                (GdiPlus.PointF*)p, points.Length,
                (GdiPlus.FillMode)fillMode));
        }

        GC.KeepAlive(brush);
    }

    /// <inheritdoc cref="FillPolygon(Brush, Point[], FillMode)"/>
    public void FillPolygon(Brush brush, Point[] points) => FillPolygon(brush, points, FillMode.Alternate);

#if NET9_0_OR_GREATER
    /// <inheritdoc cref="FillPolygon(Brush, Point[], FillMode)"/>
    public void FillPolygon(Brush brush, params ReadOnlySpan<Point> points) => FillPolygon(brush, points, FillMode.Alternate);
#endif

    /// <summary>
    ///  Fills the interior of a polygon defined by an array of points.
    /// </summary>
    /// <param name="brush">The <see cref="Brush"/> to fill the polygon with.</param>
    /// <param name="points">An array points that represent the vertices of the polygon.</param>
    /// <param name="fillMode">A <see cref="FillMode"/> enumeration that specifies the fill mode of the polygon.</param>
    public void FillPolygon(Brush brush, Point[] points, FillMode fillMode) =>
        FillPolygon(brush, points.OrThrowIfNull().AsSpan(), fillMode);

    /// <inheritdoc cref="FillPolygon(Brush, Point[], FillMode)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void FillPolygon(Brush brush, ReadOnlySpan<Point> points, FillMode fillMode)
    {
        ArgumentNullException.ThrowIfNull(brush);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipFillPolygonI(
                NativeGraphics,
                brush.NativeBrush,
                (GdiPlus.Point*)p, points.Length,
                (GdiPlus.FillMode)fillMode));
        }

        GC.KeepAlive(brush);
    }

    /// <summary>
    ///  Fills the interior of an ellipse defined by a bounding rectangle.
    /// </summary>
    public void FillEllipse(Brush brush, RectangleF rect) => FillEllipse(brush, rect.X, rect.Y, rect.Width, rect.Height);

    /// <summary>
    ///  Fills the interior of an ellipse defined by a bounding rectangle.
    /// </summary>
    public void FillEllipse(Brush brush, float x, float y, float width, float height)
    {
        ArgumentNullException.ThrowIfNull(brush);

        CheckErrorStatus(PInvokeGdiPlus.GdipFillEllipse(
            NativeGraphics,
            brush.NativeBrush,
            x, y, width, height));

        GC.KeepAlive(brush);
    }

    /// <summary>
    ///  Fills the interior of an ellipse defined by a bounding rectangle.
    /// </summary>
    public void FillEllipse(Brush brush, Rectangle rect) => FillEllipse(brush, (float)rect.X, rect.Y, rect.Width, rect.Height);

    /// <summary>
    ///  Fills the interior of an ellipse defined by a bounding rectangle.
    /// </summary>
    public void FillEllipse(Brush brush, int x, int y, int width, int height) => FillEllipse(brush, (float)x, y, width, height);

    /// <summary>
    ///  Fills the interior of a pie section defined by an ellipse and two radial lines.
    /// </summary>
    public void FillPie(Brush brush, Rectangle rect, float startAngle, float sweepAngle) =>
        FillPie(brush, rect.X, rect.Y, rect.Width, rect.Height, startAngle, sweepAngle);

    /// <summary>
    ///  Fills the interior of a pie section defined by an ellipse and two radial lines.
    /// </summary>
    /// <param name="brush">A Brush that determines the characteristics of the fill.</param>
    /// <param name="rect">
    ///  A Rectangle structure that represents the bounding rectangle that defines the ellipse from which
    ///  the pie section comes.
    /// </param>
    /// <param name="startAngle">
    ///  Angle in degrees measured clockwise from the x-axis to the first side of the pie section.
    /// </param>
    /// <param name="sweepAngle">
    ///  Angle in degrees measured clockwise from the <paramref name="startAngle"/> parameter
    ///  to the second side of the pie section.
    /// </param>
    public void FillPie(Brush brush, RectangleF rect, float startAngle, float sweepAngle) =>
        FillPie(brush, rect.X, rect.Y, rect.Width, rect.Height, startAngle, sweepAngle);

    /// <summary>
    ///  Fills the interior of a pie section defined by an ellipse and two radial lines.
    /// </summary>
    public void FillPie(Brush brush, float x, float y, float width, float height, float startAngle, float sweepAngle)
    {
        ArgumentNullException.ThrowIfNull(brush);

        CheckErrorStatus(PInvokeGdiPlus.GdipFillPie(
            NativeGraphics,
            brush.NativeBrush,
            x, y, width, height,
            startAngle,
            sweepAngle));

        GC.KeepAlive(brush);
    }

    /// <summary>
    ///  Fills the interior of a pie section defined by an ellipse and two radial lines.
    /// </summary>
    public void FillPie(Brush brush, int x, int y, int width, int height, int startAngle, int sweepAngle)
        => FillPie(brush, (float)x, y, width, height, startAngle, sweepAngle);

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
    public void FillClosedCurve(Brush brush, params PointF[] points) =>
        FillClosedCurve(brush, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void FillClosedCurve(Brush brush, params ReadOnlySpan<PointF> points)
    {
        ArgumentNullException.ThrowIfNull(brush);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipFillClosedCurve(
                NativeGraphics,
                brush.NativeBrush,
                (GdiPlus.PointF*)p, points.Length));
        }

        GC.KeepAlive(brush);
    }

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
    public void FillClosedCurve(Brush brush, PointF[] points, FillMode fillmode) =>
        FillClosedCurve(brush, points, fillmode, 0.5f);

#if NET9_0_OR_GREATER
    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
    public void FillClosedCurve(Brush brush, ReadOnlySpan<PointF> points, FillMode fillmode) =>
        FillClosedCurve(brush, points, fillmode, 0.5f);
#endif

    /// <summary>
    ///  Fills the interior of a closed curve defined by an array of points.
    /// </summary>
    /// <param name="brush">The <see cref="Brush"/> to fill the closed curve with.</param>
    /// <param name="points">An array of points that make up the closed curve.</param>
    /// <param name="fillmode">A <see cref="FillMode"/> enumeration that specifies the fill mode of the closed curve.</param>
    /// <param name="tension">A value greater than, or equal to zero that specifies the tension of the curve.</param>
    public void FillClosedCurve(Brush brush, PointF[] points, FillMode fillmode, float tension) =>
        FillClosedCurve(brush, points.OrThrowIfNull().AsSpan(), fillmode, tension);

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void FillClosedCurve(Brush brush, ReadOnlySpan<PointF> points, FillMode fillmode, float tension)
    {
        ArgumentNullException.ThrowIfNull(brush);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipFillClosedCurve2(
                NativeGraphics,
                brush.NativeBrush,
                (GdiPlus.PointF*)p, points.Length,
                tension,
                (GdiPlus.FillMode)fillmode));
        }

        GC.KeepAlive(brush);
    }

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
    public void FillClosedCurve(Brush brush, params Point[] points) =>
        FillClosedCurve(brush, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void FillClosedCurve(Brush brush, params ReadOnlySpan<Point> points)
    {
        ArgumentNullException.ThrowIfNull(brush);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipFillClosedCurveI(
                NativeGraphics,
                brush.NativeBrush,
                (GdiPlus.Point*)p, points.Length));
        }

        GC.KeepAlive(brush);
    }

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
    public void FillClosedCurve(Brush brush, Point[] points, FillMode fillmode) =>
        FillClosedCurve(brush, points, fillmode, 0.5f);

#if NET9_0_OR_GREATER
    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
    public void FillClosedCurve(Brush brush, ReadOnlySpan<Point> points, FillMode fillmode) =>
        FillClosedCurve(brush, points, fillmode, 0.5f);

#endif

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
    public void FillClosedCurve(Brush brush, Point[] points, FillMode fillmode, float tension) =>
        FillClosedCurve(brush, points.OrThrowIfNull().AsSpan(), fillmode, tension);

    /// <inheritdoc cref="FillClosedCurve(Brush, PointF[], FillMode, float)"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void FillClosedCurve(Brush brush, ReadOnlySpan<Point> points, FillMode fillmode, float tension)
    {
        ArgumentNullException.ThrowIfNull(brush);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipFillClosedCurve2I(
                NativeGraphics,
                brush.NativeBrush,
                (GdiPlus.Point*)p, points.Length,
                tension,
                (GdiPlus.FillMode)fillmode));
        }

        GC.KeepAlive(brush);
    }

    /// <summary>
    ///  Draws the specified text at the specified location with the specified <see cref="Brush"/> and
    ///  <see cref="Font"/> objects.
    /// </summary>
    /// <param name="s">The text to draw.</param>
    /// <param name="font"><see cref="Font"/> that defines the text format.</param>
    /// <param name="brush"><see cref="Brush"/> that determines the color and texture of the drawn text.</param>
    /// <param name="x">The x-coordinate of the upper-left corner of the drawn text.</param>
    /// <param name="y">The y-coordinate of the upper-left corner of the drawn text.</param>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="brush"/> is <see langword="null"/>. -or- <paramref name="font"/> is <see langword="null"/>.
    /// </exception>
    public void DrawString(string? s, Font font, Brush brush, float x, float y) =>
        DrawString(s, font, brush, new RectangleF(x, y, 0, 0), null);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="DrawString(string?, Font, Brush, float, float)"/>
    public void DrawString(ReadOnlySpan<char> s, Font font, Brush brush, float x, float y) =>
        DrawString(s, font, brush, new RectangleF(x, y, 0, 0), null);
#endif

    /// <summary>
    ///  Draws the specified text at the specified location with the specified <see cref="Brush"/> and
    ///  <see cref="Font"/> objects.
    /// </summary>
    /// <param name="s">The text to draw.</param>
    /// <param name="font"><see cref="Font"/> that defines the text format.</param>
    /// <param name="brush"><see cref="Brush"/> that determines the color and texture of the drawn text.</param>
    /// <param name="point"><see cref="PointF"/>structure that specifies the upper-left corner of the drawn text.</param>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="brush"/> is <see langword="null"/>. -or- <paramref name="font"/> is <see langword="null"/>.
    /// </exception>
    public void DrawString(string? s, Font font, Brush brush, PointF point) =>
        DrawString(s, font, brush, new RectangleF(point.X, point.Y, 0, 0), null);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="DrawString(string?, Font, Brush, PointF)"/>
    public void DrawString(ReadOnlySpan<char> s, Font font, Brush brush, PointF point) =>
        DrawString(s, font, brush, new RectangleF(point.X, point.Y, 0, 0), null);
#endif

    /// <summary>
    ///  Draws the specified text at the specified location with the specified <see cref="Brush"/> and
    ///  <see cref="Font"/> objects using the formatting attributes of the specified <see cref="StringFormat"/>.
    /// </summary>
    /// <param name="s">The text to draw.</param>
    /// <param name="font"><see cref="Font"/> that defines the text format.</param>
    /// <param name="brush"><see cref="Brush"/> that determines the color and texture of the drawn text.</param>
    /// <param name="x">The x-coordinate of the upper-left corner of the drawn text.</param>
    /// <param name="y">The y-coordinate of the upper-left corner of the drawn text.</param>
    /// <param name="format">
    ///  <see cref="StringFormat"/> that specifies formatting attributes, such as line spacing and alignment,
    ///  that are applied to the drawn text.
    /// </param>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="brush"/> is <see langword="null"/>. -or- <paramref name="font"/> is <see langword="null"/>.
    /// </exception>
    public void DrawString(string? s, Font font, Brush brush, float x, float y, StringFormat? format) =>
        DrawString(s, font, brush, new RectangleF(x, y, 0, 0), format);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="DrawString(string?, Font, Brush, float, float, StringFormat?)"/>
    public void DrawString(ReadOnlySpan<char> s, Font font, Brush brush, float x, float y, StringFormat? format) =>
        DrawString(s, font, brush, new RectangleF(x, y, 0, 0), format);
#endif

    /// <summary>
    ///  Draws the specified text at the specified location with the specified <see cref="Brush"/> and
    ///  <see cref="Font"/> objects using the formatting attributes of the specified <see cref="StringFormat"/>.
    /// </summary>
    /// <param name="s">The text to draw.</param>
    /// <param name="font"><see cref="Font"/> that defines the text format.</param>
    /// <param name="brush"><see cref="Brush"/> that determines the color and texture of the drawn text.</param>
    /// <param name="point"><see cref="PointF"/>structure that specifies the upper-left corner of the drawn text.</param>
    /// <param name="format">
    ///  <see cref="StringFormat"/> that specifies formatting attributes, such as line spacing and alignment,
    ///  that are applied to the drawn text.
    /// </param>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="brush"/> is <see langword="null"/>. -or- <paramref name="font"/> is <see langword="null"/>.
    /// </exception>
    public void DrawString(string? s, Font font, Brush brush, PointF point, StringFormat? format) =>
        DrawString(s, font, brush, new RectangleF(point.X, point.Y, 0, 0), format);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="DrawString(string?, Font, Brush, PointF, StringFormat?)"/>
    public void DrawString(ReadOnlySpan<char> s, Font font, Brush brush, PointF point, StringFormat? format) =>
        DrawString(s, font, brush, new RectangleF(point.X, point.Y, 0, 0), format);
#endif

    /// <summary>
    ///  Draws the specified text in the specified rectangle with the specified <see cref="Brush"/> and
    ///  <see cref="Font"/> objects.
    /// </summary>
    /// <param name="s">The text to draw.</param>
    /// <param name="font"><see cref="Font"/> that defines the text format.</param>
    /// <param name="brush"><see cref="Brush"/> that determines the color and texture of the drawn text.</param>
    /// <param name="layoutRectangle"><see cref="RectangleF"/>structure that specifies the location of the drawn text.</param>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="brush"/> is <see langword="null"/>. -or- <paramref name="font"/> is <see langword="null"/>.
    /// </exception>
    /// <remarks>
    ///  <para>
    ///   The text represented by the <paramref name="s"/> parameter is drawn inside the rectangle represented by
    ///   the <paramref name="layoutRectangle"/> parameter. If the text does not fit inside the rectangle, it is
    ///   truncated at the nearest word. To further manipulate how the string is drawn inside the rectangle use the
    ///   <see cref="DrawString(string?, Font, Brush, RectangleF, StringFormat?)"/> overload that takes
    ///   a <see cref="StringFormat"/>.
    ///  </para>
    /// </remarks>
    public void DrawString(string? s, Font font, Brush brush, RectangleF layoutRectangle) =>
        DrawString(s, font, brush, layoutRectangle, null);

#if NET8_0_OR_GREATER
    /// <remarks>
    ///  <para>
    ///   The text represented by the <paramref name="s"/> parameter is drawn inside the rectangle represented by
    ///   the <paramref name="layoutRectangle"/> parameter. If the text does not fit inside the rectangle, it is
    ///   truncated at the nearest word. To further manipulate how the string is drawn inside the rectangle use the
    ///   <see cref="DrawString(ReadOnlySpan{char}, Font, Brush, RectangleF, StringFormat?)"/> overload that takes
    ///   a <see cref="StringFormat"/>.
    ///  </para>
    /// </remarks>
    /// <inheritdoc cref="DrawString(string?, Font, Brush, RectangleF)"/>
    public void DrawString(ReadOnlySpan<char> s, Font font, Brush brush, RectangleF layoutRectangle) =>
        DrawString(s, font, brush, layoutRectangle, null);
#endif

    /// <summary>
    ///  Draws the specified text in the specified rectangle with the specified <see cref="Brush"/> and
    ///  <see cref="Font"/> objects using the formatting attributes of the specified <see cref="StringFormat"/>.
    /// </summary>
    /// <param name="s">The text to draw.</param>
    /// <param name="font"><see cref="Font"/> that defines the text format.</param>
    /// <param name="brush"><see cref="Brush"/> that determines the color and texture of the drawn text.</param>
    /// <param name="layoutRectangle"><see cref="RectangleF"/>structure that specifies the location of the drawn text.</param>
    /// <param name="format">
    ///  <see cref="StringFormat"/> that specifies formatting attributes, such as line spacing and alignment,
    ///  that are applied to the drawn text.
    /// </param>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="brush"/> is <see langword="null"/>. -or- <paramref name="font"/> is <see langword="null"/>.
    /// </exception>
    /// <remarks>
    ///  <para>
    ///   The text represented by the <paramref name="s"/> parameter is drawn inside the rectangle represented by
    ///   the <paramref name="layoutRectangle"/> parameter. If the text does not fit inside the rectangle, it is
    ///   truncated at the nearest word, unless otherwise specified with the <paramref name="format"/> parameter.
    ///  </para>
    /// </remarks>
    public void DrawString(string? s, Font font, Brush brush, RectangleF layoutRectangle, StringFormat? format) =>
        DrawStringInternal(s, font, brush, layoutRectangle, format);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="DrawString(string?, Font, Brush, RectangleF, StringFormat?)"/>
    public void DrawString(ReadOnlySpan<char> s, Font font, Brush brush, RectangleF layoutRectangle, StringFormat? format) =>
        DrawStringInternal(s, font, brush, layoutRectangle, format);
#endif

    private void DrawStringInternal(ReadOnlySpan<char> s, Font font, Brush brush, RectangleF layoutRectangle, StringFormat? format)
    {
        ArgumentNullException.ThrowIfNull(brush);

        if (s.IsEmpty)
        {
            return;
        }

        ArgumentNullException.ThrowIfNull(font);

        fixed (char* c = s)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawString(
                NativeGraphics,
                c, s.Length,
                font.NativeFont,
                (RectF*)&layoutRectangle,
                format.Pointer(),
                brush.NativeBrush));
        }

        GC.KeepAlive(font);
        GC.KeepAlive(brush);
        GC.KeepAlive(format);
    }

    /// <param name="charactersFitted">Number of characters in the text.</param>
    /// <param name="linesFilled">Number of lines in the text.</param>
    /// <inheritdoc cref="MeasureString(string?, Font, SizeF, StringFormat?)"/>
    public SizeF MeasureString(
        string? text,
        Font font,
        SizeF layoutArea,
        StringFormat? stringFormat,
        out int charactersFitted,
        out int linesFilled) =>
        MeasureStringInternal(text, font, new(default, layoutArea), stringFormat, out charactersFitted, out linesFilled);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="MeasureString(string?, Font, SizeF, StringFormat?, out int, out int)"/>
    public SizeF MeasureString(
        ReadOnlySpan<char> text,
        Font font,
        SizeF layoutArea,
        StringFormat? stringFormat,
        out int charactersFitted,
        out int linesFilled) =>
        MeasureStringInternal(text, font, new(default, layoutArea), stringFormat, out charactersFitted, out linesFilled);
#endif

    public SizeF MeasureStringInternal(
        ReadOnlySpan<char> text,
        Font font,
        RectangleF layoutArea,
        StringFormat? stringFormat,
        out int charactersFitted,
        out int linesFilled)
    {
        if (text.IsEmpty)
        {
            charactersFitted = 0;
            linesFilled = 0;
            return SizeF.Empty;
        }

        ArgumentNullException.ThrowIfNull(font);

        RectF boundingBox = default;

        fixed (char* c = text)
        fixed (int* fitted = &charactersFitted)
        fixed (int* filled = &linesFilled)
        {
            CheckStatus(PInvokeGdiPlus.GdipMeasureString(
                NativeGraphics,
                c,
                text.Length,
                font.NativeFont,
                (RectF*)&layoutArea,
                stringFormat.Pointer(),
                &boundingBox,
                fitted,
                filled));
        }

        GC.KeepAlive(font);
        GC.KeepAlive(stringFormat);

        return new SizeF(boundingBox.Width, boundingBox.Height);
    }

    /// <param name="origin"><see cref="PointF"/> structure that represents the upper-left corner of the text.</param>
    /// <inheritdoc cref="MeasureString(string?, Font, SizeF, StringFormat?)"/>
    public SizeF MeasureString(string? text, Font font, PointF origin, StringFormat? stringFormat)
        => MeasureStringInternal(text, font, new(origin, default), stringFormat, out _, out _);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="MeasureString(string?, Font, PointF, StringFormat?)"/>
    public SizeF MeasureString(ReadOnlySpan<char> text, Font font, PointF origin, StringFormat? stringFormat)
        => MeasureStringInternal(text, font, new(origin, default), stringFormat, out _, out _);
#endif

    /// <inheritdoc cref="MeasureString(string?, Font, SizeF, StringFormat?)"/>
    public SizeF MeasureString(string? text, Font font, SizeF layoutArea) => MeasureString(text, font, layoutArea, null);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="MeasureString(string?, Font, SizeF)"/>
    public SizeF MeasureString(ReadOnlySpan<char> text, Font font, SizeF layoutArea) => MeasureString(text, font, layoutArea, null);
#endif

    /// <param name="stringFormat">
    ///  <see cref="StringFormat"/> that represents formatting information, such as line spacing, for the text.
    /// </param>
    /// <param name="layoutArea">
    ///  <see cref="SizeF"/> structure that specifies the maximum layout area for the text.
    /// </param>
    /// <inheritdoc cref="MeasureString(string?, Font, int, StringFormat?)"/>
    public SizeF MeasureString(string? text, Font font, SizeF layoutArea, StringFormat? stringFormat) =>
        MeasureStringInternal(text, font, new(default, layoutArea), stringFormat, out _, out _);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="MeasureString(string?, Font, SizeF, StringFormat?)"/>
    public SizeF MeasureString(ReadOnlySpan<char> text, Font font, SizeF layoutArea, StringFormat? stringFormat) =>
        MeasureStringInternal(text, font, new(default, layoutArea), stringFormat, out _, out _);
#endif

    /// <summary>
    ///  Measures the specified text when drawn with the specified <see cref="Font"/>.
    /// </summary>
    /// <param name="text">Text to measure.</param>
    /// <param name="font"><see cref="Font"/> that defines the text format.</param>
    /// <returns>
    ///  This method returns a <see cref="SizeF"/> structure that represents the size, in the units specified by the
    ///  <see cref="PageUnit"/> property, of the text specified by the <paramref name="text"/> parameter as drawn
    ///  with the <paramref name="font"/> parameter.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   The <see cref="MeasureString(string?, Font)"/> method is designed for use with individual strings and
    ///   includes a small amount of extra space before and after the string to allow for overhanging glyphs. Also,
    ///   the <see cref="DrawString(string?, Font, Brush, PointF)"/> method adjusts glyph points to optimize display
    ///   quality and might display a string narrower than reported by <see cref="MeasureString(string?, Font)"/>.
    ///   To obtain metrics suitable for adjacent strings in layout (for example, when implementing formatted text),
    ///   use the <see cref="MeasureCharacterRanges(string?, Font, RectangleF, StringFormat?)"/> method or one of
    ///   the <see cref="MeasureString(string?, Font, int, StringFormat?)"/> methods that takes a StringFormat, and
    ///   pass <see cref="StringFormat.GenericTypographic"/>. Also, ensure the <see cref="TextRenderingHint"/> for
    ///   the <see cref="Graphics"/> is <see cref="TextRenderingHint.AntiAlias"/>.
    ///  </para>
    /// </remarks>
    /// <exception cref="ArgumentNullException"><paramref name="font"/> is null.</exception>
    public SizeF MeasureString(string? text, Font font) => MeasureString(text, font, new SizeF(0, 0));

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="MeasureString(string?, Font)"/>
    public SizeF MeasureString(ReadOnlySpan<char> text, Font font) => MeasureString(text, font, new SizeF(0, 0));
#endif

    /// <param name="width">Maximum width of the string in pixels.</param>
    /// <inheritdoc cref="MeasureString(string?, Font)"/>
    public SizeF MeasureString(string? text, Font font, int width) => MeasureString(text, font, new SizeF(width, 999999));

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="MeasureString(string?, Font, int)"/>
    public SizeF MeasureString(ReadOnlySpan<char> text, Font font, int width) =>
        MeasureString(text, font, new SizeF(width, 999999));
#endif

    /// <param name="format">
    ///  <see cref="StringFormat"/> that represents formatting information, such as line spacing, for the text.
    /// </param>
    /// <inheritdoc cref="MeasureString(string?, Font, int)"/>
    public SizeF MeasureString(string? text, Font font, int width, StringFormat? format) =>
        MeasureString(text, font, new SizeF(width, 999999), format);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="MeasureString(string?, Font, int, StringFormat?)"/>
    public SizeF MeasureString(ReadOnlySpan<char> text, Font font, int width, StringFormat? format) =>
        MeasureString(text, font, new SizeF(width, 999999), format);
#endif

    /// <summary>
    ///  Gets an array of <see cref="Region"/> objects, each of which bounds a range of character positions within
    ///  the specified text.
    /// </summary>
    /// <param name="text">Text to measure.</param>
    /// <param name="font"><see cref="Font"/> that defines the text format.</param>
    /// <param name="layoutRect"><see cref="RectangleF"/> structure that specifies the layout rectangle for the text.</param>
    /// <param name="stringFormat">
    ///  <see cref="StringFormat"/> that represents formatting information, such as line spacing, for the text.
    /// </param>
    /// <returns>
    ///  This method returns an array of <see cref="Region"/> objects, each of which bounds a range of character
    ///  positions within the specified text.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   The regions returned by this method are resolution-dependent, so there might be a slight loss of accuracy
    ///   if text is recorded in a metafile at one resolution and later played back at a different resolution.
    ///  </para>
    /// </remarks>
    /// <exception cref="ArgumentNullException"><paramref name="font"/> is <see langword="null"/>.</exception>
    public Region[] MeasureCharacterRanges(string? text, Font font, RectangleF layoutRect, StringFormat? stringFormat) =>
        MeasureCharacterRangesInternal(text, font, layoutRect, stringFormat);

#if NET8_0_OR_GREATER
    /// <inheritdoc cref="MeasureCharacterRanges(string?, Font, RectangleF, StringFormat?)"/>
    public Region[] MeasureCharacterRanges(ReadOnlySpan<char> text, Font font, RectangleF layoutRect, StringFormat? stringFormat) =>
        MeasureCharacterRangesInternal(text, font, layoutRect, stringFormat);
#endif

    private Region[] MeasureCharacterRangesInternal(
        ReadOnlySpan<char> text,
        Font font,
        RectF layoutRect,
        StringFormat? stringFormat)
    {
        if (text.IsEmpty)
            return [];

        ArgumentNullException.ThrowIfNull(font);

        int count;
        CheckStatus(PInvokeGdiPlus.GdipGetStringFormatMeasurableCharacterRangeCount(stringFormat.Pointer(), &count));

        if (count == 0)
        {
            return [];
        }

        GpRegion*[] gpRegions = new GpRegion*[count];
        Region[] regions = new Region[count];

        for (int f = 0; f < count; f++)
        {
            regions[f] = new Region();
            gpRegions[f] = regions[f].NativeRegion;
        }

        fixed (char* c = text)
        fixed (GpRegion** r = gpRegions)
        {
            CheckStatus(PInvokeGdiPlus.GdipMeasureCharacterRanges(
                NativeGraphics,
                c,
                text.Length,
                font.NativeFont,
                &layoutRect,
                stringFormat.Pointer(),
                count,
                r));
        }

        GC.KeepAlive(stringFormat);
        GC.KeepAlive(font);

        return regions;
    }

    /// <summary>
    ///  Draws the specified image at the specified location.
    /// </summary>
    public void DrawImage(Image image, PointF point) => DrawImage(image, point.X, point.Y);

    public void DrawImage(Image image, float x, float y)
    {
        ArgumentNullException.ThrowIfNull(image);
        Status status = PInvokeGdiPlus.GdipDrawImage(NativeGraphics, image.Pointer(), x, y);
        IgnoreMetafileErrors(image, ref status);
        CheckErrorStatus(status);
    }

    public void DrawImage(Image image, RectangleF rect) => DrawImage(image, rect.X, rect.Y, rect.Width, rect.Height);

    public void DrawImage(Image image, float x, float y, float width, float height)
    {
        ArgumentNullException.ThrowIfNull(image);
        Status status = PInvokeGdiPlus.GdipDrawImageRect(NativeGraphics, image.Pointer(), x, y, width, height);
        IgnoreMetafileErrors(image, ref status);
        CheckErrorStatus(status);
    }

    public void DrawImage(Image image, Point point) => DrawImage(image, (float)point.X, point.Y);

    public void DrawImage(Image image, int x, int y) => DrawImage(image, (float)x, y);

    public void DrawImage(Image image, Rectangle rect) => DrawImage(image, (float)rect.X, rect.Y, rect.Width, rect.Height);

    public void DrawImage(Image image, int x, int y, int width, int height) => DrawImage(image, (float)x, y, width, height);

    public void DrawImageUnscaled(Image image, Point point) => DrawImage(image, point.X, point.Y);

    public void DrawImageUnscaled(Image image, int x, int y) => DrawImage(image, x, y);

    public void DrawImageUnscaled(Image image, Rectangle rect) => DrawImage(image, rect.X, rect.Y);

    public void DrawImageUnscaled(Image image, int x, int y, int width, int height) => DrawImage(image, x, y);

    public void DrawImageUnscaledAndClipped(Image image, Rectangle rect)
    {
        ArgumentNullException.ThrowIfNull(image);

        int width = Math.Min(rect.Width, image.Width);
        int height = Math.Min(rect.Height, image.Height);

        // We could put centering logic here too for the case when the image
        // is smaller than the rect.
        DrawImage(image, rect, 0, 0, width, height, GraphicsUnit.Pixel);
    }

    public void DrawImage(Image image, PointF[] destPoints)
    {
        // Affine or perspective blt
        //
        //  destPoints.Length = 3: rect => parallelogram
        //      destPoints[0] <=> top-left corner of the source rectangle
        //      destPoints[1] <=> top-right corner
        //      destPoints[2] <=> bottom-left corner
        //  destPoints.Length = 4: rect => quad
        //      destPoints[3] <=> bottom-right corner
        //
        //  @notes Perspective blt only works for bitmap images.

        ArgumentNullException.ThrowIfNull(image);
        ArgumentNullException.ThrowIfNull(destPoints);

        int count = destPoints.Length;
        if (count is not 3 and not 4)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidLength);

        fixed (PointF* p = destPoints)
        {
            Status status = PInvokeGdiPlus.GdipDrawImagePoints(NativeGraphics, image.Pointer(), (GdiPlus.PointF*)p, count);
            IgnoreMetafileErrors(image, ref status);
            CheckErrorStatus(status);
        }
    }

    public void DrawImage(Image image, Point[] destPoints)
    {
        ArgumentNullException.ThrowIfNull(image);
        ArgumentNullException.ThrowIfNull(destPoints);

        int count = destPoints.Length;
        if (count is not 3 and not 4)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidLength);

        fixed (Point* p = destPoints)
        {
            Status status = PInvokeGdiPlus.GdipDrawImagePointsI(NativeGraphics, image.Pointer(), (GdiPlus.Point*)p, count);
            IgnoreMetafileErrors(image, ref status);
            CheckErrorStatus(status);
        }
    }

    public void DrawImage(Image image, float x, float y, RectangleF srcRect, GraphicsUnit srcUnit)
    {
        ArgumentNullException.ThrowIfNull(image);

        Status status = PInvokeGdiPlus.GdipDrawImagePointRect(
            NativeGraphics,
            image.Pointer(),
            x, y,
            srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height,
            (Unit)srcUnit);

        IgnoreMetafileErrors(image, ref status);
        CheckErrorStatus(status);
    }

    public void DrawImage(Image image, int x, int y, Rectangle srcRect, GraphicsUnit srcUnit)
        => DrawImage(image, x, y, (RectangleF)srcRect, srcUnit);

    public void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit)
    {
        ArgumentNullException.ThrowIfNull(image);

        Status status = PInvokeGdiPlus.GdipDrawImageRectRect(
            NativeGraphics,
            image.Pointer(),
            destRect.X, destRect.Y, destRect.Width, destRect.Height,
            srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height,
            (Unit)srcUnit,
            imageAttributes: null,
            callback: 0,
            callbackData: null);

        IgnoreMetafileErrors(image, ref status);
        CheckErrorStatus(status);
    }

    public void DrawImage(Image image, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit)
        => DrawImage(image, destRect, srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height, srcUnit);

    public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit)
    {
        ArgumentNullException.ThrowIfNull(image);
        ArgumentNullException.ThrowIfNull(destPoints);

        int count = destPoints.Length;
        if (count is not 3 and not 4)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidLength);

        fixed (PointF* p = destPoints)
        {
            Status status = PInvokeGdiPlus.GdipDrawImagePointsRect(
                NativeGraphics,
                image.Pointer(),
                (GdiPlus.PointF*)p, destPoints.Length,
                srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height,
                (Unit)srcUnit,
                imageAttributes: null,
                callback: 0,
                callbackData: null);

            IgnoreMetafileErrors(image, ref status);
            CheckErrorStatus(status);
        }
    }

    public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, ImageAttributes? imageAttr) =>
        DrawImage(image, destPoints, srcRect, srcUnit, imageAttr, null, 0);

    public void DrawImage(
        Image image,
        PointF[] destPoints,
        RectangleF srcRect,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttr,
        DrawImageAbort? callback) =>
        DrawImage(image, destPoints, srcRect, srcUnit, imageAttr, callback, 0);

    public void DrawImage(
        Image image,
        PointF[] destPoints,
        RectangleF srcRect,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttr,
        DrawImageAbort? callback,
        int callbackData)
    {
        ArgumentNullException.ThrowIfNull(image);
        ArgumentNullException.ThrowIfNull(destPoints);

        int count = destPoints.Length;
        if (count is not 3 and not 4)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidLength);

        fixed (PointF* p = destPoints)
        {
            Status status = PInvokeGdiPlus.GdipDrawImagePointsRect(
                NativeGraphics,
                image.Pointer(),
                (GdiPlus.PointF*)p, destPoints.Length,
                srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height,
                (Unit)srcUnit,
                imageAttr.Pointer(),
                callback is null ? 0 : Marshal.GetFunctionPointerForDelegate(callback),
                (void*)(nint)callbackData);

            GC.KeepAlive(imageAttr);
            GC.KeepAlive(callback);
            IgnoreMetafileErrors(image, ref status);
            CheckErrorStatus(status);
        }
    }

    public void DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit) =>
        DrawImage(image, destPoints, srcRect, srcUnit, null, null, 0);

    public void DrawImage(
        Image image,
        Point[] destPoints,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttr) => DrawImage(image, destPoints, srcRect, srcUnit, imageAttr, null, 0);

    public void DrawImage(
        Image image,
        Point[] destPoints,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttr,
        DrawImageAbort? callback) => DrawImage(image, destPoints, srcRect, srcUnit, imageAttr, callback, 0);

    public void DrawImage(
        Image image,
        Point[] destPoints,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttr,
        DrawImageAbort? callback,
        int callbackData)
    {
        ArgumentNullException.ThrowIfNull(image);
        ArgumentNullException.ThrowIfNull(destPoints);

        int count = destPoints.Length;
        if (count is not 3 and not 4)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidLength);

        fixed (Point* p = destPoints)
        {
            Status status = PInvokeGdiPlus.GdipDrawImagePointsRectI(
                NativeGraphics,
                image.Pointer(),
                (GdiPlus.Point*)p, destPoints.Length,
                srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height,
                (Unit)srcUnit,
                imageAttr.Pointer(),
                callback is null ? 0 : Marshal.GetFunctionPointerForDelegate(callback),
                (void*)(nint)callbackData);

            GC.KeepAlive(imageAttr);
            GC.KeepAlive(callback);

            IgnoreMetafileErrors(image, ref status);
            CheckErrorStatus(status);
        }
    }

    public void DrawImage(
        Image image,
        Rectangle destRect,
        float srcX,
        float srcY,
        float srcWidth,
        float srcHeight,
        GraphicsUnit srcUnit) => DrawImage(image, destRect, srcX, srcY, srcWidth, srcHeight, srcUnit, null);

    public void DrawImage(
        Image image,
        Rectangle destRect,
        float srcX,
        float srcY,
        float srcWidth,
        float srcHeight,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttrs) => DrawImage(image, destRect, srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, null);

    public void DrawImage(
        Image image,
        Rectangle destRect,
        float srcX,
        float srcY,
        float srcWidth,
        float srcHeight,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttrs,
        DrawImageAbort? callback) => DrawImage(image, destRect, srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, callback, IntPtr.Zero);

    public void DrawImage(
        Image image,
        Rectangle destRect,
        float srcX,
        float srcY,
        float srcWidth,
        float srcHeight,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttrs,
        DrawImageAbort? callback,
        IntPtr callbackData)
    {
        ArgumentNullException.ThrowIfNull(image);

        Status status = PInvokeGdiPlus.GdipDrawImageRectRect(
            NativeGraphics,
            image.Pointer(),
            destRect.X, destRect.Y, destRect.Width, destRect.Height,
            srcX, srcY, srcWidth, srcHeight,
            (Unit)srcUnit,
            imageAttrs.Pointer(),
            callback is null ? 0 : Marshal.GetFunctionPointerForDelegate(callback),
            (void*)callbackData);

        GC.KeepAlive(imageAttrs);
        GC.KeepAlive(callback);
        IgnoreMetafileErrors(image, ref status);
        CheckErrorStatus(status);
    }

    public void DrawImage(
        Image image,
        Rectangle destRect,
        int srcX,
        int srcY,
        int srcWidth,
        int srcHeight,
        GraphicsUnit srcUnit) => DrawImage(image, destRect, (float)srcX, srcY, srcWidth, srcHeight, srcUnit, null);

    public void DrawImage(
        Image image,
        Rectangle destRect,
        int srcX,
        int srcY,
        int srcWidth,
        int srcHeight,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttr) => DrawImage(image, destRect, (float)srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttr, null);

    public void DrawImage(
        Image image,
        Rectangle destRect,
        int srcX,
        int srcY,
        int srcWidth,
        int srcHeight,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttr,
        DrawImageAbort? callback) => DrawImage(image, destRect, (float)srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttr, callback, IntPtr.Zero);

    public void DrawImage(
        Image image,
        Rectangle destRect,
        int srcX,
        int srcY,
        int srcWidth,
        int srcHeight,
        GraphicsUnit srcUnit,
        ImageAttributes? imageAttrs,
        DrawImageAbort? callback,
        IntPtr callbackData) => DrawImage(image, destRect, (float)srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, callback, callbackData);

    /// <summary>
    ///  Draws a line connecting the two specified points.
    /// </summary>
    public void DrawLine(Pen pen, PointF pt1, PointF pt2) => DrawLine(pen, pt1.X, pt1.Y, pt2.X, pt2.Y);

    /// <inheritdoc cref="DrawLines(Pen, Point[])"/>
    public void DrawLines(Pen pen, params PointF[] points) => DrawLines(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawLines(Pen, Point[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawLines(Pen pen, params ReadOnlySpan<PointF> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawLines(NativeGraphics, pen.NativePen, (GdiPlus.PointF*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws a line connecting the two specified points.
    /// </summary>
    public void DrawLine(Pen pen, int x1, int y1, int x2, int y2) =>
        DrawLine(pen, (float)x1, y1, x2, y2);

    /// <summary>
    ///  Draws a line connecting the two specified points.
    /// </summary>
    public void DrawLine(Pen pen, Point pt1, Point pt2) => DrawLine(pen, (float)pt1.X, pt1.Y, pt2.X, pt2.Y);

    /// <summary>
    ///  Draws a series of line segments that connect an array of points.
    /// </summary>
    /// <param name="pen">The <see cref="Pen"/> that determines the color, width, and style of the line segments.</param>
    /// <param name="points">An array of points to connect.</param>
    public void DrawLines(Pen pen, params Point[] points)
    {
        ArgumentNullException.ThrowIfNull(pen);
        ArgumentNullException.ThrowIfNull(points);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawLinesI(NativeGraphics, pen.NativePen, (GdiPlus.Point*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawLines(Pen, Point[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawLines(Pen pen, params ReadOnlySpan<Point> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawLinesI(NativeGraphics, pen.NativePen, (GdiPlus.Point*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  CopyPixels will perform a gdi "bitblt" operation to the source from the destination with the given size.
    /// </summary>
    public void CopyFromScreen(Point upperLeftSource, Point upperLeftDestination, Size blockRegionSize) =>
        CopyFromScreen(upperLeftSource.X, upperLeftSource.Y, upperLeftDestination.X, upperLeftDestination.Y, blockRegionSize);

    /// <summary>
    ///  CopyPixels will perform a gdi "bitblt" operation to the source from the destination with the given size.
    /// </summary>
    public void CopyFromScreen(int sourceX, int sourceY, int destinationX, int destinationY, Size blockRegionSize) =>
        CopyFromScreen(sourceX, sourceY, destinationX, destinationY, blockRegionSize, CopyPixelOperation.SourceCopy);

    /// <summary>
    ///  CopyPixels will perform a gdi "bitblt" operation to the source from the destination with the given size
    ///  and specified raster operation.
    /// </summary>
    public void CopyFromScreen(Point upperLeftSource, Point upperLeftDestination, Size blockRegionSize, CopyPixelOperation copyPixelOperation) =>
        CopyFromScreen(upperLeftSource.X, upperLeftSource.Y, upperLeftDestination.X, upperLeftDestination.Y, blockRegionSize, copyPixelOperation);

    public void EnumerateMetafile(Metafile metafile, PointF destPoint, EnumerateMetafileProc callback) =>
        EnumerateMetafile(metafile, destPoint, callback, IntPtr.Zero);

    public void EnumerateMetafile(Metafile metafile, PointF destPoint, EnumerateMetafileProc callback, IntPtr callbackData) =>
        EnumerateMetafile(metafile, destPoint, callback, callbackData, null);

    public void EnumerateMetafile(Metafile metafile, Point destPoint, EnumerateMetafileProc callback) =>
        EnumerateMetafile(metafile, destPoint, callback, IntPtr.Zero);

    public void EnumerateMetafile(Metafile metafile, Point destPoint, EnumerateMetafileProc callback, IntPtr callbackData) =>
        EnumerateMetafile(metafile, destPoint, callback, callbackData, null);

    public void EnumerateMetafile(Metafile metafile, RectangleF destRect, EnumerateMetafileProc callback) =>
        EnumerateMetafile(metafile, destRect, callback, IntPtr.Zero);

    public void EnumerateMetafile(Metafile metafile, RectangleF destRect, EnumerateMetafileProc callback, IntPtr callbackData) =>
        EnumerateMetafile(metafile, destRect, callback, callbackData, null);

    public void EnumerateMetafile(Metafile metafile, Rectangle destRect, EnumerateMetafileProc callback) =>
        EnumerateMetafile(metafile, destRect, callback, IntPtr.Zero);

    public void EnumerateMetafile(Metafile metafile, Rectangle destRect, EnumerateMetafileProc callback, IntPtr callbackData) =>
        EnumerateMetafile(metafile, destRect, callback, callbackData, null);

    public void EnumerateMetafile(Metafile metafile, PointF[] destPoints, EnumerateMetafileProc callback) =>
        EnumerateMetafile(metafile, destPoints, callback, IntPtr.Zero);

    public void EnumerateMetafile(
        Metafile metafile,
        PointF[] destPoints,
        EnumerateMetafileProc callback,
        IntPtr callbackData) => EnumerateMetafile(metafile, destPoints, callback, IntPtr.Zero, null);

    public void EnumerateMetafile(Metafile metafile, Point[] destPoints, EnumerateMetafileProc callback) =>
        EnumerateMetafile(metafile, destPoints, callback, IntPtr.Zero);

    public void EnumerateMetafile(Metafile metafile, Point[] destPoints, EnumerateMetafileProc callback, IntPtr callbackData) =>
        EnumerateMetafile(metafile, destPoints, callback, callbackData, null);

    public void EnumerateMetafile(
        Metafile metafile,
        PointF destPoint,
        RectangleF srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback) => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, IntPtr.Zero);

    public void EnumerateMetafile(
        Metafile metafile,
        PointF destPoint,
        RectangleF srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback,
        IntPtr callbackData) => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, callbackData, null);

    public void EnumerateMetafile(
        Metafile metafile,
        Point destPoint,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback) => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, IntPtr.Zero);

    public void EnumerateMetafile(
        Metafile metafile,
        Point destPoint,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback,
        IntPtr callbackData) => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, callbackData, null);

    public void EnumerateMetafile(
        Metafile metafile,
        RectangleF destRect,
        RectangleF srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback) => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, IntPtr.Zero);

    public void EnumerateMetafile(
        Metafile metafile,
        RectangleF destRect,
        RectangleF srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback,
        IntPtr callbackData) => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, callbackData, null);

    public void EnumerateMetafile(
        Metafile metafile,
        Rectangle destRect,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback) => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, IntPtr.Zero);

    public void EnumerateMetafile(
        Metafile metafile,
        Rectangle destRect,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback,
        IntPtr callbackData) => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, callbackData, null);

    public void EnumerateMetafile(
        Metafile metafile,
        PointF[] destPoints,
        RectangleF srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback) => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, IntPtr.Zero);

    public void EnumerateMetafile(
        Metafile metafile,
        PointF[] destPoints,
        RectangleF srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback,
        IntPtr callbackData) => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, callbackData, null);

    public void EnumerateMetafile(
        Metafile metafile,
        Point[] destPoints,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback) => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, IntPtr.Zero);

    public void EnumerateMetafile(
        Metafile metafile,
        Point[] destPoints,
        Rectangle srcRect,
        GraphicsUnit srcUnit,
        EnumerateMetafileProc callback,
        IntPtr callbackData) => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, callbackData, null);

    /// <summary>
    ///  Transforms an array of points from one coordinate space to another using the current world and page
    ///  transformations of this <see cref="Graphics"/>.
    /// </summary>
    /// <param name="destSpace">The destination coordinate space.</param>
    /// <param name="srcSpace">The source coordinate space.</param>
    /// <param name="pts">The points to transform.</param>
    public void TransformPoints(Drawing2D.CoordinateSpace destSpace, Drawing2D.CoordinateSpace srcSpace, params PointF[] pts)
    {
        ArgumentNullException.ThrowIfNull(pts);
        TransformPoints(destSpace, srcSpace, pts.AsSpan());
    }

    /// <inheritdoc cref="TransformPoints(Drawing2D.CoordinateSpace, Drawing2D.CoordinateSpace, PointF[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void TransformPoints(Drawing2D.CoordinateSpace destSpace, Drawing2D.CoordinateSpace srcSpace, params ReadOnlySpan<PointF> pts)
    {
        fixed (PointF* p = pts)
        {
            CheckStatus(PInvokeGdiPlus.GdipTransformPoints(
                NativeGraphics,
                (GdiPlus.CoordinateSpace)destSpace,
                (GdiPlus.CoordinateSpace)srcSpace,
                (GdiPlus.PointF*)p,
                pts.Length));
        }
    }

    /// <inheritdoc cref="TransformPoints(Drawing2D.CoordinateSpace, Drawing2D.CoordinateSpace, PointF[])"/>
    public void TransformPoints(Drawing2D.CoordinateSpace destSpace, Drawing2D.CoordinateSpace srcSpace, params Point[] pts)
    {
        ArgumentNullException.ThrowIfNull(pts);
        TransformPoints(destSpace, srcSpace, pts.AsSpan());
    }

    /// <inheritdoc cref="TransformPoints(Drawing2D.CoordinateSpace, Drawing2D.CoordinateSpace, PointF[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void TransformPoints(Drawing2D.CoordinateSpace destSpace, Drawing2D.CoordinateSpace srcSpace, params ReadOnlySpan<Point> pts)
    {
        fixed (Point* p = pts)
        {
            CheckStatus(PInvokeGdiPlus.GdipTransformPointsI(
                NativeGraphics,
                (GdiPlus.CoordinateSpace)destSpace,
                (GdiPlus.CoordinateSpace)srcSpace,
                (GdiPlus.Point*)p,
                pts.Length));
        }
    }

    /// <summary>
    ///  GDI+ will return a 'generic error' when we attempt to draw an Emf
    ///  image with width/height == 1. Here, we will hack around this by
    ///  resetting the Status. Note that we don't do simple arg checking
    ///  for height || width == 1 here because transforms can be applied to
    ///  the Graphics object making it difficult to identify this scenario.
    /// </summary>
    private static void IgnoreMetafileErrors(Image image, ref Status errorStatus)
    {
        if (errorStatus != Status.Ok && image.RawFormat.Equals(ImageFormat.Emf))
            errorStatus = Status.Ok;

        GC.KeepAlive(image);
    }

    /// <summary>
    ///  Creates a Region class only if the native region is not infinite.
    /// </summary>
    internal Region? GetRegionIfNotInfinite()
    {
        GpRegion* regionHandle;
        CheckStatus(PInvokeGdiPlus.GdipCreateRegion(&regionHandle));

        try
        {
            BOOL isInfinite;
            PInvokeGdiPlus.GdipGetClip(NativeGraphics, regionHandle);

            CheckStatus(PInvokeGdiPlus.GdipIsInfiniteRegion(
                regionHandle,
                NativeGraphics,
                &isInfinite));

            if (isInfinite)
            {
                // Infinite
                return null;
            }

            Region region = new(regionHandle);
            regionHandle = null;
            return region;
        }
        finally
        {
            if (regionHandle is not null)
            {
                PInvokeGdiPlus.GdipDeleteRegion(regionHandle);
            }
        }
    }

    /// <summary>
    ///  Represents an object used in connection with the printing API, it is used to hold a reference to a
    ///  PrintPreviewGraphics (fake graphics) or a printer DeviceContext (and maybe more in the future).
    /// </summary>
    internal object? PrintingHelper
    {
        get => _printingHelper;
        set
        {
            Debug.Assert(_printingHelper is null, "WARNING: Overwriting the printing helper reference!");
            _printingHelper = value;
        }
    }

    /// <summary>
    ///  CopyPixels will perform a gdi "bitblt" operation to the source from the destination with the given size
    ///  and specified raster operation.
    /// </summary>
    public void CopyFromScreen(int sourceX, int sourceY, int destinationX, int destinationY, Size blockRegionSize, CopyPixelOperation copyPixelOperation)
    {
        switch (copyPixelOperation)
        {
            case CopyPixelOperation.Blackness:
            case CopyPixelOperation.NotSourceErase:
            case CopyPixelOperation.NotSourceCopy:
            case CopyPixelOperation.SourceErase:
            case CopyPixelOperation.DestinationInvert:
            case CopyPixelOperation.PatInvert:
            case CopyPixelOperation.SourceInvert:
            case CopyPixelOperation.SourceAnd:
            case CopyPixelOperation.MergePaint:
            case CopyPixelOperation.MergeCopy:
            case CopyPixelOperation.SourceCopy:
            case CopyPixelOperation.SourcePaint:
            case CopyPixelOperation.PatCopy:
            case CopyPixelOperation.PatPaint:
            case CopyPixelOperation.Whiteness:
            case CopyPixelOperation.CaptureBlt:
            case CopyPixelOperation.NoMirrorBitmap:
                break;
            default:
                throw new InvalidEnumArgumentException(nameof(copyPixelOperation), (int)copyPixelOperation, typeof(CopyPixelOperation));
        }

        int destWidth = blockRegionSize.Width;
        int destHeight = blockRegionSize.Height;

        using var screenDC = GetDcScope.ScreenDC;
        if (screenDC == 0)
        {
            // ERROR_INVALID_HANDLE - if you pass an empty handle to BitBlt you'll get this error.
            // Checking here to better describe test failures (and avoids taking the Graphics HDC lock).
            throw new Win32Exception(6);
        }

        HDC targetDC = (HDC)GetHdc();
        try
        {
            BOOL result = PInvokeCore.BitBlt(
                targetDC,
                destinationX,
                destinationY,
                destWidth,
                destHeight,
                screenDC,
                sourceX,
                sourceY,
                (ROP_CODE)copyPixelOperation);

            if (!result)
            {
                throw new Win32Exception();
            }
        }
        finally
        {
            if (!targetDC.IsNull)
            {
                ReleaseHdc();
            }
        }
    }

    public Color GetNearestColor(Color color)
    {
        uint nearest = (uint)color.ToArgb();
        CheckStatus(PInvokeGdiPlus.GdipGetNearestColor(NativeGraphics, &nearest));
        return Color.FromArgb((int)nearest);
    }

    /// <summary>
    ///  Draws a line connecting the two specified points.
    /// </summary>
    public void DrawLine(Pen pen, float x1, float y1, float x2, float y2)
    {
        ArgumentNullException.ThrowIfNull(pen);
        CheckErrorStatus(PInvokeGdiPlus.GdipDrawLine(NativeGraphics, pen.NativePen, x1, y1, x2, y2));
        GC.KeepAlive(pen);
    }

    /// <inheritdoc cref="DrawBeziers(Pen, Point[])"/>
    public void DrawBeziers(Pen pen, params PointF[] points) =>
        DrawBeziers(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawBeziers(Pen, Point[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawBeziers(Pen pen, params ReadOnlySpan<PointF> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (PointF* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawBeziers(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.PointF*)p, points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Draws a series of cubic Bézier curves from an array of points.
    /// </summary>
    /// <param name="pen">The <paramref name="pen"/> to draw the Bézier with.</param>
    /// <param name="points">
    ///  Points that represent the points that determine the curve. The number of points in the array
    ///  should be a multiple of 3 plus 1, such as 4, 7, or 10.
    /// </param>
    public void DrawBeziers(Pen pen, params Point[] points) => DrawBeziers(pen, points.OrThrowIfNull().AsSpan());

    /// <inheritdoc cref="DrawBeziers(Pen, Point[])"/>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void DrawBeziers(Pen pen, params ReadOnlySpan<Point> points)
    {
        ArgumentNullException.ThrowIfNull(pen);

        fixed (Point* p = points)
        {
            CheckErrorStatus(PInvokeGdiPlus.GdipDrawBeziersI(
                NativeGraphics,
                pen.NativePen,
                (GdiPlus.Point*)p,
                points.Length));
        }

        GC.KeepAlive(pen);
    }

    /// <summary>
    ///  Fills the interior of a path.
    /// </summary>
    public void FillPath(Brush brush, GraphicsPath path)
    {
        ArgumentNullException.ThrowIfNull(brush);
        ArgumentNullException.ThrowIfNull(path);

        CheckErrorStatus(PInvokeGdiPlus.GdipFillPath(
            NativeGraphics,
            brush.NativeBrush,
            path._nativePath));

        GC.KeepAlive(brush);
        GC.KeepAlive(path);
    }

    /// <summary>
    ///  Fills the interior of a <see cref='Region'/>.
    /// </summary>
    public void FillRegion(Brush brush, Region region)
    {
        ArgumentNullException.ThrowIfNull(brush);
        ArgumentNullException.ThrowIfNull(region);

        CheckErrorStatus(PInvokeGdiPlus.GdipFillRegion(
            NativeGraphics,
            brush.NativeBrush,
            region.NativeRegion));

        GC.KeepAlive(brush);
        GC.KeepAlive(region);
    }

    public void DrawIcon(Icon icon, int x, int y)
    {
        ArgumentNullException.ThrowIfNull(icon);

        if (_backingImage is not null)
        {
            // We don't call the icon directly because we want to stay in GDI+ all the time
            // to avoid alpha channel interop issues between gdi and gdi+
            // so we do icon.ToBitmap() and then we call DrawImage. This is probably slower.
            DrawImage(icon.ToBitmap(), x, y);
        }
        else
        {
            icon.Draw(this, x, y);
        }
    }

    /// <summary>
    ///  Draws this image to a graphics object. The drawing command originates on the graphics
    ///  object, but a graphics object generally has no idea how to render a given image. So,
    ///  it passes the call to the actual image. This version crops the image to the given
    ///  dimensions and allows the user to specify a rectangle within the image to draw.
    /// </summary>
    public void DrawIcon(Icon icon, Rectangle targetRect)
    {
        ArgumentNullException.ThrowIfNull(icon);

        if (_backingImage is not null)
        {
            // We don't call the icon directly because we want to stay in GDI+ all the time
            // to avoid alpha channel interop issues between gdi and gdi+
            // so we do icon.ToBitmap() and then we call DrawImage. This is probably slower.
            DrawImage(icon.ToBitmap(), targetRect);
        }
        else
        {
            icon.Draw(this, targetRect);
        }
    }

    /// <summary>
    ///  Draws this image to a graphics object. The drawing command originates on the graphics
    ///  object, but a graphics object generally has no idea how to render a given image. So,
    ///  it passes the call to the actual image. This version stretches the image to the given
    ///  dimensions and allows the user to specify a rectangle within the image to draw.
    /// </summary>
    public void DrawIconUnstretched(Icon icon, Rectangle targetRect)
    {
        ArgumentNullException.ThrowIfNull(icon);

        if (_backingImage is not null)
        {
            DrawImageUnscaled(icon.ToBitmap(), targetRect);
        }
        else
        {
            icon.DrawUnstretched(this, targetRect);
        }
    }

    public void EnumerateMetafile(
        Metafile metafile,
        PointF destPoint,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
    {
        using EnumerateMetafileDataAdapter adapter = new(callback);
        PInvokeGdiPlus.GdipEnumerateMetafileDestPoint(
            NativeGraphics,
            metafile.Pointer(),
            (GdiPlus.PointF*)&destPoint,
            adapter.NativeCallback,
            (void*)callbackData,
            imageAttr.Pointer()).ThrowIfFailed();

        GC.KeepAlive(imageAttr);
        GC.KeepAlive(metafile);
        GC.KeepAlive(this);
    }

    public void EnumerateMetafile(
        Metafile metafile,
        Point destPoint,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
        => EnumerateMetafile(metafile, (PointF)destPoint, callback, callbackData, imageAttr);

    public void EnumerateMetafile(
        Metafile metafile,
        RectangleF destRect,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
    {
        using EnumerateMetafileDataAdapter adapter = new(callback);
        PInvokeGdiPlus.GdipEnumerateMetafileDestRect(
            NativeGraphics,
            metafile.Pointer(),
            (RectF*)&destRect,
            adapter.NativeCallback,
            (void*)callbackData,
            imageAttr.Pointer()).ThrowIfFailed();

        GC.KeepAlive(imageAttr);
        GC.KeepAlive(metafile);
        GC.KeepAlive(this);
    }

    public void EnumerateMetafile(
        Metafile metafile,
        Rectangle destRect,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr) => EnumerateMetafile(metafile, (RectangleF)destRect, callback, callbackData, imageAttr);

    public void EnumerateMetafile(
        Metafile metafile,
        PointF[] destPoints,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
    {
        ArgumentNullException.ThrowIfNull(destPoints);

        if (destPoints.Length != 3)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidParallelogram);

        fixed (PointF* p = destPoints)
        {
            using EnumerateMetafileDataAdapter adapter = new(callback);
            PInvokeGdiPlus.GdipEnumerateMetafileDestPoints(
                NativeGraphics,
                metafile.Pointer(),
                (GdiPlus.PointF*)p, destPoints.Length,
                adapter.NativeCallback,
                (void*)callbackData,
                imageAttr.Pointer()).ThrowIfFailed();

            GC.KeepAlive(imageAttr);
            GC.KeepAlive(metafile);
            GC.KeepAlive(this);
        }
    }

    public void EnumerateMetafile(
        Metafile metafile,
        Point[] destPoints,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
    {
        ArgumentNullException.ThrowIfNull(destPoints);

        if (destPoints.Length != 3)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidParallelogram);

        fixed (Point* p = destPoints)
        {
            using EnumerateMetafileDataAdapter adapter = new(callback);
            PInvokeGdiPlus.GdipEnumerateMetafileDestPointsI(
                NativeGraphics,
                metafile.Pointer(),
                (GdiPlus.Point*)p, destPoints.Length,
                adapter.NativeCallback,
                (void*)callbackData,
                imageAttr.Pointer()).ThrowIfFailed();

            GC.KeepAlive(imageAttr);
            GC.KeepAlive(metafile);
            GC.KeepAlive(this);
        }
    }

    public void EnumerateMetafile(
        Metafile metafile,
        PointF destPoint,
        RectangleF srcRect,
        GraphicsUnit unit,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
    {
        using EnumerateMetafileDataAdapter adapter = new(callback);
        PInvokeGdiPlus.GdipEnumerateMetafileSrcRectDestPoint(
            NativeGraphics,
            metafile.Pointer(),
            (GdiPlus.PointF*)&destPoint,
            (RectF*)&srcRect,
            (Unit)unit,
            adapter.NativeCallback,
            (void*)callbackData,
            imageAttr.Pointer()).ThrowIfFailed();

        GC.KeepAlive(imageAttr);
        GC.KeepAlive(metafile);
        GC.KeepAlive(this);
    }

    public void EnumerateMetafile(
        Metafile metafile,
        Point destPoint,
        Rectangle srcRect,
        GraphicsUnit unit,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr) => EnumerateMetafile(
            metafile,
            (PointF)destPoint,
            (RectangleF)srcRect,
            unit,
            callback,
            callbackData,
            imageAttr);

    public void EnumerateMetafile(
        Metafile metafile,
        RectangleF destRect,
        RectangleF srcRect,
        GraphicsUnit unit,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
    {
        using EnumerateMetafileDataAdapter adapter = new(callback);
        PInvokeGdiPlus.GdipEnumerateMetafileSrcRectDestRect(
            NativeGraphics,
            metafile.Pointer(),
            (RectF*)&destRect,
            (RectF*)&srcRect,
            (Unit)unit,
            adapter.NativeCallback,
            (void*)callbackData,
            imageAttr.Pointer()).ThrowIfFailed();

        GC.KeepAlive(imageAttr);
        GC.KeepAlive(metafile);
        GC.KeepAlive(this);
    }

    public void EnumerateMetafile(
        Metafile metafile,
        Rectangle destRect,
        Rectangle srcRect,
        GraphicsUnit unit,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr) => EnumerateMetafile(metafile, (RectangleF)destRect, srcRect, unit, callback, callbackData, imageAttr);

    public void EnumerateMetafile(
        Metafile metafile,
        PointF[] destPoints,
        RectangleF srcRect,
        GraphicsUnit unit,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
    {
        ArgumentNullException.ThrowIfNull(destPoints);

        if (destPoints.Length != 3)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidParallelogram);

        fixed (PointF* p = destPoints)
        {
            using EnumerateMetafileDataAdapter adapter = new(callback);
            PInvokeGdiPlus.GdipEnumerateMetafileSrcRectDestPoints(
                NativeGraphics,
                metafile.Pointer(),
                (GdiPlus.PointF*)p, destPoints.Length,
                (RectF*)&srcRect,
                (Unit)unit,
                adapter.NativeCallback,
                (void*)callbackData,
                imageAttr.Pointer()).ThrowIfFailed();

            GC.KeepAlive(imageAttr);
            GC.KeepAlive(metafile);
            GC.KeepAlive(this);
        }
    }

    public void EnumerateMetafile(
        Metafile metafile,
        Point[] destPoints,
        Rectangle srcRect,
        GraphicsUnit unit,
        EnumerateMetafileProc callback,
        IntPtr callbackData,
        ImageAttributes? imageAttr)
    {
        ArgumentNullException.ThrowIfNull(destPoints);

        if (destPoints.Length != 3)
            throw new ArgumentException(SR.GdiplusDestPointsInvalidParallelogram);

        fixed (Point* p = destPoints)
        {
            using EnumerateMetafileDataAdapter adapter = new(callback);
            PInvokeGdiPlus.GdipEnumerateMetafileSrcRectDestPointsI(
                NativeGraphics,
                metafile.Pointer(),
                (GdiPlus.Point*)p, destPoints.Length,
                (Rect*)&srcRect,
                (Unit)unit,
                adapter.NativeCallback,
                (void*)callbackData,
                imageAttr.Pointer()).ThrowIfFailed();

            GC.KeepAlive(imageAttr);
            GC.KeepAlive(metafile);
            GC.KeepAlive(this);
        }
    }

    /// <summary>
    ///  Combines current Graphics context with all previous contexts.
    ///  When BeginContainer() is called, a copy of the current context is pushed into the GDI+ context stack, it keeps track of the
    ///  absolute clipping and transform but reset the public properties so it looks like a brand new context.
    ///  When Save() is called, a copy of the current context is also pushed in the GDI+ stack but the public clipping and transform
    ///  properties are not reset (cumulative). Consecutive Save context are ignored with the exception of the top one which contains
    ///  all previous information.
    ///  The return value is an object array where the first element contains the cumulative clip region and the second the cumulative
    ///  translate transform matrix.
    ///  WARNING: This method is for internal FX support only.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Obsolete("Use the Graphics.GetContextInfo overloads that accept arguments for better performance and fewer allocations.", DiagnosticId = "SYSLIB0016", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
    [SupportedOSPlatform("windows")]
    public object GetContextInfo()
    {
        GetContextInfo(out Matrix3x2 cumulativeTransform, calculateClip: true, out Region? cumulativeClip);
        return new object[] { cumulativeClip ?? new Region(), new Matrix(cumulativeTransform) };
    }

    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;

        if (!cumulativeTransform.IsIdentity)
        {
            currentOffset = cumulativeTransform.Translation;
        }

        while (context is not null)
        {
            if (!context.TransformOffset.IsEmpty())
            {
                cumulativeTransform.Translate(context.TransformOffset);
            }

            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.

                // 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;
            }

            // 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)
            {
                // 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;

            // Ignore subsequent cumulative contexts.
            do
            {
                context = context.Previous;

                if (context is null || !context.Next!.IsCumulative)
                {
                    break;
                }
            }
            while (context.IsCumulative);
        }

        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.

            // 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);
        }
    }

    (HDC hdc, int saveState) IGraphicsContextInfo.GetHdc(ApplyGraphicsProperties apply, bool alwaysSaveState)
    {
        bool applyTransform = apply.HasFlag(ApplyGraphicsProperties.TranslateTransform);
        bool applyClipping = apply.HasFlag(ApplyGraphicsProperties.Clipping);

        int saveState = 0;
        HDC hdc = HDC.Null;

        Region? clipRegion = null;
        PointF offset = default;
        if (applyClipping)
        {
            GetContextInfo(out offset, out clipRegion);
        }
        else if (applyTransform)
        {
            GetContextInfo(out offset);
        }

        using (clipRegion)
        {
            applyTransform = applyTransform && !offset.IsEmpty;
            applyClipping = clipRegion is not null;

            using var graphicsRegion = applyClipping ? clipRegion!.GetRegionScope(this) : default;
            applyClipping = applyClipping && !graphicsRegion!.Region.IsNull;

            hdc = (HDC)GetHdc();

            if (alwaysSaveState || applyClipping || applyTransform)
            {
                saveState = PInvokeCore.SaveDC(hdc);
            }

            if (applyClipping)
            {
                // If the Graphics object was created from a native DC the actual clipping region is the intersection
                // between the original DC clip region and the GDI+ one - for display Graphics it is the same as
                // Graphics.VisibleClipBounds.

                GDI_REGION_TYPE type;

                using RegionScope dcRegion = new(hdc);
                if (!dcRegion.IsNull)
                {
                    type = PInvokeCore.CombineRgn(graphicsRegion!, dcRegion, graphicsRegion!, RGN_COMBINE_MODE.RGN_AND);
                    if (type == GDI_REGION_TYPE.RGN_ERROR)
                    {
                        throw new Win32Exception();
                    }
                }

                type = PInvokeCore.SelectClipRgn(hdc, graphicsRegion!);
                if (type == GDI_REGION_TYPE.RGN_ERROR)
                {
                    throw new Win32Exception();
                }
            }

            if (applyTransform)
            {
                PInvokeCore.OffsetViewportOrgEx(hdc, (int)offset.X, (int)offset.Y, lppt: null);
            }
        }

        return (hdc, saveState);
    }

    /// <summary>
    ///  Gets the cumulative offset.
    /// </summary>
    /// <param name="offset">The cumulative offset.</param>
    [EditorBrowsable(EditorBrowsableState.Never)]
    [SupportedOSPlatform("windows")]
    public void GetContextInfo(out PointF offset)
    {
        GetContextInfo(out Matrix3x2 cumulativeTransform, calculateClip: false, out _);
        Vector2 translation = cumulativeTransform.Translation;
        offset = new PointF(translation.X, translation.Y);
    }

    /// <summary>
    ///  Gets the cumulative offset and clip region.
    /// </summary>
    /// <param name="offset">The cumulative offset.</param>
    /// <param name="clip">The cumulative clip region or null if the clip region is infinite.</param>
    [EditorBrowsable(EditorBrowsableState.Never)]
    [SupportedOSPlatform("windows")]
    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
    {
        get
        {
            if (PrintingHelper is PrintPreviewGraphics ppGraphics)
                return ppGraphics.VisibleClipBounds;

            RectF rect;
            CheckStatus(PInvokeGdiPlus.GdipGetVisibleClipBounds(NativeGraphics, &rect));
            return rect;
        }
    }

    /// <summary>
    ///  Saves the current context into the context stack.
    /// </summary>
    private void PushContext(GraphicsContext context)
    {
        Debug.Assert(context is not null && context.State != 0, "GraphicsContext object is null or not valid.");

        if (_previousContext is not null)
        {
            // Push context.
            context.Previous = _previousContext;
            _previousContext.Next = context;
        }

        _previousContext = context;
    }

    /// <summary>
    ///  Pops all contexts from the specified one included. The specified context is becoming the current context.
    /// </summary>
    private void PopContext(int currentContextState)
    {
        Debug.Assert(_previousContext is not null, "Trying to restore a context when the stack is empty");
        GraphicsContext? context = _previousContext;

        // Pop all contexts up the stack.
        while (context is not null)
        {
            if (context.State == currentContextState)
            {
                _previousContext = context.Previous;

                // This will dispose all context object up the stack.
                context.Dispose();
                return;
            }

            context = context.Previous;
        }

        Debug.Fail("Warning: context state not found!");
    }

    public GraphicsState Save()
    {
        GraphicsContext context = new(this);
        uint state;
        Status status = PInvokeGdiPlus.GdipSaveGraphics(NativeGraphics, &state);
        GC.KeepAlive(this);

        if (status != Status.Ok)
        {
            context.Dispose();
            throw Gdip.StatusException(status);
        }

        context.State = (int)state;
        context.IsCumulative = true;
        PushContext(context);

        return new GraphicsState((int)state);
    }

    public void Restore(GraphicsState gstate)
    {
        CheckStatus(PInvokeGdiPlus.GdipRestoreGraphics(NativeGraphics, (uint)gstate._nativeState));
        PopContext(gstate._nativeState);
    }

    public GraphicsContainer BeginContainer(RectangleF dstrect, RectangleF srcrect, GraphicsUnit unit)
    {
        GraphicsContext context = new(this);

        uint state;
        Status status = PInvokeGdiPlus.GdipBeginContainer(NativeGraphics, (RectF*)&dstrect, (RectF*)&srcrect, (Unit)unit, &state);
        GC.KeepAlive(this);

        if (status != Status.Ok)
        {
            context.Dispose();
            throw Gdip.StatusException(status);
        }

        context.State = (int)state;
        PushContext(context);

        return new GraphicsContainer((int)state);
    }

    public GraphicsContainer BeginContainer()
    {
        GraphicsContext context = new(this);
        uint state;
        Status status = PInvokeGdiPlus.GdipBeginContainer2(NativeGraphics, &state);
        GC.KeepAlive(this);

        if (status != Status.Ok)
        {
            context.Dispose();
            throw Gdip.StatusException(status);
        }

        context.State = (int)state;
        PushContext(context);

        return new GraphicsContainer((int)state);
    }

    public void EndContainer(GraphicsContainer container)
    {
        ArgumentNullException.ThrowIfNull(container);
        CheckStatus(PInvokeGdiPlus.GdipEndContainer(NativeGraphics, (uint)container._nativeGraphicsContainer));
        PopContext(container._nativeGraphicsContainer);
    }

    public GraphicsContainer BeginContainer(Rectangle dstrect, Rectangle srcrect, GraphicsUnit unit)
        => BeginContainer((RectangleF)dstrect, (RectangleF)srcrect, unit);

    public void AddMetafileComment(byte[] data)
    {
        ArgumentNullException.ThrowIfNull(data);
        fixed (byte* b = data)
        {
            CheckStatus(PInvokeGdiPlus.GdipComment(NativeGraphics, (uint)data.Length, b));
        }
    }

    public static IntPtr GetHalftonePalette()
    {
        if (s_halftonePalette.IsNull)
        {
            lock (s_syncObject)
            {
                if (s_halftonePalette.IsNull)
                {
                    GdiPlusInitialization.EnsureInitialized();
                    s_halftonePalette = PInvokeGdiPlus.GdipCreateHalftonePalette();
                }
            }
        }

        return s_halftonePalette;
    }

    /// <summary>
    ///  GDI+ will return a 'generic error' with specific win32 last error codes when
    ///  a terminal server session has been closed, minimized, etc... We don't want
    ///  to throw when this happens, so we'll guard against this by looking at the
    ///  'last win32 error code' and checking to see if it is either 1) access denied
    ///  or 2) proc not found and then ignore it.
    ///
    ///  The problem is that when you lock the machine, the secure desktop is enabled and
    ///  rendering fails which is expected (since the app doesn't have permission to draw
    ///  on the secure desktop). Not sure if there's anything you can do, short of catching
    ///  the desktop switch message and absorbing all the exceptions that get thrown while
    ///  it's the secure desktop.
    /// </summary>
    private void CheckErrorStatus(Status status)
    {
        if (status == Status.Ok)
        {
            return;
        }

        // Generic error from GDI+ can be GenericError or Win32Error.
        if (status is Status.GenericError or Status.Win32Error)
        {
            WIN32_ERROR error = (WIN32_ERROR)Marshal.GetLastWin32Error();
            if (error == WIN32_ERROR.ERROR_ACCESS_DENIED || error == WIN32_ERROR.ERROR_PROC_NOT_FOUND ||
                    // Here, we'll check to see if we are in a terminal services session.
                    (((PInvokeCore.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_REMOTESESSION) & 0x00000001) != 0) && (error == 0)))
            {
                return;
            }
        }

        GC.KeepAlive(this);

        // Legitimate error, throw our status exception.
        throw Gdip.StatusException(status);
    }

#if NET8_0_OR_GREATER

    /// <summary>
    ///  Draws the given <paramref name="cachedBitmap"/>.
    /// </summary>
    /// <param name="cachedBitmap">The <see cref="CachedBitmap"/> that contains the image to be drawn.</param>
    /// <param name="x">The x-coordinate of the upper-left corner of the drawn image.</param>
    /// <param name="y">The y-coordinate of the upper-left corner of the drawn image.</param>
    /// <exception cref="ArgumentNullException">
    ///  <para><paramref name="cachedBitmap"/> is <see langword="null"/>.</para>
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///  <para>
    ///   The <paramref name="cachedBitmap"/> is not compatible with the <see cref="Graphics"/> device state.
    ///  </para>
    ///  <para>
    ///  - or -
    ///  </para>
    ///  <para>
    ///   The <see cref="Graphics"/> object has a transform applied other than a translation.
    ///  </para>
    /// </exception>
    public void DrawCachedBitmap(CachedBitmap cachedBitmap, int x, int y)
    {
        ArgumentNullException.ThrowIfNull(cachedBitmap);

        CheckStatus(PInvokeGdiPlus.GdipDrawCachedBitmap(
            NativeGraphics,
            (GpCachedBitmap*)cachedBitmap.Handle,
            x, y));

        GC.KeepAlive(cachedBitmap);
    }
#endif

#if NET9_0_OR_GREATER
    /// <inheritdoc cref="DrawImage(Image, Effect, RectangleF, Matrix?, GraphicsUnit, ImageAttributes?)"/>
    public void DrawImage(
        Image image,
        Effect effect) => DrawImage(image, effect, srcRect: default, transform: default, GraphicsUnit.Pixel, imageAttr: null);

    /// <summary>
    ///  Draws a portion of an image after applying a specified effect.
    /// </summary>
    /// <param name="image"><see cref="Image"/> to be drawn.</param>
    /// <param name="effect">The effect to be applied when drawing.</param>
    /// <param name="srcRect">The portion of the image to be drawn. <see cref="RectangleF.Empty"/> draws the full image.</param>
    /// <param name="transform">The transform to apply to the <paramref name="srcRect"/> to determine the destination.</param>
    /// <param name="srcUnit">Unit of measure of the <paramref name="srcRect"/>.</param>
    /// <param name="imageAttr">Additional adjustments to be applied, if any.</param>
    public void DrawImage(
        Image image,
        Effect effect,
        RectangleF srcRect = default,
        Matrix? transform = default,
        GraphicsUnit srcUnit = GraphicsUnit.Pixel,
        ImageAttributes? imageAttr = default)
    {
        PInvokeGdiPlus.GdipDrawImageFX(
            NativeGraphics,
            image.Pointer(),
            srcRect.IsEmpty ? null : (RectF*)&srcRect,
            transform.Pointer(),
            effect.Pointer(),
            imageAttr.Pointer(),
            (Unit)srcUnit).ThrowIfFailed();

        GC.KeepAlive(effect);
        GC.KeepAlive(imageAttr);
        GC.KeepAlive(image);
        GC.KeepAlive(this);
    }
#endif

    private void CheckStatus(Status status)
    {
        status.ThrowIfFailed();
        GC.KeepAlive(this);
    }
}
