※この記事は文系的冗長性及び反知性的曖昧主義に支配されています1。
プレ金を、きっかけに!
〜とあるプレミアムフライデー2の深夜〜
わたし「ふぅ、なんとか日付が変わる前に月曜朝の会議資料をPDF化できたぞ!……うん?」
ピロン!ピロン!ピロン!
上司A「議題追加3するからPDFの先頭に加えておいて。他の資料番号とページ番号は振りなおしね。」
上司B「表紙にはページ番号振らないでって言ったよね?3」
上司C「資料番号のフォントは統一してって言ったよね?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
を作成し、資料作成に係る一部の作業を自動化しました。この記事は、コードの解説とプラグイン作成のコツを記すものです。ちなみに冒頭の茶番はフィクションです。特定しないで。
軽業師とジャポネズリー
弊社において会議は複数の議題からなり、その資料は以下の要件を満たす必要があります。会議資料なんて読めれば何でも良いのでは 日本らしい細やかな気遣いが光りますね。
- 資料はWordファイル、Excelファイル、PowerPointファイル、PDFを結合して作成する。
- ページ番号は議題ごとではなく、全資料を通して右下側に小さく付番する。ただし、表紙には付番せず2枚目を1ページ目とし、書式は「1 / n」とする。
- 機密性及び共有範囲を資料の左上に小さく表示する。
- 資料番号は資料の右上に大きく表示し、枠線で囲む。
- 会議ごとに同じパスワードをかける。
VBAやWindowsバッチで解決することもできますが、せっかくなので未体験のAdobe Acrobat SDKを使うことにしました。具体的には、Adobe Acrobatの標準機能で1.を行ったのち、自作のコードにて2.〜5.の定型作業を自動化します。設定画面の表示と文書の操作、セキュリティの設定ができれば良さそうです。
Super Great Ganbari development Kit
2020年3月プレミアムフライデー現在、最新版であるAdobe Acrobat DC4上で作業を自動化する方法として、主に以下が挙げられます。
- アクションウィザードでAcrobat上のバッチ処理を行う
- デバッガー・コンソールでJavaScriptを実行する
- Document Level JavaScriptを書く
- Folder Level JavaScript Fileを作成する
- 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_JIS7です。
- Adobe社「Acrobat DC SDK Documentation」
開発したFolder Level JavaScript File(.jsファイル)の設置場所は、デバッガーコンソールで下記のコマンドを実行して確認します。
app.getPath("app","javascript");
デバッガー・コンソールの使い方やFolder Level Scriptsに関する詳細な解説は下記の記事をご参照ください。ファイル名は拡張子さえ.jsなら何でも良いのですが、取り急ぎ「config.js」とします。
- itaya yuichi氏「Adobe Acrobat proでJavaScriptを使って自動でPDFをExcelに変換した話」(Qiita記事、2018年5月)
- 「Instructions for Installing Folder Level Scripts (Automation Tools) and Plug-ins」8(pdfscripting.com)
なお、Folder Level Scriptsの使用のために特別な設定をする必要はありませんが、以下の手順でJavaScriptを有効化しておかないと警告文が表示されます。使用に差し支えはありませんが、見た目が悪いため、メニューの「編集」→「環境設定」から下記の設定を行いましょう。
- Acrobat社「セキュリティリスクとしての PDF の JavaScript」(Adobe Acrobat マニュアル、2017年6月)
Folder Level Scriptsではグローバルオブジェクトとしてapp
9が利用可能となっているため、例えば以下のコードでデフォルトメニューに自作の項目を追加することができます。
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()
もapp
のメソッドです。デバッガー・コンソールも使える心の広い方はconsole.println()
もオススメです。
また、Folder Level Scriptsのトップレベルスコープのthis
は、表示中のPDF文書に関するオブジェクトDoc
10を指します。
const numPages = this.numPages(); //文書のページ数を取得
for (var i = numPages - 1; i >= 0; i--) { //ページを逆順に変更
this.movePage(i);
}
なお、ファイル選択ダイアログはapp.browseForDoc()
またはField.browseForFileToSubmit()
で呼び出せますが、前者はPDFのみ、後者は全種類のファイルを選択できます。今回のコードでは使用していませんが、後者は気付きづらいので要注意。
文字追加編
例えばページ番号を追加する場合、以下のようなコードになります。
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()
で文字表示用の領域を追加すると同時に描画用オブジェクトField
11を得ることができます。Field
オブジェクトのプロパティを変更することで、その表示内容や様式を設定しています。
表示位置は4つの要素からなる配列rect
で設定します。四角形の左下のX座標、Y座標、右上のX座標、Y座標(いずれも左上をゼロとした絶対値)で指定します。単位は"unit size"、デフォルトで”1 unit size = 72 inch”です。悪い文明12ですね。バグを生まないよう、下記のような換算関数を準備しておきましょう。
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 プロパティ(個人サイト、2015年5月)
PDFは印刷を前提としたフォーマットなので、一言で「ページサイズ」といっても様々な概念(Box)が存在します。上記では取り急ぎBleed Boxを使っていますが、沼に沈みたい方は他のBoxも調べてみてください。
- PDF Page Boxes(ActivePDF Support、2019年4月)
- Finding page boundaries(AcrobatUsers.com、2006年9月)
ダイアログ編
メニューを選択しただけで処理を走らせても良いですが、せっかくなので自動化処理の設定画面がほしいところ。というわけで処理前に設定ダイアログを表示します。
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()
にDialog box handlersを渡してダイアログを表示します。Dialog
オブジェクトのメソッド内でthis
は当然Dialog
となるため、Doc
オブジェクトはDialog box handlersに仕込んでおきましょう。ダイアログ内の各種メソッドの中からでも表示中の文書を示すDoc
オブジェクトを触れるようになります。
ダイアログに表示するフォームはdescription
に記載します。配置から文字設定まで独自構文です。幸い、ドキュメントのサンプルが非常に充実しています。レイアウト周りの設定が未だによく分かりませんが、気合で読んで、雰囲気で動かしましょう13。
なお、フォームにitem_id(4文字からなる一意の文字列)を設定し、同名のメソッドをDialog box handlersに登録しておけば、フォームに入力や変更があったような場合に毎回呼び出されます。
セキュリティ編
毎回資料に手打ちでパスワードをかけると、タイプミスにより二度と開けないゴミが完成する可能性があります(3敗)。したがって、事前にパスワードに関するセキュリティポリシーを作成しておき、それを適用することにします。
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()
内でなければ動作させることができません。開始時にapp.beginPriv()
を、終了時にapp.endPriv()
を忘れないようにしましょう。
- Using Trusted Functions(pdfscripting.com)
余談
null安全教の信者である私はTypeScriptの採用も検討しましたが、型定義ファイルの作成に痺れて断念。ちなみに、私のTypeScriptのイメージに最も近いのは以下のツイート。
先生「お型付けしましょうね~」
— ザ・世界草原ニュース (@shikamiya) December 6, 2019
園児「はーい!」
「any」
「AnyRef」
「Object」
Programmierung macht frei
穴を掘っては埋めるだけのような無味乾燥な毎日に潤いを与えるのは、ほんの少しのスパイスです。死んだ魚のような目で単純作業を続けるのではなく、新手一生をスローガンに、これからも知らない言語やSDKに人知れずチャレンジし続けたいと思います。
-
良い記事を書くためのガイドライン?知らんなぁ……。 ↩
-
2020年2月頃までは虫の息ながら続いていたプレミアムフライデーですが、昨今の新型コロナウイルス騒ぎにより完全にトドメを刺されたようです。2020年3月28日現在、「次回のプレミアムフライデーは3/27」という表示のままとなっています。 ↩
-
Adobe Acrobat Reader DCではない点に注意 ↩
-
それは乙女的にNO! そのような選択肢はNO! 退路は無い!! ↩
-
弊社さんは……そうやって私が欲しいものを全て奪っていくんですね ↩
-
この女はやはり…… 何の躊躇いもなくShift_JISに躰を預ける性欲の化身 文字コードを食い物としか見ていない下賤の女なんだわ…… ↩
-
記事で解説されている "user" フォルダはデフォルトでは存在しないようです。 ↩