<Python/pikepdf>送るメニューからPDFファイル達を一括で暗号化する方法

目次

こんな感じ。

まず、下のサイトからPythonをダウンロードしてインストールする。

コマンドプロンプトを開いて以下のコマンドを実行しPDFファイルを操作するのに使うpikepdfというライブラリをインストールする。

pip install pikepdf

以下のコードをコピペして適当なファイル名(例えば「sonohi_encrypt_pdf.py」とか)で適当なフォルダに保存する。このときファイルのエンコードはBOM付きのUTF-8にしておく。

#
# コマンドライン引数で指定されたPDFファイル達を暗号化する。
#
# 準備:
# 1. パッケージpikepdfをインストールする。
#    C:\> pip install pikepdf
#

#
# モジュールをインポートする。
#
import os
import sys
import shutil
import pikepdf
import tkinter as tk
import tkinter.messagebox as msgbox
import ctypes

#
# 定数を宣言する。
#
SCALE = 1.0
CAPTION = 'Sonohi PDF Encryption'
ICON = 'C:/Program Files/MyScripts/icons/~~sonohi_encryptpdf.ico'


#
# ダイアログを表示して暗号化に使用するパスワードを取得する。
#
def get_password():
    # ダイアログの位置とサイズを用意する。
    screen_width = root.winfo_screenwidth(); screen_height = root.winfo_screenheight()
    window_width = int(550 * SCALE); window_height = int(272 * SCALE)
    # ダイアログに位置とサイズを設定する。
    root.geometry('%dx%d+%d+%d' % (window_width, window_height, (screen_width - window_width) / 2, (screen_height - window_height) / 2))
    # ダイアログのリサイズを禁止する。
    root.resizable(width=False, height=False)
    # 閉じるマークボタン押下時用のハンドラを登録する。
    root.wm_protocol('WM_DELETE_WINDOW', lambda: cancel_button_handler(None))

    # ガイドメッセージ用のラベルを作成する。
    tk.Label(root, text='閲覧者用と所有者用のパスワードを入力してください。').place(x=int(20*SCALE), y=int(15*SCALE))

    # ラベルの位置とサイズを用意する。
    label_left = int(20 * SCALE)
    label_top = int(62 * SCALE)

    # 閲覧者用パスワード入力用のラベルを作成する。
    tk.Label(root, text='閲覧者用のパスワード:').place(x=label_left, y=label_top)
    tk.Label(root, text='閲覧者用のパスワード(確認):').place(x=label_left, y=label_top+int(31*SCALE))
    # 所有者用パスワード入力用のラベルを作成する。
    tk.Label(root, text='所有者用のパスワード:').place(x=label_left, y=label_top+9+int(31*2*SCALE))
    tk.Label(root, text='所有者用のパスワード(確認):').place(x=label_left, y=label_top+9+int(31*3*SCALE))

    # エディットボックスの位置とサイズを用意する。
    editbox_left = int(210 * SCALE)
    editbox_top = int(61 * SCALE)
    editbox_width = 38

    # 閲覧者用パスワード入力用のエディットボックスを作成する。
    editbox_user_password = tk.Entry(root, width=editbox_width, show='*', text='user_password')
    editbox_user_password.place(x=editbox_left, y=editbox_top)
    editbox_user_confirm = tk.Entry(root, width=editbox_width, show='*', text='user_confirm')
    editbox_user_confirm.place(x=editbox_left, y=editbox_top+int(31*SCALE))
    # 所有者用パスワード入力用のエディットボックスを作成する。
    editbox_owner_password = tk.Entry(root, width=editbox_width, show='*', text='owner_password')
    editbox_owner_password.place(x=editbox_left, y=editbox_top+9+int(31*2*SCALE))
    editbox_owner_confirm = tk.Entry(root, width=editbox_width, show='*', text='owner_confirm')
    editbox_owner_confirm.place(x=editbox_left, y=editbox_top+9+int(31*3*SCALE))

    # 閲覧者用エディットボックス内のリターンキー押下時の処理を登録する。
    editbox_user_password.bind('<Return>', lambda event: ok_button_handler(event))
    editbox_user_confirm.bind('<Return>', lambda event: ok_button_handler(event))
    editbox_user_confirm.bind('<Alt-Return>',
        lambda event: [
            editbox_user_confirm.delete(0, tk.END),
            editbox_user_confirm.insert(tk.END, editbox_user_password.get()),
            ok_button_handler(event)
        ])
    # 所有者用エディットボックス内のリターンキー押下時の処理を登録する。
    editbox_owner_password.bind('<Return>', lambda event: ok_button_handler(event))
    editbox_owner_confirm.bind('<Return>', lambda event: ok_button_handler(event))
    editbox_owner_confirm.bind('<Alt-Return>',
        lambda event: [
            editbox_owner_confirm.delete(0, tk.END),
            editbox_owner_confirm.insert(tk.END, editbox_owner_password.get()),
            ok_button_handler(event)
        ])
    # 閲覧者用エディットボックス内のエスケープキー押下時の処理を登録する。
    editbox_user_password.bind('<Escape>', lambda event: cancel_button_handler(event))
    editbox_user_confirm.bind('<Escape>', lambda event: cancel_button_handler(event))
    # 所有者用エディットボックス内のエスケープキー押下時の処理を登録する。
    editbox_owner_password.bind('<Escape>', lambda event: cancel_button_handler(event))
    editbox_owner_confirm.bind('<Escape>', lambda event: cancel_button_handler(event))

    # OKボタン押下時用のハンドラを定義する。
    def ok_button_handler(event):
        # 閲覧者用のエディットボックスからパスワードを取り出す。
        user_password = editbox_user_password.get()
        user_confirm = editbox_user_confirm.get()
        # 所有者用のエディットボックスからパスワードを取り出す。
        owner_password = editbox_owner_password.get()
        owner_confirm = editbox_owner_confirm.get()
        # 閲覧者用のエディットボックスが空欄なら。。。
        if len(user_password) == 0:
            msgbox.showwarning(CAPTION, f'閲覧者用のパスワードを入力してください。')
            editbox_user_password.focus_set()
            return
        # 次のエディットボックスへ。。。
        elif event.widget['text'] == 'user_password':
            editbox_user_confirm.focus_set()
            return
        # 閲覧者用のパスワードが一致しないなら。。。
        elif user_password != user_confirm:
            msgbox.showwarning(CAPTION, f'閲覧者用のパスワードが一致しません。\rもう一度入力してください。')
            editbox_user_confirm.focus_set()
            editbox_user_confirm.selection_range(0, editbox_width)
            return
        # 次のエディットボックスへ。。。
        elif event.widget['text'] == 'user_confirm':
            editbox_owner_password.focus_set()
            return
        # 所有者用のエディットボックスが空欄なら。。。
        elif len(owner_password) == 0:
            msgbox.showwarning(CAPTION, f'所有者用のパスワードを入力してください。')
            editbox_owner_password.focus_set()
            return
        # 次のエディットボックスへ。。。
        elif event.widget['text'] == 'owner_password':
            editbox_owner_confirm.focus_set()
            return
        # 所有者用のパスワードが一致しないなら。。。
        elif owner_password != owner_confirm:
            msgbox.showwarning(CAPTION, f'所有者用のパスワードが一致しません。\rもう一度入力してください。')
            editbox_owner_confirm.focus_set()
            editbox_owner_confirm.selection_range(0, editbox_width)
            return
        # ここまで来たらエラーなしと判断し呼び出し元へ返る。
        root.quit()

    # キャンセルボタン押下時用のハンドラを定義する。
    def cancel_button_handler(event):
        res = msgbox.askokcancel(CAPTION, f'キャンセルされました。\r処理を中断します。', default=msgbox.CANCEL)
        if res:
            sys.exit(255)
        return

    # ボタンの位置とサイズを用意する。
    frame_top = int(215 * SCALE)
    frame_height = int(57 * SCALE)
    button_left = int(324 * SCALE)
    button_top = int(15 * SCALE)
    button_width = int(90 * SCALE)
    button_height = int(30 * SCALE)

    # ボタンを貼るフレームを作成する。
    frame = tk.Frame(root, width=window_width, height=frame_height, background='#f0f0f0')
    frame.place(x=0, y=frame_top)

    # マウスオーバー時にボタンの色を変える。
    def change_button_background(event):
        event.widget['background'] = '#e5f1fb'
        event.widget.master['highlightbackground'] = '#0078d7'
    def restore_button_background(event):
        event.widget['background'] = '#e1e1e1'
        event.widget.master['highlightbackground'] = '#adadad'

    # OKボタンの枠線を作成する。
    ok_button_border = tk.Frame(frame, width=button_width, height=button_height, highlightbackground='#adadad', highlightthickness=1)
    ok_button_border.place(x=button_left, y=button_top)

    # OKボタンを作成する。
    ok_button = tk.Button(
        ok_button_border, text='OK', relief=tk.FLAT,
        background='#e1e1e1', activebackground='#cce4f7'
    )
    ok_button.place(x=0, y=0, width=button_width-2, height=button_height-2)
    ok_button.bind('<ButtonRelease-1>', lambda event: ok_button_handler(event))
    ok_button.bind('<Return>', lambda event: ok_button_handler(event))
    ok_button.bind('<Escape>', lambda event: cancel_button_handler(event))
    ok_button.bind('<Enter>', lambda event: change_button_background(event))
    ok_button.bind('<Leave>', lambda event: restore_button_background(event))

    # キャンセルボタンの枠線を作成する。
    cancel_button_border = tk.Frame(frame, width=button_width, height=button_height, highlightbackground='#adadad', highlightthickness=1)
    cancel_button_border.place(x=button_left+button_width+int(15*SCALE), y=button_top)

    # キャンセルボタンを作成する。
    cancel_button = tk.Button(
        cancel_button_border, text='キャンセル', relief=tk.FLAT,
        background='#e1e1e1', activebackground='#cce4f7'
    )
    cancel_button.place(x=0, y=0, width=button_width-2, height=button_height-2)
    cancel_button.bind('<ButtonRelease-1>', lambda event: cancel_button_handler(event))
    cancel_button.bind('<Return>', lambda event: cancel_button_handler(event))
    cancel_button.bind('<Escape>', lambda event: cancel_button_handler(event))
    cancel_button.bind('<Enter>', lambda event: change_button_background(event))
    cancel_button.bind('<Leave>', lambda event: restore_button_background(event))

    # ダイアログを表示する。
    root.deiconify()
    # ダイアログを前面に移動してアクティブにする。
    root.lift()
    root.focus_force()
    # パスワード入力用のエディットボックスにフォーカスをセットする。
    editbox_user_password.focus_set()
    # メッセージループを回す。
    root.mainloop()

    # パスワードを返す。
    return editbox_user_password.get(), editbox_owner_password.get()


#
# メインルーチン
#
if __name__ == '__main__':

    # 例外処理。。。
    try:

        # 高DPI対策を行う。
        ctypes.windll.shcore.SetProcessDpiAwareness(True)

        # メインウインドウを作成する。
        root = tk.Tk()
        root.withdraw()

        # ディスプレイのDPIから拡大率125%に対する倍率を求める。
        SCALE = ctypes.windll.user32.GetDpiForWindow(root.winfo_id()) / 96 / 1.25

        # ダイアログのキャプションとアイコンを設定する。
        root.title(CAPTION)
        if os.path.isfile(ICON):
            root.iconbitmap(ICON)
        # ダイアログの背景色を設定する。
        root.tk_setPalette(background='White')

        # コマンドライン引数を確認する。
        if len(sys.argv) == 1:
            msgbox.showerror(CAPTION, f'ファイルが選択されていません。\rファイルを1件以上選択して実行してください。\r処理を中断します。')
            sys.exit(255)

        # 選択されたPDFファイルについて。。。
        targets = set([])
        for file in sys.argv[1:]:
            # 指定されたファイルが存在しなければプログラムを終了する。
            if not os.path.isfile(file):
                msgbox.showerror(CAPTION, f'指定されたファイルが存在しないかフォルダが選択されました。\r処理を中断します。\r\r{file}')
                sys.exit(255)
            # 指定されたファイルの拡張子が".pdf"でなければプログラムを終了する。
            ext = os.path.splitext(file)[1]
            if ext.lower() != '.pdf':
                msgbox.showerror(CAPTION, f'指定されたファイルの拡張子が".pdf"でありません。\r処理を中断します。\r\r{file}')
                sys.exit(255)
            # 指定されたファイルが暗号化されているか確認する。
            try:
                # パスワードを指定しないでファイルを開いてみる。
                with pikepdf.open(file) as org:
                    pass
            # 暗号化されている。
            except pikepdf.PasswordError:
                # 指定されたファイルが既に暗号化されていたらプログラムを終了する。
                msgbox.showerror(CAPTION, f'指定されたファイルは既に暗号化されています。\r処理を中断します。\r\r{file}')
                sys.exit(255)
            # バックアップ先のファイルと同じ名前のファイルが存在していたらプログラムを終了する。
            bak = os.path.splitext(file)[0] + '.bak.pdf'
            if os.path.exists(bak):
                msgbox.showerror(CAPTION, f'ファイルのバックアップができません。\rバックアップ先のファイルと同じ名前のファイルが存在しています。\r処理を中断します。\r\r{bak}')
                sys.exit(255)
            # ファイルを処理対象に加える。
            targets.add(file)

        # ダイアログを表示して閲覧者用のパスワードと所有者用のパスワードを取得する。
        user_password, owner_password = get_password()

        # 閲覧者に対しすべての権限を制限する。
        restrict_all = pikepdf.Permissions(
            extract=False,
            modify_annotation=False,
            modify_assembly=False,
            modify_form=False,
            modify_other=False,
            print_highres=False,
            print_lowres=False
        )

        # 処理対象のPDFファイルについて。。。
        for pdf in targets:
            # バックアップ先ファイルが存在しないことを確認する。
            bak = os.path.splitext(pdf)[0] + '.bak.pdf'
            if os.path.exists(bak):
                msgbox.showerror(CAPTION, f'ファイルのバックアップができません。\rバックアップファイルと同じ名前のファイルが存在しています。\r\r{bak}')
                sys.exit(255)
            # ファイルをバックアップする。
            shutil.move(pdf, bak)
            # ファイルを暗号化する。
            with pikepdf.open(bak) as org:
                org.save(pdf, encryption=pikepdf.Encryption(user=user_password, owner=owner_password, allow=restrict_all))

        # 終了メッセージを表示する。
        msgbox.showinfo(CAPTION, f'すべての処理が完了しました。\r\r暗号化したファイル数: %d件' % len(targets))

        # 終了する。
        sys.exit(0)

    # すべての例外をキャッチする。
    except Exception as ex:

        # もしTkinterが使えれば。。。
        msgbox.showerror(CAPTION, f'例外が発生しました。\rコンソール上で起動してスタックトレースを確認して下さい。\r\r{ex}')
        # 例外を表示する。
        print(f'\n%s\n' % repr(ex))
        # スタックトレースを表示する。
        from traceback import TracebackException
        from traceback import StackSummary
        tb = TracebackException.from_exception(ex)
        summary = StackSummary.from_list(tb.stack)
        print(''.join(summary.format()))

以下のようなリンク先を持つショートカットを作る。下のは、上のPythonのコードを「%ProgramFiles%\MyScripts\sonohi_encrypt_pdf.py」にコピペした場合。ショートカットは適当なファイル名(例えば「PDFファイルを暗号化」とか)に変更する。

python.exe "%ProgramFiles%\MyScripts\sonohi_encrypt_pdf.py"

作ったショートカットを以下のフォルダにコピーする。

%USERPROFILE%\AppData\Roaming\Microsoft\Windows\SendTo

PDFファイル達を選択して右クリックし表示された送るメニューから上で作ったショートカットをクリックする。パスワードを入力するダイアログが表示されるので適当に入力するとPDFファイル達が暗号化される。

以上

コメント

コメント一覧 (1件)

コメントする

コメントは日本語で入力してください(スパム対策)。

CAPTCHA

目次