DTP Booster 013の予習:スクリプト作成過程を実況してみた

注:この記事の内容は、アップの数日前(DTP Booster 013受講前)に書かれたものです。アップするかどうか受講後に悩みましたが、結局貧乏性(モッタイナイ)に負けて載せてしまうことにしました。


参加予定のDTP Booster 013(Omotesando/100602)スクリプトがテーマ。
事前に講師のうちのお一人であるたけうちとおる氏がブログでお題を出されていたので、せっかくだから先に挑戦してみることにしました。
いい機会なので、以前からやってみたいと思っていた「スクリプト作成過程の公開」をしてみます。なんのために? もちろん自分のために。アウトプットは人のためならず。
セミナー前に書いておいて、セミナー後に無編集でアップする予定です。調べたり考えたり失敗したり、とにかく全部書きとめていくので凄まじく長いです。結果だけ見たいという場合はここから飛べます。

JavaScriptで、InDesign CS4で動くことを目指して作成します。動作確認はWindows XPInDesign 6.0.5のみで行います。

まずは、お題を読む

「開いているドキュメントをすべてPDF書き出しする」スクリプトを作って下さい。

DTP Booster 13のお題 - たけうちとおるのスクリプトノート

シンプルだ。そして実用的。
たぶんスクリプト書く人ならとっくに自分で作っていそうな気がするし、作らない人でもWebで公開されている数々のありがたいスクリプトを利用しているのではないかと思う。私は一つずつドキュメントを確かめながら書き出していくから使ってなかったけど。
っていうかそもそもたけうち氏のブログに置いてなかったっけ、と思って調べたら案の定ありました。ダイアログの出る親切設計。でもお題なので、今だけ見なかったことにしよう。

この時点で、この処理を実現するには「PDFを書き出す処理を」「開いているドキュメントすべてに対して繰り返し行う」という形のスクリプトになるなあとぼんやり考える。
繰り返し処理は基本なので簡単。開いているドキュメントをすべて取得する方法もわかる。あとはPDF書き出し処理か……どうやって書くんだっけかな。一度調べようとしたことがあるはずだけど、もう完全に忘れ去りましたね。

お題をさらに読む

書き出すPDFの設定その他について追記がありました。

書き出されるPDFはドキュメントと同じファイル名で(拡張子が.pdfになる)同一階層に保存されるとします。

とりあえず、処理するドキュメントのファイル名(と、たぶんパス)を取得する必要があるっぽい。

PDF書き出しプリセットは「PDFx/1-a」です。

たぶん書き出しするメソッドにプリセットを指定する引数があるんだな……などと、普通はリファレンス見てから知ることだけど今回はヒントのおかげで知る。でも具体的な指定方法はやっぱり調べないとね。

PDFを書き出したドキュメントは保存せずに閉じます。

ふむ。これはとりあえず書き出しが終わった後のことだから後で考えよう。

20行以内で出来ると思います。
多少のエラー処理はしなくて結構です。

行数指定きたー。でもあんまり気にしないでおこう。ただの目安と思っておく。
多少のエラー処理っていうのは、たぶんドキュメントがひとつも開かれていない場合とかそんな感じのことだろう。お言葉に甘えてそのへんはざっくり省略することに。

わかるところから書き始めてみる

なんとなく方向性がみえたところでおもむろに書き始めます。
まずは骨組みとしてドキュメント取得と繰り返し処理の部分を用意。後で必要になる処理をコメントで書き込んでおこう。

var docs = app.documents; // 現在開いているすべてのドキュメントを得る

// ドキュメントの数だけループ
for( var i = 0, docLen = docs.length; i < docLen; i++ ) {
  var tmpDoc = docs[i]; // 今処理してるドキュメント

  // ★ドキュメントのPDF書き出しを実行

  // ★ドキュメントを閉じる(保存しない)

} // ここまでループ

forループのときに繰り返す回数を変数に入れておくのは癖のようなもの。処理中のオブジェクトを変数に入れておくのも同じ。プロパティへのアクセスは結構時間がかかるものなんだとWebな人たちから聞いたので。この程度だと大して変わらない気がするけど。

あとは、★のついたところを埋めていくだけだな!

PDF書き出し処理はどう書くの

というところがわからないので、ここでリファレンスの出番です。ESTK付属の「オブジェクトモデルビューア」を起動。表示するオブジェクトモデルで「InDesign CS4(6.0)」を選ぶのを忘れずに。
「書き出し」ってくらいだからたぶんexportとか検索したら出てくるんじゃないの、と試しに検索してみたところ、

検索結果が多すぎます。検索内容を絞ってください。

(ノ`Д´)ノ彡┻━┻

……気を取り直して続ける。
「ドキュメントを」PDFファイルに書き出すって動作をすることになるので、たぶんDocumentクラス(クラスでいいのかな*1)のあたりにそういうメソッドが用意されてるんじゃないかと予想を立てる。このへんはただの勘のようなもので、なぜそう思ったか説明できない……ちなみに、この予想が外れたりそもそも見当がつかなかったりした場合は潔くぐぐります。

ともかく、まずはDocumentクラスを調べてみる。オブジェクトモデルのブラウザでDocumentを選び、下に出てくるメソッド群からexportっぽいものを探す。ありました。「exportFile」メソッド。

Document.exportFile (format, to, showingOptions, using, withGrids, versionComments, forceSave)


Exports the object(s) to a file.

オブジェクト(この場合はドキュメント)をファイルに書き出す。そのままだ。
あとは引数の正体を調べる。

引数 引数の型 意味
format - 書き出すファイルの形式。
to File 書き出すファイル。
showingOptions Boolean オプション。書き出しのときダイアログを出すかどうか。デフォルトはfalse。
using PDFExportPreset オプション。書き出し方式。ここがPDFの書き出しプリセットっぽい。
withGrids Boolean オプション。グリッドを書き出すかどうか。デフォルトはfalse。
versionComments String オプション。このバージョンのためのコメント?
forceSave Boolean オプション。強制的に上書き保存するかどうか? デフォルトはfalse。

最後のほう適当ですがあんまり関係なさそうなので……というか、普段こんなきっちり考えない。使いそうな引数だけ拾い読みですよ。英語だし。

ということで、このメソッドを実行するときにPDF形式を選べるようだ。「ファイル名」と「プリセット」も設定できそう。

PDF形式で書き出す指定

引数formatのところでPDF形式で書き出すよう指定するらしい。リファレンスには「Can accept: ExportFormat enumerator or String.」と書いてある……文字列で指定できるようだけど、きっと適当に「PDF」とか書いてもダメなんだろうな。
とりあえず「ExportFormat」をオブジェクトビューアで検索して、同名のクラスに「PDF_TYPE」ってプロパティがあるのを発見。たぶん、これを書けばいいんだと思う。やってみてダメだったらぐぐろう。

Fileオブジェクト?

出たな妖怪! いや妖怪じゃないけど、個人的にいまだに理解しきれない領域、それがFileとFolder。
書き出しメソッドの引数toには、書き出すファイルのFileオブジェクト(Fileクラスのインスタンス? っていうの?)を用意しなきゃならないらしい。
Fileオブジェクトを生成する方法は、

var fileObj = new File("ファイル名まで含んだパスの文字列");

でいいはず。この場合に必要な文字列は、「ドキュメントのファイルパスから拡張子を取り除いた文字列」+「.pdf」。
というわけで、まずは処理中のドキュメントのフルパスを取得しなければ。

var tmpDocFileObj = tmpDoc.fullName; // tmpDocは処理中のドキュメント

fullNameプロパティの値はFileオブジェクトらしいのでそのままでは使えない。さらにこのファイルのフルパスを取得。

var fileStr = tmpDocFileObj.fullName; // tmpDocFileObjはさっき取得したFileオブジェクト

さっきと同じfullNameって名前のプロパティだからややこしいけど、これで処理中のドキュメントのファイルパスが文字列で取得できたことになる。
でもって、これだと変数fileStrには拡張子「.indd」までついているはず。なのでそれを取っ払って、拡張子「.pdf」をくっつけてやらないといけない。
後ろ5文字削ってから足してもいいけど、ここは正規表現置換といこうじゃないか。*2

// 行末にある「.(なんでもおk)」を「.pdf」に置換
fileStr = fileStr.replace(/\.[^.]*$/,".pdf");

これでやっと、書き出すPDFファイルのFileオブジェクトを作ることができる。

var fileObj = new File(fileStr); // fileStrは(以下略

せっかくだからここまでの過程を一行にまとめてみよう。

// tmpDocは処理中のドキュメント
var fileObj = new File(tmpDoc.fullName.fullName.replace(/\.[^.]*$/,".pdf"));

このfileObjを、書き出しメソッドの引数として使えばいいはずだ!

さっきかららしいとかはずとかばっかり。手探りでやってるから仕方ない。
でも正直、今書いている過程でちょっと理解が進みました。とりあえず今回の妖怪は退治できた……かな?

PDF書き出しプリセット

最後の大物、書き出しプリセットの指定。例によって「PDFExportPreset」をオブジェクトモデルビューアで検索してみる。どうでもいいけどこの検索窓、テキストのペーストがうまくいかないですね。いちいち手打ち……
Applicationオブジェクトのプロパティに「pdfExportPresets」を発見。複数形ってことはたくさんあるプリセットが全部ここにまとまって入ってるんだろう。その中から目的のプリセット「PDFx/1-a」を取り出して使う。
オブジェクトのコレクション(っていうの?)から目的のオブジェクトだけを取得する方法はいくつかあるけど、今回は名前をキーにして取得することになる。

var preset = app.pdfExportPresets.item("PDFx/1-a");

このへんの書き方はどのクラスでもたいてい同じ。itemメソッドの引数はインデックスの数字でも名前の文字列でもOK。他にitemByNameってメソッドもあって、そちらは名前の文字列だけが使えるんだけど……itemメソッドで充分な気がするのに、なんで用意されてるんだろう? まあいいか。

材料が揃ったので

PDF書き出し処理のところを書いてはめ込んでみよう。
あ、忘れてたけどshowingOptions(書き出すときダイアログを出すかどうか)の値はfalseにしておくことにします。それ以外の必須でない引数は省略。

var docs = app.documents; // 現在開いているすべてのドキュメントを得る

// ドキュメントの数だけループ
for( var i = 0, docLen = docs.length; i < docLen; i++ ) {
  var tmpDoc = docs[i]; // 今処理してるドキュメント

  // 今回追加した部分ここから ------------------------------

  // 書き出すファイルのFileオブジェクトを作成
  var fileObj = new File(tmpDoc.fullName.fullName.replace(/\.[^.]*$/,".pdf"));
  // プリセットのオブジェクトを取得
  var preset = app.pdfExportPresets.item("PDFx/1-a");

  // ドキュメントのPDF書き出しを実行!
  tmpDoc.exportFile(ExportFormat.PDF_TYPE, fileObj, false, preset);

  // 今回追加した部分ここまで ------------------------------

  // ★ドキュメントを閉じる(保存しない)

} // ここまでループ

ドキュメントを閉じるのは後回しで、とりあえずこれが動くかどうかやってみよう(`・ω・´)
ESTKに貼り付けて、適当なinddドキュメントを4つくらい作って開いて実行!

イベント 'exportFile' のパラメータ 'using' の値が無効です。予想される値は PDFExportPreset ですが、値 nothing を受け取りました。

( ゚д゚)...

なにか間違えたようです。

修正しよう

エラーメッセージを見るに、プリセットのあたりで間違えているらしい。スペルミスはないと思うんだけど。
実際に処理が止まってしまった(エラーで赤く染まった)行は、書き出し実行のところ。ということは、プリセットがきちんと取得できてなかった(ので、変数presetの値がnothingだった)ということだろう。その原因は……あっさり判明。

( ´д`)...プリセットの名前、違うじゃん。

お題で指定されたのは確かに「PDF/X-1a」だけど、私の環境ではInDesignにこの名前のPDF書き出しプリセットはなかった。そりゃあ動かないわ。お題に従うならこのコードであってるけど、とりあえず動かすには修正しなくちゃ*3
ということで、InDesignのPDF書き出しプリセットメニューを見て、(デフォルトで用意されてる)PDF/X-1aで書き出すプリセットの名前を調べる。「[PDF/X-1a:2001 (日本)]」、これだな(Win版InDesign CS4の場合)。

var docs = app.documents; // 現在開いているすべてのドキュメントを得る

// ドキュメントの数だけループ
for( var i = 0, docLen = docs.length; i < docLen; i++ ) {
  var tmpDoc = docs[i]; // 今処理してるドキュメント

  // 書き出すファイルのFileオブジェクトを作成
  var fileObj = new File(tmpDoc.fullName.fullName.replace(/\.[^.]*$/,".pdf"));
  // プリセットのオブジェクトを取得
  var preset = app.pdfExportPresets.item("[PDF/X-1a:2001 (日本)]"); // ← ココ

  // ドキュメントのPDF書き出しを実行!
  tmpDoc.exportFile(ExportFormat.PDF_TYPE, fileObj, false, preset);

  // ★ドキュメントを閉じる(保存しない)

} // ここまでループ

今度こそ、と実行ボタンぽちっ。

キタ.*・゜゚・*:.。..。.:*・゚ヽ(゚∀゚)ノ ゚・*:.。. .。.:*・゜゚・*ー!!!!!

ドキュメントと同じフォルダに、PDFがきっちり書き出されました。
これでほぼスクリプトは出来上がったようなものではないだろうか。一気に完成させてしまおう。

あとはドキュメントを閉じるだけ……のはず

これはもう何度も書いたことある処理なので悩むところはないと思う。でも一応、リファレンスを確認しておこうかな。Documentクラスのcloseメソッドだよね。

Document.close (saving, savingIn, versionComments, forceSave)


Close the Document

引数 引数の型 意味
saving SaveOptions オプション。閉じる前に保存するかどうか。デフォルトはSaveOptions.ASK。
savingIn File オプション。保存するファイル。
versionComments String オプション。バージョンコメント?
forceSave Boolean オプション。強制的に上書き保存するかどうか?

ん、SaveOptionsってなんだ。trueかfalseで指定するわけじゃないのか。とりあえずオブジェクトモデ(ryで検索する。

SaveOptions.ASK 変更を保存するかどうかのプロンプトを表示する。
SaveOptions.NO 変更を保存しない。
SaveOptions.YES 変更を保存する。

今まで作ってきたスクリプトでは、保存するかどうかはデフォルトのままにしていました。それがSaveOptions.ASKで、今回は保存しないのでNOを指定すればいいようです。

var docs = app.documents; // 現在開いているすべてのドキュメントを得る

// ドキュメントの数だけループ
for( var i = 0, docLen = docs.length; i < docLen; i++ ) {
  var tmpDoc = docs[i]; // 今処理してるドキュメント

  // 書き出すファイルのFileオブジェクトを作成
  var fileObj = new File(tmpDoc.fullName.fullName.replace(/\.[^.]*$/,".pdf"));
  // プリセットのオブジェクトを取得
  var preset = app.pdfExportPresets.item("[PDF/X-1a:2001 (日本)]");

  // ドキュメントのPDF書き出しを実行!
  tmpDoc.exportFile(ExportFormat.PDF_TYPE, fileObj, false, preset);

  // 今回追加した部分ここから ------------------------------

  // 保存せずにドキュメントを閉じる
  tmpDoc.close(SaveOptions.NO);

  // 今回追加した部分ここまで ------------------------------

} // ここまでループ

残っていた★の行も埋まって、これで完成だ! ということでもう一度ESTKに貼り付けて、ドキュメントを4つ開いて実行してみる。

順調に書き出し→ドキュメント閉じる、という処理が進んでいく……と思ったら、3つ目のドキュメントの処理中になぜかストップ?

オブジェクトが無効です

(;゚Д゚)...アレ?

なんで?

エラーが出たのは書き出すファイルのFileオブジェクトを作成している行。でも、2つ目のドキュメントまでは一切引っかからずきちんと進んでる。なんでだ。なんでだ! エラーならエラーでいいけどもう少し教えてくれよー!

まず疑うべきは、追加したばかりの「ドキュメントを閉じる処理」のところだろう。ここを追加する前は4つのドキュメントをすべて書き出せたんだから。ただ、2つ目までは閉じることに成功して次のループに入ってる。この行の書き方を間違えたわけじゃなさそうな気がする。試しにドキュメントを閉じる処理をコメントアウトしてみたら、ちゃんと4つのPDFが書き出せました。
閉じる処理をすることで何かが起きてるらしいけど……と、ふと思い立ってエラーで処理が止まったままのESTKで「データブラウザ」を開いてみたところ、

なんでdocs.lengthが2になってるの……

つまり、こういうこと。自分の理解のためにクドい説明をしますよ。
最初にドキュメントを4つ開いたとき、app.documents(このスクリプトではdocsで参照してる)のlengthは4で、0〜3のインデックスがつけられています。

index ドキュメント
0 ひとつめ.indd
1 ふたつめ.indd
2 みっつめ.indd
3 よっつめ.indd

でもって、1つ目のドキュメントまで処理し終わった段階で、「ひとつめ.indd」は閉じられる。そうすると、app.documentsのindexは新たに振りなおされるのだ。

index ドキュメント
0 ふたつめ.indd
1 みっつめ.indd
2 よっつめ.indd

ここでiが1増えて、i == 1の状態で処理が行われる。つまりここで処理されるのは「みっつめ.indd」。閉じる処理まで終わったときには次の状態になります。

index ドキュメント
0 ふたつめ.indd
1 よっつめ.indd

ここでiが1増えるとどうなるか? i == 2だけど、docs[2](つまりapp.documents[2])は存在しない。だから「オブジェクトが無効です」というエラーが発生する!

こ、これは恥ずかしい。わかってみれば簡単なんだけどこんなことに気づかなかったとは。最初に得意げにループを書いた時点でこういう問題が起きることが決まっていただなんて!
ていうかこれ読んでる人はきっとわかってて、いつ気づくかなー(・∀・)ニヤニヤとか思ってたんだろうなー! あああ……orz

修正しよう・2

まあ、原因がわかってしまえばあとは簡単。0番目から処理するせいでドキュメントを閉じたときにインデックスが前にずれてしまうわけだから、最後から逆に処理していけばいいのだ。

var docs = app.documents; // 現在開いているすべてのドキュメントを得る

// ドキュメントの数だけループ
for( var i = docs.length - 1; i > -1; i-- ) { // ★
  var tmpDoc = docs[i]; // 今処理してるドキュメント

  // 書き出すファイルのFileオブジェクトを作成
  var fileObj = new File(tmpDoc.fullName.fullName.replace(/\.[^.]*$/,".pdf"));
  // プリセットのオブジェクトを取得
  var preset = app.pdfExportPresets.item("[PDF/X-1a:2001 (日本)]");

  // ドキュメントのPDF書き出しを実行!
  tmpDoc.exportFile(ExportFormat.PDF_TYPE, fileObj, false, preset);

  // 保存せずにドキュメントを閉じる
  tmpDoc.close(SaveOptions.NO);

} // ここまでループ

修正したのは★の行。iの初期値を「ドキュメントの数-1」にする。これが最後のドキュメントのインデックスで、そこから1ずつ減らしていくことで0番目のドキュメントまで処理できる。
今度こそ何も問題はないはずだ! 例によって4つドキュメントを開いて実行!

Execution finished.

ヤッタ.*・゜゚・*:.。..。.:*・゚ヽ(;∀;)ノ ゚・*:.。. .。.:*・゜゚・*ー!!!!!
最後まできちんとPDFを書き出して、ドキュメントもすべて閉じました。ドキュメントを一部修正してから実行した場合でも、PDFにはその修正が反映され、元のドキュメントは保存せず閉じられています。ドキュメントの数を増やしても減らしても問題なし。やっと完成! です! やったー!

完成!

試しにコメントを取っ払ってみたところ、8行になりました。

var docs = app.documents;
for( var i = docs.length - 1; i > -1; i-- ) {
  var tmpDoc = docs[i];
  var fileObj = new File(tmpDoc.fullName.fullName.replace(/\.[^.]*$/,".pdf"));
  var preset = app.pdfExportPresets.item("[PDF/X-1a:2001 (日本)]");
  tmpDoc.exportFile(ExportFormat.PDF_TYPE, fileObj, false, preset);
  tmpDoc.close(SaveOptions.NO);
}

20行よりはだいぶ少なくて済みましたね。ただ、やっぱりドキュメントを1つも開いてないときのチェックくらいは入れてもいい気がしてきます。ついでに、無名関数でくるんでしまおう。

(function(){
  var docs = app.documents;
  if( docs.length == 0 ) { alert("ドキュメントが開かれていません。"); return; }
  for( var i = docs.length - 1; i > -1; i-- ) {
    var tmpDoc = docs[i];
    var fileObj = new File(tmpDoc.fullName.fullName.replace(/\.[^.]*$/,".pdf"));
    var preset = app.pdfExportPresets.item("[PDF/X-1a:2001 (日本)]");
    tmpDoc.exportFile(ExportFormat.PDF_TYPE, fileObj, false, preset);
    tmpDoc.close(SaveOptions.NO);
  }
})();

ドキュメントの数が0だったときはエラーメッセージを出して、returnで処理を終了してしまうようにしています。returnが使えるのは全体を関数にしてしまったおかげ。
行数は増えましたが、その分ちょっとだけ親切になりました。普通に使うにはこれで充分かな。

蛇足:ループの書き方を変えてみる

ドキュメント数が減るのに対応するためにforループを逆転してみたけど、減っていくとわかっているならwhileでも書けると思ったので、せっかくだから書いてみる。

(function(){
  var docs = app.documents;
  if( docs.length == 0 ) { alert("ドキュメントが開かれていません。"); return; }
  while( docs.length != 0 ) { // ★1
    var tmpDoc = docs[0]; // ★2
    var fileObj = new File(tmpDoc.fullName.fullName.replace(/\.[^.]*$/,".pdf"));
    var preset = app.pdfExportPresets.item("[PDF/X-1a:2001 (日本)]");
    tmpDoc.exportFile(ExportFormat.PDF_TYPE, fileObj, false, preset);
    tmpDoc.close(SaveOptions.NO);
  }
})();

変更したのは★のついた2行。ドキュメントの中で最初のものを処理の対象にして(★2)、処理が終わったら閉じる。それをドキュメントの数が0になるまで繰り返す(★1)、という感じ。毎回0番目に来てるドキュメントを対象にするので、ドキュメントが閉じてインデックスがずれても問題ありません。
でもよく考えてみると、この形のループは「うっかりドキュメントが閉じられなかった場合」に無限ループに陥る危険がある気がします(docs.lengthがいつまでたっても0にならないから)。最初に処理したいドキュメントの数は決まるのだから、forで回すほうが安全かもしれないです。

さいごにひとこと

こんなに苦労するはずではなかったんですけどね! 考えが甘かったです。最後まで読んでくださった方、いるかどうかわかりませんがありがとうございました。ツッコミ大歓迎です。
いろんなエラーが出て、原因を突き止める過程まで書き留めつつ修正していったおかげで完成まで諦めずに済みましたし、なんとなく修正してなんとなくできあがるよりも理解が深まった気がします。時間はかかったけど。あと、だいぶ恥ずかしいけど。
今後また機会があったら実況風に書き留めてみたいなと思います。

……でもこれ、たぶんPlanetDTP@jpに全文載っちゃうんじゃないかしら。載せてくれるのはありがたいことだけど、長くてクドくてすいません!(今更……

*1:JavaScriptから入ったクチだからか、クラスとかインスタンスとかいうのがよくわかっていません

*2:たぶん指定文字数削って足すほうが速いんだろうけど

*3:修正の方向としては「PDF/X-1a」という名前のプリセットを自分で作るっていうのもありなんだけど