Печатное представление документа
Оглавление серии «Документы в вебе» · Урок 9 из 10.
В «Листе» есть изображение документа, извлечённый текст и отдельные заметки. Подготовим печатное представление текста вместе с заметками. Результат этого урока — обычный HTML-документ, который браузер может отправить в диалог печати. Он будет иметь собственную структуру и стили; точное внешнее оформление исходных страниц PDF в этом представлении не сохраняется.
Такой результат полезен для рабочего конспекта. Читатель получает последовательность страниц с обнаруженным текстом и комментариями, а элементы управления просмотрщиком не занимают место на бумаге. Одновременно важно обозначить границу: список текста и заметок не является изменённым подписанным PDF или копией всех иллюстраций оригинала.
Выберем содержимое печатного документа
В предыдущих уроках уже появился ограниченный индекс текста по страницам. Используем его как исходные данные печатного представления. К каждой записи индекса присоединим заметки с тем же номером страницы. Таким образом, номер связывает два набора, но они остаются разными сущностями. Исходный текст взят из PDF, комментарий принадлежит читателю.
Для каждой страницы создаём отдельную секцию. В её заголовке явно пишем «извлечённый текст». Если строк нет, вставляем пояснение об отсутствии текстового слоя. Это лучше пустого места, которое можно ошибочно принять за отсутствие содержания. Сканы и сложные графические страницы могут быть важными, даже когда выбранный способ извлечения не даёт слов.
Полный модуль print.js строит элементы DOM:
export function fillPrintSheet(index, notes, target) {
target.replaceChildren();
for (const entry of index) {
const section = document.createElement('section');
const heading = document.createElement('h2');
heading.textContent = `Страница ${entry.page}: извлечённый текст`;
const text = document.createElement('p');
text.textContent = entry.text || 'На странице не найден текстовый слой';
section.append(heading, text);
for (const note of notes.filter(item => item.page === entry.page)) {
const paragraph = document.createElement('p');
paragraph.textContent = `Заметка: ${note.text}`;
section.append(paragraph);
}
target.append(section);
}
}
Перед заполнением очищаем старое печатное представление. Иначе второй вызов накопил бы повторные секции и заметки. Данные добавляются через textContent; даже если собственная заметка содержит HTML-подобную строку, она печатается как текст. Мы не создаём отдельное окно с собранной строкой HTML и не подставляем в него содержимое документа без проверки.
В этом шаге заметки не имеют координат прямоугольника или ссылок на конкретный символ. Поэтому они идут после текста своей страницы. Для заметок на полях понадобится другой договор привязки и более сложная вёрстка. Номер страницы достаточен для первого конспекта, но не позволяет вычислить, рядом с каким абзацем следует поставить комментарий. Не нужно выводить такую связь из положения заметки в массиве.
Печать использует другую таблицу стилей
Экранный интерфейс предназначен для управления просмотрщиком. На печати нужны последовательное чтение, поля листа и переносы. Добавим правила @media print, которые скрывают кнопки, canvas, поиск и формы, оставляя только print-sheet. Заголовок самого приложения также скрываем: печатное представление содержит свои смысловые заголовки.
@media print {
body {
margin: 0;
max-width: none;
color: black;
background: white;
font: 12pt/1.45 serif;
}
main > :not(#print-sheet), header { display: none !important; }
#print-sheet { display: block; }
#print-sheet section { break-before: page; }
#print-sheet section:first-child { break-before: auto; }
#print-sheet p { white-space: pre-wrap; overflow-wrap: anywhere; }
}
@page { size: A4; margin: 18mm; }
На экране print-sheet скрыт обычным правилом. Во время печати он появляется, а остальные прямые дети main скрываются. Селектор относится к нашему небольшому каркасу. Если позднее обернуть форму и печатную секцию общим контейнером, старое правило нужно пересмотреть: CSS-селектор не знает замысел автора и действует по фактической структуре DOM.
Перед каждой секцией просим разрыв страницы. Первая секция получает исключение, чтобы не создавать пустой лист в начале. Это не означает, что одному номеру PDF всегда соответствует ровно один бумажный лист. Длинный извлечённый текст и заметки могут занять несколько листов. Мы сохранили логическое начало страницы, но позволили браузеру переносить длинное содержимое внутри секции.
@page задаёт предпочитаемый размер A4 и поля. Пользовательский диалог, драйвер принтера и настройки браузера могут повлиять на окончательный результат. Нельзя обещать одинаковую пагинацию на всех устройствах без отдельного просмотра. Документация печатных стилей помогает разделить экранные правила и правила печати, но не заменяет оценку конкретного документа в выбранной среде.
Подготовим данные до открытия диалога
Кнопка получает действующий PDF и проверяет наличие заметок. Затем ожидает индекс через общую функцию getIndex. Если индекс уже построен поиском, используем готовый массив; если нет, присоединяемся к его построению. Не запускаем второй независимый обход страниц только потому, что появился другой способ использовать текст.
const pdf = viewer.pdf;
if (!pdf || !notes) return;
const current = await getIndex(pdf);
if (!current || viewer.pdf !== pdf) return;
index = current;
fillPrintSheet(index, notes.list(), document.querySelector('#print-sheet'));
window.print();
Этот фрагмент расположен в обработчике кнопки; полный main.js также обрабатывает отказ. После ожидания снова проверяем документ. При смене файла старый индекс не должен подготовить печать для нового интерфейса. Снимок заметок получаем после актуального индекса, поэтому в конспект входят записи действующего набора на момент формирования DOM.
Вызов window.print открывает механизм печати браузера. Он не выдаёт приложению путь к готовому файлу и не доказывает, что пользователь действительно распечатал документ. Пользователь может отменить диалог или выбрать сохранение в PDF, если среда предлагает такой вариант. Поэтому статус «файл успешно сохранён» после простого вызова метода был бы неверным. Описание метода print относится к открытию печатного диалога и его поведению.
Почему не печатаем один canvas
Текущий canvas представляет одну показанную страницу. Если просто скрыть кнопки и вызвать печать, в результат попадёт только она, а заметки могут остаться в другом месте интерфейса. Рендер всех страниц на отдельных canvas решает другую задачу, но способен выделить много памяти и требует управления окончанием всех работ. В нашей лаборатории выбираем текстовый конспект, который использует уже ограниченный индекс.
Такой подход имеет собственную цену. Иллюстрации, геометрия таблиц и оформление исходных листов не переносятся в конспект. Для юридически значимого документа или точной копии отчёта нужно отдельно предложить исходный PDF и подходящий процесс печати. Конспект полезен именно как производное представление для чтения и собственных записей; он не заменяет оригинал во всех сценариях.
На собственном двухстраничном образце ожидаются две логические секции с английским текстом и заметками к соответствующим номерам. Кнопки поиска, изменения масштаба и импорта не должны попадать в печатную версию. Длинная заметка должна переноситься, а не обрезаться по ширине. Эти условия предназначены для самостоятельного просмотра печати; при подготовке урока диалог печати и генерация результата не запускались.
В последнем уроке завершим границы просмотрщика: ограничим входные байты, количество страниц и поверхность рендера, добавим открытие локального файла и корректное завершение работы. Это позволит обсуждать не только удобный результат, но и стоимость обработки документа, который ещё не был известен приложению заранее.
Полные файлы этого шага находятся в папке lesson-09 архива «Лист». Учебный просмотрщик, библиотека и генератор PDF при подготовке не запускались; результаты выше ожидаются из разбора кода.