自作の手帳PDFをGoodNotesに入れたところ、右端の月タブを押しても日付を押しても、リンクが1本も反応しませんでした。アプリの設定やタップの仕方を疑って手を入れたものの、何も変わりません。原因はPDFを書き出す側にあって、使っていたfpdf2がリンクの注釈を仕様と違う形で書いていたのです。
アプリ側を疑っている間は、1つも動かなかった
実機で確かめた2026年9月3日の時点で、その手帳は594ページあり、リンクは全体で15,242本、多いページでは1枚に55本入っていました。数が多いので押し損ねているのかとも考えましたが、どこを押しても同じです。
最初に立てた仮説は「リンクがページの端に密着しているのが悪い」でした。右端の縦タブは幅48ポイントの帯にぴったり収めてあり、いかにも怪しく見えたのです。ところが小さい検証用のPDFを作って試すと、端に密着したリンクでも普通に飛びました。ここは外れです。
GoodNotesでリンクが飛ばないと分かったら、アプリの操作を変える前に自分のPDFの中身を1回開いたほうが早いと思います。見るのは注釈の並び、PDFの中で/Annotsと書かれている項目だけです。
fpdf2は注釈をページの中に直接書いている
最新版のfpdf2 2.8.8で、2ページ目へ飛ぶ内部リンクを3本だけ置いたPDFを作り、そのページの/Annotsを表示してみます。返ってきたのがこれです。
/Annots [<</Type/Annot/Subtype/Link/Border[0 0 0]
/Dest[5 0 R/XYZ 0 841.89 null]/F 4
/Rect[31.18 707.33 81.78 721.33]>>
<</Type/Annot/Subtype/Link ...>>
<</Type/Annot/Subtype/Link ...>>]
注釈の中身そのものが、ページの中に並べて書かれています。一方、PDFの仕様書に定められているのは次の形です。
Annots — (Optional) An array of annotation dictionaries that shall contain indirect references to all annotations associated with the page.
(Annots — そのページに結び付いたすべての注釈について、独立したオブジェクトへの参照を入れた配列)
配列に入れるのは参照であって辞書そのものではない、と読めます。fpdf2の出力は、参照を置くはずの場所に中身を直接書いている形です。仕様と違う書き方をしているところで、厳格に読むビューアが引っかかっていたのでしょう。
この形だから飛ばない、とは言い切れなかった
ここで原因が分かったと書きたくなるのですが、切り分けを続けると、話はもう少し細かいところにあるのです。88ページ・0.4MBの小さい版を作って比べたところ、この埋め込み形式の注釈が1個だけのPDFは、GoodNotesで普通に飛んだのです。
つまり「この書き方をしたら飛ばない」とは言えません。私が確かめられたのは、15,242本すべてをこの形で書いた594ページのPDFでは1本も認識されなかった、という範囲までです。何本からおかしくなるのかは測っていません。
原因を絞るなら、本番の594ページで試すより88ページの検証版を作るほうが速く進みます。1回の書き出しが数秒で終わるので、思いついた仮説をその場で潰していけるからです。
fpdf2をやめず、書き出したあとにリンクだけ入れ直す
直す方向として、ライブラリを乗り換える道と、書き出したPDFを後から直す道の2つがあります。レイアウトの処理を全部書き直したくなかったので、私は後者を選び、PyMuPDFで全リンクを読んで独立したオブジェクトとして入れ直しています。
ここに落とし穴が1つありました。埋め込み形式の注釈はxrefという通し番号を持たないため、page.delete_link()では消えません。消したつもりで新しいリンクを足すと、同じ場所にリンクが二重に乗ります。配列そのものを空にしてから入れ直すのが正解でした。
# 埋め込み形式の注釈は xref を持たず delete_link() では消せない。
# 配列そのものを差し替える。
doc.xref_set_key(page.xref, "Annots", "[]")
for lk in links:
page.insert_link({"kind": pymupdf.LINK_GOTO, "from": lk["from"],
"page": lk["page"], "to": pymupdf.Point(0, 0)})
通したあとの同じページを、もう一度表示します。
/Annots [12 0 R 13 0 R 14 0 R]
12 0 obj
<< /A << /S /GoTo /D [ 5 0 R /XYZ 0 841.89 0 ] >>
/Rect [ 31.18 707.33 81.78 721.33 ] /Subtype /Link >>
参照だけが並ぶ配列に変わり、行き先の指定も/Dest単独から/A << /S /GoTo >>というアクション形式になりました。ファイルは4.6MBから3.1MBに縮みましたが、これは作り直しのときに不要なオブジェクトを掃除して圧縮し直しているからで、リンクの形とは関係がありません。
いま配っている手帳PDFを開き直して数えると、488ページに11,060本のリンクがあり、ページの中に直接書かれた注釈は0個です。書き出しのたびにこの処理が走るようにしたので、元の形に戻ることはありません。
同じところで止まったときに見る順番
もう一度同じことが起きたら、私はこの順で確かめます。まず自分のPDFの/Annotsを開いて、中に<</Type/Annotと書かれているか、数字の参照が並んでいるかを見るのです。辞書が直接書かれていたらそこが疑わしいので、次に実際に動いている手帳PDFを1つ手に入れて、同じ場所を並べて比べます。
比べる相手を用意するのが、いちばん効いたと思っています。仕様書だけ読んでいると「この読み方でも通るのでは」と考えてしまいますが、実際に飛ぶPDFの中身は迷いようがありません。仕様を根拠にするより、動いている実物を根拠にしたほうが速いというのが、ここで得たものです。
自作のPDFでリンクが飛ばないとき、アプリの設定を変える前に、PDFの中身を開いて見たことはありますか。
あわせて読む
出典
- PDF 32000-1:2008 Table 30 – Entries in a page object(2026-09-17に原文を取得して確認)
- 再現に使ったのは fpdf2 2.8.8(2026-08-09公開)と PyMuPDF 1.28.2


コメント