arinoth's memo

arinothのメモ

InDesignで連番を挿入するスクリプト

章通しで「図1」「図2」と入れるシリーズ書籍を担当していて、連番を入れるのが毎度大変なのでスクリプトを書いてみました。「特定の文字スタイルを設定した半角数字」を検索し、それを出現順になるよう並べ替えて番号を入れて行きます。

https://github.com/lwohtsu/InddRenban

使い方

テキストフレームに文字スタイルを設定した数値を入れておきます(下図の00のところに「図版連番(スクリプト用)」という文字スタイルを設定しています。ここ重要です)。テキストフレームはインラインにしてもOKです。

※テキストフレームの座標で順番を決めているので、1つのテキストフレームに複数の数値を入れるとうまく順番が取れません。

f:id:arinoth:20140422230122j:plain

文字スタイルは連番にする対象を特定するために付けるので、書式は何も設定していなくても構いません。名前も何でもOKです。また、表と図などで連番を振り分けたい場合は、文字スタイルを複数作成します。

f:id:arinoth:20140422235505j:plain

スクリプトパネルから「LWRenban」を実行します。

f:id:arinoth:20140422235515j:plain

文字スタイルとゼロパディングの形式を選択して〈OK〉をクリックします。

f:id:arinoth:20140422235522j:plain

連番が挿入されました。画像では1ページのみですが、複数ページでもちゃんと連番になります。

f:id:arinoth:20140422235535j:plain

表のセルに文字スタイル付きの数値を入れておいて、連番を振ることも可能です。 ただし、1ファイルで通し番号を振る仕様なので、1つのストーリーだけに連番を振るといったことはできません。

InDesignスクリプトの話

InDesignスクリプトは始めて触ったのですが、JavaScriptとはいえ色々独自仕様が色々あって何やらケッタイな感じですね。

とりあえず公式のスクリプティングガイドとAPIリファレンス

Adobe® InDesign® CS6 スクリプティングガイド: JavaScript

APIリファレンス

ダイアログの作成

ダイアログの作り方については公式スクリプティングガイドをそのままマネすればOKです。コントロールのコレクションがあってそこに新規をaddしながら作っていくイメージです。あまり作りやすくない気がします。

リストボックスに文字スタイル名の一覧を表示させたいので、ルートにある「app」オブジェクトのactiveDocumentプロパティでアクティブな文書を取得します。アクティブドキュメントのcharacterStylesプロパティから文字スタイルのコレクションが取得できるので、名前のみ取り出して配列を作成してリストボックスに指定します。

function myDisplayDialog(){
    //文字スタイルの一覧を取得
    var myDocument = app.activeDocument;
    var l = myDocument.characterStyles.length;
    var stylenames = [];
    for(var i=0; i<l; i++){
        stylenames.push(myDocument.characterStyles.item(i).name);
    }
    
    //ダイアログ生成
    var myDialog = app.dialogs.add({name:"Renban"});
    with(myDialog.dialogColumns.add()){
        //説明ラベル
        staticTexts.add({staticLabel:"特定の文字スタイルを設定した数字に対して連番を挿入します"});
        staticTexts.add({staticLabel:"数値がテキストフレームに入っていないとうまくY座標を取れないかも"});
        //パネルを追加
        with(borderPanels.add()){
            //ラベル
            with(dialogColumns.add()){
                staticTexts.add({staticLabel:"文字スタイルを選択"});
            }
            //リストボックス
            with(dialogColumns.add()){
                var listboxCharaStyle = dropdowns.add
                    ({stringList: stylenames, selectedIndex:0});
            }
        }
        //パネルを追加
        with(borderPanels.add()){
            //ラベル
            with(dialogColumns.add()){
                staticTexts.add({staticLabel:"ゼロ埋めの形式"});
            }
            //ラジオボタン
            var myRadioButtonGroup = radiobuttonGroups.add();
            with(myRadioButtonGroup){
                radiobuttonControls.add({staticLabel:"1"});
                radiobuttonControls.add({staticLabel:"01", checkedState:true});
                radiobuttonControls.add({staticLabel:"001"});
            }
        }
    }
    
    myReturn = myDialog.show();
    if (myReturn == true){
        g_stylename = stylenames[listboxCharaStyle.selectedIndex];
        g_zerotype = myRadioButtonGroup.selectedButton;
        myDialog.destroy();
        return true;
    } else {
        myDialog.destroy();
        return false;
    }       
}

正規表現で検索する

このスクリプトでは「特定の文字スタイルが設定された数値」を正規表現で検索しています。やり方はスクリプティングガイドの108ページに書いてあるとおりです。

findChangeGrepOptionsに検索オプションを指定し、findGrepPreferences.findWhatに正規表現を、findGrepPreferences.appliedCharacterStyleに文字スタイルを指定して、findGrepメソッドを実行すると、Textオブジェクトの配列が返されます。

最初はInDesignの検索ダイアログで試した「¥d+」を貼り付けて実行したのですが、なぜだかうまく検索できませんでした。

/連番実行
function doRenban(){
    //alert("スタイルは" + g_stylename);
    //alert("ゼロ埋め形式は" + g_zerotype);
    var myDocument = app.activeDocument;
    //前回の検索結果を消去
    app.findGrepPreferences = NothingEnum.nothing;
    app.changeGrepPreferences = NothingEnum.nothing;
    //検索条件を設定
    app.findChangeGrepOptions.includeFootnotes = false;
    app.findChangeGrepOptions.includeHiddenLayers = false;
    app.findChangeGrepOptions.includeLockedLayersForFind = false;
    app.findChangeGrepOptions.includeLockedStoriesForFind = false;
    app.findChangeGrepOptions.includeMasterPages = false;
    app.findGrepPreferences.findWhat = "[0-9]+";    //1つ以上の数値
    app.findGrepPreferences.appliedCharacterStyle = g_stylename;    //文字スタイル
    //検索実行
    var findresult = myDocument.findGrep();
    ……(後略)……

ページ順、y座標、x座標順に並べ替える

InDesignの検索機能では文字の出現順ではなく、(たぶん)テキストフレームの作成順に検索されるのですが、findGrepの結果も同じように作成順になります。なので、そのまま連番を入れるとデタラメな順番になってしまいます。ページ順、y座標、x座標でソートし直さなければいけません(横組みの場合)。

そこでページと座標を調べるexplorePageAndPos関数を作り(後述)、その結果をソートしてから連番を挿入するようにしました。

  ……doRenbanの続き……
    //ページと座標を調べる
    var exresult = explorePageAndPos(findresult);
    //並べ替え
    exresult.sort(function(a, b){
        //座標は1000倍して整数にする
        var a_page = parseInt(a.pagenumber), 
            a_x = Math.round(a.cordx*1000), a_y = Math.round(a.cordy*1000);
        var b_page = parseInt(b.pagenumber), 
            b_x = Math.round(b.cordx*1000), b_y = Math.round(b.cordy*1000);
        //まずページ番号で比較
        if(a_page > b_page) return 1;
        if(a_page < b_page) return -1;
        //同ページ内ならy座標で比較
        if(a_page == b_page){
            if(a_y > b_y) return 1;
            if(a_y < b_y) return -1;
            //y座標がピッタリ同じならx座標で比較
            if(a_y == b_y){
                //$.write('samey' + a_y + ' ax:' + a_x + ' b.x:' + b_x);
                if(a_x > b_x) return 1;
                if(a_x < b_x) return -1;             
            }
        }
        return 0;
    });
    //連番挿入
    for(var i=0; i<exresult.length; i++){
        var renban = String(i + 1);
        if(g_zerotype > 0){
            renban = ('000' + renban).slice(-(g_zerotype+1));
        }
        exresult[i].text.contents = renban;
    }
    //検索結果を消去
    app.findGrepPreferences = NothingEnum.nothing;
    app.changeGrepPreferences = NothingEnum.nothing;
}

sortメソッドの使い方は普通のJavaScriptと同じです。最初座標値をそのまま使っていて「if(a_page == b_page)」のところではまっていたのですが、実数値で「==」はダメでした。 なので、1000倍してから四捨五入したものを比較するようにしています。

※よく考えると「if(a_page == b_page)」や「if(a_y == b_y)」はなくてもいいのか……。

ページとテキストフレームの座標を調べる

ここが一番手間が掛かったところです。検索結果のTextオブジェクトからその座標やページを調べるには、parentを上にたどっていってTextFrameやPageオブジェクトを見つけなければならんのです。

ただし、InDesignのテキストオブジェクト階層は、下図のとおり「ストーリー(テキストの繋がり)」と「ページアイテム類(テキストフレーム・ページ・レイヤーなど)」の2系列に分かれています。

f:id:arinoth:20140422233615j:plain

座標やページ番号を調べるにはページアイテム側の階層をたどらなければいけません。 スクリプティングガイドの90ページあたりに、テキストフレームの階層を上方向にたどりたい場合はparentTextFrameプロパティを参照しろとあります。


以下ができあがったコードです。大まかにはdo〜while文で下から上へたどっていき、最終的にページ番号と座標、検索結果のTextオブジェクトをまとめた配列を返します。

InDesignのオブジェクトはconstructor.nameプロパティでオブジェクトの種類を判定できるので、それでparentプロパティの内容がStoryかそれ以外かを見分けています。

TextFrameの座標を調べるにはvisibleBoundsプロパティ、Pageの番号を調べるにはnameプロパティを利用します。

//parentをさかのぼって位置とページを調べる
function explorePageAndPos(findresult){
    var newresult = [];
    
    for(var i=0; i<findresult.length; i++){
        var parent = null, x = null, y = null, page = null, counter=0;
        var current = findresult[i];
        //$.write('--------');
        do{
            parent = current.parent;
            //parentがstoryの場合はテキストフレームの親を探す
            if(parent.constructor.name == 'Story'){
                parent = current.parentTextFrames[0];
            }
            //$.write(parent.constructor.name);
            //parentがTextFrameの場合は座標を調べる
            if(parent.constructor.name ==  'TextFrame'){
                if(y == null){
                    var bounds = parent.visibleBounds;
                    x = bounds[1];
                    y = bounds[0];
                    //$.write(' cord: x ' + x + ':y ' + y);
                }
            }
            //parentがCellの場合は行のインデックスを調べる
            if(parent.constructor.name == 'Cell'){
                if(y == null){
                    y = parent.parentRow.index;
                    x = parent.parentColumn.index;
                    //$.write('Cell' + parentrow.index);
                }
            }
            //parentがPageの場合はページを調べる
            if(parent.constructor.name ==  'Page'){
                page = parent.name;
                //$.write(' page: '+page);
                break;  //ループ脱出
            }
            current = parent;
            //無限ループ対策
            counter++;
            if(counter > 20) break;
        }while(current.constructor.name != 'Document');
        if(page!=null){
            newresult.push({
                text: findresult[i], cordy: y, cordx: x, pagenumber: page
            });
            //$.write('#pushed');
        }
    }
    return newresult;
}

どうも対応できない階層があるのか、時々無限ループにおちいるっぽいので、20回でループを脱出するように手を加えました。


デバッグライト($.debugメソッド)でたどっていったオブジェクトを列挙すると、InDesignがどのように管理しているのかが見えてちょっと楽しいです。 途中にCharacterオブジェクトというものが挟まっているのはインラインで図版を挿入しているからだと思います。

--------
TextFrame
 cord: x 52:y 87.5885398259197
Group
Group
Group
Character
TextFrame
Page
 page: 58
#pushed
--------
TextFrame
 cord: x 241:y 158.194597303602
Group
Group
Group
Character
TextFrame
Page
 page: 69
#pushed
--------
TextFrame
 cord: x 318.729626183868:y 102.212304203035
Group
Group
Group

……後略……