見出し画像

【Python】見開きPDFを左右に分割する

見開きのPDFをもらったとき、印刷や表示の都合で片ページずつのPDFにしたいことがある。やったことがある人はわかると思うが、実はこの作業は大変面倒である。Adobe Acrobatでまともにやるなら、ページを複製して左ページだけ残し、次は右ページだけ残す……という作業が全てのページに対して必要になる。書籍1冊ともなると、大変な時間がかかる。

そのため、もし1ページずつのPDFが欲しいのに見開きPDFしかない場合、デザイナーに「すみませんが、片ページのPDFをいただけませんか?」とお願いするのが最も早い。

もしWindowsのPCにAdobe Acrobatが入っているなら、出力時に出力サイズを調整することで左右に分割することができる。詳細は、以下のページあたりを参照のこと。

ただし、残念ながら、Macではこの手は使えない。そこでPythonである。今回は、GPT-4で目的を達成することができた。

動作内容としては、右半分と左半分に分けるだけだが、たまに片ページが混じっている場合に備えて、縦長のページは処理をスキップするように指示した。

import fitz  # PyMuPDF

def split_pdf_pages(input_pdf, output_pdf, order):
    doc = fitz.open(input_pdf)
    output_doc = fitz.open()

    for page_num in range(len(doc)):
        page = doc.load_page(page_num)
        rect = page.rect

        # 縦長のページは何もしない
        if rect.width <= rect.height:
            output_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
            continue

        middle = rect.width / 2

        if order == 'R':
            # 右半分のページを作成
            right_rect = fitz.Rect(middle, 0, rect.width, rect.height)
            right_page = output_doc.new_page(width=middle, height=rect.height)
            right_page.show_pdf_page(fitz.Rect(0, 0, middle, rect.height), doc, page_num, clip=right_rect)

            # 左半分のページを作成
            left_rect = fitz.Rect(0, 0, middle, rect.height)
            left_page = output_doc.new_page(width=middle, height=rect.height)
            left_page.show_pdf_page(left_rect, doc, page_num, clip=left_rect)
        
        elif order == 'L':
            # 左半分のページを作成
            left_rect = fitz.Rect(0, 0, middle, rect.height)
            left_page = output_doc.new_page(width=middle, height=rect.height)
            left_page.show_pdf_page(left_rect, doc, page_num, clip=left_rect)

            # 右半分のページを作成
            right_rect = fitz.Rect(middle, 0, rect.width, rect.height)
            right_page = output_doc.new_page(width=middle, height=rect.height)
            right_page.show_pdf_page(fitz.Rect(0, 0, middle, rect.height), doc, page_num, clip=right_rect)

    output_doc.save(output_pdf)
    output_doc.close()
    doc.close()

# ユーザーに入力を求める
order = input("R/L? ")

# 入力が有効か確認
if order not in ['R', 'L']:
    print("Invalid input. Please enter 'R' or 'L'.")
else:
    input_pdf = "input.pdf"
    output_pdf = "output.pdf"
    split_pdf_pages(input_pdf, output_pdf, order)

使用ライブラリはPyMuPDF。操作対象のPDFと同じフォルダーに入れて実行する。右開きも左開きも正しく処理できるように、最初に開きの方向を尋ねている。縦書き・右開きなら「R」、横書き・左開きなら「L」と入力する。もし逆に入力した場合は、2,1,4,3,6,5,8,7,…のように出力されるはず。

それにしても、同じコードを2回書いているのが気になる。今度は、Claude 3 Opusにリファクタリングしてもらう。

import fitz  # PyMuPDF

def split_pdf_pages(input_pdf, output_pdf, order):
    doc = fitz.open(input_pdf)
    output_doc = fitz.open()

    for page_num in range(len(doc)):
        page = doc.load_page(page_num)
        rect = page.rect

        if rect.width <= rect.height:
            output_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
            continue

        middle = rect.width / 2
        left_rect = fitz.Rect(0, 0, middle, rect.height)
        right_rect = fitz.Rect(middle, 0, rect.width, rect.height)

        if order == 'R':
            add_split_page(output_doc, doc, page_num, right_rect, middle, rect.height)
            add_split_page(output_doc, doc, page_num, left_rect, middle, rect.height)
        elif order == 'L':
            add_split_page(output_doc, doc, page_num, left_rect, middle, rect.height)
            add_split_page(output_doc, doc, page_num, right_rect, middle, rect.height)

    output_doc.save(output_pdf)
    output_doc.close()
    doc.close()

def add_split_page(output_doc, doc, page_num, rect, width, height):
    page = output_doc.new_page(width=width, height=height)
    page.show_pdf_page(fitz.Rect(0, 0, width, height), doc, page_num, clip=rect)

def main():
    order = input("R/L? ")

    if order not in ['R', 'L']:
        print("Invalid input. Please enter 'R' or 'L'.")
        return

    input_pdf = "input.pdf"
    output_pdf = "output.pdf"
    split_pdf_pages(input_pdf, output_pdf, order)

if __name__ == "__main__":
    main()

主な変更点:
1. add_split_page関数を追加し、ページの追加処理をこの関数に移動しました。これにより、コードの重複が削減され、可読性が向上しました。
2. 左右のページの処理をif-elifブロックで統一し、コードの重複を削減しました。
3. main関数を追加し、ユーザー入力の処理とPDF分割処理の呼び出しをこの関数内で行うようにしました。これにより、コードの構造が明確になり、再利用性が向上しました。
4. if __name__ == "__main__"ブロックを追加し、このスクリプトが直接実行された場合にのみmain関数が呼び出されるようにしました。これにより、このスクリプトを他のモジュールからインポートした場合の予期しない動作を防ぐことができます。

リファクタリング後のコードは、可読性、再利用性、およびメンテナンス性が向上しています。

Claude 3 Opus

とりあえず、問題なく動作した。これで、Macで見開きPDFの分割が簡単にできるようになった。

……と思ったら、このプログラムには問題があった。

【追記】コメントを削除せずに分割するには

分割したい元のPDFでは、コメントが入っていた。上に挙げたプログラムではコメントを削除してしまう。これではマズイ。

ということで、コメントを見たまま、PDFに埋め込むスクリプトを書いてもらった。これを実行すると、表示されているコメントがPDFに埋め込まれて、PDF上の文字列と同様の扱いになり、削除できなくなる。

import fitz  # PyMuPDF
from reportlab.pdfgen import canvas
from io import BytesIO
from PIL import Image
import tempfile
import os

def add_annotations_to_pdf(input_pdf_path, output_pdf_path, comments):
    # PDFを開く
    doc = fitz.open(input_pdf_path)
    
    for comment in comments:
        page_num = comment['page_num']
        text = comment['text']
        x = comment['x']
        y = comment['y']
        
        # 指定されたページを取得
        page = doc[page_num - 1]
        
        # コメントを注釈として追加
        annot = page.add_freetext_annot((x, y, x + 200, y + 50), text)
        annot.set_colors(stroke=(1, 0, 0), fill=(1, 1, 0))
        annot.update()
    
    # 一時ファイルに保存
    temp_pdf = BytesIO()
    doc.save(temp_pdf)
    doc.close()
    temp_pdf.seek(0)

    # 新しいPDFを作成
    output = BytesIO()
    
    # PyMuPDFでPDFを再度開く
    annotated_pdf = fitz.open("pdf", temp_pdf)
    
    # ReportLabのPDFキャンバスを作成
    c = canvas.Canvas(output)

    for page_num in range(len(annotated_pdf)):
        page = annotated_pdf.load_page(page_num)
        
        # 高解像度のPixmapを取得 (300 DPI)
        zoom = 2  # 2倍の解像度 (72 * 2 = 144 DPI)
        mat = fitz.Matrix(zoom, zoom)
        pix = page.get_pixmap(matrix=mat)
        
        # PixmapをPILイメージに変換
        img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
        
        # 一時ファイルに保存
        with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as img_file:
            img.save(img_file, format="PNG")
            img_file_path = img_file.name
        
        # ページサイズをオリジナルPDFと同じに設定
        page_width = page.rect.width * zoom
        page_height = page.rect.height * zoom
        c.setPageSize((page_width, page_height))
        
        # 画像としてPDFページをキャンバスに描画
        c.drawImage(img_file_path, 0, 0, width=page_width, height=page_height)
        c.showPage()
        
        # 一時ファイルを削除
        os.remove(img_file_path)
    
    c.save()
    output.seek(0)
    
    # 最終的なPDFファイルを保存
    with open(output_pdf_path, "wb") as f:
        f.write(output.read())

# 入力PDFのパス
input_pdf_path = "input.pdf"
# 出力PDFのパス
output_pdf_path = "output.pdf"

# コメントリスト
comments = [
    {'page_num': 1, 'text': 'This is a comment on page 1', 'x': 50, 'y': 50},
    {'page_num': 2, 'text': 'This is a comment on page 2', 'x': 100, 'y': 100},
    # 追加のコメントをここに追加
]

# コメントをPDFに追加し、印刷したようなPDFを生成
add_annotations_to_pdf(input_pdf_path, output_pdf_path, comments)

途中で解像度をいじって、元のPDFと同じになるよう調整してある。

やっとまともに動作しそうになったので、2つのスクリプトを結合してもらおう。ついでに、特に意味なく追加されているコメントリストの中身も空にする。

import fitz  # PyMuPDF
from reportlab.pdfgen import canvas
from io import BytesIO
from PIL import Image
import tempfile
import os

def add_annotations_to_pdf(input_pdf_path, output_pdf_path, comments):
    # PDFを開く
    doc = fitz.open(input_pdf_path)
    
    for comment in comments:
        page_num = comment['page_num']
        text = comment['text']
        x = comment['x']
        y = comment['y']
        
        # 指定されたページを取得
        page = doc[page_num - 1]
        
        # コメントを注釈として追加
        annot = page.add_freetext_annot((x, y, x + 200, y + 50), text)
        annot.set_colors(stroke=(1, 0, 0), fill=(1, 1, 0))
        annot.update()
    
    # 一時ファイルに保存
    temp_pdf = BytesIO()
    doc.save(temp_pdf)
    doc.close()
    temp_pdf.seek(0)

    # 新しいPDFを作成
    output = BytesIO()
    
    # PyMuPDFでPDFを再度開く
    annotated_pdf = fitz.open("pdf", temp_pdf)
    
    # ReportLabのPDFキャンバスを作成
    c = canvas.Canvas(output)

    for page_num in range(len(annotated_pdf)):
        page = annotated_pdf.load_page(page_num)
        
        # 高解像度のPixmapを取得 (300 DPI)
        zoom = 2  # 2倍の解像度 (72 * 2 = 144 DPI)
        mat = fitz.Matrix(zoom, zoom)
        pix = page.get_pixmap(matrix=mat)
        
        # PixmapをPILイメージに変換
        img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
        
        # 一時ファイルに保存
        with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as img_file:
            img.save(img_file, format="PNG")
            img_file_path = img_file.name
        
        # ページサイズをオリジナルPDFと同じに設定
        page_width = page.rect.width * zoom
        page_height = page.rect.height * zoom
        c.setPageSize((page_width, page_height))
        
        # 画像としてPDFページをキャンバスに描画
        c.drawImage(img_file_path, 0, 0, width=page_width, height=page_height)
        c.showPage()
        
        # 一時ファイルを削除
        os.remove(img_file_path)
    
    c.save()
    output.seek(0)
    
    # 最終的なPDFファイルを保存
    with open(output_pdf_path, "wb") as f:
        f.write(output.read())

def split_pdf_pages(input_pdf, output_pdf, order):
    doc = fitz.open(input_pdf)
    output_doc = fitz.open()

    for page_num in range(len(doc)):
        page = doc.load_page(page_num)
        rect = page.rect

        # 縦長のページは何もしない
        if rect.width <= rect.height:
            output_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
            continue

        middle = rect.width / 2

        if order == 'R':
            # 右半分のページを作成
            right_rect = fitz.Rect(middle, 0, rect.width, rect.height)
            right_page = output_doc.new_page(width=middle, height=rect.height)
            right_page.show_pdf_page(fitz.Rect(0, 0, middle, rect.height), doc, page_num, clip=right_rect)

            # コメントを右ページに追加
            for annot in page.annots():
                if annot.rect.x0 >= middle:
                    new_annot = right_page.add_freetext_annot(
                        annot.rect - (middle, 0, middle, 0), annot.info["content"])
                    new_annot.update()

            # 左半分のページを作成
            left_rect = fitz.Rect(0, 0, middle, rect.height)
            left_page = output_doc.new_page(width=middle, height=rect.height)
            left_page.show_pdf_page(left_rect, doc, page_num, clip=left_rect)

            # コメントを左ページに追加
            for annot in page.annots():
                if annot.rect.x1 <= middle:
                    new_annot = left_page.add_freetext_annot(annot.rect, annot.info["content"])
                    new_annot.update()

        elif order == 'L':
            # 左半分のページを作成
            left_rect = fitz.Rect(0, 0, middle, rect.height)
            left_page = output_doc.new_page(width=middle, height=rect.height)
            left_page.show_pdf_page(left_rect, doc, page_num, clip=left_rect)

            # コメントを左ページに追加
            for annot in page.annots():
                if annot.rect.x1 <= middle:
                    new_annot = left_page.add_freetext_annot(annot.rect, annot.info["content"])
                    new_annot.update()

            # 右半分のページを作成
            right_rect = fitz.Rect(middle, 0, rect.width, rect.height)
            right_page = output_doc.new_page(width=middle, height=rect.height)
            right_page.show_pdf_page(fitz.Rect(0, 0, middle, rect.height), doc, page_num, clip=right_rect)

            # コメントを右ページに追加
            for annot in page.annots():
                if annot.rect.x0 >= middle:
                    new_annot = right_page.add_freetext_annot(
                        annot.rect - (middle, 0, middle, 0), annot.info["content"])
                    new_annot.update()

    output_doc.save(output_pdf)
    output_doc.close()
    doc.close()

# コメントを追加するPDFのパス
input_pdf_path = "input.pdf"
annotated_pdf_path = "annotated_output.pdf"

# コメントリスト(空にする)
comments = []

# コメントをPDFに追加し、印刷したようなPDFを生成
add_annotations_to_pdf(input_pdf_path, annotated_pdf_path, comments)

# ユーザーに入力を求める
order = input("R/L? ")

# 入力が有効か確認
if order not in ['R', 'L']:
    print("Invalid input. Please enter 'R' or 'L'.")
else:
    split_pdf_path = "split_output.pdf"
    # コメントが追加されたPDFを分割
    split_pdf_pages(annotated_pdf_path, split_pdf_path, order)

変換したいPDFをpyスクリプトと同じ場所に保存し、スクリプト内の「input.pdf」をPDFファイルの名前に書き換えて実行。すると、コメントはPDFに埋め込まれた上で、見開きページは左右に分割され、「split_output.PDF」というファイルに出力される。

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