Photoshopのスポットカラーチャネルに特色を割り当てるExtendScript

# TL;DR
* ScriptListenerで出力されたJSファイルを参考にした
* ColorBookの解析が必要だった
* なぜなら、カラーブックに紐づくIDと特色に紐づくIDが必要だったから

# はじめに
Photoshop上で、特色を設定する際の動作をスクリプト化したかった。
具体的には、以下の動作をスクリプト化したかった。

![result.gif](https://qiita-image-store.s3.amazonaws.com/0/33347/5a9fcd66-d349-a632-4d51-1bbced94891e.gif)


## 検証環境

MacOS 10.13.6
Adobe Photoshop CC 2018

# ScriptListenerで動作を調べてみよう
ScriptListenerについては、[Adobe公式サイト](https://helpx.adobe.com/jp/photoshop/kb/downloadable-plugins-and-content.html#ScriptingListener%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3)を参照ください。
使い方も調べればいくつも見つかると思います。

一応、どういうものかについて、上記サイトの一文を引用しておきます。

> ScriptingListener プラグインは、実行可能な操作のログファイルに Java スクリプトを記録できます。

## 実行結果をのぞいてみよう
ScriptListenerを有効にして、特色を割り当ててみます。
その結果書き出されたログファイルを全て載せると長いので、該当箇所だけ抜粋すると以下の様なコードになっていました。

```js:ScriptingListenerJS.log
var idsetd = charIDToTypeID( "setd" );
    var desc8 = new ActionDescriptor();
    var idnull = charIDToTypeID( "null" );
        var ref1 = new ActionReference();
        var idChnl = charIDToTypeID( "Chnl" );
        var idOrdn = charIDToTypeID( "Ordn" );
        var idTrgt = charIDToTypeID( "Trgt" );
        ref1.putEnumerated( idChnl, idOrdn, idTrgt );
    desc8.putReference( idnull, ref1 );
    var idT = charIDToTypeID( "T   " );
        var desc9 = new ActionDescriptor();
        var idNm = charIDToTypeID( "Nm  " );
        desc9.putString( idNm, """ANPA 71-1 AdPro""" );
        var idClr = charIDToTypeID( "Clr " );
            var desc10 = new ActionDescriptor();
            var idBk = charIDToTypeID( "Bk  " );
            desc10.putString( idBk, """ANPA J[""" );
            var idNm = charIDToTypeID( "Nm  " );
            desc10.putString( idNm, """ANPA 71-1 AdPro""" );
            var idbookID = stringIDToTypeID( "bookID" );
            desc10.putInteger( idbookID, 3000 );
            var idbookKey = stringIDToTypeID( "bookKey" );
            desc10.putData( idbookKey, String.fromCharCode( 32, 55, 49, 45, 49, 68 ) );
        var idBkCl = charIDToTypeID( "BkCl" );
        desc9.putObject( idClr, idBkCl, desc10 );
    var idSCch = charIDToTypeID( "SCch" );
    desc8.putObject( idT, idSCch, desc9 );
executeAction( idsetd, desc8, DialogModes.NO );
```

はい、なんのこっちゃですね。

## 出力ファイルの中身はどうなっているの？
先達が色々調べて、考察した上で、サンプルを載せてくださっています。
わーいわーい！

[Script to assign a Pantone color to a channel in photoshop](https://graphicdesign.stackexchange.com/questions/58055/script-to-assign-a-pantone-color-to-a-channel-in-photoshop)

おや、ちょっと待ってください。
コメントアウトされている箇所があります。

> /*
> haven't observed any difference when those were left out
>
> var idbookID = stringIDToTypeID( "bookID" );
> desc3.putInteger( idbookID, 3060 );
> \*/
>/*
> the code seems to be in ASCII and represent a non-standard pantone ID
>
> var idbookKey = stringIDToTypeID( "bookKey" );
> desc3.putData( idbookKey, String.fromCharCode( 79, 82, 48, 50, 49, 67 ) );
> */

ふむ。

> haven't observed any difference when those were left out

「それらが放棄されたときに何の違いも観察しなかった」 by google先生

> the code seems to be in ASCII and represent a non-standard pantone ID

「コードはASCIIで、非標準のpantone IDを表しているようです」 by google先生

さらに、末尾のコメントにはこうあります。

> Luckily, all seems well even without those lines.

「幸運にも、これらの行がなくてもすべてがうまくいくようです」 by google先生

### 本当になくても良いのか試してみた

#### その1 「 ANPA 」の「 ANPA 71-1 AdPro 」を指定してみる → <font color=red>×</font>

実行した結果がこちら。
<img width="532" alt="error.png" src="https://qiita-image-store.s3.amazonaws.com/0/33347/1f5b8723-141d-9d14-f2d9-71cd491ad0b6.png">

そして、チャネルに当たっているのはなぜかANPAですらなく、PANTONE。

<img width="641" alt="anpa.png" src="https://qiita-image-store.s3.amazonaws.com/0/33347/9e03b42f-5ccf-74d8-d992-9bcd3f95ab1a.png">


#### その2 「 PANTONE solid coated 」の「 PANTONE YELLOW C 」を指定してみる → <font color=red>×</font>

一見、うまくいってそうに見えてうまくいっていないという、一番事故になるやつです。
「PANTONE solid coated」を指定したのに、「PANTONE+ Solid Coated」が割り当てられています。

<img width="641" alt="pantone.png" src="https://qiita-image-store.s3.amazonaws.com/0/33347/6f334432-bd54-17cb-6267-1bbb522485fb.png">


#### その3 「 DIC Color 」の「 DIC 1s 」を指定してみる → <font color=blue>◯</font>
うまくいきました。
<img width="641" alt="dic.png" src="https://qiita-image-store.s3.amazonaws.com/0/33347/a6f371ed-9361-f639-f0c0-bd561cf9ccbb.png">


#### 結論

***うまくいくのもある***が、***<font color=red>うまくいかないのもある</font>***。

よって、ColorBookに紐づくIDと、特色名に紐づくIDが必要になる。
つまり先達のコードのままじゃダメじゃないですか、やだー。

# カラーブックのデータから必要な情報を抜き出そう
私の環境の場合は、以下のパスに全26ファイルのカラーブックが存在します。

`/Applications/Adobe Photoshop CC 2018/Presets/`

先頭のファイルは、`ANPA Color.acb`でした。
拡張子はacbです。
acbとは、Adobe Photoshop Color Book Fileのことらしいです。
完全にAdobeの独自ファイルじゃないですか、やだー。

## acbファイルの仕様ってあるの？
***ないです***。

少なくとも私は発見できませんでした。
ただ、ファイルフォーマットをリバースエンジニアリングして、非公式の仕様にまとめてくださった偉大な先達がいます！

[Adobe Color Book File Format Specification](https://magnetiq.ca/pages/acb-spec/)

しゅごい...でも、わからない！

と、諦めては進めないのでこの仕様書を頼りにNode.JSを使い、ビット演算とか駆使して、最終成果物としてjsonファイルを作りました。

```json
[
  {
    "vendorID": 3000,
    "title": "ANPA Color",
    "colorList": [
      {
        "name": "71-1",
        "displayName": "ANPA 71-1 AdPro",
        "id": " 71-1D"
      },
      {
        "name": "71-2",
        "displayName": "ANPA 71-2 AdPro",
        "id": " 71-2D"
      },
      {
        "name": "71-4",
        "displayName": "ANPA 71-4 AdPro",
        "id": " 71-4D"
      },
      {
        "name": "71-6",
        "displayName": "ANPA 71-6 AdPro",
        "id": " 71-6D"
      },
      {
        "name": "71-8",
        "displayName": "ANPA 71-8 AdPro",
        "id": " 71-8D"
      },
// 以下略
```

# ExtendScriptを書こう

ここまできたら、あと少し。
とりあえず、特色を設定するところだけ抜粋して、実際に動くか試します。

以下の例では、ANPA Colorの先頭の特色を設定してみています。

```ts:sample.ts

/**
 * 特色の設定
 * @param colorBookName カラーブック名
 * @param colorName 色名
 * @param colorBookId カラーブックID
 * @param colorId 特色名ID
 */
function setColor(
	colorBookName: string,
	colorName: string,
	colorBookId: number,
	colorId: string
) {
	const idsetd = app.charIDToTypeID( "setd" );
	const mainDesc = new ActionDescriptor();
	const idnull = app.charIDToTypeID( "null" );
	const ref = new ActionReference();
	const idChnl = app.charIDToTypeID( "Chnl" );
	const idOrdn = app.charIDToTypeID( "Ordn" );
	const idTrgt = app.charIDToTypeID( "Trgt" );
	ref.putEnumerated( idChnl, idOrdn, idTrgt );
	mainDesc.putReference( idnull, ref );
	const idT = app.charIDToTypeID( "T   " );
	// channel名をcolorNameで置換する?
	// const idNm0 = app.charIDToTypeID("Nm  ");
	// mainDesc2.putString(idNm0, colorName);
	// color
	const idClr = app.charIDToTypeID( "Clr " );
	const changeSpotColorDesc = new ActionDescriptor();
	// set the color book to use
	const idBk = app.charIDToTypeID( "Bk  " );
	changeSpotColorDesc.putString( idBk, colorBookName );
	// set the color name
	const idNm = app.charIDToTypeID( "Nm  " );
	changeSpotColorDesc.putString( idNm, colorName );
	// Adobe独自のカラーブックに紐づくID
	const idbookID = app.stringIDToTypeID( "bookID" );
	changeSpotColorDesc.putInteger( idbookID, colorBookId );
	// Adobe独自の特色に紐づく6文字固定サイズのID
	const idbookKey = app.stringIDToTypeID( "bookKey" );
	changeSpotColorDesc.putData( idbookKey, colorId );
	// BkCl = BookColor;
	const idBkCl = app.charIDToTypeID( "BkCl" );

	const setColorChannelDesc = new ActionDescriptor();
	setColorChannelDesc.putObject( idClr, idBkCl, changeSpotColorDesc );
	// Opct = opacity
	const idOpct = app.charIDToTypeID( "Opct" );
	const idPrc = app.charIDToTypeID( "#Prc" );
	setColorChannelDesc.putUnitDouble( idOpct, idPrc, 100 );
	// SCch = SetColorChannel?
	const idSCch = app.charIDToTypeID( "SCch" );
	mainDesc.putObject( idT, idSCch, setColorChannelDesc );
	// 実行

	app.executeAction( idsetd, mainDesc, DialogModes.NO );
}

setColor("ANPA", "ANPA 71-1 AdPro", 3000, " 71-1D");
```

結果として、第一引数と第二引数はなくても動くんじゃなかろうか。
そして、第四引数の先頭にスペースがありますが、IDは6文字なので必須だと思います。

あとは、

1. ユーザにどのチャネルの特色を設定したいか選ばせる
2. 特色一覧を表示して、特色を選択させる
3. `setColor`に値を渡す

でいけます。