python + batでPDFのパスワード解除を簡略化する

# 環境
Windows 10　(Mac・Linuxについては詳しくないのでサポートできません)
python 3.7

# 目次
- 動機と概要
- ディレクトリ構造とファイル内容
    - 依存パッケージ
- 解説
- 実際の使用方法
    - Tips
- 注意事項
- まとめ
- 参考文献

# 動機と概要

コロナウイルスの影響で、オンライン授業が大学では多数行われていると思います。
その為の授業資料を、学内のサービスを使用して配布されることがほとんどだと思いますが、たまにパスワードをかけられる先生がいます。そりゃ再配布しちゃうのはまずいし、履修生以外の閲覧をはじきたいのでしょうが、、こちらとしては***開くたびにパスワードを入力するのはかなりめんどくさいわけです。***
そこでパスワード解除したPDFを作成してしまおうというわけです。
「PDF　パスワード解除」などと検索すれば色々出てきますが、***今回はPDFをドラッグアンドドロップだけで解除します***。
というわけで二段階のプロセスを踏みます。

- batファイルとpythonファイルを連携させる
- PDFのパスワードを(既知の場合)解除する

python単体でも実現は可能ですが、ドラッグアンドドロップだけでパスワード解除するにはbatが必要かなぁと思います。
というわけでpython駆使してPDFのパスワードを解除しましょう。


# ディレクトリ構造とファイル内容
最初にディレクトリ構造と実装したファイル内容を示します。
classフォルダは授業の資料を置いているフォルダと解釈してください。

root/
　├ main.py
　└ decoder.bat

class/
　├ password.txt
　└ target.pdf

※または
class/
　├ password.txt
　└ 0605/
　　　└ target.pdf
のようになっていても大丈夫です。

```main.py
import sys, pathlib

from pikepdf import Pdf


def get_password(folder):
    passfile = None

    found : bool = False
    # まず同一フォルダ内を探索する
    for f in folder.glob("*"):
        if "password" == f.stem:
            passfile = f
            found = True

    # 同一階層になかった場合は更にもう一つ上の階層を探索
    if not found:
        folder = folder.parent
        for f in folder.glob("*"):
            if "password" == f.stem:
                passfile = f

    # 一つ上の階層にもpasswordファイルがない場合
    if not found:
        return None
    
    # ファイルからパスワードを取り出す
    with open(passfile, mode="r", encoding="utf-8") as f:
        password = f.read()

    return password

def main():
    # batから起動時
    try:
        path = sys.argv[1]
    # pyファイル単体で動かすとき
    except:
        print("input target path")
        path = input()

    path = pathlib.Path(path)
    # PDFファイル以外をリジェクト
    if path.suffix != ".pdf":
        print("Only PDF file accept.")
        sys.exit()
    
    password = get_password(folder=path.parent)
    # passwordファイルがない場合
    if password == None:
        print("No password file. input password = ", end="")
        password = input()
    try:
        pdf = Pdf.open(path, password=password)
    except:     # passwordが間違っている場合
        print("failed to open PDF. check password.")
        sys.exit()

    pdf_unlock = Pdf.new()
    pdf_unlock.pages.extend(pdf.pages)
    # 元のPDFと同階層に保存
    newname = f"{path.parent / path.stem}-unlocked.pdf"
    pdf_unlock.save(newname)


if __name__ == '__main__':
    main()

```

```decoder.bat
@echo off
rem classの部分をclassフォルダのフルパスで書き換えてください
cd "class"
"main.py" %*
```

## 依存パッケージ
pipできます。ラクラク。

```
pip install pikepdf
```
[pikepdf](https://github.com/pikepdf/pikepdf) というライブラリを使用して、PDFのパスワードを解除します。
参考文献[1]により存在をしりました。こちらでもサンプルプログラムが公開されていますから、参考にしてください。

# 解説

## batファイルとpythonの連携について

batファイルに対して画像ファイル等のファイルをドラッグアンドドロップして開くと、コマンドプロンプトでは

```
C:\Users\user> decoder.bat (ファイルのフルパス)
```

のようになります。この引数はbatファイル内では %* で受け取ることができます。
(本当は%0の方がいいですね・・)(細かいことは参考文献[2])

この引数をそのままpythonへと渡します。
pythonではコマンドライン引数を sys で受け取ることができます。
参考文献[3]に最低限のことが書かれています。
というわけで、sys.argv[1]に今回batファイルから渡された引数(ファイルのパス)がはいっています。

## PDFのパスワードを解除する

基本的に以下のステップです。変数名は全てmain.pyと同じです
<dl>
  <dt>パスワードを使ってPDFファイルを開く</dt>
  <dd><code>pdf = Pdf.open(path, password=password)</code></dd>
  <dt>新しいPDFファイルを用意する</dt>
  <dd><code>pdf_unlock = Pdf.new()</code></dd>
  <dt>新しいPDFに、予め開いておいたPDFをコピーする</dt>
  <dd><code>pdf_unlock.pages.extend(pdf.pages)</code></dd>
  <dt>新しいPDFを保存する</dt>
  <dd><code>pdf_unlock.save(newname)</code></dd>
</dl>
新しく保存したPDFにはパスワードがついていませんから、目的達成ということですね。


# 実際に使用してみる
<dl>
<dt>1. パスワードを準備します。</dt>
<br>
<dd>
ディレクトリ構造の図のpassword.txtには、target.pdfのパスワードを書き込んでおきます。
<strong>この時注意点として、改行してはいけません。</strong>
大体の先生のかけるパスワードって毎回同じなので、パスワードファイルを準備しておく仕組みを採用しました。<br>用意しなくてもコマンドプロンプトに打ち込めるので、毎回パスワードが違っていたらpassword.txtは作らずに実行時に打ち込みます。
</dd>
<br>
<dt>2. ショートカット作成</dt>
<br>
<dd>
decoder.batのショートカットをデスクトップなりに置くとよいでしょう。デスクトップから実行できた方が便利そうなので。別に必須ではありません。
</dd>
<br>
<dt>3. 実行</dt>
<br>
<dd>
2で作成したショートカットに、PDFファイルをドラッグアンドドロップします。するとコマンドプロンプトが(一瞬)立ち上がり、パスワード解除されたPDFファイルが元々のファイルと同じ場所に保存されています。<br>
パスワードファイルを作っていない場合は、コマンドプロンプトで入力を求められます。入力してください。
</dd>
</dl>
~~なお、password.txtの改行禁止はpython側で一行目だけ読み込むように実装すれば解決します~~

## Tips
### pythonファイル単体での動作

main.pyだけでも動作します。その場合はPDFの存在する(絶対)パスの入力を求められます。
この時にPDFファイルをコマンドプロンプトにドラッグアンドドロップしても動きます。

### password.txtについて
以下のように二つ用意した場合は、PDFファイルに近い方が優先されます。

class/
　├ password.txt
　└ 0605/
　　　├ password.txt 
　　　└ target.pdf

日によってパスワードを変えるような先生だと、その日ごとのフォルダにpassword.txtを作るとよいかもしれませんね。

# 注意事項
- パスワードを解除PDFファイルを先生方に無断で再配布することは絶対にやめましょう
- パスワードを他者に教えることもやめましょう。

# 追記
パスワードに全角文字が入っていると動作しないっぽいです。

# まとめ
- pythonって便利ですね！！！
- いいねしてくれると喜びます

# 参考文献
[1] [【Python】pdfファイルのパスワードを解除しよう！](https://py-memo.com/python/pdfpassword/)
[2] [バッチファイル実行時に引数を渡す](https://www.adminweb.jp/command/bat/index6.html)
[3] [Python: コマンドライン引数とは？](https://qiita.com/orange_u/items/3f0fb6044fd5ee2c3a37)