見出し画像

Python PyMuPDF メモ

AIって pythonで動いてるのか、よく分からんが難しそうだな。関わりたくない。
とか思っていたらpythonプログラムを作らされることになってしまった。
こちとら VBA と Hot Soup Processor しか知らんのだぞ。無茶言うな。
ということで、躓いた点のメモを残しておこうと思った。

以下はPyMuPDF (= import fitz) のメモであって、AIイラスト生成とは関係ない。

2girls, facing another, looking off, walking hand in hand, (shame, blush:1.2),
を指定するとめっちゃ可愛いイラストできるじゃん! という話とは全く無関係である。


とりあえず公式を見る

見てもよく分からん。手探りで頑張ろう。画像生成と同じだね


フォルダ(サブフォルダ)内のすべてのPDFファイルを読み取る

for eachPDF in glob.glob(".\**\*.pdf", recursive=True) :
    filepath = eachPDF
    filename = os.path.splitext(os.path.basename(filepath))[0]
    print("処理開始 = %s.pdf" % filename)
    
    # PDFファイル1つ分の処理をここに書く
    with fitz.open(filepath) as Document :    

        # PDFファイル内の各ページについての処理をここに書く
        for page in Document :
            print(page.number)

サブフォルダを読む必要がなければ glob.glob("*.pdf") でOK


PDFを保存するときの軽量化オプション

try :
    Document.save("Saved.pdf", incremental=True, garbage=1, clean=True, deflate=True)
except :
    Document.save("Saved.pdf")

encryption は効果が少ない気がしたので入れてない
tryするのは上書き保存の場合だけでOK。常に別名保存するなら最後の1行だけで良い


PDFの先頭2ページだけを抽出する

filepath = ("test.pdf")

# 書き込み用の空ファイルを開く
Outdoc = fitz.open()

# 対象PDFを開いて先頭2ページだけを保存
with fitz.open(filepath) as Document:
    for page in Document :
        if page.number == 0 :
            Outdoc.insert_pdf(Document, to_page=page.number, from_page=page.number+1)
            Outdoc.save(os.path.join("Toptwopage.pdf")
        continue

多数のPDFから最初のページだけを集める作業が年に2回ぐらいある。
なぜそんなことをする必要があるんですか?
if page.number == 0 の判定は無くても動くはず。
何かでエラーが出たから追加した気がするけど、細かいことは気にするな。


PDFのページを画像として保存する

for page in Document:
    # 高解像度で出力したい場合は get_pixmap(dpi=300) とする。
    pix = page.get_pixmap()
    pix.save("Outputpng.png")

pythonで注釈を入れるための座標を確認するためのPNG出力が必要になったので。
解像度は最低になるのでPDFに再変換すると文字が潰れる。
高解像度で出力したい場合は get_pixmap(dpi=300) とする


PDFのページを連番画像として保存する

#画像ファイル出力用のフォルダを作る
dir = "Outputpng"
os.makedirs(dir, exist_ok=True)

#PDFの各ページを画像ファイルとして出力する
for page in Document:
    # ページ番号をゼロ埋めして3桁にする
    num = "{:0>03}".format(page.number + 1)
    pix = page.get_pixmap()
    pix.save("Outputpng\page-%s.png" % num)

もう二度と使わない予感


PDFに注釈を記入する

filepath = ("test.pdf")

# コメントを記入する座標の指定。座標は、PDFを get_pixmap() してPNG出力してからペイントで確認する
textbrect = fitz.Rect(518, 240, 563, 249)
cloudrect = fitz.Rect(514, 215, 563, 249)

# PDF の1ページ目にテキストボックスと雲マークを記入する
with fitz.open(filepath) as Document:           
    page = Document[0]

    page.add_freetext_annot(textbrect, "TEST", fontsize=9, fontname='Helv', text_color=fontcolor, fill_color=1)

    annot = page.add_rect_annot(cloudrect)
    annot.set_border(width=1, style='S', clouds=1)
    annot.set_colors(colors="[1,0,0]")
    annot.update()

annotの公式説明.. 少なすぎじゃない...?
誰か助けてください。なんでもします。今日中に言ってもらえれば


PDF内の赤枠・雲マーク注釈の有無をチェックする

# 雲マーク注釈の有無をチェックする関数
def AnnotCheck(Ppage) : 
    # 戻り値の初期化
    RannotRed = 0
    RannotCloud = 0

    # xrefsにすべての注釈の参照番号を取り込む
    xrefs = [annot.xref for annot in Ppage.annots()]
    for xref in xrefs :
        # 注釈タイプの確認。要素数2個の場合と、3個の場合がある
        annot = Ppage.load_annot(xref)
        if len(annot.type) == 2:
            annotTint, annotTstr = annot.type
        else:
            annotTint, annotTstr, annotDummy = annot.type
        
        # デバッグ用テキスト
        print("P." + str(Ppage.number) + ", type = " + str(annotTstr) + ", border = " + str(annot.border) + ", color = " + str(annot.colors) + "\n")

        # 赤枠注釈の判定
        annotColor = annot.colors['stroke']
        if annotColor is None :
            R, G, B = (0.0, 0.0, 0.0)
        else :
            R, G, B = annotColor
            if (R>=0.9) & (G==0.0) & (B==0.0) :
                # 赤枠コメントがある場合は 1 を返す
                RannotRed = 1

        # 雲マーク注釈(四角形)の判定
        if (annotTint == 4):
            annotCloud = annot.border['clouds']
            if annotCloud == 1 :
                # 雲マークコメントがある場合は 1 を返す
                RannotCloud = 1

    return RannotRed, RannotCloud

おお、annot.borderよ。あなたはなぜ、辞書型を渡してくるのですか。

私が1日悩むことになったのは誰のせいでしょうか?
貴重な時間を無駄にさせた責任をちゃんと取ってくださいね。
さもなくば謝れ。手をついて詫びろ。



ここまで書いてから気づいたけど
社内ネットからnoteへのアクセスが遮断されてるからメモする意味がないね。
もう止めよう

ということで終了。
閉廷。


ブックマーク兼モチベーションアップ for Self

Rect型の扱いについて

範囲内の文字列を取得したり、注釈を書くときの指定に使う。
Rect型は左上x,y座標と右下x,y座標で指定される四角形の範囲。
内容は4点のフロート型なので、変数4つに展開できるし、元に戻すこともできる。
例えば下記のようにcheckrectを指定するとPDFページの右上部分を選択できる。

    x0, y0, x1, y1 = page.rect
    x0 = x1 / 2
    y1 = y1 / 10
    checkrect = x0, y0, x1, y1

Rect型の入力は以下の5種類で指定できるが、どれを使っても問題ない。
fitz.Rect を使ってRect型を明示しておくと後からコードを読み直すときに分かりやすいと思う。四則演算も可能なので、デフォルト座標をRect変数に入れておくと後から条件に合わせて選択範囲を動かすのが楽になる。

    checkrect = x0, y0, x1, y1
    checkrect = (x0, y0, x1, y1)
    checkrect = ([x0, y0, x1, y1])
    checkrect = fitz.Rect(x0, y0, x1, y1)
    checkrect = fitz.Rect([x0, y0, x1, y1])

    shift = 30 if x0 != 0 else shift
    checkrect = checkrect + fitz.Rect(1*shift, 2*shift, 3*shift, 4*shift)

このRect型の座標指定はポイントで行われるので、page.get_pixmap() してpng画像を出力し、ペイント上で確認するしかない と思われる。
ところが残念なことに、ペイントを睨みつけて座標をチェックして指定してもちゃんとテキストが読み取れなかったりする。なぜならPDFの中身は魔境だからだ。
座標を正確に指定しても文字が取り出せないじゃないか、どうやって範囲指定したらいいんだ……と悩むより、取り出した文字から必要な文字列を抽出する方法を考えたほうが建設的だと思った。

ページ内に特定のキーワードが入っているPDFを数えたい

# 各ページ目のテキストをすべて書き出し、"キーワード"の表記があるページ枚数を数える
for filepath in glob.glob("*.pdf") :
    with fitz.open(filepath) as Document:   
        PNumOffset = 0
        for pageA in Document :
            text = chr(12).join([pageA.get_text()])
            if "キーワード" in text :
                PNumOffset += 1

こんなことして何の意味があるんですか?シリーズ。
意味はある。あると思えばあるし、ないと思えばない。
特定の文字列が入っているページを数えられるなら、それに応じたページ番号注釈を追加することができる、という思いつきをここに置いておく。
例えば、総ページ枚数の異なる50種類のPDFファイルが用意されていて、全てのファイル末尾に特定の ATTACH.pdf ファイルを追加したい、それぞれのファイルごとに適切なページ番号も追記したい といった状況が発生し得るのなら有効であろう。

PDFを追加しつつ、ページ枚数に応じて右上に追番を記入する。

  # ATTACH.pdfのページ右肩にページ番号を記入する
    cnt = PNumOffset
    for pageB in Insdoc :
        # 最終ページだったら END表記を入れる。
        if pageB.number == (Insdoc.page_count - 1) :
            fonttexta = ("PAGE %i (END)" % cnt)
        else :
            fonttexta = ("PAGE %i" % cnt)

        # ページ番号を記入。黒字, フォントサイズ11pt, 中央揃え
        fontcolor  = (0,0,0)
        fontrecta = fitz.Rect(500, 20, 585, 35)
        pageB.add_freetext_annot(fontrecta, fonttexta, fontsize=11, fontname='Helv', text_color=fontcolor, fill_color=1, align=1)
        cnt += 1
            
    # ベースファイルの末尾に ATTACH.pdf を追加する
    Document.insert_pdf(Insdoc)
    Document.save("saved.pdf")
  

ちなみに公式解説だと、位置揃えは align = TEXT_ALIGN_LEFT で指定できるように見える。が、実際には数値で指定しないとエラーになる。
LEFT: 0, CENTER: 1, RIGHT: 2 で指定できるはずだ。知らんけど。

intって書いてあるやん。
というか、位置揃えのことをjustifyって呼ぶんだ。へぇボタン連打したい

PDFに画像ファイルを貼るための事前処理として正常化をかける


この意味不明な見出しご覧いただければ私がこの問題について何も理解してないことをご理解いただけると思う。オブジェクトとコンテンツの正常化と言われても何のことだかさっぱり分からんのである。
しかし、正常化してないPDFに画像ファイルを貼り付けると位置やサイズがバグりますよ と言われれば、心当たりがあるから分かるのだ。
知らないことは文章だけ読んで理解しようと頭を悩ませるよりも、実践してトラブるが起きて困ってから文献に当たったほうが理解が速いのだ。トラブルが起きずにそのまま動いてしまうならそれはそれで良い。経験則である。受験勉強するときに解くよりも先に模範解答のページを開いてしまうような横着者には、この方法が向いている。まじめに問題を解いていた人にはお勧めしない。
何の話だったっけ。

そう、PDFに画像を貼りたいんだった。
なんで画像を貼りたいんだっけ。

そうだ。ページの中に数字入り△マークを貼り付けたいのにAdobeの陰謀によってスタンプが用意されてないからだった。だからペイントで自作した画像を貼らなければいけないんだ。PDFを開いたことがあるなら誰だって自作スタンプ画像を貼り付けたい衝動に駆られたことがあるはずだ。きっとそうに違いない。
ところが get_pixmap() してpng画像をペイントで開いてスタンプ画像を貼りつける座標を割り出したというのに、insert_image君は、なぜか全く違う位置に貼りやがるのだ。
何が悪いのかと思ったらPDFのせいだった。やっぱり魔境じゃないか。

    stampRect = fitz.Rect(570, 225, 588, 240)
    stampRect = stampRect + fitz.Rect(0, 15, 0, 15)
    stampImg = open("src\stamp_mark1.png", "rb").read()

    with fitz.open("Attachment.pdf") as Document:
        page = Document[0]
        page.clean_contents(sanitize=True)
        page.insert_image(stampRect, height=10, keep_proportion=False, stream=stampImg)

ここで重要なのは page.clean_contents(sanitize=True) である。
これを実行しておけばPDF内部の破損が修復されて画像が指定通りの位置に貼れるようになる。理由は知らない。こうしたら解決すると分かったから、それで良いのだ。

このページは大事なのでリンクを貼っておこう。
もっと自己主張してくれて良いんですよ。基本ページに書いておくとか。


また次回。

この記事が気に入ったらサポートをしてみませんか?