Adobe InDesignCCでレイヤー上のpageItemsの位置情報をjsonファイルに書き出す

# はじめに
Adobe InDesignのドキュメントの、任意の二つのレイヤー上のpageItemsの位置情報をjsonファイルに書き出すJavaScriptを作りました。
なお、ここではInDesignでのJavaScriptの動かし方については触れていません。

---
# どう動くのか
 - ダイアログを表示して、ユーザに「上位レイヤー」「下位レイヤー」を選択してもらいます。順序はこれらはレイヤーの重なり順ではなく、json書き出し時の階層構造です。
 - ユーザが選択した二つのレイヤーそれぞれについて、それらに配置されているpageItemsの以下の情報を取得し、json形式で保持します。
  - 先頭ページを１としたノンブル
  - ページ左上端からの位置（垂直距離、水平距離）
  - pageItemsの高さ・幅
  - pageItemsがページの左右どちらにあるか
 - 下位レイヤー中のpageItemsは、左上端が上位レイヤーの任意のpageItems内にある場合、その任意のpageItemsのjsonの要素として包含されます。
 - jsonファイルをInDesignのドキュメントファイルと同じディレクトリに・同じ名称で出力します。
 - 位置情報の長さの単位は、InDesignの設定に関わらずミリメートルにします（19/04/14　19：35追記）。

---
# コード
```javascript:anynameok.jsx
//jsonを扱うために、json2.jsの拡張子を変更して読み込み
/*jsxincをjsxファイルと同じディレクトリに置いた場合*/
#include json2.jsxinc
 //メインの関数を実行
main();

function main() {
  //InDesignのアクティブなドキュメント
  var myDoc = app.activeDocument;
  //ドキュメントの幅
  var myDocWidth = myDoc.documentPreferences.pageWidth;
  //選択されたレイヤーのインデックスを取得
  var myLayerIndex = getSelectedLayerIndex(myDoc);

  try {
    if (myLayerIndex === -1) {
      //getSelectedLayerIndexでキャンセルボタンが押下された場合
      throw new Error('終了します');
    } else if (myLayerIndex === -2) {
      //レイヤーが1枚だった場合
      throw new Error('レイヤーが一つだけです。終了します');
    } else if (myLayerIndex === -3) {
      //双方で同じレイヤーを選択した場合
      alert("上位・下位双方で同じレイヤーを選択しています。再度選択してください。");
      //スクリプトを再度実行
      main();
    } else {
      //okボタンが押下された場合
      myUpperTmpJson = getItemsInfo(myDoc, myLayerIndex[0], myDocWidth);
      myLowerTmpJson = getItemsInfo(myDoc, myLayerIndex[1], myDocWidth);
      genResultFileAsJson(myDoc, genHierarchy(myUpperTmpJson, myLowerTmpJson));
      alert("書き出しが完了しました");
    }
  } catch (e) {
    alert(e.message);
  }
}

//ページアイテムがページの左右どちらかにあるかを返す。左なら0、右なら1
function getMyColumn(myLeftPos, myDocWidth) {
  //左右の判別はページ中央より左右どちらにあるか
  if (myDocWidth / 2 > myLeftPos) {
    return 0;
  } else {
    return 1;
  }
}

//上位レイヤーのテキストフレームと下位レイヤーのテキストフレームを比較し、適宜下位レイヤーの情報を上位レイヤーに追加して上位レイヤーを返す
/*下位レイヤーのテキストフレームの左上が任意の上位レイヤーのテキストフレームの範囲内にあれば、
任意の上位レイヤーの要素として下位レイヤーの要素を追加*/
function genHierarchy(myUpperTmpJson, myLowerTmpJson) {
  //上位レイヤーのテキストフレームを一つずつ検証
  var myUppterTmpJsonLen = myUpperTmpJson.length;
  for (var i = 0; i < myUppterTmpJsonLen; i++) {
    //該当した下位レイヤーを格納しておく変数
    var myTmpLower = [];
    //下位レイヤーを一つずつ検証
    var myLowerTmpJsonLen = myLowerTmpJson.length;
    for (var j = 0; j < myLowerTmpJsonLen; j++) {
      //下位レイヤーが上位レイヤーと同じ側にあり、左肩が上位レイヤーの上端から下端の間にあれば
      if ((myUpperTmpJson[i].column === myLowerTmpJson[j].column) &
        (myUpperTmpJson[i].top <= myLowerTmpJson[j].top) &
        (myUpperTmpJson[i].top + myUpperTmpJson[i].height >= myLowerTmpJson[j].top)) {
        //上記変数に下位レイヤーの情報を追加
        myTmpLower.push(myLowerTmpJson[j]);
      }
    }
    //上記変数を上位レイヤーに追加
    myUpperTmpJson[i].lower = myTmpLower;
  }
  return myUpperTmpJson;
}

//ユーザが情報を書き出したいレイヤーを選択するダイアログを表示
//InDesignサンプルスクリプトExportAllStoriesのダイアログ部分を改訂
function getSelectedLayerIndex(myDoc) {
  //ドキュメントのレイヤーを取得
  var myLayers = myDoc.layers;
  //もしレイヤーが1枚だけだったら
  if (myLayers.length === 1) {
    return -2;
  }
  //レイヤー名を収めるリスト
  var myLayerList = [];
  //全レイヤーについて
  var myLayersLen = myLayers.length;
  for (var i = 0; i < myLayersLen; i++) {
    //レイヤーの名称を取得
    myLayerList.push(myLayers.item(i).name);
  }
  var myDialog;
  with(myDialog = app.dialogs.add({
    name: "情報を書き出すレイヤーを選択"
  })) {
    //Add a dialog column.
    var myDialogColumn = dialogColumns.add();
    with(myDialogColumn) {
      with(borderPanels.add()) {
        staticTexts.add({
          staticLabel: "上位レイヤー　:"
        });
        //レイヤー名のリストをドロップダウンリストとして表示
        var myUpperLayer = dropdowns.add({
          stringList: myLayerList,
          selectedIndex: 1
        });
      }
      with(borderPanels.add()) {
        staticTexts.add({
          staticLabel: "下位レイヤー　:"
        });
        //レイヤー名のリストをドロップダウンリストとして表示
        var myLowerLayer = dropdowns.add({
          stringList: myLayerList,
          selectedIndex: 0
        });
      }
    }
    var myReturn = myDialog.show();
    if (myReturn === true) {
      //Get the values from the dialog box.
      var myUpperLayerIndex = myUpperLayer.selectedIndex;
      var myLowerLayerIndex = myLowerLayer.selectedIndex;
      //上位レイヤー・下位レイヤー双方で同じレイヤーを選んでいたら
      if (myUpperLayerIndex === myLowerLayerIndex) {
        return -3;
      }
      myDialog.destroy;
      //okボタン押下時は選択された上位レイヤーと下位レイヤーそれぞれのレイヤーのインデックスを返す
      return [myUpperLayerIndex, myLowerLayerIndex];
    } else {
      //キャンセルボタン押下時
      return -1;
    }
  }
}

//指定ドキュメントの指定レイヤー内のページアイテムの情報を取得
function getItemsInfo(myDoc, myLayerIndex, myDocWidth) {
  //現状の座標の原点、測定単位を格納
  var myTmpRulerOrigin = myDoc.viewPreferences.rulerOrigin;
  var myZeroPoint = myDoc.zeroPoint;
  var myHorizontalMeasurementUnits = myDoc.viewPreferences.horizontalMeasurementUnits;
  var myVetricallMeasurementUnits = myDoc.viewPreferences.verticalMeasurementUnits;
  //座標の原点をページ肩に設定
  myDoc.viewPreferences.rulerOrigin = RulerOrigin.PAGE_ORIGIN;
  myDoc.zeroPoint = [0,0]
  //位置を表す単位をミリメートルに変更
  myDoc.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.millimeters;
  myDoc.viewPreferences.verticalMeasurementUnits = MeasurementUnits.millimeters;
  //myDocでオブジェクトの情報を取得したいレイヤー。
  var myLayer = myDoc.layers.item(myLayerIndex);
  //解析情報を収める
  var myTmpJson = [];
  //指定レイヤー上の一つ一つのページアイテムについて、
  var myPageItemsLen = myLayer.pageItems.length;
  for (var i = 0; i < myPageItemsLen; i++) {
    var myLayerPageItem = myLayer.pageItems[i];
    //連想配列を作る
    var myLayerPageItemInfo = {};
    //上から順にノンブル・上端からの位置・左端からの位置・ページの左右いずれにあるか・高さ・幅
    myLayerPageItemInfo['page'] = myLayerPageItem.parentPage.documentOffset + 1;
    myLayerPageItemInfo['top'] = myLayerPageItem.visibleBounds[0];
    myLayerPageItemInfo['left'] = myLayerPageItem.visibleBounds[1];
    myLayerPageItemInfo['column'] = getMyColumn(myLayerPageItemInfo['left'], myDocWidth);
    myLayerPageItemInfo['height'] = myLayerPageItem.visibleBounds[2] - myLayerPageItem.visibleBounds[0];
    myLayerPageItemInfo['width'] = myLayerPageItem.visibleBounds[3] - myLayerPageItem.visibleBounds[1];
    //ページアイテムの情報を解析情報に追加
    myTmpJson.push(myLayerPageItemInfo);
  }
  //結果のソート。ページ昇順、天からの位置昇順
  myTmpJson.sort(function(itemA, itemB) {
    if (itemA.page > itemB.page) return 1;
    if (itemA.page < itemB.page) return -1;
    if (itemA.column > itemB.column) return 1;
    if (itemA.column < itemB.column) return -1;
    if (itemA.top > itemB.top) return 1;
    if (itemA.top < itemB.top) return -1;
    return 0;
  });
  //座標の原点や単位を元に戻す
  myDoc.viewPreferences.horizontalMeasurementUnits = myHorizontalMeasurementUnits;
  myDoc.viewPreferences.verticalMeasurementUnits = myVetricallMeasurementUnits;
  myDoc.zeroPoint = myZeroPoint;
  myDoc.viewPreferences.rulerOrigin = myTmpRulerOrigin;
  return myTmpJson;
}

//jsonファイルを書き出し
function genResultFileAsJson(myDoc, myTmpJson) {
  var myDocNameLen = myDoc.name.indexOf(".");
  //myDocのドキュメント名。拡張子ナシ
  var myDocName = myDoc.name.substr(0, myDocNameLen);
  //myDocのパス。
  var myDocPath = decodeURI(myDoc.filePath);
  //jsonオブジェクトの生成
  myResultJson = JSON.stringify(myTmpJson);
  //出力ファイルのパス。ドキュメントと同じディレクトリ。
  var myResulFilePath = myDocPath + "/" + myDocName + ".json";
  //出力ファイルの作成
  myResultFile = new File(myResulFilePath);
  //出力ファイルへのjsonオブジェクトの書き込み・保存
  myResultFile.open("w");
  myResultFile.writeln(myResultJson);
  myResultFile.close();
}
```

---
# サンプルドキュメントと実行結果
**サンプルドキュメント**
シアン枠でダミーテキストを流したボックス群とマゼンタベタのボックス群とでレイヤーを分けています。前者を上位レイヤー、後者を下位レイヤーとして指定しました。
![sample.jpg](https://qiita-image-store.s3.amazonaws.com/0/383285/b3eaeb8b-82f9-656c-ad66-f31adefb1ff7.jpeg)

---
**結果**
下位レイヤー上のpageItemsは上位レイヤーの要素lower中に配列として出力されます。
なお、このサンプルは整形済みのものですが、実際は未整形で出力されます。

```json:sample.json
[{
  "page": 1,
  "top": 19.749995277813,
  "left": 19.749995277813,
  "column": 0,
  "height": 84.5000047221869,
  "width": 83.0000097221374,
  "lower": [{
    "page": 1,
    "top": 44.4999999997482,
    "left": 43.5,
    "column": 0,
    "height": 30.5,
    "width": 59.0000049999504
  }]
}, {
  "page": 1,
  "top": 106.749999999748,
  "left": 19.7499952778131,
  "column": 0,
  "height": 95,
  "width": 83.0000097221374,
  "lower": [{
    "page": 1,
    "top": 112.116666666415,
    "left": 43.45,
    "column": 0,
    "height": 30.5,
    "width": 59.0000049999504
  }, {
    "page": 1,
    "top": 173.499999999748,
    "left": 58,
    "column": 0,
    "height": 19.8833333335852,
    "width": 41.1500048610687
  }]
}, {
  "page": 1,
  "top": 204.249999999496,
  "left": 19.7499952778131,
  "column": 0,
  "height": 67.0000000005036,
  "width": 83.0000097221374,
  "lower": [{
    "page": 1,
    "top": 237.699999999748,
    "left": 40.1999998611183,
    "column": 0,
    "height": 30.5,
    "width": 59.0000049999504
  }]
}, {
  "page": 1,
  "top": 19.7499952778131,
  "left": 108.75,
  "column": 1,
  "height": 125.500004722187,
  "width": 83.0000097221374,
  "lower": [{
    "page": 1,
    "top": 59.6499999997481,
    "left": 130,
    "column": 1,
    "height": 30.5,
    "width": 59.0000049999504
  }, {
    "page": 1,
    "top": 110,
    "left": 128,
    "column": 1,
    "height": 30.5,
    "width": 59.0000049999504
  }]
}, {
  "page": 1,
  "top": 148.249999999748,
  "left": 108.75,
  "column": 1,
  "height": 67.0000000005036,
  "width": 83.0000097221374,
  "lower": [{
    "page": 1,
    "top": 184.666666665911,
    "left": 142.45,
    "column": 1,
    "height": 19.8833333335852,
    "width": 41.1500048610687
  }]
}]
```

---
# 動作確認
 - CC2019およびCC2015で行っています。
 - 以下については、未確認です。 
    - グループ化されたpageItems
    - ロックされたレイヤー

---
# 修正履歴
 - 19/04/14 19:35
    - InDesignの長さの単位、座標の原点を実行前に保存し、終了時にその保存値を書き戻すようにしました。
 - 19/04/14 20:38
    - コード末尾の使っていない関数を削除。 

---
# 参考URL
参考にさせていただきました。ありがとうございます。

**ソートについて**
JavaScript つい忘れてしまう配列のソート方法
http://qiita.com/PianoScoreJP/items/f0ff7345229871039672

**InDesignのJavaScriptでのJSONの使用について**
ExtendScriptでJSONオブジェクトを使う
http://uske-s.hatenablog.com/entry/2018/04/06/141944