Formats約1分で読めます

PDF のテキスト編集のしくみ:内容ストリーム、フォントエンコーディング、1 行ずつしか編集できない理由

PDF のページには段落もテキストボックスもなく、あるのは「このフォントでこの位置にこのグリフを描く」という命令だけです。1 行を編集するとは、古い命令を削除して同じ位置に新しい命令を書くことであり、編集できる範囲もそこから決まります。

PDF123 · Updated 2026-10-14
PDF のテキスト編集のしくみ:内容ストリーム、フォントエンコーディング、1 行ずつしか編集できない理由

PDF のページには段落もテキストボックスもありません。あるのは命令の並びだけです。この位置に、このフォントとサイズで、これらのグリフを描く、という命令です。テキストを変更するとは、その行を描く命令を削除し、同じ位置に同じフォント、サイズ、色で新しい命令を書くことです。PDFの編集が 1 行ずつしか編集できず、折り返しもできないのはそのためで、テキストの保存方法から必然的にそうなります。

1 行のテキストがファイルの中でどう見えるか

ページの内容は内容ストリームに入っています。これは演算子でできた小さなプログラムです。テキストでは主に 4 つの演算子が働きます。BT と ET はテキストブロックを囲み、Tf はフォントとサイズを選び、Td は次の位置へ移動し、Tj は文字列を描きます。標準フォントを使う最も単純な例は次のとおりです。

BT /F1 18 Tf 20 150 Td (Hello World) Tj ET

フォント F1 を 18 ポイントで選び、x 20、y 150 に移動して Hello World を描く、という意味です。Chrome から印刷した PDF は通常これと違います。括弧の中身は文字ではなくフォント内のグリフ番号になり、位置は Tm で一度に設定されます。次は Chrome が書き出したページの Hello World from Georgia という行で、16 進の 4 桁がグリフ 1 つの番号です。

BT /F4 24 Tf 1 0 0 -1 30 52 Tm <002b0048004f004f00520003003a00520055004f0047000300490055005200500003002a004800520055004a004c0044> Tj ET

002b は H、0048 は e、004f は l、0003 はスペース、003a は W、というように続きます。これらの番号はこの 1 つのフォントの中でだけ意味を持ち、Unicode ではありません。テキストをコピーすると、ビューアはファイルの別の場所にある対応表を使って番号を文字に戻します。

ここから 2 つのことが言えます。1 つ目は、内容ストリームのどこにも「これは段落だ」と書かれていないことです。改行は位置を次の行へ動かすもう 1 つの Td か Tm にすぎず、段落が何行かは位置から推測するしかありません。2 つ目は、行に新しい文字を書くにはその文字のそのフォントでの番号が必要なのに、フォントにはファイルを書いたときに使われたグリフしか入っていないことが多いことです。これはフォントのサブセット化の結果です。

qpdf でストリームを展開できます。

qpdf --qdf --object-streams=disable in.pdf out.pdf

out.pdf をテキストエディタで開いて Tj を検索すると、ページに描かれるすべての文字列が見えます。PDF に何が入っているかについては、PDF の中身を参照してください。

エディタがグリフから「行」を組み立てる方法

エディタはまず、ばらばらのグリフを、ユーザーがクリックできる行にまとめる必要があります。フォント、サイズ、色が同じで、直前のグリフとの間隔がフォントサイズ 1 つ分以内のグリフは同じラン(隣り合う同じ書式のグリフのまとまり)に属します。改行、新しいフォント、新しい色でランは終わります。右から左へ書くテキストや縦書きのテキストはグループ化されず、編集のために開けません。

直接確認できる結果が 2 つあります。たとえば中国語の行に英単語が入っているような、2 つのフォントが混在する行は、隣り合う 2 つのランに分かれ、別々にクリックします。編集後も同じで、新しいテキストが別のフォントを使うと行は 2 つのランになり、一方を編集してももう一方は変わりません。

1 行を編集する:古いテキストを削除し、新しいテキストを書く

削除にはテキストだけを対象にした墨消しを使います。行の領域内のグリフが内容ストリームから取り除かれ、画像と線には手を付けず、古いテキストの上に色付きの箱も描かないので、コピーや検索ではもう見つかりません。変わるのはページの内容だけです。同じテキストが注釈や文書プロパティにもある場合、そちらは影響を受けません。

書き込みは、ページの内容ストリームの末尾に新しいテキスト命令を追加します。元の行のベースライン位置から始め、サイズと色を引き継ぎます。上の行を Hello World 2 に変えると、ページの末尾に追加されるのは次の内容です。

q 1 0 0 1 22.5 148.92 cm 0 0 0 rg
/Span <</ActualText <feff00480065006c006c006f00200057006f0072006c006400200032>>> BDC
BT /F4 18 Tf 0 0 Td <002b0048004f004f00520003003a00520055004f00470003> Tj ET
BT /F9 18 Tf 102.18 0 Td (2) Tj ET
EMC
Q

最初の BT は元のフォント F4 と元のグリフ番号を使うので、Hello World は元のままそっくり再利用されます。「2」は元のフォントのサブセットにないため、2 つ目の BT が標準の Helvetica フォント(F9)を新たに開始して描きます。q と Q は、このブロックの座標変換と色をブロック内に閉じ込めます。エディタは元の内容も q/Q の対で包むので、元のページの末尾で閉じられていない状態が新しいテキストに影響することはありません。これは 1 つのランの例で、数値はファイルによって変わります。

末尾への追加には 3 つの結果があります。

  • 新しいテキストは最後に描かれるため、ページ上の他のすべての上に重なります。以前は画像や色付きブロックの下にあったテキストが、編集後はその上に現れます。
  • 内容ストリームの順にテキストを取り出すツール(pdftotext -raw など)は、編集した行をそのページの最後に読みます。位置で取り出す方法(pdftotext -layout)では変化が見えません。ページの見た目は同じでも、読み取り順は変わっています。
  • エディタは新しいテキストに ActualText を付けます。これはグリフがどの実際の文字を表すかをビューアに伝えるものです。1 つのグリフが複数の文字に対応することがあり、グリフ番号だけで逆引きすると互換文字が返ることがあります(たとえば「文」を康熙部首の「⽂」として読む場合)。ActualText により、コピーされるのは入力した文字そのものになります。

新しいテキストをどのフォントで描くか

元のフォントを再利用するのが最も安全です。グリフの形、太さ、幅が隣の文字と揃うからです。再利用できるかどうかは、エディタがその文字のそのフォントでのコードを特定できるかにかかっています。

  • 上で見たような複合フォント(Type0、2 バイトのグリフ番号、Identity-H エンコーディング):フォントでその文字のグリフ番号を調べ、見つかればそれを使います。見つからなければ、その文字はサブセットにありません。
  • 1 バイトの単純フォント(TrueType、Type1)で WinAnsi または MacRoman エンコーディングのもの:文字からバイトへの対応は固定で、幅はファイル内の幅テーブルから取ります。
  • カスタマイズされたエンコーディング(Differences)や、フォント内蔵のエンコーディングに頼るフォント:信頼できる対応がないため、エディタは推測せずフォールバックフォントに回ります。そうしないと文字化けするからです。

元のフォントにない文字にはフォールバックフォントを使います。ラテン文字だけなら、どのビューアにもある標準フォントの Helvetica、Times、Courier のうち最も近いものを使うので、埋め込みは不要です。それ以外、たとえば漢字、かな、ハングル、タイ文字、アクセント付きラテン文字、キリル文字、ギリシャ文字には、オープンソースの Noto フォントを使います。サーバーはリクエスト内の文字を受け取り、オープンソースの HarfBuzz サブセットライブラリで完全なフォントからそのグリフだけを切り出します。通常は数 KB で、ブラウザがそれをファイルに埋め込みます。漢字の字形の地域差は、インターフェース言語に従います。簡体字、繁体字、日本語、韓国語のいずれかです。

フォント名を書くだけにせずグリフを埋め込むのは、名前だけで参照されたフォントは各ビューアが選んだ代替に置き換えられ、太さがビューアごとに変わり得るためです。埋め込めば、どのビューアでも同じ字形が表示されます。

1 行ずつしか編集できない理由

エディタにはレイアウトエンジンがありません。ファイル内のすべてのグリフの位置は固定されています。新しいテキストは、フォントの幅を使って開始点から右へ並べるだけで、「端で折り返す」ロジックはなく、その後ろのものも移動しません。行を長くすると、増えた文字は右へ延び続け、ページの端を越えた分は見えません。段落を書き直したり表に行を追加したりするには新しいレイアウトが必要で、それはその PDF を作った元のファイルに戻ることを意味します。

幅もフォント自身の送り幅だけで、カーニングやグリフの置換はありません。シェーピング(文脈に応じてグリフを選び、位置を決める処理)を必要としないテキスト(ラテン文字、キリル文字、ギリシャ文字、漢字)は正しく配置されます。タイ文字の声調記号は正しい位置に載せるのにグリフの位置調整が必要ですが、エディタはそれを行わないため、ずれることがあります。アラビア文字は文脈に応じたグリフ形と右から左へのレイアウトが必要で、ヘブライ文字は右から左へのレイアウトが必要で、インド系文字は子音と母音記号を組み合わせてグリフにする必要があります。これらはいずれも完全なテキストシェーピングを必要とし、エディタは現在それを行わないため、誤った文字を出力する代わりに入力を拒否します。

ファイル自体に由来する制限がさらに 2 つあります。スキャンされたページの内容は 1 枚の画像で、テキスト演算子がないため、削除や変更の対象がありません。デジタル署名付きのファイルを編集すると、署名が保護していた元の内容が変わるため、署名は無効になります。

自分で確認する

ダウンロードしたファイルで、上に述べた変更を 3 つのコマンドで確認できます。

pdftotext -raw edited.pdf -      # 内容ストリームの順に抽出:古いテキストは消え、編集した行が最後に来る
pdffonts edited.pdf              # フォント一覧:再利用された元のフォントと、新しく埋め込まれたフォントのサブセット
qpdf --qdf --object-streams=disable edited.pdf edited.qdf   # 内容ストリームを展開:追加された BT ... ET ブロックが末尾にある

関連記事

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool