実現したいこと
入力されたPDFをpypdfで加工して、目次を作成したいと考えています。
【前提】
・既にしおりが設定されたPDFを入力とする
・入力PDFの1ページ目は表紙、2ページ目以降に本文という構成
【目標】
・表紙の後ろ(2ページ目)に白紙ページを挿入
・白紙ページに、しおりのタイトルとリンク先のページ番号がセットになった目次を記述する
・目次に記述するページ番号は、元々の番号に対して挿入する目次分だけ加算する必要がある
・目次は、1列50項目を2列配置し、1ページに最大100個のしおりを記述する
・目次の各項目は、しおりと同じようにクリックすることで該当するページへ移動するリンクを設定する
以上の内容で、目次が1ページのみの場合には目標の通りに実装できました。
しかし、しおりが100個を超過した場合の処理に悩んでいます。
発生している問題・分からないこと
【現状】
・しおりが300個を超過しているため、目次を4ページ作成している
・リンクの移動先及び目次に記載するページ番号は、その分4加算している
・目次の2ページ目以降に記述するタイトル及びページ番号は正しい
・目次の2ページ目以降にリンクは設定できているのだが、移動先が1ページ目と同じページになっている
例.
page1
ABC…2 GHI…7
DEF…5 JKL…8
ABC…6 GHI…9
DEF…7 JKL…12
以下略
page2
MNO…20 STU…25
PQR…22 VWX…30
以下略
これで「ABC」をクリックすると6ページ目に遷移しており、これは正しい挙動です。
しかし、「MNO」をクリックした場合も、6ページ目に遷移してしまいます。
「STU」の場合も、同様に9ページ目に遷移します。
2025/3/19補足:白紙挿入の処理については、Pythonではなく事前に行うことも可能。
該当のソースコード
Python
1from pypdf import PdfReader, PdfWriter 2from reportlab.pdfgen import canvas 3from reportlab.lib.pagesizes import A4 4from reportlab.pdfbase import pdfmetrics 5from reportlab.pdfbase.ttfonts import TTFont 6from io import BytesIO 7from pypdf.generic import NameObject, ArrayObject, NumberObject 8 9# 既存のPDFを読み込む 10input_pdf_path = "Input.pdf" 11reader = PdfReader(input_pdf_path) 12 13# しおりを取得 14original_bookmarks = reader.outline 15bookmarks = [] 16 17def extract_bookmarks(outlines, bookmarks): 18 """ 再帰的にしおりを取得 """ 19 for item in outlines: 20 # ネストされたしおり 21 if isinstance(item, list): 22 extract_bookmarks(item, bookmarks) 23 else: 24 bookmarks.append((item.title, reader.get_destination_page_number(item))) 25 26extract_bookmarks(reader.outline, bookmarks) 27 28# 目次ページを作成(2列×50行で 1ページに100個*4ページ) 29entries_per_page = 100 30# 必要なページ数を算出 31num_pages = (len(bookmarks) + entries_per_page - 1) // entries_per_page 32 33toc_packets = [] 34for page_idx in range(num_pages): 35 packet = BytesIO() 36 c = canvas.Canvas(packet, pagesize=A4) 37 width, height = A4 38 39 # フォント、フォントサイズの設定 40 pdfmetrics.registerFont(TTFont("YuGoth", "YuGothM.ttc")) 41 c.setFont("YuGoth", 10) 42 43 c.drawString(50, height - 50, f"目次({page_idx + 1}/{num_pages})") 44 45 # 目次を2列で記述するためのx座標 46 left_x = 50 47 right_x = 300 48 # y座標の開始位置 49 y_position = height - 80 50 # 1列50個 51 column_split = 50 52 53 start_index = page_idx * entries_per_page 54 end_index = min(start_index + entries_per_page, len(bookmarks)) 55 56 for i in range(column_split): 57 left_idx = start_index + i 58 right_idx = start_index + i + column_split 59 60 if left_idx < end_index: 61 title, page = bookmarks[left_idx] 62 c.drawString(left_x, y_position, f"{title} … {page + 5}") 63 64 if right_idx < end_index: 65 title, page = bookmarks[right_idx] 66 c.drawString(right_x, y_position, f"{title} … {page + 5}") 67 68 # 行間を狭くする 69 y_position -= 15 70 71 c.save() 72 packet.seek(0) 73 toc_packets.append(packet) 74 75# 目次ページをPDFに変換して追加 76writer = PdfWriter() 77 78# 元のPDFの1ページ目(表紙)を追加 79writer.add_page(reader.pages[0]) 80 81# 目次ページを2ページ目から追加 82toc_readers = [PdfReader(packet) for packet in toc_packets] 83for toc_reader in toc_readers: 84 writer.add_page(toc_reader.pages[0]) 85 86# 既存のページを追加 87for i in range(1, len(reader.pages)): 88 writer.add_page(reader.pages[i]) 89 90# すべての目次ページにリンクを設定 91y_position = height - 80 92# 目次の2ページ目以降のリンク先ページを調整するための変数 93lastBookmarkPage = 0 94 95for page_idx, packet in enumerate(toc_packets): 96 start_index = page_idx * entries_per_page 97 end_index = min(start_index + entries_per_page, len(bookmarks)) 98 99 for i in range(column_split): 100 left_idx = start_index + i 101 right_idx = start_index + i + column_split 102 103 if left_idx < end_index: 104 title, page = bookmarks[left_idx] 105 rect = ArrayObject([NumberObject(50), NumberObject(y_position - 5), NumberObject(250), NumberObject(y_position + 10)]) 106 print(title + ':if文内left lastBookmarkPage=' + str(lastBookmarkPage)) 107 annotation = { 108 NameObject("/Subtype"): NameObject("/Link"), 109 NameObject("/Rect"): rect, 110 NameObject("/Border"): ArrayObject([NumberObject(0), NumberObject(0), NumberObject(0)]), 111 NameObject("/A"): { 112 NameObject("/S"): NameObject("/GoTo"), 113 NameObject("/D"): ArrayObject([NumberObject(page + 4 + lastBookmarkPage), NameObject("/Fit")]) 114 } 115 } 116 # 目次の1〜4ページ目にしおりを設定 117 for toc_page in range(1, 5): 118 writer.add_annotation(toc_page, annotation) 119 120 if right_idx < end_index: 121 title, page = bookmarks[right_idx] 122 # 目次2ページ目以降且つループが最後(同一ページ内の最後のしおり)の場合、当該しおりのページ番号を変数に保存 123 if page_idx > 0 and i == column_split - 1: 124 lastBookmarkPage = page 125 126 rect = ArrayObject([NumberObject(300), NumberObject(y_position - 5), NumberObject(500), NumberObject(y_position + 10)]) 127 print(title + ':if文内right lastBookmarkPage=' + str(lastBookmarkPage)) 128 annotation = { 129 NameObject("/Subtype"): NameObject("/Link"), 130 NameObject("/Rect"): rect, 131 NameObject("/Border"): ArrayObject([NumberObject(0), NumberObject(0), NumberObject(0)]), 132 NameObject("/A"): { 133 NameObject("/S"): NameObject("/GoTo"), 134 NameObject("/D"): ArrayObject([NumberObject(page + 4 + lastBookmarkPage), NameObject("/Fit")]) 135 } 136 } 137 # 目次の1〜4ページ目にしおりを設定 138 for toc_page in range(1, 5): 139 writer.add_annotation(toc_page, annotation) 140 # 行間を狭くする 141 y_position -= 15 142 143# 元のしおりをコピーし、目次の分をshift_amountで調整 144def add_bookmarks(outlines, writer, parent=None, shift_amount=4): 145 """ しおりのページ番号を +5 して再作成 """ 146 for item in outlines: 147 if isinstance(item, list): # ネストされたしおり 148 new_parent = writer.add_outline_item("フォルダ", page_number=None, parent=parent) 149 add_bookmarks(item, writer, new_parent, shift_amount) 150 else: 151 page_num = reader.get_destination_page_number(item) 152 if page_num is not None: 153 writer.add_outline_item(item.title, page_number=page_num + shift_amount, parent=parent) 154 155add_bookmarks(original_bookmarks, writer, shift_amount=4) 156 157# 新しいPDFを保存 158output_pdf_path = "output.pdf" 159with open(output_pdf_path, "wb") as f: 160 writer.write(f) 161 162print(f"目次ページ付きのPDFを出力しました(しおりも保持): {output_pdf_path}")
試したこと・調べたこと
- teratailやGoogle等で検索した
- ソースコードを自分なりに変更した
- 知人に聞いた
- その他
上記の詳細・結果
if page_idx > 0 and i == column_split - 1:
lastBookmarkPage = page
123行目のこの処理により、lastBookmarkPageに変数pageを代入しています。
変数page_idxは目次のページ数、変数iはページ内のしおりのカウンタ変数です。
つまり、目次の2ページ以上且つ当該ページ内の最後のしおりに到達した際に、lastBookmarkPageをそのしおりのpageで更新しています。
※ページ内の最後のしおりは常に右側の列にあるため、左側の列ではこの処理を行っていません。
実際に、106行目と127行目のprint文では、lastBookmarkPageが目的の値になっていることが確認できました。
しかし、113行目及び134行目にてlastBookmarkPageを加算しても、目次の2ページ目以降のリンクが1ページのリンクから変わりません。
113行目及び134行目にlastBookmarkPageではなく、例えば10のような固定値を加算した場合は、リンク先のページが変わることは確認できています。
lastBookmarkPageにて取得している値を、目次の2ページ目以降に適用するには、どうすればいいでしょうか?
長いコード且つ複雑な内容で申し訳ございません。
情報に不足がございましたら、ご指摘いただけますと幸いです。
補足
2025/3/19追記
大変申し訳ございません。「123行目のこの処理により…」からの内容は考え方が誤っているように思いましたので、一旦取り消し線を付与しました。
例えば目次2ページ目であれば100個目(1ページ目最後)のしおりに設定された変数pageを保存し、それを加算する前提で記載しましたが、無駄に複雑化している気がします。
ようするに、目次2ページ目であれば101個目から200個目のしおりに設定されたpageが取得できれば、それをそのまま目次のリンクに設定すれば解決する気がします。
つまり、107及び128行目で設定しているannotationにて対象とするしおりを、目次のページ数に応じた数にできればいいのではないかと推測しています。
例えば、100行目101行目のleft_idxとright_idxにpage_idx * 100(目次のページ数に100をかけた値)を加算したのですが、値は更新されませんでした(目次の1ページ目と同じリンク先になる)。
※そもそも、96行目の「start_index = page_idx * entries_per_page」で目次のページ数に100をかけているので、ページが移っても正しいしおりを取得できる気がするのですが…
お手数をおかけしますが、この補足情報を考慮した上で、目次の2から4ページ目に正しいリンクを設定する方法が分かりましたら、ご教授いただけますと幸いです。

あなたの回答
tips
プレビュー