1回目ではカラーマネジメントを有効にして描画することを説明しましたが、記事を読んで何か気になることはありませんでしたか?自分だったらものすごく気になります。「描画色のカラースペースは何なんだ?」って。

2回目はそのあたり、デバイスコンテキストの入力カラースペースについて解説しましょう。

描画色の素性?を知る

この場で解説する範囲を超えるので詳しくは書きませんが、描画される色が何色であるかというのは指定のRGB値がどのカラースペースに属しているかで決まります。同じRGB値であってもsRGBに属しているのか、Adobe RGBに属しているのかで色の解釈は異なります。

で、1回目のサンプルにおいて描画色はどのカラースペースなのかとなるわけですが、これはWindowsの既定のカラースペースということになります。

……なんのこっちゃって思わず突っ込みたくなりますね:-)

色の管理コントロールパネルの詳細設定タブを見ると、Windows 色システムの規定値のデバイスプロファイルという項目があります。

cm_prog_howto_2_fig1

それを適当なRGBカラースペースに変更してみてください。2段目の色が変わるはずです。つまり、これです。

謎は一応解けましたが、描画色のカラースペースをアプリケーション側で決められないのはちょっと困りますね。なので任意のカラースペース(に相当するICCプロファイル)を指定する方法を説明していきます。

任意のカラースペースで描画する

デバイスコンテキストには、入力と出力の2種類のカラースペースを指定することができます。描画色のカラースペースに対応するのは入力側ですが、その入力側のカラースペースを指定するICM2.0関数がSetColorSpace()関数になります。SetColorSpace()関数はデバイスコンテキストのハンドルと論理カラースペースのハンドルの2つを引数にとります。

その論理カラースペースはというと、CreateColorSpace()関数で作成し、不要になったらDeleteColorSpace()関数で破棄することになっています。CreateColorSpace()関数はLOGCOLORSPACE構造体の参照(CやC++ならポインタ)を引数にとります。

LOGCOLORSPACE構造体はlcsCSTypeメンバの値によってsRGB、Windowsの既定のカラースペース、あるいは任意のカラースペースを示します。任意のカラースペースである場合はさらに、ICCプロファイルのパスを指定するか、または一次色のCIE XYZ値とガンマ値からカラースペースを定義するかのどちらかを選択することになります。

以下はLOGCOLORSPACE構造体と関連する型の定義をC#で記述する例です。

    /// <summary>
    /// 論理カラースペースの種類
    /// </summary>
    public enum LogicalColorSpace : uint
    {
        LCS_CALIBRATED_RGB = 0,
        LCS_sRGB = 0x73524742,
        LCS_WINDOWS_COLOR_SPACE = 0x57696E20
    }

    /// <summary>
    /// レンダリングインテント
    /// </summary>
    public enum GamutMappingIntent : uint
    {
        LCS_GM_ABS_COLORIMETRIC = 0x00000008, // 絶対的な色域を維持
        LCS_GM_BUSINESS = 0x00000001, // 彩度
        LCS_GM_GRAPHICS = 0x00000002, // 相対的な色域を維持
        LCS_GM_IMAGES = 0x00000004 // 知覚的
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct CIEXYZ
    {
        public int X;
        public int Y;
        public int Z;
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct CIEXYZTRIPLE
    {
        public CIEXYZ ciexyzRed;
        public CIEXYZ ciexyzGreen;
        public CIEXYZ ciexyzBlue;
    }

    /// <summary>
    /// 論理カラースペースの定義
    /// </summary>
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    unsafe public struct LOGCOLORSPACE
    {
        uint lcsSignature;
        uint lcsVersinon;
        public uint lcsSize;
        public LogicalColorSpace lcsCSType;
        public GamutMappingIntent lcsIntent;
        public CIEXYZTRIPLE lcsEndpoints;
        public uint lcsGammaRed;
        public uint lcsGammaGreen;
        public uint lcsGammaBlue;
        public fixed byte lcsFilename[260]; // 260はMAX_PATHマクロ定数の定義による
        
        // sRGBまたは既定のカラースペースとして初期化
        public LOGCOLORSPACE(bool sRGB)
        {
            lcsSignature = 0x50534F43;
            lcsVersinon = 0x00000400;
            lcsSize = (uint)Marshal.SizeOf(typeof(LOGCOLORSPACE));
            lcsCSType = sRGB ? LogicalColorSpace.LCS_sRGB : LogicalColorSpace.LCS_WINDOWS_COLOR_SPACE;
            lcsIntent = GamutMappingIntent.LCS_GM_GRAPHICS;
            lcsEndpoints = new CIEXYZTRIPLE();
            lcsGammaRed = 0;
            lcsGammaGreen = 0;
            lcsGammaBlue = 0;
        }

        // 指定された(ICCプロファイルの)パスで初期化
        public LOGCOLORSPACE(string filename)
        {
            lcsSignature = 0x50534F43;
            lcsVersinon = 0x00000400;
            lcsSize = (uint)Marshal.SizeOf(typeof(LOGCOLORSPACE));
            lcsCSType = LogicalColorSpace.LCS_CALIBRATED_RGB;
            lcsIntent = GamutMappingIntent.LCS_GM_GRAPHICS;
            lcsEndpoints = new CIEXYZTRIPLE();
            lcsGammaRed = 0;
            lcsGammaGreen = 0;
            lcsGammaBlue = 0;

            byte[] bytes = Encoding.Default.GetBytes(filename.PadRight(260, '\0'));
            bytes[259] = 0;

            for (int i = 0; i < 260; i++)
            {
                fixed (byte* ptr = lcsFilename)
                {
                    ptr[i] = bytes[i];
                }
            }
        }

あとは実際の処理で論理カラースペースを作成し、デバイスコンテキストに指定して描画すればOKです(CIE RGBのプロファイルを使用したのはスクリーンショットが分かりやすくなるようにするためですので、Adobe RGBでもProPhoto RGBでも、RGBプロファイルであれば何を指定して構いません)。

...
                // CIE RGBで描画する
                LOGCOLORSPACE lc = new LOGCOLORSPACE("C:\\(略)\\CIERGB.icc");
                IntPtr hCS = CreateColorSpace(ref lc);
                IntPtr _hCS = SetColorSpace(hDC, hCS);
                g.FillRectangle(rBrush, 0, 120, 50, 50);
                g.FillRectangle(gBrush, 50, 120, 50, 50);
                g.FillRectangle(bBrush, 100, 120, 50, 50);
                SetColorSpace(hDC, _hCS);
                DeleteColorSpace(hCS);
...
cm_prog_howto_2_fig2

サンプルでは3段目が指定プロファイルでの描画になっています。サンプル中で指定したプロファイルと色の管理コントロールパネルの既定のデバイスプロファイルが同じであれば、2段目と3段目が揃うでしょう。

以下にサンプルのリスト全体を掲載します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace GDIICMTest
{
    /// <summary>
    /// 論理カラースペースの種類
    /// </summary>
    public enum LogicalColorSpace : uint
    {
        LCS_CALIBRATED_RGB = 0,
        LCS_sRGB = 0x73524742,
        LCS_WINDOWS_COLOR_SPACE = 0x57696E20
    }

    /// <summary>
    /// レンダリングインテント
    /// </summary>
    public enum GamutMappingIntent : uint
    {
        LCS_GM_ABS_COLORIMETRIC = 0x00000008, // 絶対的な色域を維持
        LCS_GM_BUSINESS = 0x00000001, // 彩度
        LCS_GM_GRAPHICS = 0x00000002, // 相対的な色域を維持
        LCS_GM_IMAGES = 0x00000004 // 知覚的
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct CIEXYZ
    {
        public int X;
        public int Y;
        public int Z;
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct CIEXYZTRIPLE
    {
        public CIEXYZ ciexyzRed;
        public CIEXYZ ciexyzGreen;
        public CIEXYZ ciexyzBlue;
    }

    /// <summary>
    /// 論理カラースペースの定義
    /// </summary>
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    unsafe public struct LOGCOLORSPACE
    {
        uint lcsSignature;
        uint lcsVersinon;
        public uint lcsSize;
        public LogicalColorSpace lcsCSType;
        public GamutMappingIntent lcsIntent;
        public CIEXYZTRIPLE lcsEndpoints;
        public uint lcsGammaRed;
        public uint lcsGammaGreen;
        public uint lcsGammaBlue;
        public fixed byte lcsFilename[260]; // 260はMAX_PATHマクロ定数の定義による
        
        // sRGBまたは既定のカラースペースとして初期化
        public LOGCOLORSPACE(bool sRGB)
        {
            lcsSignature = 0x50534F43;
            lcsVersinon = 0x00000400;
            lcsSize = (uint)Marshal.SizeOf(typeof(LOGCOLORSPACE));
            lcsCSType = sRGB ? LogicalColorSpace.LCS_sRGB : LogicalColorSpace.LCS_WINDOWS_COLOR_SPACE;
            lcsIntent = GamutMappingIntent.LCS_GM_GRAPHICS;
            lcsEndpoints = new CIEXYZTRIPLE();
            lcsGammaRed = 0;
            lcsGammaGreen = 0;
            lcsGammaBlue = 0;
        }

        // 指定された(ICCプロファイルの)パスで初期化
        public LOGCOLORSPACE(string filename)
        {
            lcsSignature = 0x50534F43;
            lcsVersinon = 0x00000400;
            lcsSize = (uint)Marshal.SizeOf(typeof(LOGCOLORSPACE));
            lcsCSType = LogicalColorSpace.LCS_CALIBRATED_RGB;
            lcsIntent = GamutMappingIntent.LCS_GM_GRAPHICS;
            lcsEndpoints = new CIEXYZTRIPLE();
            lcsGammaRed = 0;
            lcsGammaGreen = 0;
            lcsGammaBlue = 0;

            byte[] bytes = Encoding.Default.GetBytes(filename.PadRight(260, '\0'));
            bytes[259] = 0;

            for (int i = 0; i < 260; i++)
            {
                fixed (byte* ptr = lcsFilename)
                {
                    ptr[i] = bytes[i];
                }
            }
        }
    }

    public partial class Form1 : Form
    {
        [DllImport("user32.dll")]
        private extern static IntPtr GetDC(IntPtr hWnd);
        [DllImport("user32.dll")]
        private extern static bool ReleaseDC(IntPtr hWnd, IntPtr hDC);

        /// <summary>
        /// デバイスコンテキストのカラーマネジメント有効・無効を指定
        /// </summary>
        /// <param name="hDC">デバイスコンテキストのハンドル</param>
        /// <param name="iEnableICM">ICM_ON, ICM_OFF, ICM_QUERY, ICM_DONE_OUTSIDEDC のいずれか</param>
        /// <returns>成功なら非ゼロ値、失敗なら0。iEnableICMがICM_QUERYの場合は現在のモードを示す値が返ってくる</returns>

        [DllImport("gdi32.dll")]
        private extern static int SetICMMode(IntPtr hDC, int iEnableICM);

        const int ICM_OFF = 1;
        const int ICM_ON = 2;

        /// <summary>
        /// デバイスコンテキストの入力カラースペースを変更
        /// </summary>
        /// <param name="hDC">デバイスコンテキストのハンドル</param>
        /// <param name="hColorSpace">論理カラースペースのハンドル</param>
        /// <returns>成功なら変更前の論理カラースペースのハンドル、失敗ならNULL</returns>

        [DllImport("gdi32.dll")]
        private extern static IntPtr SetColorSpace(IntPtr hDC, IntPtr hColorSpace);

        /// <summary>
        /// 論理カラースペースを作成
        /// </summary>
        /// <param name="logColorSpace">LOGCOLORSPACE構造体への参照</param>
        /// <returns>成功なら論理カラースペースのハンドル、失敗ならNULL</returns>
        
        [DllImport("gdi32.dll")]
        private extern static IntPtr CreateColorSpace(ref LOGCOLORSPACE logColorSpace);

        /// <summary>
        /// 論理カラースペースを削除
        /// </summary>
        /// <param name="hColorSpace">論理カラースペースのハンドル</param>
        /// <returns>成功ならtrue、失敗ならfalse</returns>
        
        [DllImport("gdi32.dll")]
        private extern static bool DeleteColorSpace(IntPtr hColorSpace);

        public Form1()
        {
            InitializeComponent();

            Paint += (sender, e) =>
            {
                // ウインドウのデバイスコンテキストを取得
                IntPtr hDC = GetDC(Handle);

                Graphics g = Graphics.FromHdc(hDC);

                // 描画色を決める
                Brush rBrush = new SolidBrush(Color.FromArgb(255, 0, 0));
                Brush gBrush = new SolidBrush(Color.FromArgb(0, 255, 0));
                Brush bBrush = new SolidBrush(Color.FromArgb(0, 0, 255));

                // カラーマネジメントOFFで描画する
                SetICMMode(hDC, ICM_OFF);
                g.FillRectangle(rBrush, 0, 0, 50, 50);
                g.FillRectangle(gBrush, 50, 0, 50, 50);
                g.FillRectangle(bBrush, 100, 0, 50, 50);

                // カラーマネジメントONで描画する
                SetICMMode(hDC, ICM_ON);
                g.FillRectangle(rBrush, 0, 60, 50, 50);
                g.FillRectangle(gBrush, 50, 60, 50, 50);
                g.FillRectangle(bBrush, 100, 60, 50, 50);

                // CIE RGBで描画する
                LOGCOLORSPACE lc = new LOGCOLORSPACE("C:\\windows\\system32\\spool\\drivers\\color\\CIERGB.icc");
                IntPtr hCS = CreateColorSpace(ref lc);
                IntPtr _hCS = SetColorSpace(hDC, hCS);
                g.FillRectangle(rBrush, 0, 120, 50, 50);
                g.FillRectangle(gBrush, 50, 120, 50, 50);
                g.FillRectangle(bBrush, 100, 120, 50, 50);
                SetColorSpace(hDC, _hCS);
                DeleteColorSpace(hCS);

                // 後始末
                g.Dispose();
                ReleaseDC(Handle, hDC);
            };
        }
    }
}