Перейти к содержимому
getnextpdf.com

Почему текст в PDF на самом деле не текст

Когда вы читаете PDF, вы видите слова. Файл не содержит слов. Он содержит инструкции рисовать фигуры по координатам, и эти фигуры просто оказываются похожими на буквы. Разрыв между тем, что PDF рисует, и тем, что он означает, — причина того, что страница может выглядеть безупречно и при этом копироваться как бессмыслица. Эта страница — об этом разрыве и о маленьком отдельном отображении, которое его закрывает.

Всё, что вы делаете с PDF как с текстом — выделяете его, копируете, ищете в нём, индексируете его для поиска, читаете вслух с помощью программы чтения с экрана, — зависит от восстановления символов, которые файл никогда напрямую не хранил. Если это восстановление не удаётся, сбой невидим. Страница всё равно отрисовывается. Никто не замечает, пока кто-нибудь не скопирует абзац в письмо и не получит □□□□ или не выполнит поиск в 400-страничном договоре по пункту, который явно там есть, и не найдёт ничего.

Это дорогостоящий класс ошибок именно потому, что он переживает любую визуальную проверку. Видеть нечего. Документ выглядит авторитетно и для машинных целей нем.

  • PDF рисует глифы — визуальные фигуры, выбираемые числовыми кодами в шрифте, — а не символы Unicode.
  • Одна и та же фигура может означать разные символы, и один и тот же символ может рисоваться разными глифами. Внешний вид и смысл разделены по построению.
  • Чтобы получить текст обратно, читатель проходит путь отрисовки в обратную сторону: код → символ. CMap /ToUnicode шрифта — это таблица соответствия для этого обратного шага (Spec: ISO 32000-2, §9.10).
  • Нет /ToUnicode или он неправильный — значит, искажённое копирование и неудачный поиск, — при том что страница по-прежнему попиксельно идеальна.
  • Это проблема смысла. Выглядит ли глиф правильно — отдельная проблема внешнего вида, рассмотренная в Шрифты: самая сложная часть. NextPDF выдаёт правильный /ToUnicode, чтобы обход туда-обратно был достоверным.

Начнём с того, как текст вообще попадает на страницу. Поток содержимого не говорит «напиши слово file». Он выбирает шрифт, а затем передаёт строку кодов оператору отрисовки текста (Spec: ISO 32000-2, §9). Каждый код — это индекс, позиция в программе шрифта, и шрифт превращает этот индекс в контур глифа и рисует его. Код 70 — это не символ F. Это «слот 70 в этом шрифте», и слот 70 просто оказывается содержащим кривую в форме F. Выберите другой шрифт — и слот 70 может оказаться снежинкой.

Так что код что-то означает только относительно кодировки своего шрифта. Для простого шрифта эта кодировка — это примерно один байт на глиф. Для письменностей, которым нужны тысячи глифов, — CJK или любой документ с полным Unicode — этого места недостаточно, и PDF обращается к составному (Type 0) шрифту (Spec: ISO 32000-2, §9.7). Составной шрифт читает многобайтовые коды через CMap, превращает их в CID (идентификаторы символов) и отображает CID в глифы. Это изящная косвенность, которая позволяет одному шрифту адресовать огромный набор глифов. Это также ещё одно место, где след от «что показано» к «что это означает» может оборваться.

Теперь пройдём это в обратную сторону. Чтобы извлечь текст, читатель берёт коды, которые он находит в потоке содержимого, и должен восстановить символы (Spec: ISO 32000-2, §9.10). Кодировка, нарисовавшая глиф, шла символ → глиф; извлечению нужно код глифа → символ, и это направление не гарантированно обратимо. Шрифт-подмножество мог перенумеровать свои глифы. CID составного шрифта могут быть приватными. Фигура на странице не несёт никакого встроенного смысла Unicode.

Решение — поставлять обратное отображение вместе со шрифтом. Это отображение — CMap /ToUnicode: для каждого кода, который использует документ, оно записывает значение (или значения) Unicode, которое этот код обозначает. С ним извлечение — это чистый поиск по таблице. Без него читателю остаётся гадать по кодировкам шрифта и эвристикам, — а гадание — это ровно то место, где fi становится вопросительным знаком.

  1. Your charactersThe Unicode text you set, for example the word 'file'.
  2. Encoding → codesThe font's encoding turns characters into numeric codes; a composite font routes them through a CMap to CIDs.
  3. Codes → glyphsEach code selects a glyph outline, which is painted to the page. This is the part you see.
  4. Extraction reverses itA reader reads the codes back and looks each one up in /ToUnicode to recover Unicode.
  5. Text againWith a correct /ToUnicode, copy, search, indexing, and screen readers all get the original characters back.
The two directions of PDF text. Rendering goes character → glyph and is what you see. Extraction goes code → character and is what you can copy, search, and read aloud — and it only works when the /ToUnicode map closes the loop.

NextPDF пишет обратное отображение как нечто само собой разумеющееся. Когда он встраивает составной шрифт, он выдаёт потомка CIDFontType2, родителя Type0 и CMap /ToUnicode, чтобы коды разрешались обратно в Unicode, — работа с кодировкой, описанная в Шрифты: самая сложная часть. Здесь стоит отдельно выделить почему: поток /ToUnicode — не о том, чтобы страница выглядела правильно. Глифы и без него уже выглядят правильно. Это единственный артефакт, который сохраняет текст извлекаемым.

Самое известное место, где это проявляется, — лигатура fi. Многие шрифты рисуют f и i как один объединённый глиф, потому что точка над i сталкивается с крючком f. На странице это одна фигура, нарисованная одним кодом. Вопрос в том, что происходит, когда вы её копируете.

% A /ToUnicode entry that maps the single ligature code to TWO characters,
% so selecting the 'fi' glyph copies out as 'f' then 'i' — not one mystery box.
1 beginbfchar
<0085> <00660069> % code 0x85 -> U+0066 'f' U+0069 'i'
endbfchar

Эта одна запись — разница между совпадающим поиском по «file» и поиском по «file», который молча пропускает каждое вхождение, отрисованное с лигатурой. Отобразите код лигатуры в двухсимвольную последовательность f + i — и слово становится пригодным для поиска и копирования. Оставьте его без отображения — и страница по-прежнему показывает file безупречно, при том что текст тихо не говорит ничего, что машина могла бы прочитать.

Вот почему «fi» может извлекаться как один символ или как два, и почему эта разница не случайна — это то, что отображению /ToUnicode велели говорить. Правильное отображение разбирает лигатуру обратно на её символы. NextPDF выдаёт ровно такую запись, поэтому лигатура на странице — это две обычные буквы в буфере обмена.

Названная ловушка: «текст отрисовывается, значит, текст в порядке». Отрисовка и извлечение — это разные механизмы, читающие разные данные. Отрисовка идёт по пути «код → глиф» и есть то, что проверяют ваши глаза. Извлечение идёт по пути «код → символ» через /ToUnicode и есть то, что проверяет каждый машинный потребитель. Документ может с блеском пройти первое и полностью провалить второе, потому что по построению нет ничего визуального, что бы это выдало.

Вторая, более тонкая ловушка: предположение, что глиф «очевидно» знает, какой он символ. Он не знает. Глиф — это фигура с индексом. Отображение обратно в символ — это дополнительная информация, которую должен предоставить производитель. Если производитель её так и не записал, ни один читатель не сможет достоверно её восстановить — он может только гадать.

Faithful text extraction from documents NextPDF writes — edition availability
EditionAvailability
CoreNextPDF выдаёт правильную CMap /ToUnicode для встраиваемых им шрифтов, включая случаи лигатур и составных шрифтов, поэтому создаваемые им документы пригодны для поиска и копирования.
ProNot in this edition
EnterpriseNot in this edition

Отображение /ToUnicode может нести лишь ту информацию, которую исходный шрифт действительно раскрывает. Там, где у глифа нет определимого символа — исключительно декоративный знак, символ из области частного использования без назначения Unicode, — никакое отображение не может выдумать смысл, которого никогда не было. NextPDF выдаёт правдивое отображение, которое может вывести; он не фабрикует символы, чтобы заполнить пробел.

Эта страница — о документах, которые NextPDF пишет. Это не инструмент восстановления произвольных входящих PDF, чей /ToUnicode уже отсутствует или ошибочен; восстановление текста из таких — отдельная, эвристическая проблема. И достоверное извлечение необходимо для доступности, но это не вся доступность — порядок чтения, теги и альтернативный текст рассмотрены в Что делает PDF доступным.

Почему один и тот же PDF копируется нормально в одном средстве просмотра и плохо в другом? Разные средства просмотра по-разному откатываются, когда /ToUnicode отсутствует. Одни гадают по встроенной кодировке шрифта и угадывают на обычном латинском тексте; другие нет. Правильный /ToUnicode убирает лотерею — каждое соответствующее стандарту средство просмотра получает одни и те же символы.

Это то же самое, что невстроенный шрифт? Нет. Встраивание — о внешнем виде: рисуется ли глиф без установленного шрифта. /ToUnicode — о смысле: отображается ли код обратно в символ. Шрифт может быть идеально встроен и при этом не иметь пригодного /ToUnicode — это сбой «красиво, но не находится поиском».

Нужно ли извлечению когда-нибудь больше одного символа на код? Да — случай лигатуры именно таков: один код отображается в два символа. Запись /ToUnicode может отобразить один код в короткую последовательность, и именно так лигатуры и некоторые составные формы возвращаются как составляющие их буквы.

  • Шрифты: самая сложная часть — сопутствующая статья со стороны внешнего вида: встраивание, создание подмножеств и как строится кодировка. Эта страница — сторона смысла той же монеты.
  • Что делает PDF доступным — достоверный текст — один из ингредиентов; порядок чтения, теги и альтернативный текст — остальное.
  • Что такое PDF на самом деле — объектная модель, внутри которой живут шрифты, кодировки и потоки /ToUnicode.
  • Глиф — визуальная фигура в шрифте (контур). То, что PDF на самом деле рисует. У глифа есть индекс, но нет встроенного смысла символа.
  • Символ — единица письменного языка, идентифицируемая кодовой точкой Unicode. То, что вы имеете в виду, и то, что пытается восстановить извлечение.
  • Код — числовое значение в потоке содержимого, которое выбирает глиф из текущего шрифта. Не символ Unicode; осмыслен только относительно шрифта.
  • CID — идентификатор символа, используемый составным шрифтом для адресации глифа в большом наборе; промежуточный шаг между кодом и глифом.
  • Составной (Type 0) шрифт — шрифт, который читает многобайтовые коды через CMap, чтобы добраться до CID и глифов, используемый, когда один шрифт должен адресовать тысячи глифов.
  • /ToUnicode — поток CMap, отображающий используемые документом коды обратно в значения Unicode; то, что делает текст PDF пригодным для поиска, копирования и доступным.
  • Лигатура — единый глиф, рисующий две или более букв как одну фигуру (например, fi); его запись /ToUnicode разбирает его обратно на составляющие символы.