JavaScriptでAdobe Acrobatプラグイン開発

※この記事は文系的冗長性及び反知性的曖昧主義に支配されています[^1]。

# プレ金を、きっかけに！
〜とあるプレミアムフライデー[^2]の深夜〜

わたし「ふぅ、なんとか日付が変わる前に月曜朝の会議資料をPDF化できたぞ！……うん？」

ピロン！ピロン！ピロン！

上司Ａ「議題追加[^3]するからPDFの先頭に加えておいて。他の資料番号とページ番号は振りなおしね。」
上司Ｂ「表紙にはページ番号振らないでって言ったよね？[^3]」
上司Ｃ「資料番号のフォントは統一してって言ったよね？[^3]」
同期達「今日の同期会[^3]楽しかったね！写真を共有します！ウェーイ！」

わたし「ああああああああああああ（あぁあぁあぁああぁあぁああぁ

＿人人人人人人人人人人人人人人人人人人人＿
＞　サタデー・ナイト・フィーバー！！！　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y￣

##月月火水木金金（プレミアム）
産業革命を経て人類に課せられた十字架、「雑務」「残業」を如何に減らすべきか。ゴルゴタの丘を登りきる前に、こんな社会は滅ぼしてしまいたいところですが、私は一介の文系事務職員です。生まれながらにイチジクの葉を装備しているため、尊大な羞恥心がそれを邪魔します。臆病な自尊心を少しでも満たすべく、可能な限り作業を省力化する方向で頑張るしかありません。

具体的には、JavaScript（Adobe Acrobat SDK）を使ってプラグイン（Folder Level JavaScript File）[`Watashiwa Kaigi Skisky`](https://github.com/acknak/watashiwa-kaigi-skisky)を作成し、資料作成に係る一部の作業を自動化しました。この記事は、コードの解説とプラグイン作成のコツを記すものです。ちなみに冒頭の茶番はフィクションです。特定しないで。
<img width="527" alt="Watashiwa　Kaigi　Skisky" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193268/9495da62-bec4-57e0-7771-00d54fc2d24a.png">

<img width="384" alt="Watashiwa　Kaigi　Skisky: Set Confidentiality" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193268/dfdb9e3a-3bfd-fe18-ca10-a4dab8f3af9f.png">

# 軽業師とジャポネズリー
弊社において会議は複数の議題からなり、その資料は以下の要件を満たす必要があります。~~会議資料なんて読めれば何でも良いのでは~~ 日本らしい細やかな気遣いが光りますね。

1. 資料はWordファイル、Excelファイル、PowerPointファイル、PDFを結合して作成する。
2. ページ番号は議題ごとではなく、全資料を通して右下側に小さく付番する。ただし、表紙には付番せず2枚目を1ページ目とし、書式は「1 / n」とする。
3. 機密性及び共有範囲を資料の左上に小さく表示する。
4. 資料番号は資料の右上に大きく表示し、枠線で囲む。
5. 会議ごとに同じパスワードをかける。
<img width="702" alt="資料サンプル" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193268/cf33f5e7-a539-42ad-9858-ec6f2c77245d.png">


VBAやWindowsバッチで解決することもできますが、せっかくなので未体験のAdobe Acrobat SDKを使うことにしました。具体的には、Adobe Acrobatの標準機能で1.を行ったのち、自作のコードにて2.〜5.の定型作業を自動化します。設定画面の表示と文書の操作、セキュリティの設定ができれば良さそうです。

## Super Great Ganbari development Kit

2020年3月プレミアムフライデー現在、最新版であるAdobe Acrobat DC[^4]上で作業を自動化する方法として、主に以下が挙げられます。

1. アクションウィザードでAcrobat上のバッチ処理を行う
2. デバッガー・コンソールでJavaScriptを実行する
3. Document Level JavaScriptを書く
4. Folder Level JavaScript Fileを作成する
5. C++やC#からAdobe Acrobat SDKを触る

プログラミングしない1.は論外[^5]。2.は、デバッガー・コンソールの立ち上げとコードのコピペが面倒なのが単純にして最大の欠点。3.は、個別のExcelファイル内に保存したマクロ（VBA）のようなもので、新規作成した文書だと設定なしには実行できず、後述するセキュリティ設定もできないため却下。4.は、特定の場所に.jsファイルを置くだけでメニューの追加やダイアログの表示が可能なため、他の事務職員が使用する際の敷居を下げることができます。ちなみに5.は、弊社ではコンパイラのインストールが禁止されているため諦めました[^6]。

漢は黙ってJavaScript。Windowsのメモ帳で書いたコードをalertデバッグ。古事記にもそう書かかれている。

## 基本編

※以下の記事は、JavaScripの基礎を習得済みの方を想定して書いています

日本語はもとより、英語の解説記事すら少ないため、命綱は公式ドキュメントです。当該ドキュメントは、ページの構成上、情報が非常に探しづらいです。「JavaScript」→「JavaScript for Acrobat API Reference」→「JavaScript API」と冗長な表示を辿った後は、気合で乗り切りましょう。なお、Windows版のAdobe Acrobatを使用する場合、日本語の文字コードはShift_JIS[^7]です。

- Adobe社「[Acrobat DC SDK Documentation](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FJavaScript_API.htm)」

開発したFolder Level JavaScript File（.jsファイル）の設置場所は、デバッガーコンソールで下記のコマンドを実行して確認します。

```javascript
app.getPath("app","javascript");
```

デバッガー・コンソールの使い方やFolder Level Scriptsに関する詳細な解説は下記の記事をご参照ください。ファイル名は拡張子さえ.jsなら何でも良いのですが、取り急ぎ「config.js」とします。

- itaya yuichi氏「[Adobe Acrobat proでJavaScriptを使って自動でPDFをExcelに変換した話](https://qiita.com/itaya/items/b373ad40dc7b51b579c4)」（Qiita記事、2018年5月）
- 「[Instructions for Installing Folder Level Scripts (Automation Tools) and Plug-ins](https://www.pdfscripting.com/public/Installing_Automation_Tools.cfm)」[^8]（pdfscripting.com）

なお、Folder Level Scriptsの使用のために特別な設定をする必要はありませんが、以下の手順でJavaScriptを有効化しておかないと警告文が表示されます。使用に差し支えはありませんが、見た目が悪いため、メニューの「編集」→「環境設定」から下記の設定を行いましょう。
<img width="384" alt="警告例" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193268/978eb465-982f-474f-da33-b26b1885e1ca.png">
<img width="512" alt="設定例" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193268/ca099cc8-84f2-df37-f73e-ee4b5f8bdafb.png">

- Acrobat社「[セキュリティリスクとしての PDF の JavaScript](https://helpx.adobe.com/jp/acrobat/using/javascripts-pdfs-security-risk.html)」（Adobe Acrobat マニュアル、2017年6月）

Folder Level Scriptsではグローバルオブジェクトとして[`app`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp1.htm)[^9]が利用可能となっているため、例えば以下のコードでデフォルトメニューに自作の項目を追加することができます。

```javascript:config.js
app.addSubMenu(ParentMenuConfig); //親メニューを追加
app.addMenuItem(RenumPagesMenuConfig); //子メニューを追加

const ParentMenuConfig = { //親メニューの設定
	cName: "Watashiwa Kaigi SkiSky", //親メニュー名（日本語不可）
	cParent: "Edit" //親メニューの表示箇所（日本語だと「編集」メニュー）
}

const RenumPagesMenuConfig = { // 子メニューの設定
	cName: "Renumber Pages", //子メニュー名
	cParent: "Watashiwa Kaigi SkiSky", //親メニュー名
	cExec: "RenumPages()" //メニュークリック時に実行する関数
}
```

我らが同志、[`alert()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm%23TOC_alertbc-4&rhtocid=_6_1_8_6_1_3)も`app`のメソッドです。デバッガー・コンソールも使える心の広い方は[`console.println()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fconsole_methods.htm%23TOC_printlnbc-3&rhtocid=_6_1_8_17_0_2)もオススメです。

また、Folder Level Scriptsのトップレベルスコープの`this`は、表示中のPDF文書に関するオブジェクト[`Doc`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FDoc1.htm)[^10]を指します。

```javascript
const numPages = this.numPages(); //文書のページ数を取得
for (var i = numPages - 1; i >= 0; i--) { //ページを逆順に変更
	this.movePage(i);
}
```

なお、ファイル選択ダイアログは[`app.browseForDoc()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm%23TOC_browseForDocbc-7)または[`Field.browseForFileToSubmit()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FField_methods.htm%23TOC_browseForFileToSubmitbc-1)で呼び出せますが、前者はPDFのみ、後者は全種類のファイルを選択できます。今回のコードでは使用していませんが、後者は気付きづらいので要注意。

## 文字追加編

例えばページ番号を追加する場合、以下のようなコードになります。

```javascript:config.js
const boxWidth = 100;
const rect = doc.getPageBox("Bleed", p); //ページサイズを取得
const pageWidth = rect[2] - rect[0];
var f = doc.addField( //描画範囲を設定
	//第一引数：フィールド名（string）
	//第二引数：フィールドの種類（string）
	//第三引数：フィールドを追加するページ番号（number）
	//第四引数：表示位置（rect）
);
f.value = //略。表示する文字列。
f.fillColor = color.transparent; //背景色（ここでは透明）
f.textSize = 7.1; //文字サイズ
f.alignment = "right"; //表示位置（ここでは右揃え）
f.textFont = "HeiseiKakuGo-W5-UniJIS-UCS2-H"; //フォント（ここでは平成角ゴシック）
f.readonly = true; //読み取り専用
```

[`doc.addField()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FDoc_methods.htm%23TOC_addField1bc-2&rhtocid=_6_1_8_23_1_1)で文字表示用の領域を追加すると同時に描画用オブジェクト[`Field`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FField.htm)[^11]を得ることができます。`Field`オブジェクトのプロパティを変更することで、その表示内容や様式を設定しています。

表示位置は4つの要素からなる配列[`rect`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FField_properties.htm%23XREF_91364_rect)で設定します。四角形の左下のX座標、Y座標、右上のX座標、Y座標（いずれも左上をゼロとした絶対値）で指定します。単位は"unit size"、デフォルトで[”1 unit size = 72 inch”](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FDoc_methods.htm%23TOC_getUserUnitSizebc-64)です。悪い文明[^12]ですね。バグを生まないよう、下記のような換算関数を準備しておきましょう。

```javascript:config.js
const unitsize2mm = (us, doc, page) => us * doc.getUserUnitSize(page) / 72 * 25.4;
const mm2unitsize = (mm, doc, page) => mm / 25.4 * 72 / doc.getUserUnitSize(page);
```

使用できるフォントは非常に限られており、日本語では平成明朝か平成角ゴシックになります。

- [AFormAut : Field.TextFont プロパティ](http://pdf-file.nnn2.com/?p=759)（個人サイト、2015年5月）

PDFは印刷を前提としたフォーマットなので、一言で「ページサイズ」といっても様々な概念（Box）が存在します。上記では取り急ぎBleed Boxを使っていますが、沼に沈みたい方は他のBoxも調べてみてください。

- [PDF Page Boxes](https://support.activepdf.com/hc/en-us/articles/360002401273-PDF-Page-Boxes)（ActivePDF Support、2019年4月）
- [Finding page boundaries](https://acrobatusers.com/tutorials/print/finding-page-boundaries/)（AcrobatUsers.com、2006年9月）

## ダイアログ編

メニューを選択しただけで処理を走らせても良いですが、せっかくなので自動化処理の設定画面がほしいところ。というわけで処理前に設定ダイアログを表示します。

```javascript:config.js
function RenumPages() {
	renumPagesDialog.doc = this;
	app.execDialog(renumPagesDialog);
}

var renumPagesDialog = {
	doc: null,
	initialize: dialog => {
		//ダイアログ作成時の処理
		dialog.load({
			//略。GUIパーツの初期値をここで指定。
		})
	},
	commit: function (dialog) {
		//略。「OK」を選択した際の動作
	},
	other: function (dialog) {
		//略。「その他」を選択した際の動作
	},
	description: {
		//略。GUIパーツをここに記述。
	}
};
```

[`app.execDialog()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/index.html#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm%23TOC_execDialogbc-11&rhtocid=_6_1_8_6_1_10)に[Dialog box handlers](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/index.html#rhtocid=_6_1_8_6_1_10&t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm%23XREF_26717_Dialog_box_handlers)を渡してダイアログを表示します。`Dialog`オブジェクトのメソッド内で`this`は当然`Dialog`となるため、`Doc`オブジェクトはDialog box handlersに仕込んでおきましょう。ダイアログ内の各種メソッドの中からでも表示中の文書を示す`Doc`オブジェクトを触れるようになります。

ダイアログに表示するフォームは[`description`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/index.html#rhtocid=_6_1_8_6_1_10&t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm%23XREF_36916_description)に記載します。配置から文字設定まで独自構文です。幸い、ドキュメントのサンプルが非常に充実しています。レイアウト周りの設定が未だによく分かりませんが、気合で読んで、雰囲気で動かしましょう[^13]。

なお、フォームにitem_id（4文字からなる一意の文字列）を設定し、同名のメソッドをDialog box handlersに登録しておけば、フォームに入力や変更があったような場合に毎回呼び出されます。

## セキュリティ編

毎回資料に手打ちでパスワードをかけると、タイプミスにより二度と開けないゴミが完成する可能性があります（３敗）。したがって、事前にパスワードに関するセキュリティポリシーを作成しておき、それを適用することにします。

```javascript:config.js
var setDocSecDialog = {
	doc: null,
	initialize: dialog => getSecPoli(dialog),
	commit: function (dialog) {
		setSecPoli(dialog.store(), this.doc);
	},
	other: dialog => {
		editSecPoli(); //セキュリティポリシー設定画面（Adobe製）を表示
		getSecPoli(dialog);
	},
	description: {
		//略
	}
}

//設定可能なセキュリティポリシー一覧をtrustedFunctionで取得
const getSecPoli = app.trustedFunction(function (dialog) {
	app.beginPriv();
	dialog.load({
		"poli": security.getSecurityPolicies() //登録済みのセキュリティポリシー一覧を取得
			.filter(sp => sp.policyId !== "SP_PKI_default" && sp.policyId !== "SP_STD_default")
			.reduce((map, sp) => {
				map[sp.name] = sp.policyId;
				return map;
			}, {})
	})
	app.endPriv();
});

//セキュリティポリシーをtrustedFunctionでセット
const setSecPoli = app.trustedFunction(function (results, doc) {
	app.beginPriv();
	const aPols = security.getSecurityPolicies();
	const oMyPolicy = aPols
		.filter((aPol, i) => results["poli"][aPol.name] > 0)
		.pop(); //ダイアログで選択したセキュリティポリシーを取得
	if (oMyPolicy) { //セキュリティポリシーを適用
		const rtn = doc.encryptUsingPolicy({ oPolicy: oMyPolicy });
		//略（エラー処理）
	}
	app.endPriv();
});
```

セキュリティポリシーに関する設定や適用は、文字通りセキュリティ上の問題が生じるため、Folder Level Scriptsのトップレベルスコープから呼び出された[`app.trustedFunction()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm%23TOC_trustedFunctionbc-36&rhtocid=_6_1_8_6_1_35)内でなければ動作させることができません。開始時に[`app.beginPriv()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm%23TOC_beginPrivbc-6&rhtocid=_6_1_8_6_1_5)を、終了時に[`app.endPriv()`](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm%23TOC_endPrivbc-10&rhtocid=_6_1_8_6_1_9)を忘れないようにしましょう。

- [Using Trusted Functions](https://www.pdfscripting.com/public/Using-Trusted-Functions.cfm)（pdfscripting.com）

## 余談
[null安全教の信者](https://qiita.com/acknak/items/ae1cdc0e3c1be3b579bd)である私はTypeScriptの採用も検討しましたが、型定義ファイルの作成に痺れて断念。ちなみに、私のTypeScriptのイメージに最も近いのは以下のツイート。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">先生「お型付けしましょうね～」<br>園児「はーい！」<br>「any」<br>「AnyRef」<br>「Object」</p>&mdash; ザ・世界草原ニュース (@shikamiya) <a href="https://twitter.com/shikamiya/status/1202769617329901568?ref_src=twsrc%5Etfw">December 6, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# Programmierung macht frei
穴を掘っては埋めるだけのような無味乾燥な毎日に潤いを与えるのは、ほんの少しのスパイスです。死んだ魚のような目で単純作業を続けるのではなく、新手一生をスローガンに、これからも知らない言語やSDKに人知れずチャレンジし続けたいと思います。

[^1]: [良い記事を書くためのガイドライン](http://help.qiita.com/ja/articles/qiita-article-guideline)？知らんなぁ……。
[^2]: 2020年2月頃までは虫の息ながら続いていた[プレミアムフライデー](https://premium-friday.com)ですが、昨今の新型コロナウイルス騒ぎにより完全にトドメを刺されたようです。2020年3月28日現在、「次回のプレミアムフライデーは3/27」という表示のままとなっています。
[^3]: 聞いてない
[^4]: Adobe Acrobat **Reader** DCではない点に注意
[^5]: それは乙女的にNO! そのような選択肢はNO! 退路は無い!!
[^6]: 弊社さんは……そうやって私が欲しいものを全て奪っていくんですね
[^7]: この女はやはり…… 何の躊躇いもなくShift_JISに躰を預ける性欲の化身 文字コードを食い物としか見ていない下賤の女なんだわ……
[^8]: 記事で解説されている "user" フォルダは[デフォルトでは存在しない](https://experienceleaguecommunities.adobe.com/t5/Adobe-LiveCycle-Questions/app-level-vs-user-level-scripts-error/qaq-p/152977/comment-id/83383)ようです。
[^9]: appの[プロパティ](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_properties.htm)と[メソッド](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2Fapp_methods.htm)についてはドキュメント参照
[^10]: Docの[プロパティ](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FDoc_properties.htm)と[メソッド](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FDoc_methods.htm)についてはドキュメント参照
[^11]: Fieldの[プロパティ](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FField_properties.htm)と[メソッド](https://help.adobe.com/en_US/acrobat/acrobat_dc_sdk/2015/HTMLHelp/#t=Acro12_MasterBook%2FJS_API_AcroJS%2FField_methods.htm)についてはドキュメント参照
[^12]: 「[ヤード・ポンド法は悪い文明!! 粉砕する!!](https://www.pixiv.net/artworks/62906157)」「[ヤード・ポンド法を滅ぼそう！](https://twitter.com/Genrei_studio/status/893240620331917313)」
[^13]: 「[よくわからんが、まぁ動いてるからヨシ!](https://twitter.com/mdkgames22/status/1030048531606695936?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1030048531606695936&ref_url=https%3A%2F%2Fomoganews.com%2F2018%2F08%2F19%2F2587432610-1030048531606695936%2F)」