はじめに
ものかのさんの「なると巻き」というMac用のアプリケーションをご存知でしょうか。InDesign CS5よりも前(InDesign CS4、v6.x以下)のバージョンで作られたinddファイルをCS5以上(v7.x以上)で開くと一部の異体字に引きずられて別の文字も異体字になってしまうという不具合を解消するために開発されたアプリケーションです。
これが開発された当時と現在とでフォントを取り巻く状況が大きく変わり、この「なると巻き」は開発終了とアナウンスされました。
そんな経緯があり、ものかのさんが異体字を基底グリフに変換する部分のAppleScriptをGistで公開してくださったのですが(前掲記事の末尾参照)、それのExtendScript(jsx)版を開発させてもらいました。
このスクリプトは何をするもの?
選択したテキストのaalt字形、nalt字形(異体字)を基底グリフに置換するスクリプトです。
元のAppleScriptのとおり、複数の文字にまたがってルビが設定されていてもスクリプトでルビが消えることはありません。
スクリプト自体はGitHubに公開しています(ダウンロードのしかたが分からない方は次項を参照ください)。
ダウンロードするには
やり方は3つあります。
- GitHubからzipファイルをダウンロードする
- GitHubからリポジトリをクローンする
- ソースコードをコピーしてエディタにペーストし、自分でjsxファイルを作成する
今回は1のやり方を説明します。
これでダウンロードできます。
スクリプトの使い方
aalt/nalt字形を含んだテキストを選択し、スクリプトを実行してください。
ルビを含んだテキストも問題なく置換できるはずです。また、AppleScript版にはありませんでしたが、Undo(⌘+Z)1回でスクリプト実行前に戻れるようにしています。
jsx版について
ここから先は技術的な解説です。不要な方は読み飛ばしてどうぞ。
ものかのさんが公開したAppleScriptを読み解く作業から開始し、それをトレースして再構成する形でjsx版を開発ました。
以下より、部分的にコードの解説をしていきます。
main関数
冒頭はドキュメントが開かれているか、選択状態のチェックを行っているだけなので、Application.doScript()
メソッドで呼び出すこの関数から説明します。
function main() { for (var i = 0; i < sels.length; i++) { if (!sels[i].hasOwnProperty("findGrep")) { continue; } var tgtChrs = sels[i].characters; for (var j = 0, lenJ = tgtChrs.length; j < lenJ; j++) { var tgtOtf = tgtChrs[j].opentypeFeatures[0]; // openTypeFeatureが入るときは配列になる if (tgtOtf && (tgtOtf[0] === "aalt" || tgtOtf[0] === "nalt")) { reWrite(tgtChrs[j]); } } } }
実は元のAppleScriptとちょっと挙動を変えました。スクリプト動作対象になるオブジェクトについてです。元は選択したテキストひとつを対象にしていましたが、このjsxでは複数の選択対象、つまりテキストフレーム単位の選択も想定しています。
選択したオブジェクトをふるいに掛けるために、そのオブジェクトがfindGrep()
メソッドを持っているか、hasOwnProperty()
メソッドで調べています。
持っていればテキストだろうということで、オブジェクト内のcharacters
を1文字ずつ走査していきます。
文字ごとのopentypeFeatures
プロパティを参照するのですが、このプロパティがちょっと厄介で、aaltやnaltなどopentypeFeatureが与えられていると中身が二重配列なります。
var tgtOtf = tgtChrs[j].opentypeFeatures[0]; // openTypeFeatureが入るときは配列になる if (tgtOtf && (tgtOtf[0] === "aalt" || tgtOtf[0] === "nalt")) { reWrite(tgtChrs[j]); }
tgtChrs[j].opentypeFeatures
は必ず配列のため、まず0番目のインデックス[0]
を参照します。このときその文字にopentypeFeatureが与えられていなければundefined
、逆に何かしら値が返ってくればopentypeFeatureが与えられていることになります。なのでif (tgtOtf)
として、そもそも値が入っているかどうかをまず調べています。
その値ですが、その文字がnalt字形やaalt字形であれば、["nalt", 1]
のような値が入っています*1。
この配列の0番目のインデックス[0]
が"aalt"
や"nalt"
であればreWrite
関数を呼び出して実際に字形を変換します。
reWrite関数
こちらは元のAppleScriptをほとんどそのままトレースした形で組み立てました。なるべくネストしないよう心がけたつもりです。
function reWrite(chr) { var baseContent = chr.contents; // ルビがない場合 if (!chr.rubyFlag || chr.rubyString === "") { chr.contents = baseContent; return; } // ルビがある場合 var curParagraph = chr.paragraphs[0]; // 段落全体の情報を取得しておく var chrsRubyFlagInParagraph = { before: curParagraph.characters.everyItem().rubyFlag }; var curRubyString = chr.rubyString; chr.insertionPoints[1].contents = baseContent; chr.characters[0].remove(); chrsRubyFlagInParagraph.after = curParagraph.characters.everyItem().rubyFlag; var tgtChrIndexArray = []; for (var i = 0, len = chrsRubyFlagInParagraph.before.length; i < len; i++) { if (chrsRubyFlagInParagraph.before[i] !== chrsRubyFlagInParagraph.after[i]) { tgtChrIndexArray.push(i); } } try { curParagraph.characters.itemByRange(tgtChrIndexArray[0], tgtChrIndexArray[tgtChrIndexArray.length - 1]).properties = { rubyFlag: true, rubyString: curRubyString }; } catch (e) { alert(e); } }
基底グリフに置換する仕組み
字形を置換するからといって、実はスクリプトの内部処理的には「字形検索置換」どころか「InDesignの検索置換」は一切使っていません。どうやってaalt/nalt字形を基底グリフに置き換えているというと、文字列の再代入で実現しています。
文字列(String型の値としての文字)をInDesign上で表示される文字(Characterオブジェクト)として挿入する場合、Characters.contents = "あ"
というように、Character.contents
プロパティに値として文字列を代入するという書き方になります。こうして挿入された文字は、フォントによって紐付けられた基底グリフになります。このスクリプトはその仕様を逆手に取っているわけですね。効率的でシンプルです。
ルビが設定されていない場合
先の仕組みを利用して、元の文字のcontents
をそのまま再代入します。
ちなみに、ルビが設定されているかどうかの条件分岐をif (!chr.rubyFlag || chr.rubyString === "")
としています。InDesignのGUIだけであればどちらかだけ調べればいいのですが*2、念のため2つのプロパティを確認しています。
ルビが設定されていた場合
元のAppleScriptの内容を追っていたときに思わず声が出ました。処理的に少し回りくどい(文字を挿入するたびにルビを復元しなくてはいけない)んですが、ルビがほぼ確実に復元できる方法だと思いました。ひょっとしたらもう少し効率的な方法があるかもしれないですが、コードとしては結構シンプルだったのでそのままトレースした次第です。
処理の流れとしてはこんな感じです。
- 文字を内包する段落を取得
- 文字置換前の段落全体の文字ごとの
rubyFlag
を取得 - 設定されている
rubyString
を取得 - 該当の文字の後ろに基底グリフで文字を挿入(このときルビが消える)
- 改めて段落全体の文字ごとのrubyFlagを取得
- 置換前(2)と比較して異なるもの=ルビが消えた文字の範囲を取得する
- その範囲を指定して取っておいた
rubyString
(3)を再設定する - 以上を繰り返す
InDesignのコレクションオブジェクトに対してeveryItem()
メソッドという便利なものがあります。これを使うと、コレクション(このスクリプトではCharacters
)の各プロパティがすべて配列で得られます。for文などを使ってすべての情報を参照しながら走査するよりもこちらのほうが何倍も高速です*3。for文やwhile文が頻出しないのでコードも読みやすいです。
Characters.itemByRange()
メソッドを使うと、第一引数〜第二引数の範囲でCharacter
オブジェクトを指定できます。
先の処理の流れの6でルビが消えた文字の範囲*4を調べましたが、このメソッドの引数にするためでした。
最後にCharacter.properties
プロパティに、Object型オブジェクトを渡してルビを設定しなおしています。
try・catch文で適用しているのは元のAppleScriptに倣っているためです。
最後に
長くなってしまいましたが、ざっとこんなスクリプトになっています。
不具合や修正リクエスト等があればぜひissueやPR出してください!
改めて、なると巻きアプリの開発をされ、中のグリフ置換処理部分を公開されたものかのさんに深い感謝と敬意を表します。
*1:InDesignの情報パネルにあるOTF欄に表示されるものが配列として格納されています。
*2:InDesignのGUIではどちらかだけを設定することができません。詳しくは過去の記事を参照
uske-s.hatenablog.com
*3:例えば Document.fonts とかをfor文を使って一つずつ値を取り出すのと、everyItem() を使って配列として取り出すのでは後者のほうが超高速
*4:より厳密には、ルビが消えた文字が段落の中の何番目の文字か、という0ベースのインデックス