販売用PDFに文字と重なるリンクや添付メモが残っていないか、見本4つで確かめる

「Reference guide」としか印刷されていないPDFがあります。本文にURLはありません。それでも、その文字の上には外部サイトへのリンクが重なっています。
ここで扱うのは、Pythonで作った合成見本4つです。AIが出力したPDFは使っていません。前回の販売用PDFの検査ではページ数とフォントを見ましたが、文字の裏にあるリンクや添付物は見ていませんでした。今回はその穴の話です。
見える文字の外に残るもの
PDFのリンクは、URLの文字列がなくても置けます。ページ上のクリックできる範囲に行き先を持たせる仕組みで、リンク注釈と呼ばれます。PyMuPDFのPage.get_links()はこれを辞書で返し、外部への行き先はuriに入ります。
もう1つは埋め込みファイルです。PDF本体に別のファイルを添付できて、本文を抜き出しても出てきません。Document.embfile_names()で名前を列挙できます。
どちらも、買い手の手元に届くもの。だから販売前に、存在だけは止めておきたい。リンク先が危険かどうかまでは、この検査では判定しません。
4つの見本を作る
リンク先は無効な例示ドメインexample.invalid、添付は架空の1行メモです。実際の商品や秘密情報は入れていません。作業は一時ディレクトリで行い、Python 3.11.15とPyMuPDF 1.28.2を使いました。記事のリポジトリには何も足していません。
一時ディレクトリに仮想環境を作り、PyMuPDFを入れます。
uv venv venv
uv pip install --python venv/bin/python pymupdf==1.28.2
次のコードをhibanas-pdf-maker.pyとして保存します。
hibanas-pdf-maker.py(25行)
from pathlib import Path
import pymupdf
out = Path(__file__).parent / "hibanas-pdf-lab"
out.mkdir(exist_ok=True)
for name, link, attached in (
("clean", False, False),
("link-only", True, False),
("attachment-only", False, True),
("both", True, True),
):
doc = pymupdf.open()
page = doc.new_page()
page.insert_text((72, 80), "AI worksheet for sale")
page.insert_text((72, 110), "Reference guide")
if link:
page.insert_link({"kind": pymupdf.LINK_URI,
"from": pymupdf.Rect(72, 94, 220, 120),
"uri": "https://example.invalid/staging"})
if attached:
doc.embfile_add("internal-note.txt", b"Synthetic draft note; not for distribution.\n")
path = out / f"{name}.pdf"
doc.save(path)
doc.close()
print(f"wrote {path.name}")
問題のないcleanのほかに、リンクだけ、添付だけ、両方の3つを作ります。本文は4つとも同じです。
リンクの範囲はRect(72, 94, 220, 120)で、ちょうど「Reference guide」の行を覆う位置です。画面で見ると、ただの見出しの文字にしか見えません。添付はembfile_add()で文書に付けるだけなので、ページの見た目は何も変わりません。見本として作りたかったのは、この「見た目では区別がつかない」状態です。
venv/bin/python hibanas-pdf-maker.py
wrote clean.pdf
wrote link-only.pdf
wrote attachment-only.pdf
wrote both.pdf
検査を1つずつ足す
検査は1つのスクリプトにまとめ、引数で範囲を切り替えます。textは本文の文字だけ、linksはリンク注釈も、allは埋め込みファイルまで見ます。hibanas-pdf-check.pyとして保存しました。
hibanas-pdf-check.py(18行)
import sys
import pymupdf
mode = sys.argv[1]
for path in sys.argv[2:]:
with pymupdf.open(path) as doc:
text = "\n".join(page.get_text() for page in doc)
hits = []
if "https://" in text:
hits.append("visible URL")
if mode in ("links", "all"):
for page_no, page in enumerate(doc, 1):
for link in page.get_links():
if link.get("kind") == pymupdf.LINK_URI:
hits.append(f"page={page_no} uri={link.get('uri')}")
if mode == "all":
hits.extend(f"embedded={name}" for name in doc.embfile_names())
print(f"{path.rsplit('/', 1)[-1]}: {'FAIL' if hits else 'PASS'}", *hits, sep=" | ")
読みどころは3か所です。textでは、全ページの文字をつないでhttps://を探すだけ。linksでは、ページごとにget_links()を回し、種類がLINK_URIのものだけを拾います。ページ内の移動リンクはここでは数えません。allでは、ページの外にある添付の名前をembfile_names()で文書から取り出します。何か1つでも見つかればFAILとし、見つけた中身を同じ行に並べます。
まず文字だけです。
venv/bin/python hibanas-pdf-check.py text hibanas-pdf-lab/*.pdf
attachment-only.pdf: PASS
both.pdf: PASS
clean.pdf: PASS
link-only.pdf: PASS
4つともPASS。URLが印刷されていない以上、文字を探しても見つかりません。
次にリンク注釈を足します。
venv/bin/python hibanas-pdf-check.py links hibanas-pdf-lab/*.pdf
attachment-only.pdf: PASS
both.pdf: FAIL | page=1 uri=https://example.invalid/staging
clean.pdf: PASS
link-only.pdf: FAIL | page=1 uri=https://example.invalid/staging
リンク入りの2つは止まりました。ではattachment-only.pdfはどうか。まだ通ります。添付はページではなく文書全体に付くので、ページを見て回っても出てきません。
最後に埋め込みファイルです。
venv/bin/python hibanas-pdf-check.py all hibanas-pdf-lab/*.pdf
attachment-only.pdf: FAIL | embedded=internal-note.txt
both.pdf: FAIL | page=1 uri=https://example.invalid/staging | embedded=internal-note.txt
clean.pdf: PASS
link-only.pdf: FAIL | page=1 uri=https://example.invalid/staging
通ったのはclean.pdfだけ。ただし、自分で入れた3種類の不備を拾えたというだけで、どんなPDFでも見分けられるわけではありません。
止めたあと、どうするか
検出したリンクや添付は自動で消しません。購入者向けの正しい参考リンクや、意図して同梱した資料もありえるからです。名前と行き先を見て、不要なら制作元のデータから消して出力し直します。必要なものなら、納品仕様に書いておきます。たとえば「1ページ目の参考資料は公式ドキュメントへのリンク」「添付は記入例のCSVが1つ」のように、検査で出た行と照らせる形にしておくと、次の版でも同じ確認で済みます。
私がまだ決めきれていないのは、正当なリンクをどう通すかです。今はURIが1つでもあればFAILにしています。許可するドメインを並べる方法もありますが、その一覧の管理が新しい手間になります。販売前に毎回人が開いて確かめるほうが、結局は早いのかもしれません。
まだ見ていないものもあります。URI以外のリンク、ファイル添付注釈、JavaScript、フォームの動作、閲覧ソフトごとの表示の違いです。次は実際の販売用ワークシートの複製に同じ検査をかけます。
出典
- PyMuPDF Page:
get_links()と、URIリンクのuri - PyMuPDF Document:
embfile_names()による埋め込みファイルの列挙


