たとえばInDesignでドキュメントを新規作成して、そこにテキストフレームを1つ作ったとして、
このテキストフレームをスクリプトから取得するにはどんな方法があるでしょうか。
とりあえず
var doc = app.activeDocument;
ってドキュメントを取得しておくとして、普通にやるなら
var tf1 = doc.textFrames[0];
って書きますよね。
テキストフレームはページアイテムでもあるので
var tf2 = doc.pageItems[0];
としてもいいですね。
また、どのスプレッドかを明確にして
var tf3 = doc.spreads[0].textFrames[0];
ていうのもアリです。
あるいはまわりくどく
var tf4 = doc.stories[0].textContainers[0];
のようにストーリーを経由して取得することもできます。
ちょっとやり方を変えて、ドキュメント上でテキストフレームを選択しておいてから
var tf5 = doc.selection[0];
という手もあります。
●オブジェクトは取得した経路を記憶してる
これらのいろんな形で取得したオブジェクトは、すべて同じInDesign上のアイテムを指していますが、スクリプト上では完全に同一なオブジェクトなのでしょうか。
そんなことはなくて、たとえばtf1とtf2ではconstructorが違います。
tf1.constructor.nameは"TextFrame"で、tf2.constructor.nameは"PageItem"です。
ですがtf2もちゃんとcontentsなどTextFrameに固有のプロパティを持っています。
ではconstructorが同じなtf1とtf3は完全に同一なのかというとそうでもなくて、これらは
toSpecifier() というメソッドで見分けることができます。
これは「このオブジェクトが何を指してるのか」を文字列で取得するメソッドです。
tf1.toSpecifier() は /document[@id=1]/text-frame[0]
tf2.toSpecifier() は /document[@id=1]/page-item[0]
tf3.toSpecifier() は /document[@id=1]/spread[0]/text-frame[0]
というふうに、オブジェクトを取得した経路がそのまんま記録されていてます。ただしこれはコレクションオブジェクトから取得したオブジェクトだけで、それ以外の方法、たとえばtextContainers配列から取得したtf4や、selection配列から取得したtf5では
/document[@id=1]//text-frame[@id=275]
のように、「idを使った最短の経路」になります。
●「==」演算子の挙動
では逆に、これらのオブジェクトが同じInDesign上のアイテムを指しているかどうかを判定するにはどうしたらよいでしょう。
ページアイテム系オブジェクトの場合はidを比較するという手もありますが、
実はもっと簡単に「
tf1==tf2」でいいんです。
これでtf1とtf2が同じアイテムを指しているときのみtrueになります。
さらには
tf1===tf2 と書いても同じです。
このへんはちょっとJavaScriptの常識に反してる感がありますが、ExtendScriptだからいいんです。==や===はExtendScriptではオーバーロード可能(挙動をオブジェクトによって変えられる)な演算子なのです。
さて今度は、
var tf6 = doc.textFrames[1];
というオブジェクトを取得してみます。
今、ドキュメントにはテキストフレームが1つしかありませんが、これは存在しない2つ目のテキストフレームを取得しちゃっています(コレクションオブジェクトの添字は0から始まるので)。
それでもエラーにならずに、TextFrameオブジェクトが返ってきます。
このようなオブジェクトでは、
tf6 == null がtrueになります。もちろんtf6 === nullと書いても同じです。
また、
tf6.isValid というプロパティもあります。
このisValidプロパティは、InDesign上に存在しない(無効な)アイテムを指すオブジェクトである場合にfalseになり、ちゃんと存在する有効なオブジェクトである場合はtrueになります。
●getElementsを使ってみよう
次はドキュメント上にテキストフレームを3つ作ってみましょう。
そのうち前面(レイヤーパネル上で上)にある方から順に「1号」「2号」「3号」というテキストを入れておきます。
var tf7 = doc.textFrames[0];
var tf8 = doc.textFrames[1];
var tf9 = doc.textFrames[2];
alert(tf7.contents);
alert(tf8.contents);
alert(tf9.contents);
を実行すると、順に「1号」「2号」「3号」がアラートされますね。
今度は途中でtf7を削除してみます。
var tf7 = doc.textFrames[0];
var tf8 = doc.textFrames[1];
var tf9 = doc.textFrames[2];
tf7.remove();
alert(tf8.contents);
alert(tf9.contents);
「2号」「3号」が順にアラートされることを期待して実行してみるわけですが、残念ながらそうなりません。
alert(tf8.contents);のところで「3号」がアラートされ、
alert(tf9.contents);はエラーになります。
先ほど述べたように、doc.textFrames[1] のような形でコレクションオブジェクトから取得したオブジェクトは、「ドキュメントの2番目のテキストフレーム」というような取得した経路が記憶されています。
tf7を削除したことによりテキストフレームの順序が変わり、「3号」が2番目になり、3番目のテキストフレームは存在しないためエラーになってしまった、ということなのでしょう。
これはとても困ります。いかにもバグの元です。
こんなことを防ぐために、すべてのページアイテムは固有のidをプロパティとして持っています。そしてコレクションオブジェクトにはidを使ってオブジェクトを取得するメソッドitemByIdがあります。
var tf8 = doc.textFrames[1];
tf8 = doc.textFrames.itemById(tf8.id);
こうしてidを使って再取得しておけば、テキストフレームを消しても増やしても、ずれて別のテキストフレームに変わっていた、というようなことはありません。
しかしいちいち2行も書くのはめんどくさすぎるので、もっと簡単な方法があります。.getElements()[0]をつけるのです。
var tf7 = doc.textFrames[0].getElements()[0];
var tf8 = doc.textFrames[1].getElements()[0];
var tf9 = doc.textFrames[2].getElements()[0];
tf7.remove();
alert(tf8.contents);
alert(tf9.contents);
これで「2号」「3号」が順にアラートされるようになりました。
getElementsを使うとidを使った最も基本的な経路で再取得されます。なんで[0]が付くのかといえば、戻り値が配列だからで、なんで配列なのかといえば複数返ってくる場合があるからなのですが、この話はまたいずれ。
ちなみに
var tf2 = doc.pageItems[0];
はconstructorがPageItemになるという話をしましたが、.getElements()[0]をつけるとTextFrameになります。
pageItemsから取得したオブジェクトが何の種類のページアイテムなのかを見分けるのにも使えるということです。
●isValidの副作用の話
今度は同じことをCharactor(文字)オブジェクトでやってみましょう。
「いろは」という3文字のテキストを入れたテキストフレームを用意し、選択しておきます。
var tf = app.selection[0];
var chr1 = tf.characters[0];
var chr2 = tf.characters[1];
var chr3 = tf.characters[2];
alert(chr1.contents);
alert(chr2.contents);
alert(chr3.contents);
を実行すると、「い」「ろ」「は」が順にアラートされます。
では途中で1文字目を削除して
var tf = app.selection[0];
var chr1 = tf.characters[0];
var chr2 = tf.characters[1];
var chr3 = tf.characters[2];
chr1.remove();
alert(chr2.contents);
alert(chr3.contents);
これはやはり「は」がアラートされた後エラーになります。なので
var tf = app.selection[0];
var chr1 = tf.characters[0].getElements()[0];
var chr2 = tf.characters[1].getElements()[0];
var chr3 = tf.characters[2].getElements()[0];
chr1.remove();
alert(chr2.contents);
alert(chr3.contents);
とすると期待通り「ろ」「は」がアラートされました。
めでたしめでたし。
ではなくて、
これは不思議な現象なのです。
なぜかというとCharacterオブジェクトにはidプロパティが無いからです。
id無しでどうやってchr2は「ろ」の字、chr3は「は」の字を保持し続けられるのでしょう。
これはよく分かりません。
ためしに chr2.toSpecifier() を取得してみると、
(/document[@id=1]//story[@id=248]/character[1] to /document[@id=1]//story[@id=248]/character[1])
と返ってきました。
始めの字と終わりの字でテキストの範囲を決めているようですね。今回は1文字なのでどちらも同じ字ですが。
しかし story[@id=248]/character[1] ということは、
「idで指定したストーリーの2文字目(0から始まるから)」という意味になりますよね。
これでは「い」を削除した後には「は」になっているはずです。
でも確かに「ろ」の字のままになっている…。
理由は分かりませんがともかくテキスト系オブジェクトでは、
toSpecifier() で得られる文字列が指す範囲と、実際にそのオブジェクトが指している範囲が食い違う場合がある、ということです。
なんだか不安ですね。
不安なのでisValidプロパティを使って、有効なオブジェクトかどうかチェックしてからcontentsを表示するようにしてみましょう。
var tf = app.selection[0];
var chr1 = tf.characters[0].getElements()[0];
var chr2 = tf.characters[1].getElements()[0];
var chr3 = tf.characters[2].getElements()[0];
chr1.remove();
if (chr2.isValid) alert(chr2.contents);
else alert("オブジェクトが無効です!");
if (chr3.isValid) alert(chr3.contents);
else alert("オブジェクトが無効です!");
あれ???(わざとらしい)
さっきは「い」「う」が表示されてたのに、「う」「オブジェクトが無効です!」になってしまいました。
先ほどのコードとの違いはisValidを取得して、if-elseで分岐させただけです。
実はこれがisValidの副作用というやつでして、isValidプロパティは見るだけでそのオブジェクトの状態を変えてしまう場合があるのです。
どういう場合にそうなるかといえばまさに今のように「toSpecifier() で得られる文字列が指す範囲と、実際にそのオブジェクトが指している範囲が食い違っている場合」です。
食い違いがなくなるように、「toSpecifier() で得られる文字列が指す範囲」の方に統一されるのです。
ほかにも実験してみましょう。
テキストフレームを選択して、
var tf = app.selection[0];
var ins1 = tf.insertionPoints[0];
ins1.contents="abc";
ins1.contents="def";
を実行してみます。
ins1は「テキストフレームの最初の挿入点」なので、テキストフレームには"defabc"が入ることを期待しますが、実際はdefしか入りません。
"abc"を代入した後のins1は、InsertionPointオブジェクトでありながら、3文字の範囲テキストを指すオブジェクトになっているからです。
今度は間にisValidを挟んでみましょう。
var tf = app.selection[0];
var ins1 = tf.insertionPoints[0];
ins1.contents="abc";
ins1.isValid;
ins1.contents="def";
これは"defabc"になります。isValidを見ることでInsertionPointらしさを取り戻したのです。
テキスト以外でも起きます。「名称未設定-1」というドキュメントを開いている状態で
var doc1 = app.documents[0];
alert(doc1.name);
app.documents.add();
alert(doc1.name);
を実行してみます。2回のアラートの間でドキュメントが新規作成され、そちらが最前面のドキュメントになりますが、2回とも「名称未設定-1」が表示されます。
今度はisValidを挟んで
var doc1=app.documents[0];
alert(doc1.name);
app.documents.add();
doc1.isValid;
alert(doc1.name);
を実行してみると、1回目は「名称未設定-1」、2回目は「名称未設定-2」になります。
なんか分かったような分からんような話ですが、とりあえず、あけましておめでとうございました。
PR