PlaceMultipagePDF.idjsを作ったぞ(ExtendScriptからUXP Scripting)
今回はUXP Scripting(UXPスクリプト)の話です。
UXPスクリプトはInDesign 2023(バージョン18.0)から導入され、スクリプトパネルにもサンプルのスクリプトが搭載されました。このサンプルスクリプトは同じ内容のスクリプトをExtendScript、AppleScript(Macのみ)、VBScript(Windowsのみ)で提供しているため、言語間の違いを確認するのに非常に役に立ちます。ここに「UXPScript」というフォルダが追加され、その中に他言語のものと同じ機能のスクリプトが追加されたということです。
しかし、バージョン18.0時点ではテキストファイルを扱う機能ができておらず、そのため、次の3つのスクリプトの搭載が見送られました。
- ExportAllStories.idjs(すべてのストーリーをテキストファイルに書き出す)
- FindChangeByList.idjs(テキストファイルの内容に従って連続置換を行う。使い方はこちら)
- PlaceMultipagePDF.idjs(PDFファイルの全ページを連続して配置する)
その後(いつのタイミングか確認するのを忘れたけど18.3には存在する)、ExportAllStories.idjsについてはサンプルに追加されましたが、あとの2つはバージョン19.0.1になっても搭載されていません。
ということで、PlaceMultipagePDF.idjsを作ってみたんですが、色々注意事項があるのでメモしておきます。
まず、ExtendScriptからUXP Scriptingへの移植には「ExtendScript to UXP」というページがあるんですが、これはInDesignのオブジェクトを扱う場合に限った話で、しかもバージョン18.3以前の内容になります。バージョン18.4以降で書き方が大きく変更されたので、追加で変更しなければならない部分があります。
これに加えてファイルシステム、$オブジェクトといったExtendScriptで独自に実装された部分があります。これはUXP独自に構築された方法で書き換えなければなりません。ファイルシステムの概要はこのページにありますが、その中で「2つの方法がありますよ。LocalFileSytemとFSモジュールです」と書いてあります。詳しくは見ていないのですが、この2つは同じことができるわけではなく、またExtendScriptでできていたことをすべてカバーしているわけではなさそうです。そのため目的の機能を探すにはかなり苦労しそうです。
このあたりの移植方法はいずれまとめておかないといけないですね。今はまだ過渡期なのでまとめられないですが。
それとは別に、私にとって大変なのはJavaScriptの言語本体です。ExtendScriptはECMA Script 3準拠なのですが、UXP ScriptingはECMA Script 6対応です。そのため3から6になって変更された部分(varでなくてletを使え、とか)や拡張された部分は理解が追い付きません。これはAdobeのサイトには記述がないのでMDNとかのサイトや書籍で勉強しなければならないのです。
特に私を悩ませているのが「非同期処理」ってやつです。ずーっと「プログラムは書いた上から順に処理していきます」で暮らしていたので(変数の巻き上げは知ってますが)、ある命令の処理を待たずに次の処理が実行されるってのはパニックですよ。とりあえず引っ掛かりそうなところにalert()
を入れて確認し、処理が飛ばされたところにawait
を入れてるんですが、これが正解なのかはわかりません。
前置きが長くなってしまいましたが、ここからコードです。本来は最低限必要なところだけを書き換えればいいのですが、私の癖で、自分が一番理解できる形に書き直しちゃってます。そうしないと自分で何をやってるのか分からないので。ですから元のJSと1対1で照合できないので、皆さんの勉強にはならないのが申し訳ないです。
PlaceMultipagePDF.idjsのコード(クリックして開く)
const {app} = require("indesign");
const {PDFCrop, LocationOptions} = require("indesign");
const ufs = require('uxp').storage.localFileSystem;
const newDocName = "新規ドキュメント";
let pdfFile = await ufs.getFileForOpening();
if (pdfFile !== null) {
main();
}
function main(){
let doc, pg, i, dlg, dlc, dlr, dd;
// ドキュメントを選択
let docName = newDocName;
if (app.documents.length > 0){
let aList = [newDocName];
for (i = 0; i < app.documents.length; i++) {
aList.push(app.documents.item(i).name);
}
dlg = app.dialogs.add({name:"ドキュメント選択", canCancel:false});
dlc = dlg.dialogColumns.add();
dlr = dlc.dialogRows.add();
dlr.staticTexts.add({staticLabel:"貼り込むドキュメント:"});
dlr = dlc.dialogRows.add();
dd = dlr.dropdowns.add({stringList:aList, selectedIndex:0});
dlg.show();
docName = aList[dd.selectedIndex];
dlg.destroy();
}
// ドキュメントの指定とページの選択
if (docName === newDocName) { // 新規ドキュメントの場合
doc = app.documents.add();
pg = doc.pages.item(0);
} else { // 既存ドキュメントの場合
doc = app.documents.itemByName(docName);
aList = [];
for (i = 0; i < doc.pages.length; i++) {
aList.push(doc.pages.item(i).name);
}
dlg = app.dialogs.add({name:"ページ選択", canCancel:false});
dlc = dlg.dialogColumns.add();
dlr = dlc.dialogRows.add();
dlr.staticTexts.add({staticLabel:"ページ指定:"});
dlr = dlc.dialogRows.add();
dd = dlr.dropdowns.add({stringList:aList, selectedIndex:0});
dlg.show();
pg = doc.pages.itemByName(aList[dd.selectedIndex]);
dlg.destroy();
}
// 貼り込み
app.pdfPlacePreferences.pdfCrop = PDFCrop.cropMedia;
i = 1;
while(true) {
app.pdfPlacePreferences.pageNumber = i;
PDFObj = pg.place(pdfFile, [0,0])[0];
if (i > 1 && PDFObj.pdfAttributes.pageNumber === 1) {
pg.remove();
break;
}
pg = doc.pages.add(LocationOptions.after, pg);
i++;
}
}
以下、簡単な説明です。
const {app} = require("indesign");
これは定型文という扱いでいいと思います。InDesign 18.4以降でUXPスクリプトを作成する際は最初に書く内容です。説明はこちら。
const {PDFCrop, LocationOptions} = require("indesign");
InDesign 18.4以降で列挙を使用する場合は、appと同様にInDesignモジュールを呼び出します。ただし、列挙ではなく値を使用する場合(例:PDFCrop.cropMedia→1131573325)では必要ありません。
const ufs = require('uxp').storage.localFileSystem;
ファイルを扱うためLocalFileSytemモジュールを呼び出しています。
let pdfFile = await ufs.getFileForOpening();
ファイルを開くダイアログです。戻り値はファイル型(キャンセルされたらnull)です。このダイアログで存在しないファイル名を入力することはできませんでした。また、getFileForOpening()メソッドには引数があるんですが、使い方はまだわかっていません。
で、ここが唯一await
を入れた箇所です。入れないとダイアログが開いたらすぐに次のコードが実行されてしまいます。他にもあるかとびくびくしてたんですが、あとは大丈夫でした(私が試した限り)。
i = 1;
while (true) {
app.pdfPlacePreferences.pageNumber = i;
PDFObj = pg.place(pdfFile, [0,0])[0];
if (i > 1 && PDFObj.pdfAttributes.pageNumber === 1) {
pg.remove();
break;
}
pg = doc.pages.add(LocationOptions.after, pg);
i++;
}
この部分がPDFファイルを配置しているところです。元のコードが分かりにくかったため書き直しました。PDFのページ数+1回繰り返しているんですが、これはInDesignのPDF配置の仕様を利用しています。それは、「配置するPDFファイルの指定ページが存在しない場合に1ページ目が配置される」というものです。ですから、一旦PDFのページを指定して配置し、配置されたPDFのページ番号を調べます。それが2回目以降の配置で1ページ目が配置されている状態だった場合、指定したPDFのページが存在しないということになりますから、そのInDesignドキュメントのページを削除して終了しています。
ちょっと面倒くさいやり方ですが、それでも現状では最善の方法かなと思います(linearized PDFであればファイルの中身を読むんですが、そうでないと辛い)。
ということで「やってみた」報告でした。
あとはFindChangeByList.idjsが残ってるんですが、これが楽ではありません。というのもこのスクリプトでは「テキストファイルから1行読み込む」という動作があるんですが、調べている限りUXPで同様のものがありません。ですから今のところ、「テキストファイルの全内容を読み込んで改行で分割する」処理を追加しなければならない感じです。改行コードの置換も必要だし。あとスクリプト中でスクリプトを作成して実行するという、面倒くさいこともやっているので手間がかかりそうです。余裕があればやりたいですが、そうこうしているうちに「1行読む」処理が実装されているかもしれないですね。期待しないでください。