File: System\Drawing\Imaging\ImageAttributes.cs
Web Access
Project: src\src\System.Drawing.Common\src\System.Drawing.Common.csproj (System.Drawing.Common)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.InteropServices;
using System.IO;
using System.Runtime.CompilerServices;
#if NET9_0_OR_GREATER
using System.ComponentModel;
#endif
 
namespace System.Drawing.Imaging;
 
// sdkinc\GDIplusImageAttributes.h
 
// There are 5 possible sets of color adjustments:
//          ColorAdjustDefault,
//          ColorAdjustBitmap,
//          ColorAdjustBrush,
//          ColorAdjustPen,
//          ColorAdjustText,
 
// Bitmaps, Brushes, Pens, and Text will all use any color adjustments
// that have been set into the default ImageAttributes until their own
// color adjustments have been set. So as soon as any "Set" method is
// called for Bitmaps, Brushes, Pens, or Text, then they start from
// scratch with only the color adjustments that have been set for them.
// Calling Reset removes any individual color adjustments for a type
// and makes it revert back to using all the default color adjustments
// (if any). The SetToIdentity method is a way to force a type to
// have no color adjustments at all, regardless of what previous adjustments
// have been set for the defaults or for that type.
 
/// <summary>
///  Contains information about how image colors are manipulated during rendering.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public sealed unsafe class ImageAttributes : ICloneable, IDisposable
{
    private const int ColorMapStackSpace = 32;
 
    internal GpImageAttributes* _nativeImageAttributes;
 
    internal void SetNativeImageAttributes(GpImageAttributes* handle)
    {
        if (handle is null)
            throw new ArgumentNullException(nameof(handle));
 
        _nativeImageAttributes = handle;
    }
 
    /// <summary>
    ///  Initializes a new instance of the <see cref='ImageAttributes'/> class.
    /// </summary>
    public ImageAttributes()
    {
        GpImageAttributes* newImageAttributes;
 
        PInvokeGdiPlus.GdipCreateImageAttributes(&newImageAttributes).ThrowIfFailed();
        SetNativeImageAttributes(newImageAttributes);
    }
 
    internal ImageAttributes(GpImageAttributes* newNativeImageAttributes)
        => SetNativeImageAttributes(newNativeImageAttributes);
 
    /// <summary>
    ///  Cleans up Windows resources for this <see cref='ImageAttributes'/>.
    /// </summary>
    public void Dispose()
    {
        if (_nativeImageAttributes is null)
        {
            return;
        }
 
        Status status = !Gdip.Initialized ? Status.Ok : PInvokeGdiPlus.GdipDisposeImageAttributes(_nativeImageAttributes);
        _nativeImageAttributes = null;
        Debug.Assert(status == Status.Ok, $"GDI+ returned an error status: {status}");
        GC.SuppressFinalize(this);
    }
 
    /// <summary>
    ///  Cleans up Windows resources for this <see cref='ImageAttributes'/>.
    /// </summary>
    ~ImageAttributes() => Dispose();
 
    /// <summary>
    ///  Creates an exact copy of this <see cref='ImageAttributes'/>.
    /// </summary>
    public object Clone()
    {
        GpImageAttributes* clone;
        PInvokeGdiPlus.GdipCloneImageAttributes(_nativeImageAttributes, &clone).ThrowIfFailed();
        GC.KeepAlive(this);
 
        return new ImageAttributes(clone);
    }
 
    /// <summary>
    ///  Sets the 5 X 5 color adjust matrix to the specified <see cref='Matrix'/>.
    /// </summary>
    public void SetColorMatrix(ColorMatrix newColorMatrix) =>
        SetColorMatrix(newColorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Default);
 
    /// <summary>
    ///  Sets the 5 X 5 color adjust matrix to the specified 'Matrix' with the specified 'ColorMatrixFlags'.
    /// </summary>
    public void SetColorMatrix(ColorMatrix newColorMatrix, ColorMatrixFlag flags) =>
        SetColorMatrix(newColorMatrix, flags, ColorAdjustType.Default);
 
    /// <summary>
    ///  Sets the 5 X 5 color adjust matrix to the specified 'Matrix' with the  specified 'ColorMatrixFlags'.
    /// </summary>
    public void SetColorMatrix(ColorMatrix newColorMatrix, ColorMatrixFlag mode, ColorAdjustType type) =>
        SetColorMatrices(newColorMatrix, null, mode, type);
 
    /// <summary>
    ///  Clears the color adjust matrix to all zeroes.
    /// </summary>
    public void ClearColorMatrix() => ClearColorMatrix(ColorAdjustType.Default);
 
    /// <summary>
    ///  Clears the color adjust matrix.
    /// </summary>
    public void ClearColorMatrix(ColorAdjustType type)
    {
        PInvokeGdiPlus.GdipSetImageAttributesColorMatrix(
            _nativeImageAttributes,
            (GdiPlus.ColorAdjustType)type,
            false,
            null,
            null,
            (ColorMatrixFlags)ColorMatrixFlag.Default).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    /// <summary>
    ///  Sets a color adjust matrix for image colors and a separate gray scale adjust matrix for gray scale values.
    /// </summary>
    public void SetColorMatrices(ColorMatrix newColorMatrix, ColorMatrix? grayMatrix) =>
        SetColorMatrices(newColorMatrix, grayMatrix, ColorMatrixFlag.Default, ColorAdjustType.Default);
 
    public void SetColorMatrices(ColorMatrix newColorMatrix, ColorMatrix? grayMatrix, ColorMatrixFlag flags) =>
        SetColorMatrices(newColorMatrix, grayMatrix, flags, ColorAdjustType.Default);
 
    public void SetColorMatrices(
        ColorMatrix newColorMatrix,
        ColorMatrix? grayMatrix,
        ColorMatrixFlag mode,
        ColorAdjustType type)
    {
        ArgumentNullException.ThrowIfNull(newColorMatrix);
 
        if (grayMatrix is not null)
        {
            fixed (void* cm = &newColorMatrix.GetPinnableReference())
            fixed (void* gm = &grayMatrix.GetPinnableReference())
            {
                PInvokeGdiPlus.GdipSetImageAttributesColorMatrix(
                    _nativeImageAttributes,
                    (GdiPlus.ColorAdjustType)type,
                    enableFlag: true,
                    (GdiPlus.ColorMatrix*)cm,
                    (GdiPlus.ColorMatrix*)gm,
                    (ColorMatrixFlags)mode).ThrowIfFailed();
            }
        }
        else
        {
            fixed (void* cm = &newColorMatrix.GetPinnableReference())
            {
                PInvokeGdiPlus.GdipSetImageAttributesColorMatrix(
                    _nativeImageAttributes,
                    (GdiPlus.ColorAdjustType)type,
                    enableFlag: true,
                    (GdiPlus.ColorMatrix*)cm,
                    null,
                    (ColorMatrixFlags)mode).ThrowIfFailed();
            }
        }
 
        GC.KeepAlive(this);
    }
 
    public void SetThreshold(float threshold) => SetThreshold(threshold, ColorAdjustType.Default);
 
    public void SetThreshold(float threshold, ColorAdjustType type) => SetThreshold(threshold, type, enableFlag: true);
 
    public void ClearThreshold() => ClearThreshold(ColorAdjustType.Default);
 
    public void ClearThreshold(ColorAdjustType type) => SetThreshold(0.0f, type, enableFlag: false);
 
    private void SetThreshold(float threshold, ColorAdjustType type, bool enableFlag)
    {
        PInvokeGdiPlus.GdipSetImageAttributesThreshold(
            _nativeImageAttributes,
            (GdiPlus.ColorAdjustType)type,
            enableFlag,
            threshold).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    public void SetGamma(float gamma) => SetGamma(gamma, ColorAdjustType.Default);
 
    public void SetGamma(float gamma, ColorAdjustType type) => SetGamma(gamma, type, enableFlag: true);
 
    public void ClearGamma() => ClearGamma(ColorAdjustType.Default);
 
    public void ClearGamma(ColorAdjustType type) => SetGamma(0.0f, type, enableFlag: false);
 
    private void SetGamma(float gamma, ColorAdjustType type, bool enableFlag)
    {
        PInvokeGdiPlus.GdipSetImageAttributesGamma(
            _nativeImageAttributes,
            (GdiPlus.ColorAdjustType)type,
            enableFlag,
            gamma).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    public void SetNoOp() => SetNoOp(ColorAdjustType.Default);
 
    public void SetNoOp(ColorAdjustType type) => SetNoOp(type, enableFlag: true);
 
    public void ClearNoOp() => ClearNoOp(ColorAdjustType.Default);
 
    public void ClearNoOp(ColorAdjustType type) => SetNoOp(type, enableFlag: false);
 
    private void SetNoOp(ColorAdjustType type, bool enableFlag)
    {
        PInvokeGdiPlus.GdipSetImageAttributesNoOp(
            _nativeImageAttributes,
            (GdiPlus.ColorAdjustType)type,
            enableFlag).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    public void SetColorKey(Color colorLow, Color colorHigh) =>
        SetColorKey(colorLow, colorHigh, ColorAdjustType.Default);
 
    public void SetColorKey(Color colorLow, Color colorHigh, ColorAdjustType type) =>
        SetColorKey(colorLow, colorHigh, type, enableFlag: true);
 
    public void ClearColorKey() => ClearColorKey(ColorAdjustType.Default);
 
    public void ClearColorKey(ColorAdjustType type) => SetColorKey(Color.Empty, Color.Empty, type, enableFlag: false);
 
    private void SetColorKey(Color colorLow, Color colorHigh, ColorAdjustType type, bool enableFlag)
    {
        PInvokeGdiPlus.GdipSetImageAttributesColorKeys(
            _nativeImageAttributes,
            (GdiPlus.ColorAdjustType)type,
            enableFlag,
            (uint)colorLow.ToArgb(),
            (uint)colorHigh.ToArgb()).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    public void SetOutputChannel(ColorChannelFlag flags) => SetOutputChannel(flags, ColorAdjustType.Default);
 
    public void SetOutputChannel(ColorChannelFlag flags, ColorAdjustType type) =>
        SetOutputChannel(type, flags, enableFlag: true);
 
    public void ClearOutputChannel() => ClearOutputChannel(ColorAdjustType.Default);
 
    public void ClearOutputChannel(ColorAdjustType type) =>
        SetOutputChannel(type, ColorChannelFlag.ColorChannelLast, enableFlag: false);
 
    private void SetOutputChannel(ColorAdjustType type, ColorChannelFlag flags, bool enableFlag)
    {
        PInvokeGdiPlus.GdipSetImageAttributesOutputChannel(
            _nativeImageAttributes,
            (GdiPlus.ColorAdjustType)type,
            enableFlag,
            (ColorChannelFlags)flags).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    public void SetOutputChannelColorProfile(string colorProfileFilename) =>
        SetOutputChannelColorProfile(colorProfileFilename, ColorAdjustType.Default);
 
    public void SetOutputChannelColorProfile(string colorProfileFilename, ColorAdjustType type)
    {
        // Called in order to emulate exception behavior from .NET Framework related to invalid file paths.
        Path.GetFullPath(colorProfileFilename);
 
        fixed (char* n = colorProfileFilename)
        {
            PInvokeGdiPlus.GdipSetImageAttributesOutputChannelColorProfile(
                _nativeImageAttributes,
                (GdiPlus.ColorAdjustType)type,
                enableFlag: true,
                n).ThrowIfFailed();
        }
 
        GC.KeepAlive(this);
    }
 
    public void ClearOutputChannelColorProfile() => ClearOutputChannel(ColorAdjustType.Default);
 
    public void ClearOutputChannelColorProfile(ColorAdjustType type)
    {
        PInvokeGdiPlus.GdipSetImageAttributesOutputChannel(
            _nativeImageAttributes,
            (GdiPlus.ColorAdjustType)type,
            false,
            ColorChannelFlags.ColorChannelFlagsLast).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    /// <inheritdoc cref="SetRemapTable(ColorMap[], ColorAdjustType)"/>
    public void SetRemapTable(params ColorMap[] map) => SetRemapTable(map, ColorAdjustType.Default);
 
    /// <summary>
    ///  Sets the default color-remap table.
    /// </summary>
    /// <inheritdoc cref="SetRemapTable(ColorAdjustType, ReadOnlySpan{ColorMap})"/>
#if NET9_0_OR_GREATER
    [EditorBrowsable(EditorBrowsableState.Never)]
#endif
    public void SetRemapTable(ColorMap[] map, ColorAdjustType type)
    {
        ArgumentNullException.ThrowIfNull(map);
        SetRemapTable(type, map);
    }
 
#if NET9_0_OR_GREATER
    /// <inheritdoc cref="SetRemapTable(ColorMap[], ColorAdjustType)"/>
    public void SetRemapTable(params ReadOnlySpan<ColorMap> map) => SetRemapTable(ColorAdjustType.Default, map);
 
    /// <inheritdoc cref="SetRemapTable(ColorMap[], ColorAdjustType)"/>
    public void SetRemapTable(params ReadOnlySpan<(Color OldColor, Color NewColor)> map) => SetRemapTable(ColorAdjustType.Default, map);
#endif
 
    /// <summary>
    ///  Sets the color-remap table for a specified category.
    /// </summary>
    /// <param name="type">
    ///  An element of <see cref="ColorAdjustType"/> that specifies the category for which the color-remap table is set.
    /// </param>
    /// <param name="map">
    ///  A series of color pairs mapping an existing color to a new color.
    /// </param>
#if NET9_0_OR_GREATER
    public
#else
    private
#endif
    void SetRemapTable(ColorAdjustType type, params ReadOnlySpan<ColorMap> map)
    {
        // Color is being generated incorrectly so we can't use GdiPlus.ColorMap directly.
        // https://github.com/microsoft/CsWin32/issues/1121
 
        StackBuffer stackBuffer = default;
        using BufferScope<(ARGB, ARGB)> buffer = new(stackBuffer, map.Length);
 
        for (int i = 0; i < map.Length; i++)
        {
            buffer[i] = (map[i].OldColor, map[i].NewColor);
        }
 
        fixed (void* m = buffer)
        {
            PInvokeGdiPlus.GdipSetImageAttributesRemapTable(
                _nativeImageAttributes,
                (GdiPlus.ColorAdjustType)type,
                enableFlag: true,
                (uint)map.Length,
                (GdiPlus.ColorMap*)m).ThrowIfFailed();
        }
 
        GC.KeepAlive(this);
    }
 
#if NET9_0_OR_GREATER
    /// <inheritdoc cref="SetRemapTable(ColorAdjustType, ReadOnlySpan{ColorMap})"/>
    public void SetRemapTable(ColorAdjustType type, params ReadOnlySpan<(Color OldColor, Color NewColor)> map)
    {
        StackBuffer stackBuffer = default;
        using BufferScope<(ARGB, ARGB)> buffer = new(stackBuffer, map.Length);
 
        for (int i = 0; i < map.Length; i++)
        {
            buffer[i] = (map[i].OldColor, map[i].NewColor);
        }
 
        fixed (void* m = buffer)
        {
            PInvokeGdiPlus.GdipSetImageAttributesRemapTable(
                _nativeImageAttributes,
                (GdiPlus.ColorAdjustType)type,
                enableFlag: true,
                (uint)map.Length,
                (GdiPlus.ColorMap*)m).ThrowIfFailed();
        }
 
        GC.KeepAlive(this);
    }
#endif
 
    [InlineArray(ColorMapStackSpace)]
    private struct StackBuffer
    {
        internal (ARGB, ARGB) _element0;
    }
 
    public void ClearRemapTable() => ClearRemapTable(ColorAdjustType.Default);
 
    public void ClearRemapTable(ColorAdjustType type)
    {
        PInvokeGdiPlus.GdipSetImageAttributesRemapTable(
            _nativeImageAttributes,
            (GdiPlus.ColorAdjustType)type,
            enableFlag: false,
            0,
            null).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    public void SetBrushRemapTable(params ColorMap[] map) => SetRemapTable(map, ColorAdjustType.Brush);
 
#if NET9_0_OR_GREATER
    /// <inheritdoc cref="SetRemapTable(ColorAdjustType, ReadOnlySpan{ColorMap})"/>
    public void SetBrushRemapTable(params ReadOnlySpan<ColorMap> map) => SetRemapTable(ColorAdjustType.Brush, map);
 
    /// <inheritdoc cref="SetRemapTable(ColorAdjustType, ReadOnlySpan{ColorMap})"/>
    public void SetBrushRemapTable(params ReadOnlySpan<(Color OldColor, Color NewColor)> map) => SetRemapTable(ColorAdjustType.Brush, map);
#endif
 
    public void ClearBrushRemapTable() => ClearRemapTable(ColorAdjustType.Brush);
 
    public void SetWrapMode(Drawing2D.WrapMode mode) => SetWrapMode(mode, default, clamp: false);
 
    public void SetWrapMode(Drawing2D.WrapMode mode, Color color) => SetWrapMode(mode, color, clamp: false);
 
    public void SetWrapMode(Drawing2D.WrapMode mode, Color color, bool clamp)
    {
        PInvokeGdiPlus.GdipSetImageAttributesWrapMode(
            _nativeImageAttributes,
            (WrapMode)mode,
            (uint)color.ToArgb(),
            clamp).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    public void GetAdjustedPalette(ColorPalette palette, ColorAdjustType type)
    {
        using var buffer = palette.ConvertToBuffer();
        fixed (void* p = buffer)
        {
            PInvokeGdiPlus.GdipGetImageAttributesAdjustedPalette(
                _nativeImageAttributes,
                (GdiPlus.ColorPalette*)p,
                (GdiPlus.ColorAdjustType)type).ThrowIfFailed();
        }
 
        GC.KeepAlive(this);
    }
}