Перейти к содержанию

Текстовый слой и доступное чтение

Оглавление серии «Документы в вебе» · Урок 5 из 10.

Теперь «Лист» меняет страницы, масштаб и угол. Однако весь PDF остаётся растровым изображением на canvas. Добавим слой символов для выделения текста и отдельное текстовое представление текущей страницы. Новый результат важен не только для удобства копирования: он показывает, почему доступность документа требует большего, чем правильно нарисованные пиксели.

В этом шаге используем TextLayer из той же закреплённой версии PDF.js, что и основной модуль. В HTML внутри frame появляется контейнер с классом textLayer, а рядом — раскрываемая область «Текст текущей страницы». Они получают данные из одной выбранной страницы, но выполняют разные задачи.

Символы находятся поверх изображения

Canvas рисует внешность страницы. Текстовый слой размещает DOM-элементы в координатах, соответствующих тексту PDF. Символы обычно прозрачны, поэтому человек продолжает видеть изображение под ними. При выделении браузер работает с текстовыми элементами, а не пытается распознать буквы по пикселям. Если удалить изображение, сам слой не обязан воспроизводить все особенности оформления документа.

Координаты слоя должны соответствовать viewport, который использован для canvas. Создание текстового слоя с масштабом единица поверх изображения в полтора раза больше приведёт к расхождению областей выделения. Аналогичная ошибка появится, если забыть поворот. Поэтому текст и изображение проходят через одну цель очереди: номер, масштаб и угол фиксируются до начала работы.

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

Стиль входит в договор слоя

Для TextLayer нужны соответствующие стили закреплённого пакета. Файл vendor.mjs уже копирует каталог web, поэтому теперь подключаем vendor/web/pdf_viewer.css. Это часть реализации библиотеки, а не собственная глобальная тема ProfessorWeb. Наш учебный просмотрщик остаётся отдельным примером; его стили не добавляются в общий хедер и футер сайта.

Современный слой использует CSS-переменные масштаба. Перед его созданием указываем масштаб viewport, пользовательскую единицу страницы и их произведение. userUnit относится к геометрии PDF и не совпадает с DPR экрана. Исходник текстового слоя и его стили позволяют увидеть, почему библиотечные правила нельзя заменить только position: absolute.

Ниже участок метода Viewer.request после завершения графической отрисовки. Остальные проверки очереди сохраняются в полном viewer.js:

const layer = this.frame.querySelector('.textLayer');
layer.replaceChildren();
this.frame.style.setProperty('--scale-factor', viewport.scale);
this.frame.style.setProperty('--user-unit', viewport.userUnit);
this.frame.style.setProperty('--total-scale-factor', viewport.scale * viewport.userUnit);
const content = await page.getTextContent();
if (ticket !== this.ticket) return;
this.activeText = new TextLayer({
  container: layer,
  viewport,
  textContentSource: content
});
await this.activeText.render();
this.activeText = null;
if (ticket !== this.ticket) return;
document.querySelector('#plain-text').textContent = content.items
  .filter(item => typeof item.str === 'string')
  .map(item => item.str + (item.hasEOL ? '\n' : ' ')).join('');

Ожидаем render текстового слоя так же, как ожидали изображение. В очереди есть ещё две асинхронные границы: извлечение текста и построение элементов слоя. После каждой существенной границы проверяем, актуальна ли цель. Существующий cancel теперь действительно отменяет activeText. Старый результат не должен устанавливать статус или номер показанной страницы после выбора новой.

Отдельная область чтения

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

HTML этой области прост:

<details>
  <summary>Текст текущей страницы</summary>
  <div id="plain-text"></div>
</details>

Задаём для блока white-space: pre-wrap, чтобы сохранять обнаруженные переводы строк, и заполняем его через textContent. Содержимое PDF не трактуется как HTML нашего интерфейса. Если извлечённая строка содержит угловые скобки или похожий на тег текст, он должен остаться строкой. Вставка через innerHTML добавила бы совершенно другой договор доверия, не нужный для чтения документа.

Объединение items через пробелы и hasEOL является упрощением лаборатории. Оно не восстанавливает структуру заголовков, списков и таблиц. В частности, несколько фрагментов одной строки могут объединиться иначе, чем человек ожидает, а последовательность колонок может оказаться неудобной. В тексте интерфейса называем это извлечённым текстом текущей страницы, чтобы не обещать точную HTML-копию оригинала.

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

Что делать со сканированной страницей

Если в PDF находится только изображение, getTextContent может не дать полезных строк. Появление текстового слоя не запускает OCR автоматически. Изображение страницы останется доступным визуально, но поиск и отдельная текстовая область не получат содержание. Для сканов нужно либо заранее распознавать документы в отдельном процессе, либо предоставлять подготовленную текстовую альтернативу.

Такое ограничение связано с данными, а не только с библиотекой. Даже мощный просмотрщик не может извлечь отсутствующую семантическую структуру простым включением CSS. Аналогично цифровая подпись или форма относятся к другим возможностям PDF, которые наш код не реализует. Разделение результатов помогает обсуждать реальные требования продукта вместо общего обещания «поддерживает PDF».

Ожидаемый результат на собственном двухстраничном образце — возможность выделить английские строки над canvas и прочитать их в раскрываемом блоке. После изменения масштаба слой должен пересоздаться для нового viewport. После смены страницы обычный текст должен соответствовать показанной странице. Это условия для самостоятельного просмотра; сам учебный пример при подготовке не запускался.

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

Полные файлы этого шага находятся в папке lesson-05 архива «Лист». Учебный просмотрщик, библиотека и генератор PDF при подготовке не запускались; результаты выше ожидаются из разбора кода.