DTPab

印刷やデザイン、アドビ製アプリやスクリプトなど、雑多な技術ブログ

もくもく会#3まとめ

はじめに

もう1週間以上経過してしまいましたが、やっと開催報告です。
DTPerのスクリプトもくもく会#3、盛況のうちに無事終了いたしました。ご参加くださった皆さま、ありがとうございました。
今回は大型モニタをお借りできたということもあり、僕も含めこうちゃん(@macneko_ayu)、そしてkmutoさんがモニタを使って発表を行いました。
ただ、本来「もくもく」したかった方には煩わしかったかもしれません。そこは少し反省しつつ、たまには(といってもまだ3回しか開催してないですが)こういうスタイルも面白いな、とも思いました。

会場について

今回の会場は株式会社YUIDEAさん(http://www.yuidea.co.jp/)をお借りしました。実は今年になって合併によって社名が変わったそうで、元を辿ると株式会社シータスさんだそうです。シータスさんといえば、言わずと知れた古旗さんのご著書で僕もお世話になりました。

この書籍の出版に携わったシータスさんの方が実はもくもく会のメンバーとしてご参加くださっており、今回はそのご縁があって会場をお借りできました。この場を借りてお礼申し上げます。

モニターを使った発表

CEPとSUI(こうちゃん)

先述の通り大型モニターをお借りできたということで、まずは主催のこうちゃんより、CEP(CC2015以降対応のエクステンション)とSUI(スクリプトユーザーインターフェイス)の違いを発表がありました。「背景を透明に」の件のあのエクステンションスクリプトを元に、ふたつの構造の違い、コードの違い等を簡単に説明してくれました。

SUIで生成するフローティングウィンドウ

SUI自体はAdobeのアプリケーションで共通で利用できるユーザーインターフェイスです。基本的には、同じコードでInDesignIllustratorPhotoshopなどで利用できます。
さて、SUIで生成できるフローティングウィンドウですが、Windowオブジェクトをadd()する際の第一引数にそのウィンドウタイプを文字列型で渡します。使えるのはdialogpalettewindowで、dialogの場合はウィンドウがモーダル*1なります。
(詳しくは補足・SUIを参照)

CEPで生成するフローティングウィンドウ

CEP(エクステンション)で生成できるウィンドウは、HTMLとCSSインターフェイスを生成し、ほかのアプリケーションのパネルと同様に折りたたんだりドッキングしたりすることができます。
インターフェイス生成の注意としては、CSSの扱いがどうやら本来のブラウザの実装とちょっと動き方が違うらしく、ピンポイントで上書きするようにclassに仕込んでいました。

コールバック

SUIで作ったものは、インターフェイスを搭載したというだけでその実はスクリプトです。アプリケーション内で取得した情報を比較的容易にSUIに反映できます。
一方のCEPは、エクステンションとして動作させるmain.jsと、スクリプトとして動作させるjsxファイル(今回のCEPではhostScript.jsxというファイル)との間で、コールバックを設定してあげる必要があります。JavaScriptにおけるコールバックの説明自体は時間の都合省いていましたが、要するにスクリプト側(jsx側)で受け取った値をエクステンション側に反映させるための受け渡し処理です。
文字列型しか受け渡しができないことに注意してください、とのことでした。

スクリプト作成のためのスクリプト支援(kmutoさん)

続いて、Re:VIEWのkmutoさんが「スクリプト作成のためのスクリプト支援」と題して発表してくださいました。こちらでそのスライドも共有していただいてます。
スクリプトから検索置換を使うことは、特にInDesignでページ物を組むことが多い場合にはよく使うと思います。その現在の正規表現検索窓の設定を、スクリプトの記述に合わせて書き出すという、スクリプト制作者向けの支援スクリプトについてです。
特に面白かったのは、スライドにもある通り「U+E00B」という文字の扱いです。
検索置換パネルに登録する場合、文字・段落スタイル等は文字列かスタイルのオブジェクトそのものを渡します。それがグループにネストされているスタイルの場合は、例えば下記のように指定せねばならないところ、「U+E00B」でスタイル名をつなぐだけで指定できるというものでした。

//段落スタイルグループ「foo」の中の段落スタイル「hoge」を設定する場合
app.findGrepPreferences.appliedParagraphStyle = app.documents[0].paragraphStyleGroups.item("foo").paragraphStyles.item("hoge");
//これが、U+E00Bで挟むだけで済む!?
app.findGrepPreferences.appliedParagraphStyle = "foo"+"\uE00b"+"hoge";

これは衝撃です…!
ただ、kmutoさんによればこれも万能ではなく、TextやGrepでは大丈夫でも、オブジェクト、それもChageObjectでは使えないそうです。

Grepクエリマネージャ(自分)

kmutoさんがスライドの5枚目で僕のスクリプトに言及してくださっていたので、そのスクリプトをちょっとだけご紹介しました。前にツイートしていたものなので、その時のツイートだけ掲載しておきます。


ま、いまはこれを組み直しているので、もう全然違うインターフェイスになりつつあります…^^;;

補足

CEPとSUI

CEPのインストール

こうちゃんのこちらの記事に、CEPのインストール方法と、それに必要なZXPInstallerの入手元が載っています。ありがたや!

SUI

ScriptUserInterface、SUIです。上にも書いたように、Adobeのアプリケーションであればほとんど同じ見かけのUIを生成することができます。
SUIで生成するウィンドウ(パネル?)はWindowオブジェクトであり、生成する際にタイプを指定します。それがdialogpalettewindowです。
僕自身は基本的にwindowは使いません。独立したウィンドウになるのですが、ドキュメントの裏に隠れてしまったり、ウィンドウが大きくなりがちだったり、ちょっと使いづらい部分があるからです。
それはさておき、百聞は一見にしかず。まずはpaletteで試してみましょう。

#targetengine "session"
var dlg = new Window ('palette', "test");
dlg.show();

これだけでも(小さいですが)ウィンドウが表示されると思います。Windowオブジェクトはこのように、[タイプ](, [ウィンドウタイトル])という形で引数を渡して実行します。
1行目に#targetengineというものがありますが、これはスクリプトを動かすエンジンを指定するものです。このターゲットエンジンについての詳しい説明は今回は割愛しますが、これを指定しない場合、スクリプトmainというエンジンで実行されます。このエンジンでは、paletteが正しく表示できません。したがってmain以外の(なんでもOKです)ものを指定します。
一方、dialogについてはターゲットエンジンの指定を忘れてもちゃんと表示されます。

var dlg = new Window ('dialog', "test");
dlg.show();

…表示はされますが、ボタンなどを用意してあげないとウィンドウを閉じられません。しかもpaletteと違ってモーダルダイアログなので、もうアプリが動かなくなります(ESTKから実行している場合、スクリプトの停止ボタンが押せればなんとかなるか…?w)。
そんなわけで、閉じるボタンを作ってあげます。

var dlg = new Window ('dialog', "test");
dlg.add('button', undefined, "CLOSE").onClick = function (){
    dlg.close();
    }
dlg.show();

これが割とベーシックな実装だと(僕は)思います。
それと、シングルクォートとダブルクォートを使い分けていますが、これは僕の好みです。真似する必要はありません^^;;

JSON

JSONについては、過去の記事の後半にちょっとだけ解説をつけたので、まずはこちらからどうぞ。
それと、会場で少しだけお話しましたが、JSONの(というよりオブジェクトのプロパティの)呼び出し方にはドット記法とブラケット記法があります。

var myJSON = {"test1": "これはテスト", "test2": "これもテストです"}

//ドット記法
alert(myJSON.test1);

//ブラケット記法
alert(myJSON["test2"]);

ドット記法は通常のドキュメントオブジェクトモデルを辿っていくのと似たような感じで使うことができるし、なにより簡便です。
一方のブラケット記法は、ちょっとだけ入力が煩わしいところがありますが、こちらにはこんなメリットがあります

var myJSON = {"test1": "これはテスト", "test2": "これもテストです"}
//変数として操作
var myTest = "test";
for (var i=1; i<3; i++){
    alert(myJSON[myTest+i]);
    }
//予約語や不正なプロパティ名の値にもアクセスできる
var newJSON = {"123": "数字で始まるプロパティ", "Object": "予約語"}
alert(newJSON["123"]);

これはブラケット記法を推奨するものではなく、ブラケット記法だとこういう使い方もできる、という一例です。特に必要がなければドット記法で十分でしょう。
それと、一応こんな使い方もできますので、何かのヒントにしてください。

var myDoc = app["activeDocument"];

オブジェクトモデルビューアのファイル

お〜まちさんが教えてくださった情報です。
ExtendScriptToolkitからオブジェクトモデルビューア(デフォルトショートカットでは⌘+/)を開くと、オブジェクトモデルが記述されたxmlファイルが特定の場所に現れる、というもの。
Macの場合は、
[ユーザー]/Library/Preferences/ExtendScript Toolkit/4.0
ここにxmlファイルがバージョンごとにたくさん生成されます。かなり重たいので、CotEditorでも手を焼きました。シンタックスハイライトのないエディタで表示したほうがいいかもしれません。それくらい1ファイルが重いです^^;;

Grepクエリマネージャ

ダイアログを半透明にする

手前味噌ですが、大型モニタでお見せしたSUIのWindowオブジェクトを半透明にする方法です。

#targetengine "opacityTest"
var dlg = new Window("palette", "opacity test");
dlg.add('statictext', undefined, "はんちょう、てすとです。半調テストです。");
dlg.add('button', undefined, "半調").onClick = function (){
    if (dlg.opacity === 1) dlg.opacity = 0.6;
    else dlg.opacity = 1;
    }
dlg.show();

こうするとボタンを押す度に半調が切り替わります。
ただ僕はわざわざ半調のためだけにボタンを用意するのが嫌だったので、会場で紹介したクエリマネージャのスクリプトでは、ウィンドウそのものを(⌘キーを押しながら)クリックしたら半調になる、という仕様にしています。

#targetengine "opacityTest"
var dlg = new Window("palette", "opacity test");
dlg.addEventListener ('click', function(ev){
    if (ScriptUI.environment.keyboardState.metaKey){
        if (dlg.opacity === 1) dlg.opacity = 0.6;
        else dlg.opacity = 1;
        }
    });
dlg.show();

「⌘キーを押しながら」という条件は、ScriptUI.environment.keyboardStateで設定します。keyboardStateの詳細は今回は割愛しますが、特定のキーが押されているかどうかを判別し、押されていればtrueを返します。keyboardState.metaKeyで調べられるのは、Macでは⌘キー、Windowsではウィンドウズキーです。

Undoを記録せずに処理する

会場では少し口頭で触れた程度でしたので、ちょっとだけ補足です。
Undoをコントロールするには、doScriptメソッドを使います。使い方については過去にこの記事で取り上げました。
Illustratorにも同じ名前のメソッドがありますが、それとは全く別です。InDesignでは、実際にExtendScriptAppleScriptのコードを渡したり関数を指定することによって、そのコードを実行します。

質問への対応

最後に、もくもく会閉会間際にいただいた質問について、コードの補足です。
「フレームグリッドの行間lineAkiを調整したいのに、設定が正しく反映されない」
というご質問をまるさんからいただきました。結果的には行間lineAkiと字間characterAkiはポイント単位でしか設定できない、というオチでした。正しいプロパティまで辿り着いたのに、あんまりですよね^^;

単位換算関数

ポイントからmmへ換算、Q/Hからポイントへ換算、ということは、スクリプトを作っていく何度も遭遇するシチュエーションです。そこで単位を任意の単位に換算する関数を考えてみました。

function convertUnits (value, from, to){
    //単位換算を許容する単位系(配列として定義)
    var acceptUnits = ["HA", "Q", "POINTS", "MILLIMETERS"];
    //引数のチェック
    if (arguments.length !== 3) return new Error("引数の数が違います");
    if (isNaN(value)) return new Error("第一引数が数値型ではありません");
    if (typeof from !== "string") return new Error("第二引数が文字列型ではありません");
    if (typeof to !== "string") return new Error("第三引数が文字列型ではありません");
    if (from.indexOf(acceptUnits.join(), 0)>-1 || from === ""){
        return new Error("第二引数が許容単位ではありません");
        }
    if (to.indexOf(acceptUnits.join(), 0)>-1 || to === ""){
        return new Error("第三引数が許容単位ではありません");
        }
    //fromからvalueを一旦mmに換算
    var pt2mm = 25.4/72;
    switch (from){
        case acceptUnits[0]: value /= 4; break;
        case acceptUnits[1]: value /= 4; break;
        case acceptUnits[2]: value *= pt2mm; break;
        }
    //toからvalueを換算
    switch (to){
        case acceptUnits[0]: value *= 4; break;
        case acceptUnits[1]: value *= 4; break;
        case acceptUnits[2]: value /= pt2mm; break;
        }
    return value;
    }

この関数で計算できるのは級・歯、ミリ、ポイントですが、計算式さえ追加していけばいくらでも対応できます。
では順を追って簡単な説明をば。
まず引数ですが、順に「換算したい値value」、「現在の単位from」、「換算したい単位to」を渡します。戻り値は「換算された値」(Number)です。

//単位換算を許容する単位系(配列として定義)
var acceptUnits = ["HA", "Q", "POINTS", "MILLIMETERS"];
//引数のチェック
if (arguments.length !== 3) return new Error("引数の数が違います");
if (isNaN(value)) return new Error("第一引数が数値型ではありません");
if (typeof from !== "string") return new Error("第二引数が文字列型ではありません");
if (typeof to !== "string") return new Error("第三引数が文字列型ではありません");
if (from.indexOf(acceptUnits.join(), 0)>-1 || from === ""){
    return new Error("第二引数が許容単位ではありません");
    }
if (to.indexOf(acceptUnits.join(), 0)>-1 || to === ""){
    return new Error("第三引数が許容単位ではありません");
    }

前半のこの部分は、渡された引数を確認しています。引数の数や型に問題があるとエラーが戻るようにしてみました。
argumentsは関数の中で使えるプロパティで、関数に渡された引数です。厳密には配列ではありませんが、lengthなど配列がもつプロパティを扱うことができます。
isNaNメソッドは、渡す値が数値型でない場合trueを返します。
typeof演算子は型を調べます。一貫性のない挙動を示すことがあり(undefinedがObjectと判別される等)念のため注意が必要です。
indexOfメソッドは対象とした文字列に、語句が含まれるか、含まれる場合はそのindex(位置)を返すメソッドです。[検索対象とする文字列].indexOf([検索する語句], 検索開始位置) という使い方です。語句が見つからなければ-1が返りますので、その語句が含まれるかどうかを-1が返るかどうかで判定しています。

//fromからvalueを一旦mmに換算
var pt2mm = 25.4/72;
switch (from){
    case acceptUnits[0]: value /= 4; break;
    case acceptUnits[1]: value /= 4; break;
    case acceptUnits[2]: value *= pt2mm; break;
    }
//toからvalueを換算
switch (to){
    case acceptUnits[0]: value *= 4; break;
    case acceptUnits[1]: value *= 4; break;
    case acceptUnits[2]: value /= pt2mm; break;
    }
return value;

後半が単位から数値を換算する処理です。
まずpt2mmという変数に、ポイントからミリへ換算する係数を入れています。1/72インチが1ポイントなので、1インチ≒25.4ミリとして式を与えています。
続くswitch文で、渡された第一引数valueを第二引数fromを基に一旦mmに換算し、次のswitch文で第三引数toを基に出力する単位に換算します。
それぞれのswitch文の判定には、関数最初に定義する配列を使っています。実はこの配列に定義している文字列は、全てmeasureMentUnitから得られる文字列です。つまり、現在の単位設定をそのまま引数として渡すことを想定しています。

var myUnit = app.activeDocument.viewPreferences.typographicMeasurementUnits;
var myValue = convertUnits(100, myUnit.toString(), "POINTS");

例えばこんな感じです。typographicMeasurementUnitsは単位と増減値の環境設定のうち、組版の設定単位です。それをtoStringメソッドを使って文字列とし、引数として渡しています。

このような汎用性のある処理(関数)を書いたとき、どうせなら使う度に記述するのではなく、jsxincファイルとして読み込んでみてはどうでしょうか。外部ライブラリのように読むこむことで、ネイティブメソッドのように利用することが可能です。
どうするかというと、まずこの関数をjsxincファイルとして保存します。保存形式はjsxでもかまわないのですが、jsxincであれば「読み込んで使う(=include)」ことが明確になりますし、InDesignスクリプトパネルからダブルクリックで実行できなくなります。
分かりやすいように、関数と同じ名前で保存するといいと思います。試しにconvertUnits.jsxincとして保存しましょう。
そうしたら今度は、この関数を読み込みたい別のスクリプトに下記の1文を加えます。

#include "convertUnits.jsxinc"

ただし、これはそのスクリプトとjsxincファイルが同階層にある場合です。ただし、これだと当該スクリプトと一緒にjsxincファイルも必要になるため、jsxincファイルが増えてくるとコピーや管理に手間がかかります。ですので、汎用性のあるjsxincファイルが増えてきたら特定の場所に保存し、そこへのパスを指定してあげるなどした方がよりスマートでしょう。
パスを指定する場合は、先程の#includeの後に絶対パスを記述するか、#includepathとして#includeする対象のパスを指定するという2つの方法があります。ただどちらにしてもOSによってパスの記述方法に差があることに注意が必要です。Macの場合、Winの場合、といった書き分けを行う必要があります(お使いの環境によるので、ここでは具体的な説明はしません)。

単位設定を統一する

結局gridDataの行間設定はポイントでしかできなかった、ということで単位の扱いに悩まされたわけですが、定規の単位設定を統一する方法があるにはあります。今回のようなケースでは意味がありませんが、たった1行なので覚えておくと少しだけ楽になれることがあるかも…。

app.scriptPreferences.measurementUnit = MeasurementUnits.MILLIMETERS;

これだけです。こうすると、スクリプト中の定規設定が(この場合は)ミリメートルになります。ほかにもPOINTSHAなど、環境設定の定規設定から設定できる単位に一時的に変更してスクリプトを処理することができるようなります。便利。

以上、とても長くなりましたがもくもく会#3のまとめでした。次回は11月の予定です。皆さまのご参加、お待ちしております。

DTPerのスクリプトもくもく会 - connpass

*1:表示中はアプリケーションの操作ができない状態。反対にウィンドウを表示していてもアプリケーションの操作も可能な、所謂フローティングウィンドウ的なものはモーダレスダイアログ/モーダレスウィンドウといいます。