[InDesign]スクリプトでドキュメントの文字あふれをチェックする

InDesignのスクリプトで、ドキュメントに文字あふれがあるかどうかをチェックする方法です。

文字あふれというとよくあるのが、テキストフレームの文字があふれている場合に、長体をかけて入れるというものです。たとえばこのようなものです。

const d = 5;
var txf = app.documents[0].textFrames[0];
var sty = txf.parentStory;
while (txf.overflows) {
    if (sty.horizontalScale - d < 50) {
        alert("長体率50%より小さくなりますので中止します");
        break;
    }
    sty.horizontalScale -= d;
}

ただこれは、1つのテキストフレームだけが対象で、さらにテキストフレームが連結されていない状態を前提にしています。そのため、連結されたテキストフレームに対してはこの方法は使えません。ドキュメントのテキストフレームがすべて連結されていないという保証があればテキストフレームの数だけ繰り返せばよいですが、誰が作ったか分からないドキュメントでは別の方法を考えなければなりません。


そこで、ドキュメントに文字あふれがあるかどうかを調べる方法を考えていたら、なんと3つも見つけてしまいました。以下、順に説明しますが、どれがよいかは理解のしやすさ(メンテナンス性)と実行速度を検討しないといけないので、ドキュメントの複雑さによって異なる気がします。私は検証に適したドキュメントを持っていないので、そこは各自でご検討ください。

なお、以下の記述では「テキストコンテナ」という造語を使います。これは「テキストフレーム」と「パス上文字」を合わせたものです。毎回「テキストフレームおよび(または)パス上文字」と書いていると長くてわかりにくいので今回はこの用語を使用します。なお、テキストフレームとパス上文字は連結することができます。これを利用したのが「タイトル付き囲み枠」です。

1 ストーリーの最後のテキストコンテナがあふれているかを調べる

テキストコンテナのあふれを調べるためoverflowsプロパティですが、これは連結されている最後のテキストコンテナでしか調べることができません。最後以外のテキストコンテナの場合は「false(あふれていません)」という状態になっています。

ストーリーの最後のテキストコンテナを取得するのは次の手順になります(別の方法があるかもしれません。見つけたら教えてください)。

  1. ストーリーが配置されているテキストコンテナを取得
    Story.textContainersプロパティ(値はテキストコンテナの配列)
  2. それらのうちの最初のテキストコンテナ(配列の最初)を取得
    Story.textContainers[0]
  3. そこから最後のテキストコンテナを取得
    TextFrame.endTextFrameプロパティ

以上の3段階です。これを使ってコードを書くと次のようになります。

var i, txf;
var doc = app.documents[0];
for (i = 0; i < doc.stories.length; i++){
    txf = doc.stories[i].textContainers[0].endTextFrame;
    if (txf.overflows) {
        alert("文字あふれの状態では実行できません");
    }
}

ここで、doc.stories[i].textContainers[-1]でもいいんじゃない? と思った方は残念。textContainersプロパティの値は配列なので-1は使えないんです(コレクションなら使えるんですけどね)。次のコードは使えますが、長いのでちょっと使いにくい。

doc.stories[i].textContainers[doc.stories[i].textContainers.length - 1]

なので最初のテキストコンテナを取得して、そこかendTextFrameプロパティで最後のテキストコンテナを取得しているわけです。

任意のテキストコンテナから、連結されたテキストコンテナを取得するには次のプロパティがあります(私のオブジェクトモデルでは説明が間違っているのでいずれ直します)。

startTextFrame 連結されている最初のテキストコンテナ。連結されていなければ自身のテキストコンテナが返ります
previousTextFrame 連結されている前のテキストコンテナ。連結されていなけれnullが返ります
null(NothingEnum.NOTHING)を設定すると連結を切断します
nextTextFrame 連結されている次のテキストコンテナ。連結されていなけれnullが返ります
null(NothingEnum.NOTHING)を設定すると連結を切断します
endTextFrame 連結されている最後のテキストコンテナ。連結されていなければ自身のテキストコンテナが返ります

余談ですが、テキストコンテナを連結するにpreviousTextFrameまたnextTextFrameを使います。値TextFrameまたTextPathオブジェクトを指定すれば連結、nullを指定すれば切断です。連結の際気を付けなければいけないのが、ループ状に連結しようとするとエラーになること。当然ですね。ただ、InDesignの画面を見ずにスクリプトだけ触っていると気づかずにやってしまいますので、連結し直する際は一度切断してから連結するといった工夫が必要になります。

2 ストーリーの最後の文字が含まれるテキストコンテナを調べる

これはあふれている文字(オーバーセットテキスト)の特徴から判定する方法です。InDesignにおいてあふれている文字というのは、レイアウト上には存在しません(宙に浮いている状態です)。そのため次のプロパティを取得しようとするとエラーになります。

  • baseline
  • endBaseline
  • endHorizontalOffset
  • horizontalOffset

これらはいずれもレイアウト上の座標を取得するものなので、オーバーセットテキストではエラーになります(必ず全てのプロパティがエラーになるわけではありません。セル内のテキストで、baselineは取得できるが、endHorizontalOffsetが取得できないというケースがあったりします)。

またオーバーセットテキストは宙に浮いている状態ですのでページ番号を取得できません。よくやるページ番号の取得方法として次の書き方がありますが、これがエラーになります。

Character.parentTextFrames[0].parentPage.nmae

この文のどこがエラーになるかというparentTextFrames[0]のところです。

parentTextFramesというのはテキストオブジェクトが含まれるテキストコンテナを指します。多くの場合は1つのテキストコンテナですが、段落を指定した場合などで複数のテキストコンテナにまたがる場合があります。ですかparentTextFramesはテキストコンテナの配列になります。添字に0を指定しているので、またがった場合はそのうちの最初のテキストコンテナということになります。

ところがオーバーセットテキストの場合、これが含まれるテキストコンテナはありません。当然ですね「あふれている=入っていない」わけですから。そのたparentTextFramesの配列の中身は空になりますので、[0]を取得できずエラーになるというわけです。つまり配列が空かどうかを調べればよいということです。

var i, cha;
var doc = app.documents[0];
for (i = 0; i < doc.stories.length; i++) {
    cha = doc.stories[i].characters[-1];
    if (cha.parentTextFrames.length == 0) {
        alert("文字あふれの状態では実行できません");
    }
}

3 プリフライト機能を使う

InDesignのプリフライト機能を使えば文字あふれを調べることができます。この機能はスクリプトからもアクセスできますので、有効な手段となります。しかし私のやり方が悪いんでしょう、「使える」スクリプトになっていないのが現状です。ですから、手っ取り早く方法だけ知りたい方はこの先は読まなくて結構です。

以下、わかっていることをつらつら書いていきます。もしわかる方がいらっしゃれば「こんな方法があるよ」「ここが違っているよ」と教えて頂けたら幸いです。

プリフライトを実行する

デフォルトでプリフライトはオンになっているので通常は必要ないのですが、オフになっている状態からオンにするには次のコードを書きます。

var doc = app.documents[0];
doc.preflightOptions.preflightWorkingProfile = app.preflightProfiles[0];
doc.preflightOptions.preflightOff = false;

ここで、プリフライトプロファイルに添え字0を指定していますが、これは[基本]プロファイルです。このプロファイルは編集・削除ができない特別なものです。他のプロファイルを使ってもよいのですが、このプロファイルでは文字あふれをチェックしますので、これで問題ありません。ただ書き方には注意が必要で、

app.preflightProfiles.itemByName("[基本]");

ではエラーになります。バグなのかどうなのかわかりませんけど。

プリフライト結果を受け取る

プリフライトの結果PreflightProcessというオブジェクトが持っています。このオブジェクトはプリフライトが実行されているドキュメントごとに存在しますので次の方法で特定できます。

var doc = app.documents[0];
for (var i = 0; i < app.preflightProcesses.length; i++) {
    if (app.preflightProcesses[i].targetObject == doc) {
        var pfp = app.preflightProcesses[i]; //これがプリフライト結果
        break;
    }
}

もっとよい方法があるかもしれません。

文字あふれを調べる

プリフライト結果から文字があふれているかを知るにaggregatedResultsプロパティを調べるのですが、これが深い配列の入れ子になっているので、ちょっと面倒です。詳しい内容はそのうち書くとして(はまりそうなので書かないかもしれません)、ここでは、面倒くさいので配列をすべて繋げて文字列にしています。エラーがない場合は空文字列になります。

//直前のコードの続き
var res = pfp.aggregatedResults[2].join("");
if (res.indexOf("オーバーセットテキスト") > -1) {
    alert("文字あふれの状態では実行できません");
}

以上のコードを繋ぐと、プリフライトを実行して結果を受け取り、文字あふれがチェックできるわけですが、ここで大きな問題があります。この問題については先Usukeさんに書かれてしまったのでそちらを見てください(笑)。プリフライトの終了を把握する方法として次の2つが思いつきました。しかしいずれの方法も、プリフライトが終了してからかなり時間がたってから結果が表示されるので実用的ではありません。原因はわかりません。

プリフライトの終了を把握(1)ステータスを調べる

PreflightProcessオブジェクトdescriptionプロパティにはプリフライトの実行状況が入ります。そのうちの冒頭を調べることに取り終了したかどうかが分かります。

開始時:「State: Initializing」から始まる文字列になります

完了時:「State: Results complete」から始まる文字列になります

途中はどうなっているのか、うまく取得できなかったのでわかりません)

従ってコード全体としては

var doc = app.documents[0];
doc.preflightOptions.preflightWorkingProfile = app.preflightProfiles[0];
doc.preflightOptions.preflightOff = false;

for (var i = 0; i < app.preflightProcesses.length; i++) {
    if (app.preflightProcesses[i].targetObject == doc) {
        var pfp = app.preflightProcesses[i];
        break;
    }
}

while (true) {
    $.sleep(100); //0.1秒待機
    if (pfp.description.indexOf("State: Results complete") > -1) break;
}
var res = pfp.aggregatedResults[2].join("");
if (res.indexOf("オーバーセットテキスト") > -1) {
    alert("文字あふれの状態では実行できません");
}

これを実行すると、初回になんと1分近く(何もない新規ドキュメント58秒)かかってしまいます。2回目以降は5秒(同じく何もない新規ドキュメントの場合)になるんですが(2回やることはないので2回目以降の時間は意味がないです)。ちょっと考えられない遅さですよね。何かが阻害していると考えられますが、わかりません(途中経過がうまく取得できなかったのでそれに関係するかも)。

プリフライトの終了を把握(2)InDesignがアイドル状態になった時点で調べる

InDesignIdleTaskというオブジェクトがあります。これInDesignが内部で処理を完了し操作を受付可能になった時に実行するものです。操作を受付可能になったということはプリフライトも終了していると判断できます。

これを利用すると次のようなコードが書けます。

var doc = app.documents[0];
doc.preflightOptions.preflightWorkingProfile = app.preflightProfiles[0];
doc.preflightOptions.preflightOff = false;
var iTask = app.idleTasks.add({sleep: 0}); //sleep:0で実行後削除
iTask.addEventListener(IdleEvent.ON_IDLE, overflowCheck);

function overflowCheck() {
    for (var i = 0; i < app.preflightProcesses.length; i++) {
        if (app.preflightProcesses[i].targetObject == app.documents[0]) {
            var pfp = app.preflightProcesses[i];
            break;
        }
    }
    var res = pfp.aggregatedResults[2].join("");
    if (res.indexOf("オーバーセットテキスト") > -1) {
        alert("文字あふれの状態では実行できません");
    }
}

こちらはそれほど時間はかかりません。ただ、プリフライト以外の処理があったりすると時間は読めません。

 

以上、プリフライトの終了を把握するために2つのやり方を考えましたが、どちらも納得がいってません。今回はプリフライトを使わない方が正解だと思うのでこれ以上は突っ込みませんが、参考になりましたら幸いです。