Як працює редагування тексту в PDF: потоки вмісту, кодування шрифтів і чому можна правити лише один рядок за раз
На сторінці PDF немає абзаців і текстових блоків, лише інструкції намалювати гліфи шрифтом у заданій позиції. Правка рядка видаляє старі інструкції й записує нові на тому самому місці, і саме це визначає, що можна редагувати, а що ні.

На сторінці PDF немає ні абзаців, ні текстових блоків. Є список інструкцій: у цій позиції, цим шрифтом і цього розміру намалювати такі-то гліфи. Змінити текст означає видалити інструкції, що малюють цей рядок, і записати нові в тій самій позиції, тим самим шрифтом, того самого розміру й кольору. Тому Редагувати PDF редагує по одному рядку й не вміє переносити текст: це прямий наслідок того, як текст зберігається.
Як виглядає один рядок тексту у файлі
Вміст сторінки міститься в потоці вмісту, невеликій програмі, що складається з операторів. Для тексту основну роботу виконують чотири оператори: BT і ET обрамляють текстовий блок, Tf вибирає шрифт і розмір, Td переходить до наступної позиції, Tj малює рядок. Зі стандартним шрифтом найпростіший випадок виглядає так:
BT /F1 18 Tf 20 150 Td (Hello World) Tj ET
Це означає: вибрати шрифт F1 розміром 18 пунктів, перейти в точку x 20, y 150 і намалювати Hello World. PDF, надрукований із Chrome, зазвичай виглядає інакше: у дужках уже не символи, а номери гліфів шрифту, а позицію один раз задає Tm. Нижче рядок Hello World from Georgia зі сторінки, експортованої з Chrome; кожні чотири шістнадцяткові цифри — це один номер гліфа:
BT /F4 24 Tf 1 0 0 -1 30 52 Tm <002b0048004f004f00520003003a00520055004f0047000300490055005200500003002a004800520055004a004c0044> Tj ET
002b — це H, 0048 — e, 004f — l, 0003 — пробіл, 003a — W і так далі. Ці числа мають зміст лише всередині одного конкретного шрифту; це не Unicode. Коли ви копіюєте текст, переглядач перетворює номери назад на символи за таблицею відповідності, що зберігається в іншому місці файлу.
Звідси два висновки. По-перше, у потоці вмісту ніде не сказано «це абзац»: розрив рядка — це просто ще один Td чи Tm, що переносить позицію на наступний рядок, а скільки рядків в абзаці, доводиться виводити з позицій. По-друге, щоб записати в рядок новий символ, потрібен номер цього символу в цьому шрифті, а в шрифті часто є лише ті гліфи, що використовувалися під час запису файлу. Це результат субсетування шрифтів.
Потоки можна розгорнути за допомогою qpdf:
qpdf --qdf --object-streams=disable in.pdf out.pdf
Відкрийте out.pdf у текстовому редакторі й знайдіть Tj, щоб побачити всі рядки, намальовані на сторінці. Про те, що ще містить PDF, читайте в статті Що всередині PDF: об’єкти, потоки й таблиця перехресних посилань.
Як редактор збирає «рядок» із гліфів
Спершу редактор має згрупувати розрізнені гліфи в рядок, на який можна клацнути. Фрагмент — це група сусідніх гліфів з одним шрифтом, розміром і кольором, у яких проміжок до попереднього гліфа не перевищує одного розміру шрифту; розрив рядка, новий шрифт чи новий колір фрагмент завершують. Текст, що йде справа наліво або вертикально, не групується і не відкривається для редагування.
Два наслідки можна побачити безпосередньо. Рядок із двома шрифтами, наприклад китайський рядок з англійським словом, розділяється на два сусідні фрагменти, і клацати по них потрібно окремо. Після правки відбувається те саме: якщо новий текст набрано іншим шрифтом, рядок стає двома фрагментами, і правка одного з них не зачіпає другий.
Правка рядка: видалити старий текст, потім записати новий
Видалення використовує варіант замалювання лише для тексту: гліфи в межах області рядка прибираються з потоку вмісту, зображення й лінії лишаються на місці, поверх старого тексту не малюється кольоровий блок, тож копіювання й пошук його більше не знаходять. Змінюється лише вміст сторінки; якщо той самий текст є ще й в анотації чи у властивостях документа, їх це не зачіпає.
Запис додає нову текстову інструкцію в кінець потоку вмісту сторінки, починаючи з позиції базової лінії вихідного рядка й повторюючи його розмір і колір. Після заміни рядка вище на 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» немає у вихідній підмножині шрифту, тому другий BT підключає новий стандартний шрифт Helvetica (F9), щоб її намалювати. q і Q обмежують перетворення координат і колір цього блоку ним самим; редактор також обгортає вихідний вміст у пару q/Q, щоб незбалансований стан, залишений наприкінці вихідної сторінки, не вплинув на новий текст. Так виглядав один фрагмент; числа від файлу до файлу різняться.
Дописування в кінець має три наслідки:
- Новий текст малюється останнім, поверх усього іншого на сторінці. Текст, що перебував під зображенням чи кольоровим блоком, після правки опиняється над ним.
- Інструмент, що витягує текст у порядку потоку вмісту (наприклад,
pdftotext -raw), читає відредагований рядок останнім на цій сторінці, а витягування за позицією (pdftotext -layout) змін не показує. Сторінка виглядає так само, але порядок читання змінився. - Редактор прикріплює до нового тексту ActualText, який повідомляє переглядачеві, яким справжнім символам відповідають гліфи. Один гліф може відповідати кільком символам, і пошук лише за номером гліфа іноді повертає символ сумісності (наприклад, для 文 знаходиться радикал Кансі ⽂); ActualText гарантує, що скопійовано буде те, що ви набрали.
Яким шрифтом малюється новий текст
Найбезпечніше використати вихідний шрифт: форма гліфів, насиченість і ширина збігаються із сусіднім текстом. Чи можна його використати, залежить від того, чи здатен редактор визначити, який код має символ у цьому шрифті:
- Складений шрифт, як вище (Type0, двобайтові номери гліфів, кодування Identity-H): символ шукається у шрифті, щоб отримати номер гліфа, і якщо його знайдено, використовується. Якщо не знайдено, символа в підмножині немає.
- Однобайтовий простий шрифт (TrueType, Type1) із кодуванням WinAnsi або MacRoman: відповідність символу байтові фіксована, а ширини беруться з таблиці ширин у файлі.
- Шрифт із нетиповим кодуванням (Differences) або такий, що спирається на вбудоване кодування шрифту: надійної відповідності немає, тож редактор не вгадує і переходить до запасного шрифту, інакше текст вийшов би спотвореним.
Якщо символа немає у вихідному шрифті, використовується запасний шрифт. Звичайні латинські літери малюються найближчим із Helvetica, Times і Courier, стандартних шрифтів, які є в кожному переглядачі, тож нічого вбудовувати не потрібно. Усе інше, наприклад китайські ієрогліфи, кана, хангиль, тайське письмо, латинські літери з діакритикою, кирилиця й грецьке письмо, використовує відкритий шрифт Noto: сервер бере символи із запиту й вирізає з повного шрифту лише ці гліфи за допомогою бібліотеки HarfBuzz з відкритим кодом для створення підмножин, зазвичай кілька КБ, а браузер вбудовує їх у ваш файл. Регіональна форма китайського ієрогліфа визначається мовою інтерфейсу: спрощена, традиційна, японська чи корейська.
Гліфи вбудовуються, а не обмежуються записом назви шрифту, тому що шрифт, заданий лише назвою, кожен переглядач замінює тим запасним, який обере сам, і насиченість у різних переглядачах може відрізнятися; після вбудовування всі переглядачі показують однакові форми.
Чому лише один рядок за раз
У редактора немає рушія верстки. Позиція кожного гліфа зафіксована у файлі; новий текст просто розміщується праворуч від початкової точки за ширинами шрифту, без логіки перенесення біля краю, і ніщо після нього не зсувається. Якщо подовжити рядок, зайві символи продовжаться праворуч, а все, що вийде за край сторінки, не буде видно. Переписати абзац чи додати рядки в таблицю означає заново зверстати текст, а для цього треба повернутися до вихідного файлу, з якого зроблено PDF.
Ширини, крім того, беруться лише з самого шрифту (на скільки зсувається позиція після кожного гліфа), без кернінгу й підстановки гліфів. Текст, якому не потрібен шейпінг, тобто добір і позиціювання гліфів за контекстом (латиниця, кирилиця, грецька й китайські ієрогліфи), укладається правильно. Тайським тоновим знакам для правильного положення потрібне позиціювання гліфів, якого редактор не виконує, тож вони можуть зміститися. Арабському письму потрібні контекстні форми гліфів і розкладка справа наліво, івриту потрібна розкладка справа наліво, а індійським письмам потрібно поєднувати приголосні та голосні знаки в гліфи; усе це вимагає повного шейпінгу, якого редактор нині не виконує, тому введення такого тексту відхиляється, а не перетворюється на хибні символи.
Ще два обмеження випливають із самого файлу. Вміст відсканованої сторінки — одне зображення без текстових операторів, тож видаляти й змінювати нічого. Після правки файлу з цифровим підписом вихідний вміст, який захищав підпис, змінився, і підпис перестає бути дійсним.
Перевірте самі
Описані зміни можна побачити в завантаженому файлі трьома командами:
pdftotext -raw edited.pdf - # видобування в порядку потоку вмісту: старого тексту немає, відредагований рядок останній
pdffonts edited.pdf # список шрифтів: вихідні шрифти, використані повторно, і нова вбудована підмножина шрифту
qpdf --qdf --object-streams=disable edited.pdf edited.qdf # розгортає потік вмісту; доданий блок BT ... ET лежить наприкінці
Що почитати далі
- Чому субсетування шрифтів ламає редагування, але не читання: чому у вихідному шрифті може бракувати символа і що відбувається, коли він відсутній.
- Що всередині PDF: об’єкти, потоки й таблиця перехресних посилань: загальна картина потоків вмісту, об’єктів і структури файлу.
- Редагування тексту в PDF онлайн: виправити файл без першоджерела: як цим користуватися, для чого це підходить і коли варто повернутися до вихідного файлу.