コモノExtendScript100本ノック

超初心者のDTPオペレーターが週にひとつスクリプトを書くブログ

075.【Id】指定フォルダ内のOfficeデータから画像を抽出

画像がOfficeデータに貼っつけられたかたちで入稿されることがよくあります。
一つならまだしも、たくさんあると一つ一つzip化してmediaフォルダまで潜って…とするのはちょっと面倒
(Bridgeを使えば多少手軽になるかもですが、なぜかうちのBridgeはめちゃめちゃ重い)。
ということで、複数のOfficeデータ(docx/xlsx/pptx)をまとめて圧縮→解凍→画像だけ抽出するInDesignスクリプトを書きました。

挙動

  1. 任意のフォルダにOfficeデータを保存
  2. スクリプトを実行
  3. 実行するフォルダを問われるので、1のフォルダを指定
  4. 1のフォルダ内に📁作業用と📁抽出画像が生成される
  5. 📁作業用にOfficeデータをコピーし、圧縮
  6. 圧縮したデータを解凍
  7. 解凍したフォルダ内に📁mediaがあれば、その中身を📁抽出画像にコピー

f:id:haraguai_is_bad:20210318180424p:plain:w200

コード

メモ

分かったこと

  • getFiles()のマスクで複数の拡張子を指定する方法
    getFiles()の引数として、FileまたはFolderオブジェクトを引数にとる関数を指定することができる。
    この関数は、検索で見つかったファイルやフォルダごとに呼び出され、trueの場合そのファイルやフォルダが戻り値の配列に追加される。

積み残し

  • 画像データのコピーやフォルダの作成がうまくいかなかったときの処理
  • 画像データが無かったら📁抽出画像を削除して「画像はありませんでした」とアラート

参考

074.【Id】見出しをセクションマーカーにする

f:id:haraguai_is_bad:20210226204827p:plain:w300
こんな感じの柱をつくるとき、セクションマーカーを使うことが多いです。
セクションマーカーはとても便利なのですが、
「一つ一つマーカーを設定するのはめんどくさい…」
「項目の増減があったときに設定し忘れそう…」
といった不満や不安がありました。
そこで、特定の段落スタイルが登場したらそのページをセクションの開始ページとし、セクションマーカーを設定するスクリプトを作成しました。

特徴としては、

  • 柱の内容が「大見出し+小見出し」になる場合にも対応しています。小見出しを括弧で括ったりもできます。
  • 柱の文言を見出し以外の言葉にしたい(例えば「1章目.inddは見出しは無いけれど『はじめに』という柱にしたい」みたいな)ときもあるので、そういったこともできるようにしました。
  • 見出しの通し番号やスペース、注番号を削除するなどの整形もできるようにしています。

見出しの段落スタイルや整形内容などの指定はコード内でやっています(テキストを読み込むかたちにすればよかったかな…)。

挙動

  1. ブックを開いた状態で起動する(複数開いていた場合、1つめのブックが対象となります)
  2. 1つめのinddを開く
  3. 最初以外のセクションを全て削除し、最初のセクションもマーカーをリセットする
  4. 大見出しの段落スタイルで正規表現検索し、ページ昇順でセクションマーカーを設定していく
    (1ページ目に見出しが無い場合は、引き続き同じコンテンツが掲載されていると考えて前のinddの最後のセクションマーカーと同じ内容でマーカーを設定する)
  5. 小見出しの段落スタイルで正規表現検索し、ページ降順でセクションマーカーを設定していく

コード

メモ

分かったこと

doc.pages.nextItem()やpreviousItem()は、documentではなくspreadに属するpagesコレクション内での次/前という扱いになる(pageのindexと同じ)。
直感的な意味での次のページ/前のページを取得したい場合は別のアプローチが必要。

分からなかったこと

ページ外のアイテムのparentPageを参照するなど、アクセスした時点でエラーを吐かれる可能性があるときの回避策。
falsyな値にならないので、条件分岐でどうやって逃げたものか分からなかった。 今回はtry~catchで回避したが、ページ外のアイテムにparentPageが無いのはエラーではなく仕様なので、こういうときにtry~catchを使うのは妥当なんだろうか…?という思ってしまう。

積み残し

1ページ内に見出しが複数あった場合の処理。

073.【Id】[基本段落]があたっている段落を[段落スタイルなし]にする

挙動

  1. inddを開いた状態で実行する
  2. 「(^.+$|\r)」で正規表現検索を行う
  3. 2.で取得した段落のうち、[基本段落]があたっているものに対し[段落スタイルなし]をあてがう(あたっている段落スタイルは変えるが、現状の体裁を保持する)

InDesignの検索置換を利用しているため、ロックされたレイヤーにあるテキストフレーム・ロックされたテキストフレームは対象としない。

コード

var main = function () {
  app.scriptPreferences.enableRedraw = false;
  var doc = app.activeDocument;
  var tgts = myFindGrep("(^.+$|\r)");
  for (var i = 0; i < tgts.length; i++) {
    if (tgts[i].appliedParagraphStyle.name === "[基本段落]") {
      tgts[i].applyParagraphStyle(doc.paragraphStyles.item("[段落スタイルなし]"), false);
    }
  }
  function myFindGrep(rgx) {
    app.findGrepPreferences = NothingEnum.NOTHING;
    app.findGrepPreferences.findWhat = rgx;
    var result = app.findGrep();
    app.findGrepPreferences = NothingEnum.NOTHING;
    return result;
  }
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.FAST_ENTIRE_SCRIPT, File.decode($.fileName.replace(/^.+\//, "")));

メモ

段落スタイルの変更について

appliedParagraphStyleプロパティを変更するやり方ではオーバーライドを保持できない。
一方 applyParagraphStyle(using, [clearingOverrides]) メソッドは第二引数 [clearingOverrides] で既存の属性を消去するかどうか指定できる。
- true:既存の属性を消去する(規定値)
- false:既存の属性を消去しない

積み残し

  • テキスト終端文字(#のような制御文字で示されるあれ)だけの行が[基本段落]のままになってしまう。
    検索でテキスト終端文字がとれないようなので、地道に全テキストフレーム・表を再起処理で探っていって、linesひとつひとつのapplyParagraphStyleを見ていくのが正解?
    (テキスト終端文字だけの行はparagraphsではなくlines扱い)
  • 段落スタイルのなかで、基準スタイルが [基本段落] になっているものを [段落スタイルなし] にする。

072.【Id】セルの斜線をノセにする

InDesignの不具合なのか、表の斜線は[黒]スウォッチ100%でも自動でオーバープリント設定がかかりません。
このバグをカバーするために、表の斜線が[黒]スウォッチ100%だった場合にノセにするスクリプトを作成しました。
類似のスクリプトがいくつかWeb上に公開されていますが、自分の勉強のために一から作成してみました。

挙動

  • どのレイヤーがロックされているか記録
  • 全てのレイヤーのロックを解除
  • テキスト検索で「<0016>」を検索し、ドキュメント内のすべての表を検出
  • 表内のすべてのセルに対して以下の処理を実行
    • 線の塗りが[黒]100%でヌキだったらノセにする
    • 線の間隔の塗りが[黒]100%でヌキだったらノセにする
  • レイヤーのロックの状態を元に戻す

コード

var main = function () {
  app.scriptPreferences.enableRedraw = false;

  if (app.documents.length === 0) {
    alert("ドキュメントが開かれていません");
    exit();
  }

  var doc = app.activeDocument;
  var blk = doc.swatches.item("Black");

  //レイヤーのロックを一時的に解除
  var layersInfo = [];
  for (var i = 0; i < doc.layers.length; i++) {
    layersInfo.push({
      obj: doc.layers[i],
      locked: doc.layers[i].locked,
    });
  }
  doc.layers.everyItem().locked = false;

  var tblParents = myFindText("<0016>");
  for (var i = 0; i < tblParents.length; i++) {
    var tbl = tblParents[i].tables[0];
    var clls = tbl.cells;
    for (var j = 0; j < clls.length; j++) {
      if (!clls[j].diagonalLineStrokeOverprint && clls[j].diagonalLineStrokeColor === blk && clls[j].diagonalLineStrokeTint === 100) {
        clls[j].diagonalLineStrokeOverprint = true;
      }
      if (!clls[j].diagonalLineStrokeGapOverprint && clls[j].diagonalLineStrokeGapColor === blk && clls[j].diagonalLineStrokeGapTint === 100) {
        clls[j].diagonalLineStrokeGapOverprint = true;
      }
    }
  }

  //レイヤーのロックを戻す
  for (var i = 0; i < layersInfo.length; i++) {
    layersInfo[i]["obj"].locked = layersInfo[i]["locked"];
  }

  alert("処理が完了しました。");

  function myFindText(str) {
    app.findChangeTextOptions.includeMasterPages = true;
    app.findTextPreferences = NothingEnum.NOTHING;
    app.findTextPreferences.findWhat = str;
    var result = app.findText();
    app.findTextPreferences = NothingEnum.NOTHING;
    return result;
  }
};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.FAST_ENTIRE_SCRIPT, File.decode($.fileName.replace(/^.+\//, "")));

メモ

表の検出の方法

  • app.activeDocument.textFrames.everyItem().tables.everyItem()
     アンカー付きオブジェクト内の表や表内表にアクセスできない。
  • app.activeDocument.allPageItemsのtables.everyItem()
     アンカー付きオブジェクトは含むが表内表にまでアクセスするのが難しい。
  • app.findText()で<0016>を検索
     表を子にもつ全てのcharacterオブジェクトを取得。
     レイヤーのロック、表示/非表示、マスターにあるか等に注意する必要がある。

積み残し(2021/02/15追記)

オブジェクト自体にロックがかかっている場合の処理を追加しわすれていた。

 

ぼうっとしている間に前回の投稿から9か月以上が経ってしまいましたが、あいかわらずスクリプトを書くのが楽しいです。

071.【Id】ブックから指定したドキュメントを書き出し

ブック内のドキュメントを指定してPDFを書き出すスクリプト
例えば、書籍の途中に挟まる折り込みページだけPDFを分けたいときなどに使うつもりで作成しました。
f:id:haraguai_is_bad:20200508115805p:plain:w300
(↑「02-02_2章-2_折込」だけ別PDFにしたい)

挙動

  1. 以下のような指示書(テキストファイル)を作成する。
    f:id:haraguai_is_bad:20200508115823p:plain:w300
  2. ブックを一つ開いた状態で実行する。
  3. 1で作成した指示書を指定する。
  4. PDFの書き出しプリセットを指定する。
  5. ブックと同階層にPDFが書き出される。

コード

以前の記事のコメントでご紹介いただいた、UskeSさんによるArray.some()のポリフィル(InDesign-Scripts/array_some.jsxinc at master · UskeS/InDesign-Scripts · GitHub)をお借りしています。

//@targetengine "exportPdfFromBook"
//@include "./arraysome.jsxinc"
var main = function () {
    var myList = fileReadIn("tgtIndd", "pdfName");
    // ウィンドウを作成
    var dialog = new Window("dialog");
    dialog.text = "ブックPDF書き出し";
    dialog.orientation = "column";
    dialog.alignChildren = ["center", "top"];

    var dropdown1 = dialog.add("dropdownlist", undefined, app.pdfExportPresets.everyItem().name);
    dropdown1.preferredSize.width = 300;

    var button1 = dialog.add("button");
    button1.text = "実行";

    button1.onClick = function () {
        dialog.close(1);
    }
    var dlgResult = dialog.show();
    if (dlgResult !== 1) {
        alert("中断しました");
        exit();
    }

    // ファイル名の重複チェック
    if (chkDuplicates(myList, "pdfName").length > 0) {
        alert("ファイル名が重複しているため中断しました");
        exit();
    }


    // ファイル名に使用できない文字チェック
    var checkName = function (tgt) {
        if (/[*?"<>\\|]/g.test(tgt.pname)) {
            alert("ファイル名に使用できない文字が含まれているため中断しました");
            exit();
        }
    }
    myList.some(checkName);

    if (!dropdown1.selection) {
        alert("書き出しプリセットが選択されていないため中断しました");
        exit();
    }

    var myBook = app.books[0];
    var pPreset = app.pdfExportPresets.itemByName(dropdown1.selection.text);
    for (var i = 0; i < myList.length; i++) {
        var tmpIndds = [];
        var s = myList[i]['tgtIndd'].split(",")
        for (var j = 0; j < s.length; j++) {
            tmpIndds.push(myBook.bookContents[s[j] - 1]);
        }
        var tmpPath = new File(app.books[0].filePath + "/" + myList[i]['pdfName'] + ".pdf");
        myBook.exportFile(ExportFormat.PDF_TYPE, tmpPath, false, pPreset, tmpIndds);
    }

    function fileReadIn(value0 /*string*/ , value1 /*string*/ ) {
        var tgtFile = File.openDialog("ファイルを選んでください", "*.txt");
        if (!tgtFile || !tgtFile.exists) {
            alert("中断しました");
            exit();
        }
        var res = [];
        var fileReadFlag;
        tgtFile.open("r");
        tgtFile.encoding = "UTF-8";

        try {
            while (!tgtFile.eof) {
                var ln = tgtFile.readln().split(/\t/);
                var item = {};
                item[value0] = ln[0];
                item[value1] = ln[1];
                res.push(item);
            }
            fileReadFlag = true;
        } catch (e) {
            alert(e);
            fileReadFlag = false;
        } finally {
            tgtFile.close()
        }
        if (!fileReadFlag || res.length === 0) {
            alert("ファイルを読み込めませんでした");
            exit();
        }
        return res;
    }

    // 配列内で同じプロパティを持つオブジェクトを抽出
    function chkDuplicates(objArr, prop) {
        var exist = {};
        var result = [];
        for (var i = 0, l = objArr.length; i < l; i++) {
            if (typeof objArr[i] !== "object") {
                continue;
            }
            var tmp = objArr[i][prop];
            if (!exist[tmp]) {
                exist[tmp] = 1;
            } else if (exist[tmp] === 1) {
                exist[tmp]++;
                result.push(objArr[i]);
            } else {
                exist[tmp]++;
            }
        }
        return result;
    }
}

app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.FAST_ENTIRE_SCRIPT, "exportPdfFromBook.jsx");
alert("完了しました")

メモ

積み残し

  • 対象となるinddの指定の仕方。「1,2,3,4」ではなく「1-4」と書いても通るようにしたい。
  • PDFごとに見開きか単ページかを指定できるようにしたい。

070.【Id】Type1フォントを指定したフォントに置換する

挙動

アクティブドキュメントにType1フォントが使用されていた場合、指定したフォントに置換する。
オプションでスタイルやグリッドフォーマットの設定も変更する。
f:id:haraguai_is_bad:20200414153800p:plain:w300

コード

//@targetengine "changeType1Font"

var main = function() {
    app.scriptPreferences.enableRedraw = false;
    var doc = app.activeDocument;
    var usedT1Fonts = [];
    for (var i = 0; i < doc.fonts.length; i++) {
        if (doc.fonts[i].fontType === FontTypes.TYPE_1) { //フォントタイプは任意で変更
            usedT1Fonts.push(doc.fonts[i].fontFamily + "\t" + doc.fonts[i].fontStyleName) //doc.fonts[i].nameをとるとスタイル名が重複するため
        }
    }
    if (usedT1Fonts.length === 0) {
        alert("Type1フォントは使われていません")
        exit();
    }

    var appFonts = [];
    for (var i = 0; i < app.fonts.length; i++) {
        try {
            if (app.fonts[i].fontType === FontTypes.OPENTYPE_TT) {
                appFonts.push(app.fonts[i].name)
            }
        } catch (e) {
            //FontTypeが取得できないフォントがある?
        }
    }

    // ScriptUI
    var dialog = new Window("dialog");
    dialog.text = "Type1フォント一括置換";
    dialog.preferredSize.width = 300;
    dialog.orientation = "column";
    dialog.alignChildren = ["fill", "top"];

    var panel2 = dialog.add("panel");
    panel2.text = "ドキュメントのフォント";
    panel2.orientation = "column";
    panel2.alignChildren = ["left", "top"];

    var dropdown3 = panel2.add("dropdownlist", undefined, undefined, {
        items: usedT1Fonts
    });
    dropdown3.preferredSize.width = 200;

    var panel1 = dialog.add("panel");
    panel1.text = "次で置換";
    panel1.orientation = "row";
    panel1.alignChildren = ["left", "top"];

    var group2 = panel1.add("group");
    group2.orientation = "column";
    group2.alignChildren = ["left", "center"];

    var dropdown1 = group2.add("dropdownlist", undefined, undefined, {
        items: appFonts
    });
    dropdown1.preferredSize.width = 200;

    var checkbox1 = group2.add("checkbox");
    checkbox1.text = "スタイルおよびグリッドフォーマットを再定義";

    var button1 = dialog.add("button")
    button1.text = "実行"
    button1.onClick = function() {
        dialog.close(1);
    }

    var dlgResult = dialog.show();
    if (dlgResult !== 1) {
        alert("中断しました");
        exit();
    }
    var changeFrom = app.fonts.itemByName(dropdown3.selection.text);
    var changeTo = app.fonts.itemByName(dropdown1.selection.text);

    //レイヤーのロックを一時的に解除
    var layersInfo = [];
    for (var i = 0; i < doc.layers.length; i++) {
        layersInfo.push({
            obj: doc.layers[i],
            locked: doc.layers[i].locked
        });
    }
    doc.layers.everyItem().locked = false;

    //検索置換
    myChangeGrep(".+", "$0");

    //レイヤーのロックを戻す
    for (var i = 0; i < layersInfo.length; i++) {
        layersInfo[i]['obj'].locked = layersInfo[i]['locked'];
    }

    if (!checkbox1.value) {
        alert("完了しました");
        exit();
    }

    //文字スタイル変更
    var cStyles = doc.allCharacterStyles;
    for (var i = 0; i < cStyles.length; i++) {
        if (cStyles[i].appliedFont === changeFrom) {
            cStyles[i].appliedFont = changeTo;
        }
    }

    //段落スタイル変更
    var pStyles = doc.allParagraphStyles;
    for (var i = 0; i < pStyles.length; i++) {
        if (pStyles[i].appliedFont === changeFrom) {
            pStyles[i].appliedFont = changeTo;
        }
    }

    //フレームグリッド設定変更
    var st = doc.stories;
    for (var i = 0; i < st.length; i++) {
        //テキストフレームを回避
        try {
            if (st[i].gridData.appliedFont === changeFrom) {
                st[i].gridData.appliedFont = changeTo;
            }
        } catch (e) {}
    }

    alert("完了しました");


    //検索置換
    function myChangeGrep(findStr, changeStr) {
        app.findChangeGrepOptions.includeMasterPages = true;
        app.findChangeGrepOptions.includeFootnotes = true;
        app.findGrepPreferences = NothingEnum.NOTHING;
        app.findGrepPreferences.appliedFont = changeFrom;
        app.changeGrepPreferences = NothingEnum.NOTHING;
        app.changeGrepPreferences.appliedFont = changeTo;
        app.findGrepPreferences.findWhat = findStr;
        app.changeGrepPreferences.changeTo = changeStr;
        var result = app.changeGrep();
        app.findGrepPreferences = NothingEnum.NOTHING;
        app.changeGrepPreferences = NothingEnum.NOTHING;
        return result;
    }

}

app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.FAST_ENTIRE_SCRIPT, "changeType1Font.jsx");

メモ

分かったこと

gridDataを扱うときは親に注意する。
storyののgridDataで設定できてもtextFrameのgridDataでは設定できないプロパティもある。
InDesignを手で操作するときと同じ。

積み残し

  • システムフォントの取得がものすごーーく遅い。
  • ロックされたオブジェクト・非表示オブジェクトは対象外としている。前者は対象に含めた方がよいかもしれない。
  • テキストフレームは回避してフレームグリッドだけ扱いたい場合の処理。try~catchで逃げたけどあまりよくない気がする。
  • FontTypeが取得できないフォントがある?UNKNOWNにもならず、undefinedになる。

参考

069.【Id】作業時間を記録する

InDesignドキュメントがアクティブ状態にある時間を計測し、作業時間を算出する。

挙動

  1. ドキュメントを開かない状態で実行する(あるいはInDesign起動前にStartup Scriptsフォルダに入れておく)。
  2. ドキュメントが開かれるとデスクトップに「log.txt」が作成され、ファイル名と時刻が記録される。
  3. 同時に、ドキュメントに対して以下のイベントリスナーが設定される。
    ①アクティブになったときに時間を記録する
    ②非アクティブになったときに時間を記録する
    ③①と②の差分=作業時間を記録する

コード

//@targetengine getActiveTimes

if (app.eventListeners.itemByName("getActiveTimes").isValid) {
    app.eventListeners.itemByName("getActiveTimes").remove();
}
var myListener = app.addEventListener("afterOpen", addEvToDocs);
myListener.name = "getActiveTimes";

function addEvToDocs(ev) {
    if (ev.target.constructor.name === "Document") {
        return;
    }
    var doc = app.activeDocument;
    var session = {};
    var start = new Date();
    session.start = start;
    writeTxt("~/Desktop/log.txt", doc.name + "\t" + parseDate(start));
    if (!doc.eventListeners.itemByName("beforeListener").isValid) {
        var bListener = doc.addEventListener("beforeDeactivate", writeCSV2);
        bListener.name = "beforeListener"

        function writeCSV2(ev) {
            if (ev.target.constructor.name === "Document") {
                return;
            }
            var fin = new Date();
            session.fin = fin;
            var myTime = Math.round((session.fin - session.start) / 60000) + "分";
            writeTxt("~/Desktop/log.txt", "\t" + parseDate(fin) + "\t" + myTime + "\n")
        }
    }

    if (!doc.eventListeners.itemByName("afterListener").isValid) {
        var aListener = doc.addEventListener("afterActivate", writeCSV1);
        aListener.name = "afterListener";

        function writeCSV1(ev) {
            if (ev.target.constructor.name === "Document") {
                return;
            }
            start = new Date();
            session.start = start;
            writeTxt("~/Desktop/log.txt", doc.name + "\t" + parseDate(start));
        }
    }
}

//テキスト追記
function writeTxt(path, txt) {
    var fObj = new File(path);
    fObj.encoding = (/csv$/.test(path)) ? "Shift-JIS" : "UTF-8"; //CSVで書き出す場合はShift-JIS
    if (fObj.open("e")) {
        fObj.read();
        fObj.write(txt);
        fObj.close();
    } else {
        alert("ファイルが開けません\n" + path);
    }
}

//日付表記変換
function parseDate(date) {
    var y = date.getFullYear().toString();
    var mo = date.getMonth() + 1;
    var d = date.getDate();
    var h = date.getHours();
    var min = date.getMinutes();
    var s = date.getSeconds();
    var result = y + "/" + mo + "/" + d + " " + h + ":" + min + ":" + s;
    return (result)
}

メモ

分かったこと

afterOpenやafterActivateに紐づけたEventは、
①ドキュメントが開かれた後
②ウィンドウがロードされた後
の2回実行される。
それぞれtargetが違う(①はDocument、②はLayoutWindow)。

積み残し

  • テキストデータへの追記の仕方
    途中で強制終了したときのことを考えて、ログを変数に貯めたりせず都度書き込むかたちにしている。
    read()することでeofに飛ぶようにしている(?)が、もう少しスマートなやり方がありそう。
  • 日付表記変換ももう少しスマートにしたい
  • InDesign自体が非アクティブのときは計測から除外したい

参考