当前专题
2026-04-06
8分钟
墩墩
オンライン版: https://www.douyacun.com/pdf/remove-watermark
PDFの文字透かしを消すというと、最初は「透かしの文字列を見つけて削除すればよい」と考えがちです。ですが、実際にやってみると、その前提こそが不安定な結果を生む原因でした。
本当に難しいのは、2つの条件を同時に満たさなければならないことです。まず透かしを正しく見つけること。そして、その透かしに対応する描画命令だけを正確に削除することです。どちらか一方でも粗いと、出力はすぐに壊れます。
今回の最適化では、主に3つの問題を改善しました。重要なタイトルまで誤って消してしまうこと、候補がUI上で判別しづらいこと、そしてユーザーが1文字ずつ確認しなければならず操作コストが高かったことです。
画像透かしは、多くの場合、画像オブジェクトや図形として扱えます。これに対して文字透かしは、PDFのコンテンツストリーム内にある Tj や TJ のようなテキスト描画命令として存在し、場合によっては再利用される Form XObject の中に入っています。
ここが重要です。同じ見た目の文字列が、透かしにも、見出しにも、本文中の正当なテキストにもなり得ます。システムが文字内容だけを見ていると、それらを安定して区別できません。
しかもPDF内部では、文字はフォント固有のマッピングで符号化され、ページをまたいで再利用され、行列、透明度、色を変えて描画されます。つまり「この文字列を消す」という発想はPDFにとって本質的な操作ではなく、複雑な描画モデルの上に乗った推測にすぎません。
この問題がはっきり見えたのが、青衣说 というサンプルでした。透かし文字も 青衣说、ページ上の大きな正規タイトルにも 青衣说 が含まれていました。

初期の削除方式は、raw_text とグローバルなバイト列置換に強く依存していました。そのため、削除ロジックが対象のバイト列に一致すると、透かしだけでなく、同じ、または似たエンコードを持つタイトルまで消してしまうことがありました。
この方式には、実運用上いくつもの問題がありました。

ここで分かったのは、「検出できる」ことと「安全に削除できる」ことはまったく別だという点です。弱かったのは認識よりも、削除の側でした。
最初の改善は、最終的な見た目だけではなく、PDFの構文により近い層を見ることでした。
具体的には、コンテンツストリームから Tj や TJ などの実際のテキスト描画命令を走査し、その命令ごとに周辺情報を取り出します。たとえば、フォント、文字サイズ、変換行列、透明度、塗りや線の色、ページ本体の contents 由来か Form XObject 由来か、といった情報です。
さらに、候補をできるだけ可読なテキストとして扱えるよう、デコードも強化しました。オペランドを単なる生バイト列として扱うのではなく、可能な場合は ToUnicode や CMap などのフォントマッピングを使って復号します。これにより、以前は読めないバイト断片としてしか見えなかった候補でも、実際の文字列として扱えるケースが増えました。
その上で、文字透かし候補に対してスコアリングを導入しました。判断は1つの特徴だけでは行いません。透かしに典型的な複数の信号を組み合わせます。
この結果、高信頼の候補はデフォルトで選択済みにしつつ、確信が弱い候補も確認用として返せるようになりました。曖昧なケースに対して無理に断定しない設計です。
ここが今回もっとも重要な改善点です。
従来の削除処理は、要するに「透かし関連らしいバイト列が見つかったら、見つかる場所を置換する」という発想でした。しかしPDFでは、このやり方は本質的に危険です。どのオブジェクトを編集しているのか分からず、ただバイト列が一致したことしか分からないからです。
そこで、新しい方式では、検出時に命令単位の識別情報を保持し、適用時にもそれを再利用するようにしました。各文字候補に対して、たとえば次のような情報を持たせます。
stream_xrefop_indexoperator (Tj または TJ)削除時には、PDF全体に対して検索置換をするのではなく、対象の stream を再度パースします。その上で、ユーザーが選択した候補を同じ stream 内の正確なテキスト描画命令に対応付け、その命令だけを書き換えます。
この切り替えが、典型的な誤削除を防ぎます。透かしとタイトルの両方に 青衣说 が含まれていても、それぞれが別の命令、別の stream に存在するなら、削除ロジックは選択された透かし側だけを編集し、タイトルには触れません。
要点は明快です。精度は、より賢く推測することから生まれるのではありません。どのPDFオブジェクトを変更しているのかを正確に知ることから生まれます。
バックエンドの精度が上がっても、それだけでは十分ではありません。ユーザー側の確認体験も速く、分かりやすくなければ実用性は上がりません。
そこで候補表示は、実際の透かしの見た目に近い形を保つようにしました。色、傾き、サイズを視覚的な手掛かりとして残すことで、ユーザーは透かしを素早く見分けやすくなります。一方で、極端に低い透明度までそのまま再現すると読みにくくなるため、可読性が落ちないよう表示側で補正しています。
また、削除対象がひと目で分かるよう、取り消し線による選択フィードバックも追加しました。
レビューコストを下げるために、まとめて操作できる仕組みも拡張しています。
これは、ページ全体に斜めに繰り返し配置された文字透かしで特に効果があります。代表的な1つを見つけたあと、同じ操作を何十回も繰り返す必要がなくなります。
改善点は、単に削除できる透かしが増えたことではありません。より重要なのは、システムの振る舞いが予測しやすくなったことです。
最適化後は、次のような変化がありました。

最後の点こそ、本当の品質基準です。正当な本文を壊す可能性がある透かし除去機能は、実用的とは言えません。
この方式がもっとも効果を発揮するのは、PDF内で本物のテキストオブジェクトとして存在する文字透かしです。
一方、スキャンPDFのように、透かしが画像に焼き込まれているケースには向きません。その場合は、テキスト描画命令ではなく、画像領域での処理が必要になります。
また、複雑なテンプレート、特殊なフォントエンコーディング、強いオブジェクト再利用、独自性の高いPDFでは、依然として人手確認が必要になることがあります。私たちは、その状況を無理に隠すよりも、曖昧さを曖昧さとして見せる方を選びます。
PDFの文字透かし除去は、単に文字を消す処理ではありません。実用的な体験にするには、正確な検出、意味のあるスコアリング、構文レベルの精密削除、そして効率よく確認できるUIが一体で機能する必要があります。
初期版が不安定だった最大の理由は、削除処理が何を消しているのかを正確に理解していなかったことにあります。現在の方式が強くなったのは、検出から適用まで命令の文脈を保持し、対象となるPDFテキスト命令そのものを直接編集するようになったからです。
つまり、「それらしく見えるバイト列を消す」段階から、「ユーザーが選んだ透かしだけを正確に消す」段階へ進んだ、ということです。