DTPab

DTPにまつわるあれこれ

InDesign用サンプルスクリプトのリファクタリング(1)

はじめに

InDesignやIllustratorには、最初からサンプルスクリプトがプリインストールされているのはご存知でしょうか。
InDesignには(Macであれば)AppleScriptとJavaScriptのサンプルが用意されています。

f:id:uske_S:20190109203525p:plain

例えばJavaScriptのサンプルを取り出すとこんな感じです。

  • AddGuides.jsx
  • AddPoints.jsx
  • AddQRCode.jsx
  • AdjustLayout.jsx
  • AlignToPage.jsx
  • AnimationEncyclopedia.jsx
  • BreakFrame.jsx
  • CornerEffects.jsx
  • CreateCharacterStyle.jsx
  • CropMarks.jsx
  • ExportAllStories.jsx
  • FindChangeByList.jsx
  • ImageCatalog.jsx
  • MakeGrid.jsx
  • Neon.jsx
  • PathEffects.jsx
  • PlaceMultipagePDF.jsx
  • SelectObjects.jsx
  • SortParagraphs.jsx
  • SplitStory.jsx
  • TabUtilities.jsx

けっこうたくさんあります。実際、僕自身も仕事でいくつか使っています。
そうやって仕事で使う中で、思った通りに動いてくれないスクリプトがあります。それがAlignToPage.jsxです。
どんなスクリプトかというと、選択したオブジェクトをページ(もしくはページマージン)に対して整列してくれるスクリプトです。ところが、レイアウトグリッドを使ってマージンを定義しているとうまく整列してくれません。

f:id:uske_S:20190109205630g:plain 東アジア圏でごめんな…。

リファクタリングしよう

そこで、こいつをリファクタリングしようと思ったわけですが、せっかくやるならその過程をブログに載せようと記事にしました。少し長いスクリプトなので数回に分けて掲載します。 やりたいことは、

  • 日本語へのローカライズ
  • バグフィックス(レイアウトグリッドでも正しく動作するようにする)
  • コードの見直し

の3つです。

実際のコード

長いけど載せちゃいます。2009年に書かれたコードなんですね…。

//AlignToPage.jsx
//An InDesign JavaScript
/*  
@@@BUILDINFO@@@ "AlignToPage.jsx" 3.0.0 15 December 2009
*/
//Aligns the items in the selection to the specified location on the page.
//
//For more on InDesign/InCopy scripting see the documentation included in the Scripting SDK 
//available at http://www.adobe.com/devnet/indesign/sdk.html
//Or visit the InDesign Scripting User to User forum at http://www.adobeforums.com.
//
main();
function main(){
    //Make certain that user interaction (display of dialogs, etc.) is turned on.
    app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
    var myObjectList = new Array;
    if (app.documents.length != 0){
        if (app.selection.length != 0){
            for(var myCounter = 0;myCounter < app.selection.length; myCounter++){
                switch (app.selection[myCounter].constructor.name){
                    case "Rectangle":
                    case "Oval":
                    case "Polygon":
                    case "TextFrame":
                    case "Group":
                    case "Button":
                    case "GraphicLine":
                        myObjectList.push(app.selection[myCounter]);
                        break;
                }
            }
            if (myObjectList.length != 0){
                myDisplayDialog(myObjectList);
            }
            else{
                alert ("Please select a page item and try again.");
            }
        }
        else{
            alert ("Please select an object and try again.");
        }
    }
    else{
        alert ("Please open a document, select an object, and try again.");
    }
}
function myDisplayDialog(myObjectList){
    var myDialog = app.dialogs.add({name:"AlignToPage"});
    with(myDialog.dialogColumns.add()){
        with(dialogRows.add()){
            with(dialogColumns.add()){
                with(borderPanels.add()){
                    staticTexts.add({staticLabel:"Vertical"});
                    var myVerticalAlignmentButtons = radiobuttonGroups.add();
                    with(myVerticalAlignmentButtons){
                        radiobuttonControls.add({staticLabel:"Top", checkedState: true});
                        radiobuttonControls.add({staticLabel:"Center"});
                        radiobuttonControls.add({staticLabel:"Bottom"});
                        radiobuttonControls.add({staticLabel:"None"});
                    }
                }
            }
            with(dialogColumns.add()){
                with(borderPanels.add()){
                    staticTexts.add({staticLabel:"Horizontal"});
                    var myHorizontalAlignmentButtons = radiobuttonGroups.add();
                    with(myHorizontalAlignmentButtons){
                        radiobuttonControls.add({staticLabel:"Left", checkedState: true});
                        radiobuttonControls.add({staticLabel:"Center"});
                        radiobuttonControls.add({staticLabel:"Right"});
                        radiobuttonControls.add({staticLabel:"None"});
                    }
                }
            }
        }
        with(dialogRows.add()){
            var myConsiderMarginsCheckbox = checkboxControls.add({staticLabel:"Consider Page Margins", checkedState:false});
        }
    }
    var myResult = myDialog.show();
    if(myResult == true){
        myVerticalAlignment = myVerticalAlignmentButtons.selectedButton;
        myHorizontalAlignment = myHorizontalAlignmentButtons.selectedButton;
        myConsiderMargins = myConsiderMarginsCheckbox.checkedState;
        myDialog.destroy();
        if (!((myHorizontalAlignment == 3)&&(myVerticalAlignment == 3))){
            myAlignObjects(myObjectList, myVerticalAlignment, myHorizontalAlignment, myConsiderMargins);
        }
    }
    else{
        myDialog.destroy();
    }
}
function myAlignObjects(myObjectList, myVerticalAlignment, myHorizontalAlignment, myConsiderMargins){
    var myXCenter, myYCenter;
    var myPageHeight = app.activeDocument.documentPreferences.pageHeight;
    var myPageWidth = app.activeDocument.documentPreferences.pageWidth;
    var myOldRulerOrigin = app.activeDocument.viewPreferences.rulerOrigin;
    app.activeDocument.viewPreferences.rulerOrigin = RulerOrigin.pageOrigin;
    app.activeDocument.zeroPoint = [0,0];
    myPage = app.activeWindow.activePage;
    if(myConsiderMargins == true){
        var myMarginPreferences = myPage.marginPreferences;
        if(myPage.side == PageSideOptions.leftHand){
            var myOutsideMargin = myMarginPreferences.left;
            var myInsideMargin = myMarginPreferences.right;
            myXCenter = myOutsideMargin + ((myPageWidth - (myInsideMargin+myOutsideMargin))/2)
        }
        else{
            var myInsideMargin = myMarginPreferences.left;
            var myOutsideMargin = myMarginPreferences.right;
            myXCenter = myInsideMargin + ((myPageWidth - (myInsideMargin+myOutsideMargin))/2)
        }
        var myBottomMargin = myMarginPreferences.bottom;
        var myTopMargin = myMarginPreferences.top;
        myYCenter = myTopMargin + ((myPageHeight - (myTopMargin+ myBottomMargin))/2)
        switch(myHorizontalAlignment){
            case 0:
                myX = myInsideMargin;
                break;
            case 1:
                myX = myXCenter;
                break;
            case 2:
                myX = myPageWidth - myOutsideMargin;
                break;
            case 3:
                myX = "None";
                break;
        }
        switch(myVerticalAlignment){
            case 0:
                myY = myTopMargin;
                break;
            case 1:
                myY = myYCenter;
                break;
            case 2:
                myY = myPageHeight - myBottomMargin;
                break;
            case 3:
                myY = null;
                break;
        }
    }
    else{
        myXCenter = myPageWidth/2;
        myYCenter = myPageHeight/2;
        switch(myHorizontalAlignment){
            case 0:
                myX = 0;
                break;
            case 1:
                myX = myXCenter;
                break;
            case 2:
                myX = myPageWidth;
                break;
            case 3:
                myX = "None";
                break;
        }
        switch(myVerticalAlignment){
            case 0:
                myY = 0;
                break;
            case 1:
                myY = myYCenter;
                break;
            case 2:
                myY = myPageHeight;
                break;
            case 3:
                myY = "None";
                break;
        }
    }
    for(myCounter = 0; myCounter < myObjectList.length; myCounter ++){
        myAlignObject(myObjectList[myCounter], myX, myY, myHorizontalAlignment, myVerticalAlignment);
    }
    app.activeDocument.viewPreferences.rulerOrigin = myOldRulerOrigin;
}
function myAlignObject(myObject, myX, myY, myHorizontalAlignment, myVerticalAlignment){
    var myBounds = myObject.geometricBounds;
    var myWidth = myBounds[3]-myBounds[1];
    var myHeight = myBounds[2]-myBounds[0];
    switch(myHorizontalAlignment){
        case 0:
            break;
        case 1:
            myX = myX-(myWidth/2);
            break;
        case 2:
            myX = myX-myWidth;
            break;
        case 3:
            myX = myBounds[1];
            break;
    }
    switch(myVerticalAlignment){
        case 0:
            break;
        case 1:
            myY = myY-(myHeight/2);
            break;
        case 2:
            myY = myY-myHeight;
            break;
        case 3:
            myY = myBounds[0];
            break;
    }    
    myObject.move([myX, myY]);
}

ぱっと見てもらえば分かると思いますが…………

なんて**なコードなんだ

**はワイルドカードなのでお好きな文字を充ててくださいw
いやーこれはリファクタリングしがいがありますね〜腕が鳴ります(白目)。

サンプルコードの解読

4つの関数で構成されています。main関数、myDisplayDialog関数、myAlignObjects関数、myAlignObject関数です。それぞれ読み込んでいきます。

main関数

17行目のif文は「ドキュメントが開かれているか?」というエラー処理。
18行目のif文は「オブジェクトが選択されているか?」というエラー処理。
19行目のfor文で「選択されたオブジェクトがそれぞれ何者か=コンストラクタ名で個別に確認」という処理。ここでObject.constructor.name"Rectangle""Oval"""Polygon""TextFrame""Group""Button""GraphicLine"の7つのうちのどれかであれば、このスクリプトの処理対象としてmyObjectList変数にpush、つまり配列として末尾に追加されていくようです。
32行目のif文は、そのようにして用意されたmyObjectList変数にひとつでも要素があれば、それを引数としてmyDisplayDialog関数を実行するという処理。
以降に続くelse文によるalertは、上記3つのif文に対応しています。
これでmain関数はおしまいです。

myDisplayDialog関数

48行目、InDesign専用のダイアログオブジェクトを生成しています。以降79行目までがダイアログの整形です。
81行目のif文でmyResult変数にtrueが返ってくれば、ダイアログで設定された内容を読み取ってmyAlignObjects関数に所定の引数を渡して実行するようです。

myAlignObjects関数

95行目から101行目までが現在のドキュメントの設定の読み込みです。
104行目のif文では、ページマージンを考慮するかどうかで分岐していますが、要するにmyXCenterを算出したいようですね。整列先の水平方向の中心座標ということかな。
レイアウトグリッドで意図したとおり動かないのはこの辺かな? とちょっとアタリを付けておきます。なんか適当にコメント入れておこう。
117行目のswitch文は引数として渡された水平方向の整列先の分岐。
同じように131行目のswitch文は引数として渡された垂直方向の整列先の分岐。
そして145行目までが「ページマージンを考慮する場合」というif文で、146行目からそのelse文。やってることは同じで計算式を変えているだけです。
178行目でそれぞれのオブジェクト(myObjectList変数に追加されたオブジェクトたち)に個別にmyAlignObject関数を、これまた所定の引数を渡して実行していくようです。
その処理が終わったら、99行目で変更していた定規の設定を181行目でもとに戻しておしまい。

myAlignObject関数

移動先の座標を割り出して213行目でmoveメソッドを使って移動。以上。(飽きた

オーバービュー

ちゃんと1行ごとに読み込んだわけですが、やっぱり**なコードですね。ざっくりと所感を述べますと、

  • 関数名とその処理の内容がミスマッチ
  • 同じような処理が続けて登場していて冗長的
  • switch文好きすぎ
  • 厳密等価比較演算子===などを使ってほしい

といったところ。

関数名や変数名の命名規則については個人の好みで全然構わないと思うのですが*1関数がその名前以上の役割を担ってはいけないと僕は思っています。たとえばmyDisplayDialog関数が、ダイアログの生成にとどまらず、myAlignObjects関数を実行しています。
あとAdobeのサンプルコードによく見られるmain関数ですが、mainよりもっと肝になるような関数が別であるじゃないか…。僕はこのmainという関数名はあまり好きではないです。

同じような処理というのは、たとえば102行目のif文とそれに続く146行目のelse文が典型的です。myXmyYを決定するために計算式を変えたいのは分かりますが、引数によって処理を変える別の関数とすべきでしょう。あまりにも冗長的です。

switch文好きすぎというのはもう見ての通りです。switch文は冗長性の元とも言えますし、あまり安易に使うものではないかなと僕は思っています。break文を挟まないといけなかったり、評価式が型まで厳密に評価したりするなど、不具合を助長するからです。

厳密等価比較、要するに===を使おうということは、先のswitch文にも通じるところです。JavaScriptはいい感じに型変換を行ってくれる自由な言語ですが、それに甘えているとふとしたときにエラーの原因がわからず四苦八苦することがあります(ありました)。日頃から「この変数には文字列を入れてある」「この関数は数値型を返す」など、型を意識してコーディングすると余計な不具合に悩まず、また明快なコードになると僕は信じています…(ポエムかよ)。ま、そうは言ってもこれはJavaScriptのいいところなので厳密に型にこだわる必要はないのかな。自分が書く上では気をつけてます、という話です。

まとめ

サンプルコードをdisりたくて書いてるわけじゃないのですが、コードを紐解いていて「コードきたね〜〜〜〜〜」と思ったのは事実ですw
最初は「サンプルコードにこんなのがあって、こんなふうに使えますよ」という+Designingの続編みたいな記事にしようと思ったのですが、意図した通りに動かないものがあればこれは紹介しにくいなと。それならいっそ記事上でリファクタリングの過程を書いてみたら面白いかなと方針転換したのです。
結果的にサンプルコードをdisる感じになっちゃいましたが、要はリファクタリングの教材としてお借りすることにしました。Adobeさんありがとう。

さて、コードの解読で思ったより長くなってしまったので、実際のリファクタリングは次の記事からとします。
リファクタリングは、自分で書いたコードでもよく行います。行いますが、本来のリファクタリングはリファクタリングとして行うべきものです。言い換えると、新たに別の機能を追加したり不具合を修正するものではありません。ただ今回はリファクタリングという名のものと、サンプルコードの日本語ローカライズと不具合の解消を行ってみようという挑戦みたいなものです。引き続きご笑覧いただければと思います。

次回に続く…。

*1:関数なら動詞+目的語のようにしたほうが明示的なので僕は好きです。関数名、変数名の命名規則は本来は自由に付けて全然構わないものですが、コードの可読性を考えれば、関数名・変数名でその処理や代入されている値が想定できるというのがひとつの理想かなと思います。