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

目次
[PR]

こんな感じ。

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

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

pip install pikepdf

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

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

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

#
# 定数を宣言する。
#
SCALE = 1.0
CAPTION = 'PDFファイルを暗号化'
ICON = 'C:/Program Files/MyScripts/icons/~~encryptpdf.ico'

#
# ダイアログを表示して暗号化に使用するパスワードを取得する。
#
def get_password(description):
    # ダイアログの位置とサイズを定義する。
    screen_width = root.winfo_screenwidth(); screen_height = root.winfo_screenheight()
    window_width = int(480 * SCALE); window_height = int(195 * 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=description).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(33*SCALE))

    # エディットボックスの位置とサイズを定義する。
    editbox_left = int(140 * SCALE)
    editbox_top = int(59 * SCALE)
    editbox_width = 38

    # パスワード入力用のエディットボックスを作成する。
    editboxPassword1 = tk.Entry(root, width=editbox_width, show='*')
    editboxPassword1.place(x=editbox_left, y=editbox_top)
    editboxPassword2 = tk.Entry(root, width=editbox_width, show='*')
    editboxPassword2.place(x=editbox_left, y=editbox_top+int(33*SCALE))

    # エディットボックス内のリターンキー押下時の処理を登録する。
    editboxPassword1.bind('<Return>',
        lambda event: editboxPassword2.focus_set() if len(editboxPassword1.get()) > 0 else ok_button_handler(event))
    editboxPassword2.bind('<Return>', lambda event: ok_button_handler(event))
    # エディットボックス内のエスケープキー押下時の処理を登録する。
    editboxPassword1.bind('<Escape>', lambda event: cancel_button_handler(event))
    editboxPassword2.bind('<Escape>', lambda event: cancel_button_handler(event))

    # OKボタン押下時用のハンドラを定義する。
    def ok_button_handler(event):
        # エディットボックスからパスワードを取り出す。
        password = editboxPassword1.get()
        confirm = editboxPassword2.get()
        # 空欄なら。。。
        if len(password) == 0:
            msgbox.showwarning(CAPTION, f'1文字以上のパスワードを入力してください。')
            editboxPassword1.focus_set()
            return
        # 一致しないなら。。。
        if password != confirm:
            msgbox.showwarning(CAPTION, f'パスワードが一致しません。\rもう一度入力してください。')
            editboxPassword2.focus_set()
            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(138 * SCALE)
    frame_height = int(57 * SCALE)
    button_left = int(254 * 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ボタンの枠線を作成する。
    buttonOKBorder = tk.Frame(frame, width=button_width, height=button_height, highlightbackground='#adadad', highlightthickness=1)
    buttonOKBorder.place(x=button_left, y=button_top)

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

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

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

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

    # パスワードを返す。
    return editboxPassword1.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.showwarning(CAPTION, f'指定されたファイルの拡張子が".pdf"でありません。\rこのファイルをスキップします。\r\r{file}')
                continue
            # PDFファイルを確認する。
            with pikepdf.open(file) as org:
                # PDFファイルが既に暗号化されていたら処理をスキップする。
                if org.is_encrypted:
                    msgbox.showwarning(CAPTION, f'指定されたPDFファイルは既に暗号化されています。\rこのファイルをスキップします。\r\r{file}')
                    continue
            # PDFファイルを処理対象に加える。
            targets.add(file)

        # スキップ対象とならなかったPDFファイルの数が0件の場合はプログラムを終了する。
        if len(targets) == 0:
            msgbox.showwarning(CAPTION, f'処理対象となるPDFファイルの数が0件です。\r処理を中断します。')
            sys.exit(255)
        skipped = len(sys.argv[1:]) - len(targets)

        # ダイアログを表示して暗号化に使用するパスワードを取得する。
        password = get_password('1文字以上のパスワードを入力してください。')

        # 選択されたPDFファイルについて。。。
        for pdf in targets:
            # PDFファイルをバックアップする。
            bak = pdf + '.bak'
            if os.path.isfile(bak):
                res = msgbox.askquestion(CAPTION, f'PDFファイルのバックアップができません。\rバックアップファイルと同じ名前のファイルが存在しています。\rファイルを上書きしますか。\r\r{bak}', default=msgbox.NO)
                if res  == 'yes':
                    shutil.copy2(pdf, bak)
            else:
                shutil.copy2(pdf, bak)
            # PDFファイルを暗号化する。
            with pikepdf.open(pdf, allow_overwriting_input=True) as org:
                org.save(pdf, encryption=pikepdf.Encryption(user=password))

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

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

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

        # もしTkinterが使えれば。。。
        msgbox.showerror(CAPTION, f'例外が発生しました。\rコンソール上で起動してスタックトレースを確認して下さい。\r\r{ex}')
        # 例外を表示する。
        print(f'\n{ex}\n')
        # スタックトレースを表示する。
        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\EncryptPdfContent.py」にコピペした場合。ショートカットは適当なファイル名(例えば「PDFファイルを暗号化」とか)に変更する。

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

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

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

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

以上

コメント

コメント一覧 (2件)

  • kitasue35さん、
    コメントありがとうございます。
    > user_passwordを設定しても、編集できてしまいますね。
    一言で回答すると「そういうものです」です。
    上で紹介したコードの目的は、暗号化したときのパスワードと同じパスワードを入力しないとPDFファイルの表示ができないようにする、というものです。
    正しいパスワードの入力が成功してPDFファイルの表示ができた後に編集が許可されるかどうかはPDFファイルを開いたときに使ったアプリケーション(Acrobat Readerなど)の仕様に依存します。
    ただ、PDFの仕様としては、開くとき用と印刷・編集を行うとき用それぞれ別なパスワードをPDFファイルに設定しておき、PDFファイルの提供先には開くとき用のパスワードだけを開示して印刷・編集はできないようにする、ということも考えられているようです。
    // その日暮らし

コメントする

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

CAPTCHA

目次