Photoshop用ツールをPythonで書く!
~charIDを添えて~

はじめまして、クリーク・アンド・リバー社 COYOTE CG STUDIO テクニカルチームに新しく入った菊地茂です!

趣味は頭を空っぽにして川沿いに散歩すること。一番の記録では片道4時間ぼーっと歩き続け、帰るのに苦労しました。

PhotoshopスクリプトといえばJSX。
しかし弊社では他DCCツール用の言語はPythonに移行完了しつつあり、連携ツールを作るのであればPhotoshop用スクリプトもPythonで書きたい……
しかも編集ではなく、線を描画するなど新しく描画をしたい……!

そんなツール制作で得た知見と感想を備忘録を兼ねて共有いたします。

Photoshopコマンドで出来ることって?

まずはコマンドで出来ることを纏めるとこうです。

上段については本当に情報が少ないですが、サイトにさえ巡り合えれば可能です。
日本語に限らず、英語で調べれば数件ヒットします。

中段ですが、書き換えのやり方さえ分かっていれば楽に書けることもあります。
『Photoshop コマンド』で検索をかけたことがある方には伝わると思いますが、『script listener』でいい感じにしよう!と紹介してくださっているブログがたくさんあると思います。
そのブログに従って書き直しましょう。
ただしLisnnerで何から何まで取ってきてしまうと、一行のコマンドで済むところをcharIDを何行も書くことになってしまうので注意です。
またそもそものコマンドを掘り当てるのが大変なので、コストに見合うかどうかの見積もりもTAとしてやらなくてはなりません。

下段についてはLisnnerですら操作コマンドを探すことができないので難しいです。
もしかしたらやり方はあるかもしれませんが、僕は諦めました。

そもそもLisnnerで出来たログをそのまま使うことを避ける方は多くいると思います。
その中でも大きな理由のひとつはこれでしょう。

psErr

さっきLisnnerから引っ張て来たのにこのバージョンには対応していないってどういう事だ???と大混乱を生み出すエラー。
このせいでListenerから引っ張て来たコマンドはバージョンが変わると使えなくなる、といった認識すらされているのではと思っているほどです。
大体の原因は不必要なコマンドを巻き込んでいることです。
executeActionコマンドでエラーを起こしていますが、あくまでこのコマンドはそれまでのコマンドを実行しようとしているに過ぎないので闇雲に消しても解決にはなりません。
また下記のコードでは正常に稼働しているため、Photoshopバージョンによってコマンドが使用不能になっているわけではありません。
まずは行いたい操作をログから頑張って探してください。

Lisnnerで出てきたログをPythonに書き直す

今回書き直すのはこちらです。


ラインツールで線を画像のように引いていきます。
この動作をLisnnerで吐き出したのがこちら。線を一本書くのにこの量のスクリプトが必要になっています。

jsxコード

var idDraw = charIDToTypeID( "Draw" );
    var desc4 = new ActionDescriptor();
    var idShp = charIDToTypeID( "Shp " );
        var desc5 = new ActionDescriptor();
        var idStrt = charIDToTypeID( "Strt" );
            var desc6 = new ActionDescriptor();
            var idHrzn = charIDToTypeID( "Hrzn" );
            var idPxl = charIDToTypeID( "#Pxl" );
            desc6.putUnitDouble( idHrzn, idPxl, 190.000000 );
            var idVrtc = charIDToTypeID( "Vrtc" );
            var idPxl = charIDToTypeID( "#Pxl" );
            desc6.putUnitDouble( idVrtc, idPxl, 270.000000 );
        var idPnt = charIDToTypeID( "Pnt " );
        desc5.putObject( idStrt, idPnt, desc6 );
        var idEnd = charIDToTypeID( "End " );
            var desc7 = new ActionDescriptor();
            var idHrzn = charIDToTypeID( "Hrzn" );
            var idPxl = charIDToTypeID( "#Pxl" );
            desc7.putUnitDouble( idHrzn, idPxl, 731.000000 );
            var idVrtc = charIDToTypeID( "Vrtc" );
            var idPxl = charIDToTypeID( "#Pxl" );
            desc7.putUnitDouble( idVrPc, idPxl, 664.000000 );
        var idPnt = charIDToTypeID( "Pnt " );
        desc5.putObject( idEnd, idPnt, desc7 );
        var idWdth = charIDToTypeID( "Wdth" );
        var idPxl = charIDToTypeID( "#Pxl" );
        desc5.putUnitDouble( idWdth, idPxl, 1.000000 );
    var idLn = charIDToTypeID( "Ln  " );
    desc4.putObject( idShp, idLn, desc5 );
    var idAntA = charIDToTypeID( "AntA" );
    desc4.putBoolean( idAntA, true );
executeAction( idDraw, desc4, DialogModes.NO );

使い捨てにしている変数を大量に生み出しているので、今回は整えつつPythonに書き直していきます。
Pythonに書き直す方法については分かりやすくまとめて下さっている記事があるので、是非確認してみてください!
https://cgworld.jp/regular/202107-coyote-ta07.html
https://tech-coyote.com/archives/1561
ちなみに僕の場合、諸々のインストールはpipインストールでうまくいきました。
どうやら製作者様がいろいろ修正を加えて下さっているようです。

さて諸々を書き直したらここまで落ち着きました。

Pythonコード

import win32com.client
import photoshop.api as psapi

psPos = [190.000000, 270.000000, 731.000000, 664.000000]
antialias = True
thickness = 1.000000

psapp = win32com.client.Dispatch('PhotoShop.Application.{}'.format(_typNum))
create = win32com.client.Dispatch('PhotoShop.ActionDescriptor.{}'.format(_typNum))
pos = win32com.client.Dispatch('PhotoShop.ActionDescriptor.{}'.format(_typNum))

pos.PutUnitDouble(psapp.CharIDToTypeID('Hrzn'), psapp.CharIDToTypeID('#Pxl'), psPos[0])
pos.PutUnitDouble(psapp.CharIDToTypeID('Vrtc'), psapp.CharIDToTypeID('#Pxl'), psPos[1])
create.PutObject(psapp.CharIDToTypeID('Strt'), psapp.CharIDToTypeID('Pnt '), pos)

pos.PutUnitDouble(psapp.CharIDToTypeID('Hrzn'), psapp.CharIDToTypeID('#Pxl'), psPos[2])
pos.PutUnitDouble(psapp.CharIDToTypeID('Vrtc'), psapp.CharIDToTypeID('#Pxl'), psPos[3])
create.PutObject(psapp.CharIDToTypeID('End '), psapp.CharIDToTypeID('Pnt '), pos)

create.PutUnitDouble(psapp.CharIDToTypeID('Wdth'), psapp.CharIDToTypeID('#Pxl'), thickness)
create.PutObject(psapp.CharIDToTypeID('Shp '), psapp.CharIDToTypeID('Ln  '), create)
create.PutBoolean(psapp.CharIDToTypeID('AntA'), antialias)
psapp.ExecuteAction(psapp.CharIDToTypeID('Draw'), create, psapi.DialogModes.DisplayNoDialogs)

書き始めた当初はwin32comは使わず、psapiのみで記述していました。
しかし複数バージョンに使用することができなかったため今回のような形にしています。
psapiのままのところについては特に問題が起こらなかったため残置しています。
どうやらApplicationやActionDescriptorなどの一部の機能に原因がありそう。そこは気長に調査していきます。

ちなみにnew演算子でインスタンス作成をしていた場所は2つまでまとめています。
それぞれ座標系を宣言するために必要なもの、各種情報を内包して線を描画するものになっているためこれ以上は纏められません。

またlogの最終行のJSXは"DialogModes.NO"とシンプルですが、Pythonでは記述が異なり"DialogModes.DisplayNoDialogs"となっています。
psapiは若干コマンドが変わっていることがあるので、調べたり予測候補に従いましょう。

Suggestion

psapiについては予測候補に頼り切ると僕は決めています。

charIDを読む秘伝のタレ

いやいや、そうはいっても大量に出てくるログから必要なものを見つけるなんてできないよ……って方は是非IDを読んでみてほしいです。
"#Strt"とか"End"、"#Pxl"とかそこはかとなく望んだ動作をしてそうなところを見つけられると思います。
また見つけた後もどこをどう改変すればいいのか把握するにも使えるので、ぜひ注意深く読み込んでみてください。
IDについてまとめて下さっている海外の方がいらっしゃるので、僕はその方の公開しているGitで確認しています。CharIDToTypeID、'Draw'とかで出てくるので是非。

戦いはこれからだ!

ここまでやってきて僕は調子に乗りました。
charIDさえ読めばすべての操作をスクリプトに直せるんじゃないかと思い始めたのです。
そんな僕の心をへし折ったのがこちら。

    var idKnd = charIDToTypeID( "Knd " );
    var idKnd = charIDToTypeID( "Knd " );
    var idmouse = stringIDToTypeID( "mouse" );
    desc12.putEnumerated( idKnd, idKnd, idmouse );

シェイプパスを生成するコマンドの一部なのですが、座標系の数値を入れる場所が見当たらない。
よくよく読んでみると数値じゃなくてマウスポインタの位置を認識してシェイプを作成しているようです。
つまりただ貼り付けるだけの継ぎ接ぎコードではなく、ちゃんと理解してコピペしないとこういった落とし穴にはまりこみます。
Lisnnerで吐き出したlogから新しくスクリプトを作るときは、必ずIDを確認してから作業をしましょう!

最後に

今回は僕が引っ掛かったPhotoshop用スクリプトについてご紹介しました。
僕自身もまだまだ未熟者ですが、この記事を読んでくださった方の一助となれば幸いです。
最後までお読みいただきありがとうございました。


COYOTE 3DCG STUDIO
公式HP:COYOTE 3DCG STUDIO

COYOTE 3DCG STUDIOはクリーク・アンド・リバー社が運営するゲーム専門3DCG制作集団です。
キャラモデル、背景モデル、3Dアニメーション、テクニカルアーティストによるツール開発などを得意としています。
新規立ち上げにおけるコンサルティングから量産制作まで幅広く対応可能な体制を保有しており、出向にも柔軟に対応しております。


Author: .kikuchishigeru