【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()
とりあえず、問題なく動作した。これで、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」というファイルに出力される。
この記事が気に入ったらサポートをしてみませんか?