ClaudeでPDFの座標がずれる原因と直し方
PDFはサーバー側で制御できない寸法にラスタライズされるため、返ってきた座標を元ページへそのまま当てはめるとズレます。ズレ方からの原因の見分け方と、自前でラスタライズして座標をPDFの単位へ戻すコードを載せます。
Claudeにバウンディングボックス(画像内の要素を囲む矩形の座標)を返させると、PDFではその座標が元のページ上の位置とズレることがあります。原因は、PDFの各ページがサーバー側で制御できない寸法にラスタライズされるためです。通常の画像とは対処法が違うので、ズレの起きる仕組みと回避策を実装レベルで確認します。
座標が合っているかどうかは、OCRパイプライン・フォーム抽出・チャート解析・UI要素の位置特定のように、画像の特定領域に対して後続処理を実行するタスクで直接効いてきます。切り出した領域が対象からはみ出したり、クリック座標が別の要素を指したりするのが典型的な症状です。
PDFの座標はなぜズレるのか
バウンディングボックスとは、画像内の要素を四角形で囲んだときの左上と右下の座標([x1, y1, x2, y2])のことです。Claudeは表・フォーム項目・グラフの要素・UI部品などの領域を、この形式のピクセル座標で返せます。
つまずきやすいのは、返ってくる座標が「送った画像」ではなく「Claudeが処理した後の画像」上の座標だという点です。Claudeは画像をモデルの解像度上限に合わせてリサイズしてから処理します。通常の画像なら、送信前に自分でリサイズしておけば座標はそのまま使えます。
PDFは事情が違います。各ページはサーバー内部で自動的にラスタライズ(画像化)され、生成される画像の寸法は指定できません。公式の座標ガイドも、PDFでは返ってきた座標を元のページへ確実には対応づけられないと明記しています。
Claudeの画像リサイズとパディングの仕組み
ズレの理由を知るには、画像処理のルールを先に押さえておく必要があります。Claudeは次の2つの上限を両方満たす最大サイズへ、縦横比を保ったまま画像を縮小します。
- 辺の上限: 標準解像度層で1568px、高解像度層で2576px
- 視覚トークンの上限:
⌈幅/28⌉ × ⌈高さ/28⌉で計算されるトークン数が、標準層で1568、高解像度層で4784を超えないこと
見落としやすいのは視覚トークンの上限です。130DPIでスキャンしたA4用紙は1075×1520pxで、どちらの辺も1568px未満です。しかし視覚トークンは39×55=2,145トークンかかり、標準層の上限1,568を超えます。結果、Claudeは924×1307へ縮小してから処理します。写真やスクリーンショットの大半は、辺ではなくこのトークン上限で最終サイズが決まります。
さらにClaudeは、リサイズ後の画像を28pxの倍数まで右端と下端だけパディングします(924×1307は924×1316になります)。パディング部分に画像の内容は含まれません。正規化やリスケールの計算は、パディング後ではなくリサイズ後の寸法で行います。
同じA4スキャン(1075×1520px)をどの層のモデルに送るか
標準解像度層
2,145トークンが上限を超えるので924×1307へ縮小されます。返る座標は縮小後の画像上の値で、元画像へは約1.16倍して戻します。
高解像度層
2,145トークンは上限内なので、原寸の1075×1520のまま処理されます。返る座標がそのまま元画像の位置です。
高解像度層はClaude 4.7以降のモデルで、標準層はそれ以外の全モデルです。
ズレ方から原因を見分ける
座標がズレたときは、ズレの形を見ると原因を絞れます。以下は上記のルールから導ける目安です。
- ページ全体が一様に縮んだようにズレる: 縮小を戻していません。150DPIで作ったA4画像(1240×1755px)は標準層で924×1308へ縮むので、座標は元の約0.75倍の位置を指します。
- 下端に近いほどわずかにズレる: パディング後の寸法で割っています。924×1307が924×1316に伸びる例では、縮小後のy=1300の点が、元画像の高さ1520pxに対して約10px(1511.9と1501.5の差)ずれます。
- 画像は同じなのにモデルを変えたらズレた: 解像度層を取り違えています。標準層と高解像度層では、辺の上限が約1.6倍、トークン上限が約3倍違います。標準層の上限を高解像度層のモデルに当てはめると、縮小されていない画像まで縮小済みとして扱うことになり、座標が逆方向にズレます。
- 辺の上限だけで見積もった: 1920×1080のスクリーンショットは、辺の上限だけなら1568×882に見えますが、実際は1456×819まで縮みます。見積もりを辺で行うと、座標が体系的にズレます。
PDFは自前でラスタライズしてから送る
PDFで事前リサイズが使えないのは、事前リサイズの前提である「送った画像の寸法を自分が把握している」が成り立たないからです。回避策は、ページを自分で画像にしてから送ることです。「送った画像=Claudeが処理した画像」という前提が戻り、通常の画像と同じ手法を使えます。
PDFのページから元の位置へ座標を戻す流れ
- 1
ページをDPIを固定して画像にする
PDFレンダリングのツールでページ単位にPNG化します。DPIを固定しておけば、同じPDFの処理結果が毎回同じ寸法になります。
- 2
Claudeが処理する寸法を計算する
画像の寸法を
resized_size(下の折りたたみにある参考実装)に通し、縮小後の幅と高さを得ます。呼ぶモデルの層に合わせて上限を渡します。 - 3
画像を送り、ピクセル座標を依頼する
プロンプトで
[x1, y1, x2, y2]のピクセル座標を明示します。 - 4
返った座標をPDFの単位へ戻す
縮小後の幅と高さで割って相対座標にし、PDFページの寸法(pt)を掛けます。
手順1と4は、たとえば次のように書けます。pdftoppm(Poppler)でページを画像にし、pdfinfoで読んだページ寸法に相対座標を掛けます。
# 3ページ目だけを150DPIでPNG化(出力は page3.png)
pdftoppm -r 150 -png -f 3 -l 3 -singlefile input.pdf page3
# ページ寸法(pt)を確認
pdfinfo -f 3 -l 3 input.pdf | grep "Page.*size"def to_pdf_points(x, y, raster_w, raster_h, page_w_pt, page_h_pt, tier=(1568, 1568)):
"""Claudeが返した(x, y)を、PDFページ上の位置(pt、上端原点)へ戻す。"""
rw, rh = resized_size(raster_w, raster_h, *tier) # パディング前の寸法で割る
x = min(max(x, 0), rw) # はみ出し分は寸法内に収める
y = min(max(y, 0), rh)
return (x / rw * page_w_pt, y / rh * page_h_pt)PDF仕様では座標系は左下原点でy軸が上向きなので、左下原点のまま座標を使うライブラリへ渡すときはpage_h_pt - yへの変換が別途要ります。上端原点を使うライブラリならそのまま渡せます。
PDFをdocumentブロックで送ると、各ページの画像と抽出済みテキストの両方がClaudeに渡ります。画像だけを送ると、テキスト層は渡りません。文字の正確さが必要な処理では、PDFから抽出したテキストを別のテキストブロックとして添える構成があります。
A4ではDPIを上げても処理される寸法は変わらない
解像度を上げれば座標も正確になりそうですが、標準層のA4ではそうなりません。A4(595×842pt)のページをpdftoppmで画像にした寸法に、前述の参考実装を当てると次のようになります。
A4ページを標準層のモデルに送ったときの処理寸法
72DPI(595×842)
縮小なし
縮小前で682トークン
150DPI(1240×1755)
924×1308
縮小前は2,835トークン相当
300DPI(2480×3509)
924×1308
縮小前は11,214トークン相当
高解像度層のモデルなら、150DPIは1240×1755のまま、300DPIは1623×2296へ縮みます。縮小後の細かさを稼げるのは、この層のモデルに限られます。標準層にA4の300DPIの画像を送っても、ファイルが大きくなるだけで座標の精度は上がりません。
もう1つの落とし穴は、画像の枚数です。1回のリクエストに画像が20枚を超えると、すべての画像に通常より厳しい寸法の上限がかかります。公式は、各辺を2,000px以下にするか、画像とdocumentブロックを20個以下に抑えるよう案内しています。300DPIのA4は2480×3509pxなので、20ページ超を1リクエストで送ると拒否されるおそれがあります。
拒否されたときのエラーは、メッセージに「many-image requests」を含むinvalid_request_errorです。Amazon BedrockとGoogle Cloudでは、PDFのdocumentブロックもこの枚数に数えられます。
リサイズ後の寸法を計算する参考実装(Python)
import math
def count_image_tokens(width: int, height: int) -> int:
return math.ceil(width / 28) * math.ceil(height / 28)
def resized_size(width, height, max_edge=1568, max_tokens=1568):
def fits(w, h):
return (
math.ceil(w / 28) * 28 <= max_edge
and math.ceil(h / 28) * 28 <= max_edge
and count_image_tokens(w, h) <= max_tokens
)
if fits(width, height):
return (width, height)
if height > width:
h, w = resized_size(height, width, max_edge, max_tokens)
return (w, h)
aspect = width / height
lo, hi = 1, width
while lo + 1 < hi:
mid = (lo + hi) // 2
if fits(mid, max(round(mid / aspect), 1)):
lo = mid
else:
hi = mid
return (lo, max(round(lo / aspect), 1))
# A4スキャンの例
print(resized_size(1075, 1520)) # (924, 1307)自前でラスタライズしたページ画像をこの関数に通しておけば、送信前に「Claudeが実際に処理する寸法」を先に把握できます。高解像度層のモデルを使う場合はmax_edge=2576, max_tokens=4784に置き換えます。
小さな要素はクロップして送る
縮小は小さな要素ほど座標の精度を落とします。細かい対象(小さなチェックボックスや脚注の番号など)は、領域を切り出して送り、返った座標を切り出し位置の分だけずらして元ページへ戻します。
def crop_to_page(x, y, crop_left, crop_top):
"""クロップ画像上の座標を、クロップ前の画像上の座標へ戻す。"""
return (x + crop_left, y + crop_top)縮小が起きない大きさまでクロップできれば、その画像では座標がそのまま使えます。縮小が起きる場合は、先ほどのto_pdf_pointsと同じ手順で戻します。
座標の取り方と、ズレを防ぐ3つの方法
座標をプロンプトで頼むときは、ピクセル座標を指定します。公式の座標ガイドは、0〜1000のような正規化座標を頼むとうまく動かないと説明しています。正規化が必要なら、返ってきたピクセル座標を自分のコードで割ります。構造化されたJSONで受け取りたい場合は、structured outputsでJSON出力を固定する方法と組み合わせ、[x1, y1, x2, y2]の配列を持つスキーマを定義すると、座標を機械可読なJSONで受け取れます。
ズレへの対処は、状況に応じて3つの選択肢に分かれます。
| 状況 | 対処法 | やること |
|---|---|---|
| 画像を自分で用意できる | 対処法事前リサイズ | やること送信前にリサイズ計算を行い、Claudeが処理する寸法と一致させる |
| PDFのページを扱う | 対処法自前でラスタライズしてから事前リサイズ | やることページを画像化し、通常画像と同じ手順に落とし込む |
| リサイズの発生自体を検知したい | 対処法transformationsで明示的にエラー化 | やること画像ブロックに"oversized_image": "error"を指定する |
3つ目のtransformationsは、画像のブロックに次のように指定します。
{
"type": "image",
"source": { "type": "base64", "media_type": "image/png", "data": "..." },
"transformations": { "oversized_image": "error" }
}その画像がリサイズされるリクエストは、処理前に400のinvalid_request_errorで弾かれます。エラーには元の寸法と、リサイズせずに通せる最大寸法が入ります。1920×1080の画像を標準層のモデルに送った場合のメッセージは次の形です。
messages.0.content.0: image dimensions 1920x1080 exceed the maximum image size of a model named on this request and would be downsized to 1456x819; scale the image to at most 1456x819 or set the image's oversized_image setting to "downsize"指定は画像ブロックごとに効きます。クリックする対象のスクリーンショットにだけ"error"を付け、ロゴのように縮んでも困らない画像は既定の"downsize"のまま、1リクエストに混ぜられます。リクエストに複数のモデルを指定した場合は、すべてのモデルが受け付ける最大寸法が基準になります。PDFのdocumentブロックはこの設定を受け付けません(document内にネストしたimageブロックには使えます)。
トークン数の見積もりに使うtoken countingエンドポイントも、この設定を守ります。リクエスト前に「縮小されずに通るか」を確かめられますが、URLやファイルIDで渡した画像は取得されずに拒否されるので、base64で渡す必要があります。トークン数自体も見積もり値で、実際の入力トークン数とわずかに食い違うことがあります。
スキャンのDPIが変わると過去の座標が使えなくなる
同じPDFでも、スキャンや変換時のDPIが変われば元画像の寸法が変わり、縮小の有無や縮小率も変わります。過去に取得した座標を新しいスキャンへ使い回すと、DPIの違いだけでズレが生じます。座標データはDPIとセットで保存しておくと、後から原因を切り分けやすくなります。
リサイズを強制エラー化する設計はなぜ座標バグの再発を防ぐか
事前リサイズも自前ラスタライズも、正しく実装できていれば座標はズレません。問題は、その状態がいつまで続くかです。画像ソースを切り替えた、モデルを高解像度層に変えた、上流のシステムが仕様変更した。こうした変化はリサイズの発生条件を静かに動かし、座標はズレたまま気づかれずに残ります。
"oversized_image": "error"は、この劣化を「動くが精度が落ちる」から「動かなくなる」に変える設計です。リサイズが起きた時点でリクエストごと落とすので、座標の精度が業務に直結するパイプラインでは、遅くエラーになるより早く止まる方が扱いやすくなります。
ただし、この設定は精度を上げる仕組みではなく、劣化を検知する仕組みです。座標そのものを正確にする作業は、事前リサイズか自前ラスタライズを正しく実装することでしか進みません。
PDFをFiles APIで扱う実装手順はClaude Files APIでPDFを処理する方法にまとめています。API全体の位置付けはAnthropic API完全ガイドから辿れます。
まとめ
PDFの座標は、Claude側のラスタライズ寸法を制御できない以上、ページを自分で画像にして送らない限り元のページへ確実には戻せません。A4のページなら、標準層のモデルでは150DPIと300DPIの処理寸法が同じなので、DPIを上げても座標は細かくなりません。座標の精度が業務に効くパイプラインには、oversized_image: "error"を併せて入れておくと、モデルや入力元の変更によるズレを実行時に検知できます。