Заменить текст в PDF на Python сложно: шрифты, кернинг и подмножества. Узнайте, как восстановить параметры и избежать артефактов. Читайте и применяйте!
Представьте: вы открываете PDF, находите слово, которое нужно заменить, и вносите правку. Кажется, что это просто? Я потратил месяцы на создание PDF-редактора, и именно эта функция — «заменить слово, не трогая остальное» — съела больше всего времени. В этой статье я расскажу, почему это так сложно, и как избежать типичных ошибок. Вы узнаете, как работает текст в PDF, какие подводные камни ждут при замене, и получите практические советы для своего проекта.
Первое, что нужно понять: PDF не хранит текст как строки. Внутри файла — инструкции для отрисовки. Одна строка текста выглядит примерно так:
BT
/F1
11
Tf
72
700
Td
[
(W)
-30
(e)
15
(l)
-20
(come)
]
TJ
ETМассив TJ — это визуальное слово, разбитое на фрагменты с кернингом. В файле нет строки «Welcome», нет понятия абзаца или строки. Всё, что человек видит как структуру, — результат интерпретации.
Поэтому «найти и заменить» — это не строковая операция. Это: восстановить логический текст из позиционированных глифов, найти цель, вычислить, какие операции рисования создали эти глифы, удалить их и нарисовать новые так, чтобы выглядело естественно.
Чтобы замена была незаметной, нужны оригинальный шрифт, размер, базовая линия и цвет. Ни один из них не доступен напрямую.
Размер не просто число после Tf. Текстовая матрица может масштабировать его. Например, заголовок с Tf 1 внутри матрицы, масштабированной на 24, отображается как 24pt. Если читать только Tf, получите 1.
Базовая линия важнее, чем кажется. Если она на пару пунктов выше, глаз сразу заметит, даже если шрифт и размер идеальны.
С цветом нужно быть осторожным в сканах. Если взять цвет фона страницы и закрасить им, на кремовой бумаге получатся белые блоки.
В итоге я оценивал параметры по соседним строкам: взвешенная медиана размера, шрифта и базовой линии. И использовал оценку только тогда, когда точное значение не удавалось сопоставить. Именно в этом запасном пути жили почти все баги.
Замены в научных статьях выходили в неправильном шрифте: лёгкий sans-serif вместо serif. Любой читатель заметил бы, но я не мог воспроизвести.
Я создавал тестовые PDF, которые, как я был уверен, повторяют проблему: встроенные шрифты, subset, жирные заголовки. Все проходили. Я «исправил» баг три раза, но он продолжал проявляться на реальных документах.
В конце концов я перестал генерировать тестовые файлы и скачал реальную статью с arXiv. Она падала сразу, каждый раз. Причина — одна строка:
let serif = low.contains("times") || low.contains("roman") || ...LaTeX использует Times-клон NimbusRomNo9L. В нём есть «Rom», но нет «roman». Поэтому шрифт классифицировался как sans-serif и заменялся на Helvetica. У Computer Modern та же проблема: CMR10 и CMBX12 не содержат слова «roman».
То же предположение о названии сломало определение жирности. Nimbus называет жирное начертание -Medi, а не -Bold, поэтому заголовки выходили обычным весом.
Два сравнения строк — и они проявились только на документах, которые мой генератор не мог создать. Мои тестовые файлы создавались библиотекой, которая выдаёт чистые TrueType с аккуратными именами. Реальные документы — из LaTeX, Word, двадцатилетних сканеров — полны аббревиатур, которые никто не документирует.
PDF обычно встраивает только подмножество каждого шрифта — лишь те глифы, которые уже использованы. Это позволяет держать размер файла разумным, но это ловушка. Если в оригинальном тексте не было буквы «z», во встроенном шрифте её нет. Замените слово на то, где нужна «z», — и вы не сможете нарисовать её шрифтом документа.
Поэтому всегда нужен запасной шрифт, и он должен выглядеть похоже. Это значит, что на сервере должны быть установлены настоящие шрифтовые семейства. Если в контейнере только одно семейство, все замены будут на него, независимо от вида документа.
Начните с существующего текста. Позволить пользователю нарисовать прямоугольник и вписать текст — значит гадать о шрифте, размере и базовой линии по прямоугольнику. Если сначала определить текстовые блоки и дать выбрать один, параметры будут известны, а не выведены. Тот же интерфейс, но гораздо меньше догадок.
Тестируйте на документах, которые вы не создавали. Сгенерированные тестовые файлы разделяют ваши предположения — именно поэтому они бесполезны для поиска таких багов. Одна реальная статья с arXiv нашла больше, чем неделя синтетических случаев.
Измеряйте результат, а не процесс. «Правка применилась» — не вопрос. Вопрос — правильного ли размера замена, на правильной ли базовой линии и не пересекается ли с соседями. Это значит рендерить результат и сравнивать.
Редактор доступен на marqpdf.com. Откройте PDF — он обведёт каждую строку; кликните и введите текст или скажите, что изменить.
Если у вас есть PDF, который его ломает — необычный шрифт, государственная форма, многоколоночный макет, что-то, созданное неизвестным ПО, — я буду рад увидеть. Такие документы ценнее любых тестовых файлов, которые я могу написать.
Не пытайтесь писать замену текста в PDF с нуля. Изучите, как PDF хранит текст, восстановите параметры из соседних строк, тестируйте на реальных документах. И помните: если что-то выглядит просто, скорее всего, вы не знаете всех деталей.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →