ライブラリのアイテムを自動で配置する(InDesign CS3〜)

とってもお久しぶりです。最近アウトプット減ってて*1インプットも滞っています。
スクリプトもまとまったものはあまり書いてないんですが、久しぶりに汎用っぽいのができたのでおすそわけ。すでに書いてる人がいたらすいません……

ライブラリの各アイテムを自動でまとめて配置する

たとえばコンピュータ関連の解説書とかで「Ctrl+Pを押します」なんて書いたりして、それぞれのキーを絵で表現したりするようなとき。

フォントを作っちゃうのも手なんですが、そこまでする数じゃないような場合はライブラリを使うと便利。

ただ、ひとつずつD&Dして配置するのはめんどくさいです。私なら3つくらいやったとこでイラッと来ます。
というわけでこんなのを書きました。テストはWinXP SP3、InDesign CS4でのみ行っています。

↓↓↓

「#(半角英数のアイテム名)」という文字列が選択範囲内に見つかったら、指定したライブラリ内の同名アイテムで置換します。何も選択していない場合はドキュメント全体を処理します。
配置したオブジェクトはインラインオブジェクトになります。インラインオブジェクトの各種テキストスタイル属性は置換元の最後の文字(「#name」なら「e」)と同じになります。

// 選択部分が対象
var target = app.selection;
// 何か選択されていたらその中だけ。選択してなければドキュメント全体が対象
target = (target.length > 0) ? target[0] : app.activeDocument;

// ライブラリから読み込み。ファイル名を指定。InDesign上で開いてないとだめです
var lib = app.libraries.itemByName("keylib.indl"); // ★

// 検索条件(「#半角英数」の最長一致)
app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "#[0-9A-z]+";

// 検索実行
var f = target.findGrep();

// 検索で引っかかった部分を後ろから順番に処理
// 置換によって文字数が変わる処理なので前からやると泣きを見ます
for(var i = f.length-1; i > -1; i--){
  var name = f[i].contents.substr(1); // 検索された文字列をアイテム名として扱う。substrは「#」を取り除く処理
  var asset = lib.assets.itemByName(name); // 名前からライブラリのアイテムを特定

  try {
    asset.placeAsset(f[i].insertionPoints[-1]); // 配置(検索された文字列の最後にくっつける)
    f[i].characters.itemByRange(0, -1).remove(); // 検索された文字列を削除(最後にくっつけたアイテムは残す)
  } catch(e){
    // エラーが起きたらスルー
  }
}

注意点は画像に書いたとおり。正規表現検索の使えるCS3以上が対象、アイテム名に半角英数以外は(記号も)使えません。
使うときは★をつけた行でライブラリ名(ファイル名)を指定してください。

場合によるとは思うけど

実際使うときは、元の「#アイテム名」のテキストに文字スタイルをつけておいて、その文字スタイル限定で検索するようにしたほうが安全じゃないかと思います。
検索条件のとこに

app.findGrepPreferences.appliedCharacterStyle = "ほげほげ文字スタイル名";

とか追加したり。処理後のインラインオブジェクトに文字スタイルが残るので、あとでまとめて検索もしやすいです。

以下、蛇足

せっかくなので勉強がてらちょっぴり修正してみます。

名前を発見するたびにいちいちライブラリを見に行くのは効率悪い気がしたので、最初にアイテムをリストアップしてアイテム名をキーとするオブジェクトに格納してしまうことにしました。
ついでに、いちいち選択解除するのがめんどくさいので、テキストカーソルを立ててるだけの状態のときはドキュメント全体を処理するようにします。あとコメント消したり無名関数でくるんでしまったりその他もろもろまとめ書きして……

(function(){
  var libname = "keylib.indl";

  var target = (app.selection.length > 0) ? app.selection[0] : app.activeDocument;
  target = (target.constructor.name == "InsertionPoint") ? app.activeDocument : target;

  var assets = {};
  var lib = app.libraries.itemByName(libname);
  for(var i = 0, alen = lib.assets.length; i< alen; i++){
    var tmp_a = lib.assets[i];
    assets[tmp_a.name] = tmp_a;
  }

  app.findGrepPreferences = NothingEnum.nothing;
  app.findGrepPreferences.findWhat = "#[0-9A-z]+";

  var f = target.findGrep();
  for(var i = f.length-1; i > -1; i--){
    var tmp_f = f[i];
    try {
      assets[tmp_f.contents.substr(1)].placeAsset(tmp_f.insertionPoints[-1]);
      tmp_f.characters.itemByRange(0, -1).remove();
    } catch(e){}
  }
})();

こんな感じ。

で、よーしと思って大量に配置するサンプルを用意して処理速度を計ってみたら……ほとんど変わんなかったorz
ネックはどこにあるのかな*2。ドキュメントを描画なしで開いて処理したら速そうな予感はあるけど、心折れたので試してません。

さらに蛇足(未解決)

最初に書いたとき、置換処理は次のように書いてました。

// ~~~~~~~~~~
  var asset = lib.assets.itemByName(f[i].contents.substr(1)); // fはfindGrep()の戻り値
  try {
    asset.placeAsset(f[i].texts[0]);
  } catch(e){}
// ~~~~~~~~~~

検索で引っかかったテキスト部分をplaceAssetで上書きしてしまえばいいと思ったのです。
実際それでもうまくいっていたんですが、「#name#name」など検索対象が連続しているときに不具合が出ました(前のほうの#name、つまりあとに処理されるほうが置換されない)。
いろいろ調べた結果、二回目の(つまり前のほうの)f[i].contentsに直後のインラインオブジェクトまで含まれてしまうからということがわかりました。それで「『name(オブジェクト)』なんて名前のアイテムはない」と処理がスキップされたようです。
f[i].toSpecifier()すると範囲は5文字ぶんのはずなのに、f[i].lengthをとると6が出て、f[i].contentsを表示してみると最後にオブジェクトが入っているという謎現象。私の理解がへんなのかなあ?
……とりあえず回避するため、textを扱うのは危険ということで文字単位で追加、文字単位で削除という形にしたのでした。
うーん?

*1:ついったー? あれはアウトプットとは言えん、少なくとも私の場合は……

*2:無名関数とかやめたり、思いつく限りいろいろ削ってみたけど大差なしだった