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

Масштаб и плотность пикселей

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

В просмотрщике появилась последовательная смена страниц. Теперь добавим масштаб и поворот, сохранив одну очередь отрисовки. Одновременно разберём, почему размер canvas в пикселях не всегда совпадает с его размером на странице. Результат урока — управляемое увеличение с отдельной настройкой плотности поверхности и согласованной геометрией документа.

Пользователь выбирает масштаб из небольшого списка: 75, 100, 125 и 150 процентов. Поворот меняется на четверть оборота. Такой ограниченный договор проще свободного ввода любых чисел. Он позволяет сначала понять преобразования и граничные случаи, а затем при необходимости добавить подбор масштаба по ширине контейнера.

CSS-размер и внутренняя поверхность

У canvas есть размер внутренней поверхности, задаваемый атрибутами width и height, и размер отображения, задаваемый CSS. Если внутренняя поверхность имеет ширину 600 пикселей, а CSS растягивает её до 1200, браузер увеличивает уже готовое изображение. Новых деталей документа от такого растяжения не появляется. Для качественного увеличения PDF нужно вычислить новый viewport и повторить отрисовку.

Плотность экрана добавляет ещё один коэффициент. На устройстве с devicePixelRatio равным двум один CSS-пиксель обычно соответствует большему числу физических пикселей поверхности. Можно сохранить CSS-ширину 600 и выделить внутреннюю ширину 1200, чтобы библиотека построила более подробное изображение того же видимого размера. Это отличается от увеличения страницы, при котором меняется и видимый размер.

В «Листе» ограничим эффективный DPR двумя. Это авторская политика лаборатории: она уменьшает возможность случайно выделить очень большую поверхность на устройстве с высокой плотностью. Она не является универсальным оптимальным значением для всех продуктов. В будущем лимит нужно согласовать с размером документа, памятью устройства и требуемым качеством. На последнем шаге добавим ещё и явное ограничение числа пикселей.

Фрагмент ниже заменяет участок настройки поверхности в методе Viewer.request. Полный метод сохраняет проверки очереди и завершения из предыдущего урока:

const viewport = page.getViewport({
  scale: target.scale,
  rotation: target.rotation
});
const dpr = Math.min(window.devicePixelRatio || 1, 2);
this.canvas.width = Math.ceil(viewport.width * dpr);
this.canvas.height = Math.ceil(viewport.height * dpr);
this.canvas.style.width = `${viewport.width}px`;
this.canvas.style.height = `${viewport.height}px`;
this.frame.style.width = `${viewport.width}px`;
this.frame.style.height = `${viewport.height}px`;
this.activeRender = page.render({
  canvas: this.canvas,
  viewport,
  transform: dpr === 1 ? null : [dpr, 0, 0, dpr, 0, 0]
});
await this.activeRender.promise;

Параметр transform учитывает плотность внутренней поверхности. При этом viewport уже учитывает выбранный масштаб и угол. Если умножить его масштаб на DPR и одновременно передать такое же преобразование, коэффициент будет применён дважды. Внешне результат может напоминать неправильное увеличение или обрезанную страницу. Разделение коэффициентов помогает объяснить ошибку без случайного подбора чисел.

Поворот изменяет геометрию

При повороте на 90 градусов ширина и высота обычного листа меняются местами. Не нужно вручную переставлять значения после получения viewport. PageViewport в закреплённой версии PDF.js вычисляет соответствующее преобразование. Внутренние координаты PDF и координаты canvas имеют разные направления осей; самостоятельное частичное исправление легко нарушит уже рассчитанную матрицу.

Храним угол в одном из четырёх значений: 0, 90, 180 или 270. Следующее нажатие вычисляет (rotation + 90) % 360. Получается замкнутый цикл, в котором после полного оборота возвращается исходное положение. Номер страницы сохраняется. Поворот является состоянием просмотра, а не изменением исходного PDF: если открыть файл другой программой, этот выбор интерфейса не обязан сохраниться.

Масштаб тоже остаётся состоянием интерфейса. Значение 1,25 означает выбранное увеличение относительно viewport, а не процент от ширины текущего окна. Поэтому надпись «125%» полезно трактовать последовательно. Если позднее добавить режим «по ширине», он станет другой политикой вычисления масштаба и должен быть обозначен отдельно, иначе одинаковая подпись будет означать разные размеры на разных экранах.

Одна цель объединяет изменения

Метод request получает объект с частично заданными параметрами. При смене масштаба берём текущую страницу и угол; при переходе к другой странице берём действующий масштаб. Затем проверяем все параметры и фиксируем цель целиком. Очередь из прошлого урока уже умеет пропускать устаревшие цели, поэтому быстрое увеличение и последующий поворот не запускают независимые рендеры одного canvas.

Обработчики из main.js передают только изменённое свойство:

document.querySelector('#scale').addEventListener('change', event => {
  viewer.request({ scale: Number(event.target.value) });
});
document.querySelector('#rotate').addEventListener('click', () => {
  viewer.request({ rotation: (viewer.rotation + 90) % 360 });
});

Проверка в Viewer остаётся обязательной, хотя значения списка заданы нами. Значение из DOM является строкой, поэтому его явно преобразуют в число. Допустимый набор затем проверяется через includes. Случайная строка, бесконечность или отрицательное число не должны попасть в вычисление размеров поверхности. Интерфейсный элемент помогает правильному вводу, но не отменяет договор метода.

Не изменяйте только transform: scale(...) у элемента canvas, сохраняя прежние размеры контейнера. Такой CSS-приём может увеличить изображение визуально, но кнопки, прокрутка и будущий текстовый слой получат другую геометрию. В нашем варианте canvas и frame получают размеры одного viewport. Когда появится текстовый слой, он сможет использовать ту же модель координат.

При изменении размеров окна выбранный процент пока остаётся прежним. Это сознательно простая модель, которую можно объяснить читателю без скрытых пересчётов. Если интерфейс позже получит режим подбора по ширине, полезно хранить отдельно имя режима и рассчитанное числовое значение. Тогда ручной выбор 125 процентов выключит автоматический подбор, а изменение контейнера обновит только автоматический режим. Иначе обработчик изменения окна способен неожиданно отменить только что сделанный выбор пользователя.

Где проходит граница качества

После самостоятельного запуска ожидается изменение видимого размера страницы при выборе масштаба. Поворот должен сохранять всю страницу, меняя ориентацию. На экране с высокой плотностью изображение может использовать более крупную внутреннюю поверхность при прежнем CSS-размере. Здесь нет опубликованных замеров резкости или скорости: описываем механизм и ожидаемое поведение, а не результат выполненного сравнения устройств.

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

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

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