DTPab

DTPにまつわるあれこれ

DTPerのスクリプトもくもく会#8開催報告

f:id:uske_S:20180606135601j:plain

先日、DTPerのスクリプトもくもく会#8を開催しました。
今回は9名の方にご参加いただきましたが、その半数以上がお〜まちさんのIDJS教室に参加された方々で、いつもよりフレッシュな(当社比)もくもく会になりました!

会場は、前々回と同じYUIDEAさんの会議室をお借りしました。本当にいつもありがとうございます!
参加者の方の義理のご兄弟がYUIDEAさんで働いていらっしゃる…というお話が出て、業界は狭いなぁと^^;;

また、今回飛び入りで参加してくださった方がいらっしゃったのですが、当日連絡なく来られてしまうと会場の都合などもあっていろいろと困るので、やはりきちんとconnpassのイベントページから正式に参加表明の上、ご参加お願いたします。事前に主催者へご相談いただければその旨ご案内もできたのですが…。
このことも含めていろいろありましたので、もくもく会の参加ルールを改めて見直そうかなと考えています。

いただいた質問など

覚えている範囲で、僕に直接いただいたご質問・ご相談は以下のとおりです。

  • 縦組みで奇数ページの最終行の見出し(段落スタイル)を判別したい
  • SUIのIconButtonがCCのいくつかから表示がおかしい
  • InDesignの検索置換パネルを利用した検索のしかた
  • SUI生成関数について(!)

このSUI生成関数は過去のもくもく会で僕が話した「SUIを簡易的に生成する」ためのモジュールです。これをご存知の方がいらっしゃって、びっくりしました。もうひとに教えることはないのかなと思っていたのでw コード自体は1年以上前に書いたもので今見るとあちこち気になるのですが、どうしても必要なところだけリライトして最後に再掲します。

縦組みで奇数ページの最終行の見出しを判別したい

これは特定の段落スタイル(見出し)を検索し、その段落(の先頭の文字)が左右どちらのページにあるのか判別します。そのうえで、その段落(の先頭の文字)がページの最後の行かどうかを調べようと思います。

ページの左右

そのものズバリのプロパティがPageオブジェクトにあります。Page.sideプロパティです(値は列挙値型オブジェクト)。
ドキュメントの設定が「見開きページ」になっていれば、ページパネルでは見開きページにノドが表示され(ページよりも上下に長い線)、それよりも左側は左ページ、右側は右ページという扱いです。ページの角にドッグイヤーがつくことでも判別できます。「見開きページ」がオフの場合はすべてPageSideOptions.SINGLE_SIDEDという値になります(下図参照)。

f:id:uske_S:20180723164953p:plain

質問された際、ノンブル(Page.nameプロパティ)を取得し、それが奇数であれば左、偶数であれば右というやり方も相談されましたが、それを推奨しない理由は以下のとおりです。

  • ドキュメントが縦組み/横組み(というより右開き/左開き)かを事前に取得する必要がある(そうしないとどちらが右でどちらが左か決められない)
  • 奇数・偶数で左右を決定できないノンブルの振り方も設定できる(「選択スプレッドの移動を許可」オプション)
  • 単純に2で割れないノンブルも設定できる(ローマ数字、アルファベット、漢数字等)

ということで、ページの左右の判定はPage.sideプロパティを調べたほうがスマートです。

テキストフレームの最終行

これ、けっこう難しいなと思いました。ほかにいい方法がありそうな気がしてます。
僕が採用した方針は(ご質問が縦組みだったので)X座標を取得する方法です。
要するに、当該段落のX座標と、当該段落を含むテキストフレームの最終行のX座標を比較し、それが一致すれば最終行だと判別するというアルゴリズムです。
まずは段落のX座標ですが、Paragraph.horizontalOffsetです。これを、検索して見つかった段落と、その親のテキストフレームの最終行の段落とで比較します。コードとしては以下のような感じです。

var doc = app.activeDocument;
var find = doc.findGrep();

for (var i=0; i<find.length; i++) {
    //当該段落のX座標
    var ho = find[i].horizontalOffset;
    var lines = find[i].parentTextFrames[0].lines;
    //当該段落の親テキストフレームの最終行のX座標
    var lastLineOffset = lines[lines.length-1].horizontalOffset;
    
    if (ho === lastLineOffset) {
        alert((i+1)+"番目の段落はフレームの最終行です");
    }
}

以上の2つを組み合わせて、奇数ページ(左ページ)の最終行を判定しましょう。

まとめ

2つを関数にしてみました。お好みで書き換えてください。

var doc = app.activeDocument;
//検索条件は適宜変更のこと
var find = doc.findGrep();

var isLeftPage = function(text) {
    /**
     * try・catch構文に入れているのは、ペーストボード上のテキストや、
     * アンカー付きオブジェクトなどのエラー処理を考えていないためです。
     * 余裕があればtry・catch構文を使わずにエラー処理を実装してみてください。
     */
    try {
        if (text.parentTextFrames[0].parentPage.side === PageSideOptions.LEFT_HAND) {
            return true;
        }
    } catch(e) {
        alert(e); //例外が発生した場合は一応アナウンスする
    }
    return false;
};


var getLastLinePara = function(list) {
    var temp = []; //対象の段落をまとめてreturnするための空の配列
    for (var i=0; i<list.length; i++) {
        //左ページにあるか?
        if (!isLeftPage(list[i])) {
            continue;
        }
        //当該段落のX座標
        var ho = list[i].horizontalOffset;
        var lines = list[i].parentTextFrames[0].lines;
        //当該段落の親テキストフレームの最終行のX座標
        var lastLineOffset = lines[lines.length-1].horizontalOffset;
        if (ho === lastLineOffset) {
            //配列に加えていますが、処理が単純ならここで直接オブジェクトを操作してもOK
            //その場合は変数tempという空の配列やそれをreturnする文は不要
            temp.push(list[i]);
        }
    }
    return temp;
};

//左ページかつ最終行にある段落だけがgetLastLinePara関数の戻り値として取得できる
$.writeln(getLastLinePara(find));

コードにも書いてありますが、以下の点に注意してください。

  • 検索条件は適宜変更してください。これだと検索置換パネルの正規表現タブの内容で検索するだけです
  • isLeftPage関数にtry・catch構文を使っていますが、ペーストボード上(ページの外)のテキストやアンカー付きオブジェクト内のテキストでエラーになる可能性があるからです
  • getLastLinePara関数が左ページの末尾にある段落をまとめて新しい配列として返してきます

SUIのIconButtonがCCのいくつかから表示がおかしい

ScriptUI(SUI)が予告なく仕様変更され、見た目が大きく変わったのがInDesign CC2017です。通常のボタン、静的テキスト、アイコンボタンで比較すると下図のような変遷を辿っています。

f:id:uske_S:20180723165052p:plain

コードはこちら(画像ファイルは用意していません)。

#targetengine session

var w = new Window("dialog", "testDialog");
w.add("button", [0,0,15,30], "A");
w.add("button", undefined, "button");
w.add("statictext", [0,0,30,30], "AAAAA");
w.add("statictext", undefined, "statictext");
w.add("iconbutton", [0,0,30,30], File("~/A.png"));
w.add("iconbutton", undefined, File("~/coloredA.png" ));

w.show();

CC2015からアイコンボタンは画像を中央に配置できなくなりました(たぶんやればできるけどデフォルトではなくなった)。CC2017からはフラットデザインになったし、丸がデザインの基調になりました。おかげで極端に小さなサイズを指定していたボタンはパスがおかしくなっているし、アイコンボタンの周りに丸が描かれることになり、大変遺憾なデザインになっております。地味なところでは文字サイズも微妙に違うため、エリア指定している静的テキストなどは表示できる範囲が微妙に違ったりしています。
こうした仕様変更にも負けず、バージョンに左右されない意図通りのSUIを実装するためには、

  • アイコンボタンは使わない
  • 座標指定によるレイアウトをしない
  • 新しいInDesignのバージョンが出たら、既存のSUIを実装したスクリプトを一旦試す

ということが必要です。最悪の場合はバージョンによってUIを作り変えるという処理が必要かもしれません(特に有料のものであれば)。
その上で、ではアイコンボタンの代わりに何を使えばよいかというと、もう画像そのものを配置して、それに対してイベントを定義してあげる方法がスマートかと思います。このあたりはまた別の機会に。

また、CCからはインターフェイスの色を選べるようになりました。SUIのstatictextなどは一定値以上の暗いグレーでは自動で白黒が反転します。ですので、UIに「●」と「◯」などの文字の白黒で選択肢をトグルさせたりするものは、ユーザーの環境によって意図が伝わらない場合があります。さらに、png画像を使って背景を透過させたものは、UIのカラーによって見えなくなる場合があります。先程の画像をダークカラーのUIで表示すると以下の通りです(画像はCC2017のダークカラー)。

f:id:uske_S:20180723173531p:plain

なので、画像を表示させてその背景の透過を利用する場合(特にテキスト用途として表示するケース)は工夫が必要になることがあります。

InDesignの検索置換パネルを利用した検索のしかた

InDesignの検索置換パネルを用いた検索では、ドキュメント、ストーリー、選択範囲など、現在の選択状態からいくつかのバリエーションで対象を絞り込んで実行することができます。実は、スクリプトだともっと細かい範囲で検索を実行できます。
ESTKのオブジェクトモデルビューアから、ブラウザを適当なInDesignにして「findGrep」を検索すると、けっこうたくさん見つかります。 f:id:uske_S:20180723165442p:plain

親のオブジェクト 検索対象
Application アプリケーション=全ドキュメント
Document ドキュメント
Cell セル
Table 表組
Column 表組の列
Row 表組の行
Text テキスト全般
Character 文字
Word 単語
Line テキストの1行
TextColumn 段組の段
Paragraph 段落
TextStyleRange 文字スタイルが適用されたひとまとまりのテキスト
InsertionPoint 挿入点
TextFrame テキストフレーム
Story ストーリー
TextPath パステキスト
XMLElement XMLエレメント?
XMLStory XMLストーリー?

一部、例えばInsertionPointなどは検索対象として機能しないわけですが、Textオブジェクトの仲間なのでfindGrepメソッドも用意されているという感じです。
で、これらを利用すると非常に細かい範囲で検索置換を実行することができます。基本的に、テキストに分類されそうなオブジェクトであればまずfindGrepメソッドを持っていると考えていいでしょう。
一方、検索置換パネルで検索対象として選べる「ストーリーの最後へ」というものが、スクリプトからは参照できません。これを実現するには対象をStoryとし、現在の挿入点より後ろのindexは除外するといった一工夫が必要になります。
XMLまわりの最後の2つは使ったことがないので、もし詳しい情報をお持ちの方がいたらぜひお教えください。

SUI生成関数

ずいぶん前のもくもく会か、もしくはTwitterで紹介したSUIの生成を手助けするモジュールを、1年前くらいに作りました。このモジュールを知ってる方が今回参加してくださっていて、使い方について質問されました!まじか!
今見返すとですね…非常に…こっ恥ずかしいというか…まぁそんなコードなのです。evalメソッドモリモリ。かといってこれをゼロからリファクタリングするほど暇もないので、ほとんどそのまま再掲します。以降は、この関数がどのようにSUIの生成を手助けするかとその使い方を説明します。

と思ったのですが、とても長くなりそうなので記事を分けるかDropbox Paperにでもアップします。ちょっとだけお待ち下さい^^;;
コード自体もgistなどで共有しようかと思います。

次回告知

次回は9/22(土)を予定しています。
準備でき次第connpassのイベントページで告知しますので、よろしくお願いします!