PDF.js: просмотр документов в интерфейсе — мастерская и 7 сценариев
Счета, отчёты, инструкции и схемы внутри сервиса. Семь сценариев PDF.js, поиск по тексту, локальные файлы, PNG, PHP-пример и готовые исходники для Windows и Linux.

Клиент открывает счёт в личном кабинете, сотрудник проверяет отчёт в CRM, инженер смотрит схему рядом с заявкой. Если каждый документ приходится сначала скачивать и искать в папке загрузок, человек теряет контекст. Встроенный просмотр помогает оставить документ рядом с задачей: перелистать страницы, увеличить нужное место, найти фразу и при необходимости сохранить оригинал.
PDF.js позволяет построить такой интерфейс на JavaScript. В этой статье — семь живых сценариев, пять собственных учебных PDF на двенадцати страницах, поиск, масштаб, поворот, локальное открытие файла и экспорт страницы в PNG. Разберём код, ошибки, ограничения мобильного браузера, выдачу PDF через PHP и размещение готового просмотрщика на Linux VPS. Исходники используют PDF.js 6.5.438 и Bootstrap 5.3.8.
Что такое PDF.js и какой результат получает пользователь
PDF.js читает PDF и предоставляет программный интерфейс для работы с документом и его страницами. В нашей мастерской библиотека рисует выбранную страницу на canvas, а обычный HTML отвечает за кнопки, список страниц, поиск и сообщения. Пользователь остаётся внутри приложения и управляет документом понятными действиями, не меняя привычный рабочий контекст.
Для разработчика важна возможность связать просмотр с задачей сервиса. Можно открыть конкретный договор из карточки клиента, перейти на нужную страницу инструкции или показать счёт перед оплатой. При этом PDF.js не принимает решение, кому разрешён доступ к документу: авторизация и правила бизнеса остаются на серверной стороне приложения.
Наша реализация — небольшой самостоятельный просмотрщик, который легко прочитать и адаптировать. Он показывает страницы и извлечённый текст, но не превращает PDF в редактируемый документ. Если задача требует сложных форм, аннотаций, структуры доступности или полноценной панели профессионального viewer, эти функции нужно планировать отдельно и выбирать соответствующий уровень интеграции.
Официальная документация: слои PDF.js и начало работы.
Где применять: кабинет, CRM, база знаний и документооборот
В личном кабинете клиенту нужно быстро проверить сумму, период и состав работ. Встроенный счёт позволяет сделать это до скачивания: рядом остаются статус оплаты, контакт менеджера и действия по заявке. Для такого сценария полезны простая навигация, ясное название файла и заметная кнопка оригинала.
В CRM сотрудник может открыть отчёт или вложение, не теряя карточку обращения. Для длинных документов важнее переход к странице и поиск, для схем — увеличение и поворот, для сканированных актов — честное сообщение о том, что текст не распознан. Один интерфейс может поддерживать эти задачи, если объясняет человеку состояние документа и доступные действия.
В базе знаний просмотрщик удобен для инструкций, которые уже выпускаются в PDF и должны сохранять исходную верстку. Однако он не заменяет обычную HTML-статью, когда материал можно сделать адаптивным и хорошо структурированным. Выбирайте PDF там, где важна форма документа, а HTML — там, где важнее чтение на разных экранах, ссылки и доступная семантика.
Встроенный браузерный просмотр или собственный интерфейс
Самый быстрый вариант — дать ссылку на PDF либо встроить его штатным средством браузера. Для простого публичного файла этого бывает достаточно. Собственный интерфейс появляется там, где нужны одинаковые элементы управления, связь с состоянием приложения и предусмотренные сценарии ошибок, а не зависимость от того, как конкретный браузер оформил свою панель.
С PDF.js вы управляете окружающим интерфейсом: показываете имя и статус документа, выбираете стартовую страницу, объясняете ограничение размера, открываете подходящий раздел поиска. Пользователь видит знакомые кнопки сервиса. Цена этой гибкости — ответственность за загрузку, отмену операций, мобильную геометрию, доступность и корректную реакцию на неподдерживаемый файл.
Не стоит начинать с копирования всех функций большого viewer. Сначала определите рабочее действие человека: проверить счёт, прочитать инструкцию, рассмотреть схему. Затем добавьте только необходимые органы управления и проверьте полный путь от открытия до результата. Семь сценариев мастерской показывают, как один компактный набор функций покрывает несколько реальных ситуаций.
Мини-мастерская: семь сценариев, которые можно попробовать
Начните со счёта на двух страницах: перейдите вперёд и сопоставьте итог с составом работ. Затем откройте пятистраничный отчёт и выберите страницу номером. В инструкции найдите фразу «резервная копия»: результаты приведут к страницам, а совпадения будут выделены в текстовой расшифровке под просмотром.
Схема демонстрирует увеличение и поворот, скан — границу поиска без OCR. Шестой сценарий позволяет открыть собственный PDF локально в этой вкладке, без отправки на сервер. Седьмой намеренно запрашивает отсутствующий файл, получает настоящий HTTP 404 и предлагает вернуться к исправному документу без перезагрузки всей страницы.
Можно скачать текущий оригинал, PNG показанной страницы и минимальный JavaScript-рецепт с выбранными параметрами. Все встроенные документы вымышленные и созданы для статьи: клиентских счетов и личных данных в них нет. Проект содержит готовые PDF и генератор, поэтому для первого запуска не нужно устанавливать инструменты создания документов или искать подходящие примеры в Интернете.
На телефоне для схем и длинных документов удобнее открыть мастерскую отдельно. Встроенная область прокручивается внутри статьи.
Подключение: библиотека, worker и ресурсы одной версии
В архиве зависимости закреплены в package-lock.json. Команда сборки копирует PDF.js, worker, вспомогательные ресурсы и Bootstrap в public/vendor. Во время просмотра внешние CDN не нужны: приложение загружает файлы со своего сайта. Это делает состав выпуска понятным и позволяет проверить его до переноса на сервер.
Главный модуль и worker должны принадлежать одной версии. Worker выполняет значительную часть разбора документа отдельно от интерфейса, а приложение общается с ним через API. Если после обновления браузер получил новый модуль и старый worker из кэша, может возникнуть несовпадение версий. Обновляйте эти файлы согласованно и используйте подходящую политику кэширования.
Не ограничивайтесь двумя файлами, если ваш набор PDF требует дополнительных ресурсов. В проекте настроены пути к CMap, стандартным шрифтам, WASM и ICC. Проверяйте их доступность и типы ответа при размещении. Ошибка пути к вспомогательному файлу иногда проявляется только на определённом документе, хотя простой тестовый счёт уже открылся.
import { getDocument, GlobalWorkerOptions }
from './vendor/pdfjs/build/pdf.js';
GlobalWorkerOptions.workerSrc = new URL(
'./vendor/pdfjs/build/pdf.worker.js', import.meta.url
).href;
Первый рендер: получить документ, страницу и viewport
Работа начинается с getDocument. Метод возвращает задачу загрузки; после ожидания её promise получается объект документа. Затем можно запросить страницу по номеру и построить viewport, который описывает её размеры с учётом масштаба и поворота. Номер первой страницы в этом API — один, что удобно помнить при связи с массивами JavaScript.
Рендеринг тоже асинхронный. Сначала создаётся задача page.render, затем ожидается её завершение. Это важно не только для сообщения «готово», но и для скачивания PNG: пользователь должен получить полностью нарисованную страницу, а не промежуточный холст. В полном проекте кнопка экспорта привязана к состоянию готовности.
Код ниже показывает базовую последовательность. Он намеренно короткий: настоящая мастерская дополнительно учитывает HiDPI, бюджет пикселей, отмену предыдущего рисования и ошибки. Не воспринимайте три строки render как законченный просмотрщик для произвольных пользовательских документов. Интерфейс должен сопровождать весь жизненный цикл операции, включая неудачную загрузку и переключение файла.
const loading = getDocument({
url: './docs/invoice.pdf',
cMapUrl: './vendor/pdfjs/cmaps/', cMapPacked: true,
standardFontDataUrl: './vendor/pdfjs/standard_fonts/',
wasmUrl: './vendor/pdfjs/wasm/',
iccUrl: './vendor/pdfjs/iccs/',
isEvalSupported: false
});
const pdf = await loading.promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 });
const canvas = document.querySelector('canvas');
canvas.width = Math.ceil(viewport.width);
canvas.height = Math.ceil(viewport.height);
await page.render({
canvasContext: canvas.getContext('2d'), viewport
}).promise;
Официальная документация: последовательность загрузки и отображения.
Сценарий 1: счёт в личном кабинете
В счёте важнее не количество функций, а уверенность, что клиент смотрит нужный документ. Покажите название, число страниц и доступный оригинал. Если рядом есть действие оплаты, сумма и статус должны приходить из вашей бизнес-системы; не стоит автоматически считать произвольный извлечённый текст надёжным источником финансовых данных.
В учебном счёте две страницы. Первая даёт общий контекст, вторая позволяет проверить детали работ. Кнопки «Назад» и «Вперёд» отключаются на границах, а подпись показывает текущую страницу. Человеку не приходится гадать, произошло ли переключение и есть ли продолжение документа за видимым листом.
Такой просмотр можно встроить в модальное окно, боковую панель или отдельный раздел кабинета. Для телефона обычно удобнее широкая отдельная область, чем узкое модальное окно с несколькими вложенными прокрутками. Сохраните путь назад к задаче и не прячьте скачивание: клиенту может понадобиться оригинал для бухгалтерии, печати или собственного архива.
Сценарий 2: многостраничный отчёт и точная навигация
У длинного отчёта меняется поведение пользователя. Он может знать нужный номер страницы, переходить между разделами или возвращаться к таблице после чтения выводов. Поэтому одних стрелок недостаточно: в мастерской есть прямой выбор номера, а номер и общее количество остаются видимыми после каждого перехода.
Здесь одновременно рисуется одна страница. Для учебного просмотрщика это понятный компромисс по памяти и сложности. Если вашему продукту нужна непрерывная лента с десятками страниц, потребуется управление видимыми областями, освобождение старых холстов и предварительная загрузка рядом с текущей позицией. Простое создание большого canvas на каждую страницу быстро увеличивает расход памяти.
Пятистраничный пример специально достаточно длинный, чтобы пройти навигацию, но не заставляет ждать тяжёлую загрузку. В реальном отчёте проверьте таблицы, мелкий текст, разные размеры листов и длинные названия. Возвращение к документу после закрытия панели также может требовать сохранения номера страницы в состоянии приложения, привязанном к ID и версии файла.
Сценарий 3: поиск по инструкции и извлечение текста
Поиск начинается с получения текстового содержимого страниц. Наш проект собирает строки из getTextContent, запоминает их и ищет введённую фразу. Результат — список страниц, на которых есть совпадение. После перехода нужные слова выделяются элементами mark в отдельной текстовой области под холстом.
Это простой поиск подстроки, а не полнотекстовый движок с морфологией, ранжированием и пониманием смысла. Порядок извлечённых фрагментов может отличаться от визуальной вёрстки, особенно в колонках, таблицах и необычно сформированных PDF. Фраза, которая выглядит цельной на листе, иногда оказывается разбитой в данных, поэтому поведение нужно проверять на своих документах.
В инструкции попробуйте запрос «резервная копия» и перейдите по найденным страницам. Мы честно показываем место выделения: оно находится в расшифровке, а не поверх картинки. Для выделения прямо на странице потребуется text layer с согласованной геометрией, а для большого архива документов может быть полезна отдельная серверная индексация.
const page = await pdf.getPage(1);
const content = await page.getTextContent();
const text = content.items
.filter(item => typeof item.str === 'string')
.map(item => item.str + (item.hasEOL ? '\n' : ' '))
.join('').trim();
// Выводим как текст, не как HTML из документа.
document.querySelector('#pageText').textContent = text;
Сценарий 4: схемы, масштаб и поворот
Альбомный документ часто плохо помещается в колонку статьи или на экран телефона. Режим «по ширине» подбирает масштаб по доступному месту. Для чтения мелких обозначений можно выбрать увеличение, после чего широкая страница прокручивается внутри просмотрщика. Весь сайт при этом не должен становиться шире экрана.
Поворот меняет viewport и запускает новый рендер. В мастерской каждый щелчок добавляет девяносто градусов, затем цикл возвращается к исходному положению. Это помогает рассмотреть страницу, но не переписывает оригинальный PDF. Если скачать текущий документ, пользователь получит исходный файл; PNG отражает показанный вариант с текущим поворотом.
Для инженерной схемы важно сохранить возможность приблизить фрагмент, однако бесконечное увеличение растрового холста не бесплатно. Наш интерфейс ограничивает число физических пикселей, поэтому на больших масштабах чёткость имеет предел. Для серьёзного технического просмотрщика стоит отдельно проверить реальные планы, поведение мыши и касаний, удобство панорамирования и требования к печати.
const rotation = 90;
const viewport = page.getViewport({
scale: 1.5, rotation
});
Сценарий 5: скан — изображение есть, текста нет
PDF может состоять из растровой картинки страницы. Человек видит буквы, но внутри документа нет текстовых объектов, которые можно получить через getTextContent. В нашем учебном скане именно такая структура: изображение отображается, а извлечённый текст пуст. Поиск сообщает об отсутствии совпадений и объясняет возможную причину.
PDF.js не выполняет OCR автоматически. Если сервису нужно искать по сканированным актам или распознавать реквизиты, добавляется отдельный этап распознавания. Его результат нужно проверять: ошибки в цифрах, фамилиях и таблицах могут иметь практические последствия. Затем текст можно хранить для поиска или формировать документ с корректным текстовым слоем.
Показывать пользователю «ничего не найдено» без объяснения здесь неудобно. Лучше отличать отсутствие извлекаемого текста от обычного неудачного запроса и предлагать доступные действия: увеличить изображение, скачать оригинал, запустить предусмотренное распознавание. В мастерской последнего действия нет, поэтому интерфейс не обещает функцию, которая фактически не реализована.
Сценарий 6: собственный PDF без загрузки на сервер
В локальном сценарии File API читает выбранный файл прямо в браузере. Содержимое превращается в Uint8Array и передаётся PDF.js. Отдельного запроса загрузки этого документа на сервер нет. Это удобно для небольшого инструмента предварительного просмотра, когда человеку нужно быстро открыть файл и проверить страницы до дальнейших действий.
Ограничения мастерской — непустой PDF до 12 МиБ и не более восьмидесяти страниц. Проверяются расширение, размер, наличие характерного заголовка и результат разбора библиотекой. Эти проверки помогают дать понятное сообщение, но не являются полноценной экспертизой доверия к неизвестному файлу. PDF.js нужно поддерживать в актуальном состоянии, как любую библиотеку обработки сложного формата.
Документы с паролем здесь не открываются: вместо диалога ввода пользователь получает объяснение. После переключения документа старый Blob URL освобождается, а результаты прежней загрузки не должны применяться к новому файлу. На слабом устройстве даже разрешённый по размеру PDF может оказаться тяжёлым из-за изображений и внутренней структуры, поэтому лимит байтов не равен гарантии быстрого просмотра.
const file = input.files[0];
// Сначала проверки размера и имени по правилам приложения.
const bytes = new Uint8Array(await file.arrayBuffer());
const task = getDocument({ data: bytes, isEvalSupported: false });
const pdf = await task.promise;
// Полный проект также настраивает worker, ресурсы и лимит страниц.
Сценарий 7: настоящий 404 и восстановление интерфейса
Ошибка загрузки — обычное состояние сервиса: документ удалён, ссылка устарела, срок доступа истёк или сеть временно недоступна. Просмотрщик должен ясно сообщить, что произошло, остановить недоступные действия и предложить понятный следующий шаг. Бесконечный индикатор загрузки оставляет человека без решения.
В демонстрации запрашивается отсутствующий missing-demo.pdf. Сервер отвечает HTTP 404, сообщение попадает в интерфейс, а кнопка открывает исправный счёт. Это реальный неуспешный запрос, а не заранее нарисованная надпись. После восстановления снова работают страницы, масштаб, поиск и скачивание.
В рабочем приложении полезно различать технические и пользовательские причины, не раскрывая лишние сведения о чужих документах. Например, сообщение может предлагать обновить карточку или обратиться к ответственному сотруднику. Не показывайте стек исключения и внутренние пути сервера посетителю. Технические подробности сохраняйте в журнале с идентификатором события, который помогает поддержке найти проблему.
Мобильный экран: размер CSS и физические пиксели canvas
У canvas есть две геометрии: размер на странице в CSS-пикселях и число физических пикселей растрового буфера. На экране с высокой плотностью эти величины отличаются. Если учитывать только CSS, текст может выглядеть размытым; если без ограничений умножать всё на devicePixelRatio, память быстро растёт при увеличении страницы.
В проекте коэффициент ограничивается DPR до двух, стороной холста до 4096 и площадью около восьми миллионов пикселей. Это правила конкретной мастерской, а не пределы PDF.js. При большом логическом размере физический буфер может быть меньше, поэтому интерфейс сохраняет управляемый расход памяти ценой предельной чёткости такого увеличения.
Режим по ширине пересчитывается после изменения окна. Панель управления переносится, а широкий лист прокручивается внутри своей области. Проверять нужно не только стартовый вид: откройте схему, увеличьте масштаб, поверните страницу, выполните поиск и вернитесь к счёту. Именно последовательности действий обычно выявляют лишнюю горизонтальную прокрутку и неудобные кнопки на телефоне.
const factor = Math.min(
2, devicePixelRatio || 1,
4096 / viewport.width,
4096 / viewport.height,
Math.sqrt(8_000_000 / (viewport.width * viewport.height))
);
canvas.width = Math.max(1, Math.floor(viewport.width * factor));
canvas.height = Math.max(1, Math.floor(viewport.height * factor));
Быстрые переключения: отмена render и устаревших результатов
Пользователь может быстро нажать несколько страниц или переключить документ до завершения загрузки. Если каждое действие независимо рисует в один canvas, старый результат способен перекрыть новый. Интерфейс покажет выбранную страницу пять, а на экране останется страница два, завершившаяся позже. Это ошибка управления состоянием, а не проблема внешнего вида.
Полный проект отменяет предыдущую задачу рендеринга, дожидается завершения её очереди и проверяет идентификаторы текущей загрузки и рисования. После переключения документа старые асинхронные ответы игнорируются. Такой порядок особенно важен при медленной сети и сложных PDF, где операции завершаются с заметной задержкой.
При собственном развитии просмотрщика сохраняйте этот принцип и для миниатюр, поиска, загрузки метаданных и экспорта. Каждому результату нужен понятный владелец: конкретный документ и состояние интерфейса. Одной переменной loading недостаточно, если одновременно работают разные операции. Проверяйте быстрые повторные действия, а не только аккуратный последовательный сценарий, который выполняет разработчик.
Скачать оригинал и экспортировать страницу в PNG
Скачивание PDF и сохранение PNG решают разные задачи. Оригинал сохраняет структуру многостраничного документа, текст и предусмотренные им особенности. PNG содержит растровое изображение только текущей страницы в том виде, в котором она нарисована: с выбранным поворотом и ограничениями размера холста.
В мастерской PNG создаётся через canvas.toBlob. Это удобно для иллюстрации в задаче, быстрой вставки страницы в презентацию или согласования визуального фрагмента. Такой экспорт не заменяет подписанный исходный документ, не сохраняет текстовый поиск и не делает растровую картинку редактируемым PDF. Название действия должно объяснять пользователю этот результат.
Если нужен серверный конвейер подготовки изображений, просмотрщик может быть лишь первым шагом интерфейса. Дальнейшие преобразования и хранение вариантов проектируются отдельно, например с помощью Sharp. При этом нельзя обещать пользователю, что выбранный в локальном сценарии файл останется на устройстве, если вы добавили последующую загрузку: новый этап должен быть явным и понятным.
canvas.toBlob(blob => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'page-1.png';
link.click();
setTimeout(() => URL.revokeObjectURL(url), 30_000);
}, 'image/png');
Доступность: canvas, текст и ограничения простого viewer
Canvas показывает изображение страницы, но сам по себе не передаёт её текстовую структуру вспомогательным технологиям. Поэтому рядом в мастерской есть отдельная текстовая область, обычные HTML-кнопки, подписанные поля и сообщения состояния. Пользователь может прочитать извлечённые строки и увидеть найденные фрагменты без попытки выделить текст на картинке.
Это полезная альтернатива, но не полная реализация доступного PDF-просмотрщика. Порядок строк может отличаться от визуального, таблица теряет связи заголовков и ячеек, а скан вообще не даёт текста. Для серьёзного кабинета нужно учитывать структуру самого PDF, text layer, клавиатурную навигацию и проверку скринридером на целевых устройствах.
Не объявляйте интерфейс полностью доступным только потому, что у canvas появился aria-label. Такое описание называет область, но не заменяет содержание документа. Там, где можно, предоставляйте важную информацию ещё и обычным HTML: сумму счёта, дату, статус, основные выводы отчёта. Тогда PDF остаётся документом, а выполнение задачи не зависит исключительно от его визуального просмотра.
Выдача PDF через PHP: разрешённый ID вместо произвольного пути
В архиве есть небольшой PHP-endpoint для чтения пяти учебных документов. Он принимает идентификатор из фиксированного списка и сам выбирает соответствующий файл. Пользователь не передаёт путь файловой системы, поэтому запрос не превращается в произвольный readfile по строке из URL. Неизвестный ID возвращает 404, неподдерживаемый HTTP-метод — 405.
Ответ содержит application/pdf, корректную длину и Content-Disposition с безопасным известным именем. Пример отдаёт полный файл; он не реализует Range самостоятельно. Это сознательно небольшой участок серверной логики, который удобно проверить. Для защищённой выдачи крупных документов часто разумнее поручить передачу веб-серверу после проверки прав приложением.
Сам endpoint не содержит базы пользователей и индивидуальной авторизации. В настоящем кабинете перед выдачей проверяют, что текущий пользователь имеет доступ к конкретному документу, а не только знает его ID. Закрытая страница просмотра не защищает публичный URL оригинала. Правила доступа должны действовать на сам файл и на все способы его получения.
$files = ['invoice' => 'invoice.pdf', 'report' => 'report.pdf'];
$id = $_GET['id'] ?? 'invoice';
if (!is_string($id) || !isset($files[$id])) {
http_response_code(404);
exit;
}
// До выдачи в приложении проверяются пользователь и его права.
$path = $trustedDirectory . '/' . $files[$id];
header('Content-Type: application/pdf');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: private, no-store');
readfile($path);
HTTP Range, CORS и загрузка всего документа
В обычной интеграции getDocument может получать URL и использовать возможности частичной загрузки при подходящих условиях сервера. Для этого важны корректные ответы HTTP и поддержка диапазонов. Но один заголовок Accept-Ranges не доказывает, что конкретный просмотрщик действительно скачивает документ частями: нужно смотреть сетевые запросы и выбранный способ загрузки.
В нашей мастерской встроенные небольшие PDF сначала загружаются через fetch целиком, проверяются по размеру и затем передаются как массив байтов. Поэтому она не демонстрирует ленивую загрузку больших документов по Range. Локальный сервер и Caddy умеют отвечать на диапазонный запрос, а в README есть отдельная команда проверки ответа 206 и Content-Range.
Документ с другого домена требует корректного CORS и, если нужны учётные данные, согласованной политики credentials. Не обходите ограничение произвольным публичным прокси, принимающим любой URL. Для кабинета обычно проще и понятнее выдавать документ через собственный проверенный маршрут, где уже есть авторизация, журнал доступа и ограничения на источник файла.
curl -H 'Range: bytes=0-99' -D - -o /tmp/pdf-head.bin \
http://127.0.0.1:8134/docs/invoice.pdf
Официальная документация: параметры загрузки документа и ресурсов.
Windows: запускаем исходники и локальный сервер
Для проекта нужен Node.js 24. В PowerShell откройте папку распакованного архива, выполните npm ci, сборку и тесты, затем запустите сервер. Команда ci использует lock-файл, а сборка копирует согласованные версии зависимостей в public. Исходные учебные PDF уже приложены, поэтому Python для просмотра не требуется.
Сервер слушает только loopback на порту 8134 и выдаёт только каталог public. Это инструмент разработки, который не нужно выставлять в Интернет. Открывать index.html двойным щелчком тоже не следует: ES-модули, worker и загрузка документов должны работать через HTTP или HTTPS, а не через случайные ограничения режима file.
После запуска пройдите все семь сценариев, откройте собственный небольшой PDF и проверьте скачивание страницы. Для PHP-примера нужен отдельный установленный PHP 8.3 или новее; его встроенный сервер запускается только локально. В Windows при проверке заголовков используйте curl.exe, чтобы команда не зависела от алиасов конкретной версии PowerShell.
npm ci
npm run build
npm test
npm start
# Открыть http://127.0.0.1:8134
# Дополнительный PHP-пример в другом терминале:
php -S 127.0.0.1:8137 -t examples/php
curl.exe -I "http://127.0.0.1:8137/document.php?id=invoice"
Linux: локальный запуск и собственные учебные PDF
На Linux используется тот же package-lock.json и последовательность npm ci, build, test, start. Порт можно задать через переменную окружения перед командой. Проверяйте выдачу worker как JavaScript и PDF как application/pdf: ошибочная конфигурация статического сервера часто возвращает HTML-страницу вместо отсутствующего файла, что затем выглядит как непонятная ошибка библиотеки.
Если нужно пересоздать учебные документы, в архиве есть Python-генератор на ReportLab и Pillow и приложенный Noto Sans с лицензией OFL. Он создаёт пять вымышленных PDF: счёт, отчёт, инструкцию, схему и растровый скан. Готовые файлы сопровождаются manifest с SHA256, чтобы можно было проверить состав набора.
Генерация документов и просмотр — разные этапы. В рабочем сервисе PDF может формироваться через PHP, другой backend или поступать из внешней системы, а PDF.js отвечает за отображение уже полученного документа. Если библиотека создания PDF устанавливается через Composer, используйте привычный процесс PHP-зависимостей; самому браузерному PDF.js Composer не нужен.
npm ci
npm run build
npm test
PORT=8134 npm start
# Только для пересоздания учебных документов:
python3 -m pip install reportlab pillow
python3 scripts/fixtures.py
Linux VPS: статика через Caddy, домен и HTTPS
После сборки просмотрщик представляет собой статический каталог public. Для его размещения Node.js на сервере не обязателен: Caddy может выдавать HTML, JavaScript, worker, шрифты и учебные PDF напрямую. В README есть последовательность с отдельной папкой выпуска, символьной ссылкой current и проверкой конфигурации перед перезагрузкой.
Сначала подготовьте домен и DNS, установите Caddy по инструкции для своей системы и обеспечьте доступность необходимых портов. Переносите именно public, а не весь исходный проект вместе с инструментами сборки и тестами. Конфигурацию сайта добавляйте отдельным блоком, сохраняя существующие сайты сервера. Условный pdf.example.com заменяется вашим реальным доменом.
После размещения откройте не только главную страницу, но и worker, PDF, WASM и шрифтовые ресурсы. Проверьте полный сценарий просмотра через HTTPS и журнал Caddy. Если стенд предназначен для ограниченного круга людей, закройте весь сайт Basic Auth, включая оригинальные документы. Это удобная защита небольшого стенда, но индивидуальные права клиентов всё равно требуют логики приложения.
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://pdf.example.com/
curl -I https://pdf.example.com/vendor/pdfjs/build/pdf.worker.js
sudo journalctl -u caddy -n 100 --no-pager
CSP, WASM и обновления библиотеки
Политика Content Security Policy должна соответствовать ресурсам, которые действительно использует просмотрщик. В проекте worker загружается со своего сайта, а некоторые шрифтовые и файловые представления используют Blob URL. Декодерам WASM требуется предусмотренное разрешение. Если скопировать чрезмерно узкую политику без проверки, часть PDF перестанет отображаться, хотя простые страницы ещё будут работать.
В основном интерфейсе задано isEvalSupported:false. Это ограничивает динамическую компиляцию соответствующих функций PDF.js, но не отменяет необходимость отдельно учитывать WebAssembly. Мы не подключаем scripting sandbox полноценного viewer и не запускаем PDF JavaScript через собственные обработчики. Сохраните эти границы при добавлении новых возможностей и сверяйтесь с API выбранной версии.
Обновления PDF.js важны, потому что библиотека разбирает сложный входной формат. Собирайте новый выпуск отдельно, выполняйте тесты и проверяйте реальные документы. Не заменяйте worker и главный модуль по одному в работающем каталоге. Согласованный выпуск с возможностью вернуть предыдущую папку значительно проще диагностировать, чем набор файлов разных дат из браузерного кэша.
Что входит в тесты и чего они не обещают
Тесты проекта действительно открывают и рендерят все двенадцать страниц пяти учебных PDF через PDF.js. Для скана проверяется отсутствие извлечённого текста, для инструкции — наличие ожидаемой фразы. Отдельные проверки покрывают ограничения входа, поиск и расчёт размеров холста. Это подтверждает работу конкретного набора и основных правил приложения.
Такие тесты не доказывают совместимость с каждым PDF, который может прислать пользователь. У документов бывают необычные шрифты, повреждения, пароли, большие изображения и особенности формата. Нужны свой репрезентативный набор, понятные ошибки и проверка целевых браузеров. Запуск тестов в Node.js также не равен проверке физического телефона или скринридера.
Для интеграции в сервис добавьте проверки прав доступа, загрузки через реальный backend, поведения после выхода пользователя и истечения ссылки. Отдельно оцените память на слабом устройстве и многократное переключение документов. Интерфейс должен оставаться управляемым даже тогда, когда файл не удалось прочитать: это такая же часть качества, как красивое отображение нормальной страницы.
Как развить просмотрщик и где взять исходники
Архив содержит HTML5-интерфейс, JavaScript-модули, локальные зависимости, пять PDF, генератор документов, PHP-пример, Caddyfile и тесты. Собственный код доступен под MIT, PDF.js — под Apache-2.0, Bootstrap — под MIT, Noto Sans — под OFL. Уведомления лицензий включены в проект; при распространении сохраняйте их вместе с соответствующими ресурсами.
Следующий шаг зависит от продукта. Для кабинета добавьте проверку пользователя и выбор документа по ID. Для базы знаний — связь найденной страницы с разделом статьи. Для загрузки вложений можно соединить просмотр с FilePond, явно отделив локальное открытие от отправки на сервер. Для размещения полезен подробный материал о Caddy, HTTPS и reverse proxy.
Начните с задачи пользователя и сохраните понятные границы: что сейчас открыто, где идёт поиск, что скачивается и куда передаются данные. Такой просмотрщик полезен именно в составе рабочего сценария. Человеку важно быстро проверить документ и продолжить действие, а библиотека и настройки должны обеспечивать этот результат предсказуемо.


