【Photoshop】psdファイルのフォーマットについて

業務で psd ファイルを解析する必要があったのでその調査した内容についてまとめます。
ファイルフォーマット仕様に関しては[Adobeが公開している](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml)のでこちらの情報に準じます。

以下のような階層を持った psd ファイルを用意します。
![スクリーンショット 2022-09-05 22.07.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/6cec5df9-e5fd-7801-e193-d196867bf4e3.png)


### 各データのセクションについて
データはいくつかのセクションに分かれており、
・File Header セクション
・Color Mode Data セクション
・Image Resources セクション
・Layer and Mask Information セクション
・Image Data セクション

に分かれています。
Color Mode Dataセクション、Image Resources セクション、Image Dataセクションに関しては今回は使用しなかったので割愛します。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/53c5cd2b-a93f-2e35-3bab-2b3131bb96b3.jpeg" width="500">


#### データ型
この記事では各種データ型サイズは以下のように定義します。

| 型名 | サイズ |
|:-----------|------------:|
| byte   | 1byte |
| short  | 2byte |
| int    | 4byte |
| long   | 8byte |
| double | 8byte |

と定義します。
また、psd ファイルでは上記データ型は全てビッグエンディアンである事に注意してください。
また、文字列はPascal文字列とUnicode文字のどちらかが使われます。

#### PascalString
先頭の1バイトに文字列長が定義されている文字列です。
2バイト、もしくは4バイトでパディングすることがあります。
(パディング長はプロパティによって異なるので注意)

#### UnicodeString
先頭4バイトに文字数が含まれる ビッグエンディアンの2バイト文字列です。
この文字数はバイト数ではないので1文字2バイトで計算する必要がある点に注意してください。


##### Rect
レイヤーの描画範囲、マスクの範囲などで使われるデータ構造です。
| データサイズ | プロパティ名  |
|:-----------|------------:|
| int    | Top |
| int    | Left |
| int    | Bottom |
| int    | Right |



#### File Header セクション

| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------|
| byte[4]    | Signature        | 常に`8BPS`の４文字が含まれる |
| short     | Version          | 常に `1` である事が期待される(ただし**PSB**ファイルの場合は2である) |
| byte[6]   | Reserved         | 常に全て0である(使用されない) |
| short     | NumberOfChannels | チャンネル数(範囲は **1~56** まで) |
| int       | Height           | キャンバスサイズ縦(範囲は **1~30,000** まで)|
| int       | Width            | キャンバスサイズ横(範囲は **1~30,000** まで)|
| short     | Depth            | ビット深度(1, 8, 16, 32) |
| short     | ColorMode        | カラーモード(定義は後述) |

##### カラーモード定義
**enum** として定義すると下記のようになります。

```csharp
enum ColorMode
{
    Bitmap = 0,
    Grayscale = 1,
    Indexed = 2,
    RGB = 3,
    CMYK = 4,
    Multichannel = 7,
    Duotone = 8,
    Lab = 9
}
```

今回はRGBしか使用しなかったので本記事ではカラーモードは **RGB** であるという前提で書きます。


#### Color Mode Data セクション
| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------:|
| int        | Length      | セクション長さ |

カラーモードが Indexed, Duotoneである場合に使用されるらしいのですが、
使用しなかったので内容がよくわからないので本記事ではスキップします。
(Indexed, Duotone以外はこのセクションのサイズは **0** です)

#### Image Resources セクション
| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------:|
| int        | Length      | セクション長さ |


下記のような構造をしたデータブロックが連続して続いているセクションです。
今回は使用しなかったのでこちらの詳細はスキップします。

| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------:|
| byte[4]    | Signature   | 常に`8BIM`の4文字が含まれる |
| short      | ResourceID  | リソースのIDが含まれる。詳細は[こちら](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_38034) |
| PascalString | Name   | 2バイトでパディングされたPascal文字列。<br> <font color="#4682b4">**null**</font> で合った場合も2バイト含まれる(中身は0) |
| int | <font color="#4682b4">Length</font> | Data のサイズです  |
| byte[<font color="#4682b4">Length</font>] | Data | 各ResourceID別に格納されたデータのサイズ |




#### Layer and Mask Information セクション
レイヤー情報が細かく定義されているセクションです。
この記事では主にこのセクションについての説明を行います。

| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------:|
| int | Length | このセクション全体のサイズ |
| <font color="#4682b4">LayerInfo</font> | LayerInfo | レイヤーの基本情報が含まれる<br>(次の項目で説明) |
| <font color="#4682b4">GlobalLayerMaskInformation</font> | GlobalLayerMaskInformation | 使っていないので本記事では説明なし |


##### LayerInfo

| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------:|
| int | Length | このデータブロックのサイズ |
| short | LayerCount | レイヤー数 <font color="#AA0000">(※1</font> |
| <font color="#4682b4">LayerRecord</font>[] | LayerRecords | 各レイヤーの情報が含まれます。(次の項目で説明) |
| <font color="#4682b4">ChannelImageData</font>[] | ChannelImageData | LayerRecordsの各レイヤーの画素情報が順番に定義されています。 |

:::note alert
<font color="#AA0000">※1)</font> このレイヤー数は負の数である場合があります。
その場合は絶対値を使用します。
どうやら一番最初のレイヤーのアルファチャンネルに透明度データが含まれる場合、
負の数になるようです。
:::

##### LayerRecord

| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------|
| Rect | Rect | レイヤーの範囲 |
| short | **ChannelCount** | チャンネル数 |
| <font color="#4682b4">Channel</font>[**ChannelCount**] | Channels | チャンネル情報が含まれます。(次の項目で説明) |
| byte[4] | BlendSignature | ブレンドモードのシグネチャ(常に`8BIM`) |
| byte[4] | BlendMode | 4文字で表すブレンドモード |
| byte | Opacity | 透明度(0〜255) |
| byte | Clipping | クリッピングフラグ |
| byte | Flags | ビットフラグ |
| | ビット0 | 「透明性を保護」にチェックが入っている場合 1<br>そうでない場合は 0 |
| | ビット1 | レイヤーが非表示になっている場合は 1<br>表示状態である場合は 0 |
| | ビット2 | 廃止 |
| | ビット3 | Photoshop 5.0 以降では 1、ビット 4 が有用な情報を持っているかどうかを示す。 |
| | ビット4 | ドキュメントの外観とは無関係なピクセルデータであるか |
| byte | Filler | 常に 0 |
| int | ExtraDataFieldLength | 次のデータブロックの長さ |
|LayerMaskAdjustmentLayerData| Layer mask / adjustment layer data | レイヤーマスクや調整レイヤーの情報が含まれているらしい。<br>あまり使っていないので割愛 |
|LayerBlendingRangesData| Layer blending ranges data | 使っていないので割愛 |
|PascalString| LayerName | レイヤー名<font color="#AA0000"> (※1</font> <br> 4バイトパディング |
|<font color="#4682b4">AdditionalLayerInformation</font>[]| AdditionalLayerInformations | レイヤーに付属する情報<br>(レイヤー名やテキストレイヤーであればテキスト情報、レイヤーグループであるかどうかなど) |

:::note warn
<font color="#AA0000"> ※1)</font>　このレイヤー名は32文字までしか含まれず、フルネームを取得したい場合はAdditionalLayerInformationから **'luni'** タグに含まれる名前を取得したほうが良いです。
:::

##### ブレンドモード
| Key | Blend Mode Name | | 
|:-----------|:-----------:|:-----------:|
| pass | pass | 通過 |
| norm | normal | 通常 |
| diss | dissolve | ディザ合成 |
| dark | darken | 比較（暗） |
| 'mul ' | multiply | 乗算(`mul + 半角スペース1文字`なので注意) |
| idiv | color burn | 焼き込みカラー |
| lbrn | linear burn | 焼き込み(リニア) |
| dkCl | darker color | カラー比較（暗） |
| lite | lighten | 比較（明） |
| scrn | screen | スクリーン |
| 'div ' | color dodge | 覆い焼きカラー |
| lddg | linear dodge | 覆い焼き（リニア）-加算 |
| lgCl | lighter color | カラー比較（明） |
| over | overlay | オーバーレイ |
| sLit | soft light | ソフトライト |
| hLit | hard light | ハードライト |
| vLit | vivid light | ビビッドライト |
| lLit | linear light | リニアライト |
| pLit | pin light | ピンライト |
| hMix | hard mix | ハードミックス |
| diff | difference | 差の絶対値 |
| smud | exclusion | 除外 |
| fsub | subtract | 減算 |
| fdiv | divide | 除算 |
| 'hue ' | hue | 色相　|
| 'sat ' | saturation |　彩度 |
| colr  | color |　カラー |
| 'lum ' | luminosity | 輝度 |

わかりやすくまとめているサイトがありました。
http://applehaguki.blog55.fc2.com/blog-entry-103.html

#### Channel
レイヤーのチャンネル情報
RGBモードであればR, G, B, Transparencyの４種類のチャンネル情報が1つのレイヤーに含まれている。

| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------|
| short | ChannelId | チャンネルID |
| int | Length | チャンネルデータサイズ |

##### チャンネルID
```cpp
enum ChannelId
{
    Red = 0,
    Green = 1,
    Blue = 2,
    Transparency = -1,
    UserLayerMask = -2,
    RealUserLayerMask = -3,
}
```

##### Channel Image Data部分
| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------|
| short | Compression | 圧縮形式 |
| byte[Length] | ImageData | チャンネルの画素情報 |


実際のチャンネルデータ(画素情報)は LayerRecords を全て読み終えた後の領域に存在する点に注意
(便宜上別名にしていますが ChannelImageData と Channel は同じものです。)
コードとして実装するとこのような実装になります。


```csharp
public class LayerInfo
{
    private LayerRecord[] m_layerRecords;
    // LayerInfo の情報を読み取る
    public void Read(IPsdReader reader)
    {
        // データブロックサイズを取得
        int length = reader.ReadInt();

        // レイヤー数を取得
        short layerCount = Math.Abs(reader.ReadShort());

        // LayerRecord を読み込む
        m_layerRecords = new Layer[layerCount];
        for (int i = 0; i < layerCount; ++i)
        {
            m_layerRecords[i] = new LayerRecord();
            m_layerRecords[i].Read(reader);
        }

        // 全 LayerRecord を読み込んだら ChannelImage を読み取る
        foreach (var layerRecord in m_layerRecords)
        {
            layerRecord.ReadChannelImage();
        }
    }
}

public class LayerRecord
{
    private readonly Channel[] m_channels;
    private readonly List<AdditionalLayerInformation> m_additionalLayerInformations = new();

    public void Read(IPsdReader reader)
    {
        Rect = Rectangle.Read(reader); // Rect 情報読み取り
        short channelCount = reader.ReadShort(); // チャンネル数
        m_channels = new Channel[channelCount];
        // チャンネル情報読み取り
        for (int i = 0; i < m_channels.Length; ++i)
        {
            m_channels[i] = new Channel();
            m_channels[i].Read(reader);
        }
        reader.Skip(4); // `8BIM`
        BlendMode = reader.ReadString(4);
        Opacity = reader.ReadByte();
        reader.Skip(1); // Clipping
        Flags = reader.ReadByte();
        reader.Skip(1); // Filler

        int extraDataLength = reader.ReadInt();

        // Layer mask / adjustment layer data
        int layerMaskAndAdjustmentDataLength = reader.ReadInt();
        reader.skip(layerMaskAndAdjustmentDataLength);

        // Layer blending ranges data.
        int layerBlendingDataLength = reader.ReadInt();
        reader.Skip(layerBlendingDataLength);

        LayerName = reader.ReadPascalString(padding: 4);

        // Additional Layer Information
        while (AdditionalLayerInformation.TryReadBlock(this, reader, out AdditionalLayerInformation info))
        {
            m_additionalLayerInformations.Add(info);
        }
    }

    // ImageData を読み取る
    public void ReadChannelImage(IPsdReader reader)
    {
        foreach (var channel in m_channels)
        {
            channel.ReadChannelImage(reader);
        }
    }
}


public class Channel
{
    public ChannelId ChannelId { get; private set; }
    private readonly int m_dataLength;
    public short Compression { get; private set; }
    public byte[] ImageData {get; private set; }

    public void Read(IPsdReader reader)
    {
        ChannelId = reader.ReadShort();
        m_dataLength = reader.ReadInt();
    }

    public void ReadChannelImageData(IPsdReader reader)
    {
        Compression = (Compression)reader.ReadShort();
        ImageData = reader.ReadBytes(m_dataLength - sizeof(Compression));
    }
}
```

<font color="#AA0000">
注) IPsdReader というのはpsdファイルのストリームを持っていてint, short, long, doubleなどのデータ型に変換しながら読み進めることの出来る機能を持ったインターフェースであるという体で読んでください。
</font>

```csharp
interface IPsdReader
{
    byte ReadByte();
    byte[] ReadBytes(long count);
    short ReadShort();
    int ReadInt();
    long ReadLong();
    double ReadDouble();
    void Skip(int byteCount);
    string ReadPascalString(int padding);
    string ReadUnicodeString();
}
```


#### AdditionalLayerInformation
レイヤーに付属する情報ブロック
4文字の識別情報(Code)と紐づいて定義されています。

**共通情報**
| データサイズ | プロパティ名  |  |
|:-----------|:-----------:|:------------:|
| byte[4] | Signature | 常に `8BIM` |
| byte[4] | Code | 識別情報 |
| 以下、識別情報別にそれぞれ定義されたデータを保持する | | |


##### 識別情報
| Code | 概要  | 補足 |
|:-----------|:-----------:|:------------:|
| luni | Unicode文字が含まれるブロック | レイヤー名が含まれる |
| lnsr | レイヤーIDで使われるレイヤー名?を持つブロック | 使い方はよくわかってない |
| lyid | レイヤーIDを持つブロック | 使い方はよくわかってない |
| lsct | レイヤーグループ情報を持つブロック | Section Divider Settingと呼ばれている。<br>レイヤーグループとグループの終端となるレイヤー情報(`</Layer group>`というレイヤーがグループの終端に置かれる)に付加されている。|
| TySh | Type Tool Objectを持つブロック | テキストレイヤーの場合、テキストの情報が含まれる |


##### 識別情報別のデータ構造
###### luni
| データサイズ | | 
|:-----------|:-----------:|
| UnicodeString | Unicode文字列 |

###### lnsr
| データサイズ | | 
|:-----------|:-----------:|
| int | Id for the layer name | |


###### lyid
| データサイズ | | 
|:-----------|:-----------:|
| int | layer id | |

###### lsct
| データサイズ | | |
|:-----------|:-----------:|:-----------:|
| int | SectionDividerType | エディタ上でフォルダが展開されているか or 閉じているか、またはレイヤーグループの終了地点であるかどうか |
| **以下、Lengthが12以上の場合** | | |
| byte[4] | Signature | 常に`8BIM` |
| byte[4] | BlendModeKey | ブレンドモード？ |
| **以下、Lengthが16以上の場合** | | |
| int | SubType | 使っていないのでよくわからない |

###### SectionDividerType
enum として定義すると以下のような感じ

```csharp
// レイヤーグループの状態
enum SectionDividerType
{
    AnyOtherType = 0,
    // 展開されている
    OpenFolder = 1,
    // 閉じている
    ClosedFolder = 2,
    // グループの終了地点(</Layer Group>)である
    BoundingSectionDivider = 3,
}
```

この場合、SectionDividerSettings は以下のような値になります。
![psdファイルフォーマット資料 (6).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/50fcc65b-1d9a-0b64-11d6-be30c8b4e5f6.jpeg)

TypeToolObjectは説明が長くなるので別の記事で書く予定です。


#### レイヤー順番

レイヤーの順番は一番下から順になっており、この場合は 

wood
↓
＜/Layer group＞ (もしくは ＜/Layer set＞ )
↓
img
↓
tape shadow
という順番になります。
構造を図にすると以下のような感じです。

![psdファイルフォーマット資料 (5).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/ec47d53e-d2ec-5e53-0b67-0cf01b91d200.jpeg)


#### Image Data セクション
このセクションには全てのレイヤーを統合した画像情報が含まれます。
色情報はChannelImageと異なり、Red, Green, Blueの順番で格納されているデータ



#### データブロックをスキップしたい場合
だいたいのデータブロックは先頭に4バイトのデータ長が含まれているので必要ないブロックに関しては取得したデータブロックサイズ分だけストリームを進めればスキップ可能


[続く](https://qiita.com/kawawa1989/items/500f02197cc95d46ae0c)