PDFファイルに文字や印影を追加し隊 (PyMuPDF編)

いきさつ

お仕事の依頼は、メールに添付されるPDFファイルで請けています。
業務内容によっては依頼書が報告書を兼ねていて、そこに記入する作業が発生します。

記入する内容は、屋号・氏名・作業日、あと押印。

それを手書きでやってもいいんですが、PDFファイルのハンドリングがしやすいMacなので、プレビュー.appで毎回記入と押印していました。

ただ、毎回同じ作業の繰り返しをしていると、煩雑な作業を自動化したくなるのが人の性。

分析

Macには自動化するシステムがいろいろあります。ショートカット.appAutomatorAppleScript、あとはPythonなどなど。

自分は、ショートカット.appでタスクを自動化することが多いです。
理由としては、iOSでも使えるから。

例えば、iPhoneで撮った写真を報告用にファイル名をつけてAirDropでMacに転送、報告書のファイル名を日付と管理番号に変更する、報告のメールを自動で作成するとか。

ショートカット.appはいろいろ制約はあるけど、シェルスクリプトやAppleScriptを組み合わせれば大体のことはできる感じがします。

というのも、ChatGPTに聞いてスクリプトを組んでもらっているので、イチから自分で組むっていうことは全然してません。
バクフィックスもChatGPT。すごいです。

今回も、基本的な流ればショートカット.appで処理して、PDFファイルを操作するところはPythonです。

要件定義

  • 記入する内容
    • 屋号
    • 氏名
    • 日付(月・日)
    • 印影
  • PDFファイルのフォーマットは不変
  • 記入する内容が変化するのは、日付だけ
  • 処理するOSはmacOS
  • 使用するシステム
    • ショートカット.app
    • Python
      • PyMuPDF
    • Swift

PyMuPDFとはなんぞや

PyMuPDFは、今回初めて使います。

詳しくはこちらを参照ください。
https://pymupdf.readthedocs.io/ja/latest/

簡単に説明すると、PythonでPDFからデータを抽出したり分析や操作をするモジュールです。

今回は、文字と画像を挿入するのに使用します。

処理の流れ

  1. Finderで選択されているファイルのフルパスを取得する(ショートカット.app)
  2. PDFファイルから「管理番号」を取得する(PyMuPDF + swift)
  3. 挿入する日付の入力を要求する(ショートカット.app)
  4. 入力された日付を、処理に必要なフォーマットに整形する(ショートカット.app)
  5. PDFファイルに文字と画像(印影)を挿入する(PyMuPDF)
  6. 処理されたPDFファイルのファイル名を変更する(ショートカット.app)

2. PDFファイルから「管理番号」を取得する

すべての処理の最後に、ファイル名を「日付_管理番号.pdf」へ変更したいので、PDFファイルの内容から管理番号を取得します。

コードはこちら。

import sys
import fitz  # PyMuPDF

# PDFファイルを開く
pdf_path = sys.stdin.read()
doc = fitz.open(pdf_path)

# 最初のページを取得
first_page = doc[0]

# ページのテキストを辞書形式で取得
page_text_dict = first_page.get_text("dict")

# テキストブロックをループして処理
for block in page_text_dict["blocks"]:
    # テキストブロックからテキスト行を取得
    if "lines" in block:  # ブロックがテキストを含む場合
        for line in block["lines"]:
            # テキスト行から実際のテキスト(span)を取得
            for span in line["spans"]:
                # テキストが「abc-」を含む場合にのみ出力
                if "abc-" in span["text"]:
                    print(span["text"])

# ドキュメントを閉じる
doc.close()

ここでは、最初に「abc-」(実際は違う)が含まれる文字列のを探してます。

ただ、返り値の最後に改行が含まれてしまうという問題発生。

この問題は、printが出力する文字列の最後には必ず改行が入ってしまうのが原因らしい。(ChatGPTがそう言ってた)

ということで、改行を削除する処理が必要になります。

方法としては、管理番号は10桁固定なので、文字列の左から10文字だけ取り出すという処理を入れます。

処理方法としては、swiftで10文字だけ返すスクリプトを組む。

そのswiftコードがこちら。

import Foundation; let input = readLine()!; print(input.prefix(10), terminator: "")

これをローカルのどこかに保存しておいて、ショートカットの「シェルスクリプトを実行」で呼ぶ。

これで、PDFからキレイな文字列を取得できました。

いまさらですが、正規表現で取り出す方法の方が良いなぁとは思っています。

5. PDFファイルに文字と画像を挿入する

最終的なコードはこちら。
一部、公開用に変更しています。

import sys
import fitz  # PyMuPDF

# コマンドライン引数を取得
pdf_path = sys.argv[1]  # PDFファイルのパス
month = sys.argv[2]  # 月
day = sys.argv[3]  # 日

# PDFファイルを開く
doc = fitz.open(pdf_path)

# 最初のページを取得
first_page = doc[0]

# テキストとその位置
texts_and_rects = [
    (month, fitz.Rect(110, 662, 130, 682)),
    (day, fitz.Rect(135, 662, 155, 682)),
    ("屋号", fitz.Rect(110, 623, 300, 643)),
    ("氏名", fitz.Rect(110, 643, 300, 663)),
]

for text, rect in texts_and_rects:
    # 英数字のみかどうかをチェック
    if text.isascii():
        # 英数字のみの場合はHelveticaフォントを使用
        fontname = "Helvetica"
    else:
        # 日本語を含む場合はjapanフォントを使用
        fontname = "japan"
    
    # テキストボックスを挿入
    first_page.insert_textbox(rect, text, fontsize=11, fontname=fontname, align=fitz.TEXT_ALIGN_CENTER)

# 画像として挿入するPDFファイルを開く
stamp_pdf_path = 'stamp.pdf' # 画像PDFファイルの正しいパス
stamp_doc = fitz.open(stamp_pdf_path)

# 画像(PDFのページ)を取得
stamp_page = stamp_doc[0]

# 画像を挿入したい位置とサイズを指定
rect = fitz.Rect(400, 560, 430, 590)

# 画像(PDFページ)を挿入
first_page.show_pdf_page(rect, stamp_doc, 0)  # ここで、rectは画像を挿入する位置とサイズ、0はstamp_docの最初のページを意味します

# 編集したPDFファイルを保存
doc.saveIncr()  # 保存先のパスも指定

# ドキュメントを閉じる
doc.close()
stamp_doc.close()

ChatGPTにスクリプトを生成してもらったので、自分で調べたことはあまりありません。

ポイント

日本語文字列の挿入

マルチバイト文字(日本語)を挿入しようとしてハマりました。

そこについては、こちらの記事を参照して処理したところ上手く動作しました。

https://ymt-lab.com/post/2022/pymupdf-create-pdf/

当初、英数字以外を挿入すると「?」となってしまいました。
原因は、挿入する際に使用するフォントが英数字か文字を持っておらず、文字化けしてしまうということ。

じゃあフォントを変えればいいじゃんか、ということなんですがそれが簡単ではなかったです。

PDFファイルにフォントを埋め込むのか、埋め込まれたフォントを使用することになるのですが、上手くいかない。
フォントを埋め込めたとしても、挿入する際にフォントの指定がうまくいかない。

結果的に、こういう指定をすれば文字化けは解決です。

insert_textbox(rect, text, fontsize=11, fontname="japan", align=fitz.TEXT_ALIGN_CENTER)

japanとは(笑)

ただ、英数字をjapanで挿入すると全角文字になってしまうので、文字種によってフォントを変更する処理が入っています。

今回は文字を中央揃えにしたかったので、textboxになっています。

スクリプトに引数を渡す

今回、3つの情報をこのスクリプトに渡しています。

  1. PDFファイルのパス
  2. 挿入する「月」の数値
  3. 挿入する「日」の数値

これは先にsysをインポートして、sys.argvで引数を変数に渡してます。

あとは、日付の処理やらをショートカットで組んで行って、最終的にファイル名を希望する文字列になるように変更すればできあがり。

まとめ

今回もChatGPTにコーディングを丸投げしました。

やりたいことと条件を伝えれば、必要なことを添えながらコーディングしてくれるのは本当にありがたいです。

よく間違った生成をしますが、エラーを添えてデバックを依頼すると、文句も言わずにちゃんとコードを書き直してくれます。

イチから勉強して開発した方が良いのは分かっているのですが、目的を達成することに集中できて、結果を早く出せるよう助けてくれるアシスタントとしては超優秀。

これからも生成AIのお世話になっていくのは間違いないです。

今回得られた知見

  • swiftのコードって、シェルでも使えるのね
  • マルチバイト文字の処理は、問題発生の可能性が高い
  • 繰り返し作業は、躊躇せず自動化すべし
  • ChatGPTには、遠慮せず言いたいことをガンガン言え

コメントを残す