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を作ってみたんですが、色々注意事項があるのでメモしておきます。


まず、ExtendScriptUXP 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行読む」処理が実装されているかもしれないですね。期待しないでください。