ImageMagick で PSD のレイヤー情報を JSON 形式で取り出す & 関連情報

~~PSD を JavaScript で扱いたかったのですが、[PSD.js](https://github.com/meltingice/psd.js) がなぜか自分の手持ちの PSD ファイルのパース時にエラーが発生して使えなかっため、ImageMagick で JavaScript から利用しやすい形式に変換することにしました。~~

<font color="#ff8080">※2022/03/31 追記: おそらく PSD.js のバージョンアップで不具合が解決されたため、他の不具合がなければ PSD.js を利用した方が便利かと思います。</font>

参考「[PSD.js by meltingice](https://meltingice.github.io/psd.js/)」
参考「[meltingice/psd.js: A Photoshop PSD file parser for NodeJS and browsers - GitHub](https://github.com/meltingice/psd.js)」

---

動作確認は WSL の Ubuntu になります。

ImageMagick のバージョンは 6.8.9 です。

# 1. コマンドの例

`input.psd` のレイヤー情報を `output.json` に出力。 

```bash:コマンド
convert input.psd json: | \
    nkf -Sw | \
    sed -r -z \
        -e 's/^/[\n/' -e 's/$/]\n/' \
        -e 's/(\n[^\n]*)"(scene|origingeometry)": ([^\n"]*)\n/\1"\2": "\3",\n/g' \
        -e 's/\}\n\{/\},\n\{/g' | \
    jq '[.[].image] | map({ x: .pageGeometry.x, y: .pageGeometry.y, label: .properties.label, opacity: .artifacts["psd:layer.opacity"], compose: .compose })' \
        >output.json
```

※ `nkf ` は状況に応じて使用。

```json:出力の例
[
  {
    "x": "0",
    "y": "0",
    "label": null,
    "opacity": null,
    "compose": "Over"
  },
  {
    "x": "0",
    "y": "0",
    "label": "レイヤー3",
    "opacity": "0",
    "compose": "Over"
  },
  {
    "x": "0",
    "y": "0",
    "label": "レイヤー2",
    "opacity": "0",
    "compose": "Over"
  },
  {
    "x": "0",
    "y": "0",
    "label": "レイヤー1",
    "opacity": "0",
    "compose": "Over"
  }
]

```

※レイヤーの階層情報はおそらく取得不可。

# 2. 説明

## 2.1. `convert` で PSD の情報を JSON 出力する

`convert input.psd json:` で、PSD のレイヤーごとの情報を JSON 出力できます。ただし、バグと仕様で一部**不正な JSON が出力される**ので、一部の文字列置換が必要になります。

※ ImageMagick のバージョンによって変更される可能性あり。

自分が確認したところ、`origingeometry` と `scene` にダブルクォートが付いていませんでした。

```json:不正な&nbsp;JSON&nbsp;の例
    "pageGeometry": {
      "width": "517",
      "height": "246",
      "x": "0",
      "y": "1354"
    },
    "origingeometry": +0+1354
    "dispose": "Undefined",
    "iterations": "0",
    "scene": 9 of 86
    "compression": "RLE",
    "orientation": "Undefined",
```

また、レイヤーごとのオブジェクトの配列ではなく、単にレイヤーごとの JSON 文字列を連結しただけの出力になるため、まるごと JSON として扱うには文字列の書き換えが必要です。

扱いやすい出力: `[{}, {}, {}, {} ...]`
実際の出力: `{}{}{}{} ...`

ここでは、`sed` を用いて文字列置換します。

```bash
sed -r -z \
    -e 's/^/[\n/' -e 's/$/]\n/' \
    -e 's/(\n[^\n]*)"(scene|origingeometry)": ([^\n"]*)\n/\1"\2": "\3",\n/g' \
    -e 's/\}\n\{/\},\n\{/g'
```

|オプション|説明|
|---|---|
|`-r`|拡張正規表現を使用する|
|`-z`|行の区切りを NULL 文字にする (改行を含めて検索できるようにする)|
|`-e`|正規表現を実行する|

## 2.2. PSD のレイヤー名の文字コードの問題

PSD ファイル内の一部のデータは Unicode で記述されますが、レイヤー名の文字コードは仕様書で定義されていません。

参考「[Layer records - Adobe Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_13084)」

そのため、PSD を扱うソフトウェアごとに文字コードの実装が異なります。
例えば、Windows 版の FireAlpaca ではレイヤー名が Shift-JIS になります。

JavaScript から扱うには何かと UTF-8 が都合が良いので、ここでは `nkf` を用いて Shift-JIS から UTF-8 に変換します。

```bash
nkf -Sw
```

|**文字コード**|**入力**|**出力**|
|---|---|---|
|ISO-2022-JP|J|j|
|Shift-JIS|S|s|
|EUC-JP|E|e|
|UTF <sup>※</sup>|W|w|

※ UTF はさらにオプションで細かく指定可能ですが、デフォルトは UTF-8 です。

## 2.3. `jq` で JSON から必要な情報だけ取り出す

`jq` コマンドを使用することで、コマンドラインから JSON を便利に操作することができます。

参考「[jq Manual (development version)](https://stedolan.github.io/jq/manual/)」
参考「[jq コマンドを使う日常のご紹介 - Qiita](https://qiita.com/takeshinoda@github/items/2dec7a72930ec1f658af)」

各レイヤー情報のオブジェクトのルートに `.image` があり、その下に具体的な情報が格納されています。

|パス|説明|詳細|
|---|---|---|
|.pageGeometry.x|X 座標|レイヤー画像が配置される座標|
|.pageGeometry.y|Y 座標|レイヤー画像が配置される座標|
|.properties.label|レイヤー名|文字コードを調べて対応する必要がある|
|.artifacts["psd:layer.opacity"]|透明度|0: 不透明<br>65535: 透明<br><br>※一般的に扱われる「不透明度」と逆になっていることに注意|
|.compose|ブレンドモード<br>レイヤー表示有無|・**レイヤー非表示**の場合は `None` になり、本来のブレンドモードを**取得できない**<br>・ブレンドモード `Normal` は `Over` 表記<br>・その他のブレンドモードの表記にしては後述の表参照|

※クリッピングの有無は取得できないようです。
※レイヤーの階層情報はおそらく取得不可。

## 2.4. ブレンドモードの対応関係

### 2.4.1. 早見表

別記事に分離しました。

参考「[CSS または JavaScript におけるブレンドモード対応 - Qiita](https://qiita.com/kerupani129/items/13b56fd35e4d228da8a2)」

### 2.4.2. Compose における表記の注意点

- `None` の場合、レイヤーが**非表示**であることが分かるが、表示したときのブレンドモードを**取得できない**。
- PSD の仕様的には「減算」合成も「除算」合成も対応しているが、ImageMagick では「通常」扱い (バージョンによって変更される可能性あり) 。
- 「通貨」フォルダは「通常」フォルダ扱い。
- `LinearDodge` は本来、単なる「加算」合成をさすが、ソフトウェアによっては「加算 (発光)」合成として使用される。

## 2.5. レイヤーの階層情報は取得できない？

`convert` で得られる JSON にはフォルダのレイヤーレコードも含まれますが、JSON のキーや値を参照してもレイヤーかどうかを区別することができないため、少なくとも本記事の手法では階層情報は得られなそうです…。残念…。 

PSD レイヤーの階層情報が欲しい場合は他の手段を使う必要がありそうです。

# 3. 関連情報

## 3.1. レイヤーごとに情報を取得する

`input.psd[5]` のように添え字をつけることで、n 番目のレイヤーの情報のみを取得することができます。
ただし、`[0]` はレイヤーでなく全ての表示レイヤーを結合した画像になる <sup>※</sup> ため、`1` 始まりで指定する。

※ PSD の仕様的に、結合した画像データを含めないこともできるため、全ての PSD について ImageMagick が `[0]` で結合画像の情報を返すかどうかは要確認。

参考「[Input Filename - ImageMagick - Command-line Processing](https://imagemagick.org/script/command-line-processing.php#input)」(※ "Selecting Frames" の項目参照)

## 3.2. レイヤー画像を出力する

```bash:レイヤー画像を&nbsp;3&nbsp;桁の連番で&nbsp;png&nbsp;出力
convert input.psd output%03d.png
```

オプションなしだと、各レイヤーごとに異なるサイズで出力されます (同じサイズの可能性もありますが) 。

オプションでサイズを統一することもできますが、プログラムから利用することを考えると、レイヤーごとに適切なサイズになっていた方が容量を押さえられて動作も快適になり、望ましいと思います。

## 3.3. 直接 webp ファイルにレイヤー分割できない？

`convert` コマンドで webp 形式でレイヤーを出力しようとしたところ、単一の webp ファイルに格納されてしまい、分割することができませんでした…。
`+adjoin` オプションを使用しても不可でした。

※ ImageMagick 6 系列で webp を扱うには別途パッケージ `webp` のインストールが必要。
※ ImageMagick 7 系列では webp 変換に必要なパッケージが異なるため注意。

現状、webp にしたい場合は、一旦 png 等で出力してから以下のようなシェルスクリプトで一括変換することになるかと思います。

```bash:png&nbsp;を一括で&nbsp;webp&nbsp;に変換
#!/bin/sh

images=`find . -type f -name '*.png'`

for image in $images;
do
    cwebp -lossless $image -o ${image%.*}'.webp' >/dev/null 2>&1
done
```