【Photoshop】psdファイルのフォーマットについて その3 RLE圧縮の展開

#### 前回
[psdファイルのフォーマットについて](https://qiita.com/kawawa1989/items/16e7bf44dff9c61c6687)
[psdファイルのフォーマットについて その2 TypeToolObject](https://qiita.com/kawawa1989/items/500f02197cc95d46ae0c)

チャンネルの画像データは圧縮形式が無圧縮であればそのままで問題ないのですが、RLE圧縮形式(PackBits)である場合があるのでそれを展開する必要があります。

#### RLEとは
ランレングス圧縮 【連長圧縮】 RLEと呼ばれる圧縮方式で、最も基本的な圧縮アルゴリズムの一つ。

連続して現れる符号を、繰り返しの回数を表す値に置き換える方式。
可逆圧縮なので圧縮時に品質が損なわれるなどはない

psdではRLE圧縮されている画素データは **PackBits** という方式で符号が連続するケース、連続しないケースで区別して処理するようになっています。

この **符号が連続するケース** と **連続しないケース** は正の数であれば連続、負の数であれば非連続
というように説明されることが多いですが、psdでは逆で正の数が非連続データ、負の数が連続データとなっています。
(この辺りは調べても記事によって説明が異なるのでややこしいですね...)

PackBits は連続するデータである場合、`-連続数 + 1` の値が符号に付け足されます。
逆に非連続データである場合は `非連続数 - 1` の値が符号に付け足されます。
![psdファイルフォーマット資料.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/454a5cfc-96da-79d4-07c6-090560483f0c.jpeg)


まず先頭(画像の縦幅×2バイトの領域)に画像の１行１行のデータサイズが定義されています。
その後、圧縮されたデータが含まれます。
![psdファイルフォーマット資料 (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/ca6d9461-e7a6-f2c6-77a0-dfbeca105f93.jpeg)


各行のlength分だけデータを読み取り、規則に従って展開していくと画像横幅と同じデータサイズになります。
![psdファイルフォーマット資料 (3).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/c449e537-81ae-62de-df42-e8618c92a612.jpeg)




実装にすると以下のようになります。

```csharp
public static class RleDecompressor
{
    public static void Decompress(byte[] data, int height, int width, int depthDataSize, out byte[] output)
    {
        output = new byte[height * width * depthDataSize];

        // 配列要素の0〜(height*2)までに長さ情報が含まれる(長さ情報は1要素2byteであるため、height*2のサイズとなる。)
        // (height*2)以降に実データが含まれる
        Span<byte> input = data.AsSpan(2 * height);
        using (MemoryStream stream = new(data, 0, height * 2))
        {
            using (PsdReader reader = new(stream, false))
            {
                for (int i = 0; i < height; ++i)
                {
                    int length = reader.ReadShort();
                    DecompressRow(input, length, output.AsSpan(i * width * depthDataSize));
                    input = input.Slice(length);
                }
            }
        }
    }

    /// <summary>
    /// PackBits 形式の連続符号データを展開する。
    /// </summary>
    /// <remarks>
    /// input に含まれる値を length まで読み込む。
    /// 1バイト読み込み、読み込んだ値が正の数である場合、その値は非連続データとして扱われる。(長さは非連続データの数 - 1のとして設定されるので、1加算する必要がある)
    /// 例えば "abcd" という値である場合、非連続データが4つ続くので "3abcd" となる
    ///
    /// 負の数である場合は連続データとして扱われる。(範囲は-1 〜 -127までであり、-128は含まれない)
    /// -連続する回数 + 1の値が指定される。
    /// 例えば、 "aaaa" という値である場合、連続データが4つ続くので -4 + 1 で -3 で "-3a" になる。
    /// </remarks>
    /// <param name="input">入力データ</param>
    /// <param name="length">入力データの長さ</param>
    /// <param name="output">出力先</param>
    /// <see href="http://www.snap-tck.com/room03/c02/comp/comp02.html"/>
    private static void DecompressRow(Span<byte> input, int length, Span<byte> output)
    {
        int inputPosition = 0;
        int outputPosition = 0;
        while (inputPosition < length)
        {
            sbyte readByte = (sbyte)input[inputPosition];
            inputPosition += 1;

            // 正の数である場合
            if (readByte >= 0)
            {
                // 非連続データ長 - 1の値が含まれているため、1加算する
                for (int i = 0; i < readByte + 1; ++i)
                {
                    output[outputPosition] = input[inputPosition];
                    outputPosition += 1;
                    inputPosition += 1;
                }
            }
            else if (readByte != -128)
            {
                // 連続データ長 - 1 であるため、1加算する
                for (int i = 0; i < 1 - readByte; ++i)
                {
                    output[outputPosition] = input[inputPosition];
                    outputPosition += 1;
                }

                inputPosition += 1;
            }
        }
    }
}
```