Статьи

Приключения в Аурелии: создание пользовательского средства просмотра PDF

Эта статья была рецензирована Vildan Softic . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Обработка PDF-файлов в веб-приложении всегда была трудной задачей. Если вам повезет, вашим пользователям нужно всего лишь скачать файл. Однако иногда вашим пользователям нужно больше. В прошлом мне везло, но на этот раз нашим пользователям понадобилось наше приложение для отображения PDF-документа, чтобы они могли сохранять метаданные, связанные с каждой отдельной страницей. Раньше можно было бы сделать это с помощью дорогого плагина PDF, такого как Adobe Reader, работающего внутри браузера. Однако, по прошествии некоторого времени и экспериментов, я нашел лучший способ интеграции средств просмотра PDF в веб-приложение. Сегодня мы рассмотрим, как мы можем упростить обработку PDF, используя Aurelia и PDF.js.

Обзор: цель

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

  1. Мы хотим, чтобы пользователь мог загружать документ, прокручивать и увеличивать и уменьшать его, с приличной производительностью.
  2. Мы хотим иметь возможность двустороннего связывания свойств средства просмотра (таких как текущая страница и текущий уровень масштабирования) со свойствами в нашем приложении.
  3. Мы хотим, чтобы этот просмотрщик был повторно используемым компонентом; мы хотим, чтобы в наше приложение одновременно можно было добавлять несколько зрителей без каких-либо конфликтов и небольших усилий.

Вы можете найти код для этого руководства в нашем репозитории GitHub , а также демонстрацию готового кода здесь .

Представляем PDF.js

PDF.js — это библиотека JavaScript, написанная Фондом Mozilla . Он загружает документы PDF, анализирует файл и связанные с ним метаданные и отображает выходные данные страницы на узле DOM (обычно это элемент <canvas> ). Средство просмотра по умолчанию, включенное в проект, поддерживает встроенный просмотрщик PDF в Chrome и Firefox и может использоваться как отдельная страница или как ресурс (встроенный в iframe).

Это, правда, довольно круто. Проблема здесь заключается в том, что средство просмотра по умолчанию, хотя и обладает широкими функциональными возможностями, предназначено для работы в качестве отдельной веб-страницы. Это означает, что, хотя он может быть интегрирован в веб-приложение, он, по сути, должен работать внутри песочницы iframe. Средство просмотра по умолчанию предназначено для ввода конфигурации через строку запроса, но мы не можем легко изменить конфигурацию после начальной загрузки и не можем легко получить информацию и события от средства просмотра. Чтобы интегрировать это с веб-приложением Aurelia — с обработкой событий и двусторонним связыванием — нам нужно создать пользовательский компонент Aurelia.

Примечание: если вам нужно освежить в PDF.js, ознакомьтесь с нашим руководством: Пользовательский рендеринг PDF в JavaScript с Mozilla PDF.js

Реализация

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

Образец

Как вы можете видеть, перейдя по ссылке выше, в скелет-приложении много файлов, многие из которых нам не понадобятся. Чтобы упростить жизнь, мы подготовили урезанную версию скелета , к которой добавили несколько вещей:

  • Задача Gulp, чтобы скопировать наши PDF-файлы в папку dist (которую Aurelia использует для пакетирования).
  • Зависимость PDF.js была добавлена ​​в package.json .
  • В корне приложения index.html и index.css получили некоторые начальные стили.
  • Добавлены пустые копии файлов, с которыми мы будем работать.
  • Файл src/resources/elements/pdf-document.css содержит некоторые стили CSS для пользовательского элемента.

Итак, давайте запустим приложение.

Прежде всего, убедитесь, что gulp и jspm установлены глобально:

 npm install -g gulp jspm 

Затем клонируйте скелет и cd в него.

 git clone [email protected]:sitepoint-editors/aurelia-pdfjs.git -b skeleton cd aurelia-pdfjs 

Затем установите необходимые зависимости:

 npm install jspm install -y 

Наконец, запустите gulp watch и перейдите по адресу http: // localhost: 9000 . Если все работает как запланировано, вы должны увидеть приветственное сообщение.

Еще немного настройки

Следующее, что нужно сделать, — это найти пару PDF-файлов и поместить их в src/documents . Назовите их one.pdf и two.pdf . Чтобы максимально протестировать наш пользовательский компонент, было бы хорошо, если бы один из PDF-файлов был действительно длинным, например, «Война и мир», который можно найти в проекте Гутенберга .

Имея PDF-файлы, откройте src/app.html и src/app.js (условно компонент App является корневым или приложение Aurelia) и замените src/app.js код содержимым этих двух файлов: src / app.html и src / app.js. Мы не будем касаться этих файлов в этом руководстве, но код хорошо прокомментирован.

Gulp обнаружит эти изменения автоматически, и вы должны увидеть интерфейс нашего приложения. Вот и все для настройки. Теперь это с шоу …

Создание пользовательского элемента Aurelia

Мы хотим создать выпадающий компонент, который можно использовать в любом представлении Aurelia . Поскольку представление Aurelia — это просто фрагмент HTML, обернутый внутри тега шаблона HTML5, пример может выглядеть следующим образом:

 <template> <require from="resources/elements/pdf-document"></require> <pdf-document url.bind="document.url" page.bind="document.pageNumber" lastpage.bind="document.lastpage" scale.bind="document.scale"> </pdf-document> </template> 

Тег <pdf-document> является примером пользовательского элемента. Он и его атрибуты (такие как scale и page ) не являются родными для HTML, но мы можем создать это с помощью пользовательских элементов Aurelia. Пользовательские элементы легко создавать, используя основные строительные блоки Aurelia: Views и ViewModels. Поэтому мы сначала создадим нашу модель ViewModel с именем pdf-document.js , вот так:

 // src/resources/elements/pdf-document.js import {customElement, bindable, bindingMode} from 'aurelia-framework'; @customElement('pdf-document') @bindable({ name: 'url' }) @bindable({ name: 'page', defaultValue: 1, defaultBindingMode: bindingMode.twoWay }) @bindable({ name: 'scale', defaultValue: 1, defaultBindingMode: bindingMode.twoWay }) @bindable({ name: 'lastpage', defaultValue: 1, defaultBindingMode: bindingMode.twoWay }) export class PdfDocument { constructor () { // Instantiate our custom element. } detached () { // Aurelia lifecycle method. Clean up when element is removed from the DOM. } urlChanged () { // React to changes to the URL attribute value. } pageChanged () { // React to changes to the page attribute value. } scaleChanged () { // React to changes to the scale attribute value. } pageHandler () { // Change the current page number as we scroll } renderHandler () { // Batch changes to the DOM and keep track of rendered pages } } 

Здесь главное отметить декоратор @bindable ; создавая привязываемые свойства с конфигурацией defaultBindingMode: bindingMode.twoWay и создавая методы-обработчики в нашем ViewModel ( urlChanged , pageChanged и т. д.), мы можем отслеживать и реагировать на изменения связанных атрибутов, которые мы pageChanged в наш пользовательский элемент. Это позволит нам контролировать нашу программу просмотра PDF, просто изменяя свойства элемента.

Затем мы создадим начальный вид для сопряжения с нашей ViewModel.

 // src/resources/elements/pdf-document.html <template> <require from="./pdf-document.css"></require> <div ref="container" class="pdf-container"> My awesome PDF viewer. </div> </template> 

Интеграция PDF.js

PDF.js разделен на три части. Есть основная библиотека, которая обрабатывает анализ и интерпретацию PDF-документа; библиотека отображения, которая создает пригодный для использования API поверх основного уровня; и, наконец, плагин для веб-просмотра, который является предварительно созданной веб-страницей, о которой мы упоминали ранее. Для наших целей мы будем использовать базовую библиотеку через API отображения; мы будем строить нашего собственного зрителя.

API отображения экспортирует объект библиотеки с именем PDFJS , который позволяет нам настраивать некоторые переменные конфигурации и загружать наш документ с помощью PDFJS.getDocument(url) . API полностью асинхронный — он отправляет и получает сообщения от веб-работника, поэтому он в значительной степени опирается на обещания JavaScript. В основном мы будем работать с объектом PDFDocumentProxy, возвращаемым асинхронно из PDFJS.getDocument() , и объектом PDFPageProxy, возвращаемым асинхронно из PDFDocumentProxy.getPage() .

Хотя документация немного скудна, в PDF.js есть несколько примеров создания базового средства просмотра здесь ( демонстрация ) и здесь ( демонстрация ). Мы будем опираться на эти примеры для нашего пользовательского компонента.

Интеграция с веб-работником

PDF.js использует веб-работника для разгрузки задач рендеринга Из-за того, что веб-работники работают в среде браузера (они эффективно изолированы от песочницы), мы вынуждены загружать веб-работника, используя прямой путь к файлу JavaScript вместо обычного загрузчика модулей. К счастью, Aurelia предоставляет абстракцию загрузчика, так что нам не нужно ссылаться на статический путь к файлу (который может измениться, когда мы связываем наше приложение).

Если вы используете нашу версию репозитория , вы уже установили пакет pdfjs-dist , в противном случае вам нужно будет сделать это сейчас (например, с помощью jspm jspm install npm:pdfjs-dist@^1.5.391 ). Затем мы внедрим абстракцию загрузчика Aurelia с помощью модуля внедрения зависимостей Aurelia и используем загрузчик для загрузки файла веб-рабочего в наш конструктор, например, так:

 // src/resources/elements/pdf-document.js import {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework'; import {PDFJS} from 'pdfjs-dist'; @customElement('pdf-document') ... // all of our @bindables @inject(Loader) export class PdfDocument { constructor (loader) { // Let Aurelia handle resolving the filepath to the worker. PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js'); // Create a worker instance for each custom element instance. this.worker = new PDFJS.PDFWorker(); } detached () { // Release and destroy our worker instance when the the PDF element is removed from the DOM. this.worker.destroy(); } ... } 

Загрузка наших страниц

Библиотека PDF.js позволяет загружать, анализировать и отображать документы PDF. Он поставляется со встроенной поддержкой частичной загрузки и аутентификации. Все, что нам нужно сделать, это предоставить URI рассматриваемого документа, и PDF.js вернет объект обещания, преобразованный в объект JavaScript, представляющий документы PDF и его метаданные.

Загрузка и отображение PDF будет зависеть от наших привязываемых атрибутов; в этом случае это будет атрибут url . По сути, когда URL-адрес изменяется, пользовательский элемент должен попросить PDF.js сделать запрос на файл. Мы сделаем это в нашем обработчике urlChanged с некоторыми изменениями в нашем конструкторе, чтобы инициализировать некоторые свойства и некоторые изменения в нашем detached методе для целей очистки.

Для каждой страницы нашего документа мы создадим элемент <canvas> в DOM, размещенный внутри прокручиваемого контейнера с фиксированной высотой. Чтобы реализовать это, мы будем использовать базовую функциональность шаблонов Aurelia, используя ретранслятор . Поскольку каждая страница PDF может иметь свой собственный размер и ориентацию, мы установим ширину и высоту каждого элемента холста на основе области просмотра страницы PDF.

Вот наш взгляд:

 // src/resources/elements/pdf-document.html <template> <require from="./pdf-document.css"></require> <div ref="container" id.bind="fingerprint" class="pdf-container"> <div repeat.for="page of lastpage" class="text-center"> <canvas id="${fingerprint}-page${(page + 1)}"></canvas> </div> </div> </template> 

После того, как мы загрузили наш PDF-документ, нам нужно получить размеры каждой страницы в PDF, чтобы мы могли сопоставить каждый размер canvas размером его страницы. (Выполнение этого в данный момент позволяет нам настроить наш просмотрщик для прокрутки; если бы мы не сделали этого сейчас, у нас не было бы правильной высоты для каждой страницы.) Итак, после загрузки каждой страницы мы ставим задачу в измените размер элемента canvas, используя абстракцию Aurelia TaskQueue . (Это сделано из соображений производительности DOM. Подробнее о микротрассах вы можете прочитать здесь ).

Вот наша ViewModel:

 // src/resources/elements/pdf-document.js import {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework'; import {TaskQueue} from 'aurelia-task-queue'; import {PDFJS} from 'pdfjs-dist'; @customElement('pdf-document') ... // all of our @bindables @inject(Loader, TaskQueue) export class PdfDocument { constructor (loader, taskQueue) { PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js'); this.worker = new PDFJS.PDFWorker(); // Hold a reference to the task queue for later use. this.taskQueue = taskQueue; // Add a promise property. this.resolveDocumentPending; // Add a fingerprint property to uniquely identify our DOM nodes. // This allows us to create multiple viewers without issues. this.fingerprint = generateUniqueDomId(); this.pages = []; this.currentPage = null; } urlChanged (newValue, oldValue) { if (newValue === oldValue) return; // Load our document and store a reference to PDF.js' loading promise. var promise = this.documentPending || Promise.resolve(); this.documentPending = new Promise((resolve, reject) => { this.resolveDocumentPending = resolve.bind(this); }); return promise .then((pdf) => { if (pdf) { pdf.destroy(); } return PDFJS.getDocument({ url: newValue, worker: this.worker }); }) .then((pdf) => { this.lastpage = pdf.numPages; pdf.cleanupAfterRender = true; // Queue loading of all of our PDF pages so that we can scroll through them later. for (var i = 0; i < pdf.numPages; i++) { this.pages[i] = pdf.getPage(Number(i + 1)) .then((page) => { var viewport = page.getViewport(this.scale); var element = document.getElementById(`${this.fingerprint}-page${page.pageNumber}`); // Update page canvas elements to match viewport dimensions. // Use Aurelia's TaskQueue to batch the DOM changes. this.taskQueue.queueMicroTask(() => { element.height = viewport.height; element.width = viewport.width; }); return { element: element, page: page, rendered: false, clean: false }; }); } // For the initial render, check to see which pages are currently visible, and render them. /* Not implemented yet. */ this.resolveDocumentPending(pdf); }); } detached () { // Destroy our PDF worker asynchronously to avoid any race conditions. return this.documentPending .then((pdf) => { if (pdf) { pdf.destroy(); } this.worker.destroy(); }) .catch(() => { this.worker.destroy(); }); } } // Generate unique ID values to avoid any DOM conflicts and allow multiple PDF element instances. var generateUniqueDomId = function () { var S4 = function() { return (((1 + Math.random()) * 0x10000) | 0) .toString(16) .substring(1); }; return `_${S4()}${S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`; } , // src/resources/elements/pdf-document.js import {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework'; import {TaskQueue} from 'aurelia-task-queue'; import {PDFJS} from 'pdfjs-dist'; @customElement('pdf-document') ... // all of our @bindables @inject(Loader, TaskQueue) export class PdfDocument { constructor (loader, taskQueue) { PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js'); this.worker = new PDFJS.PDFWorker(); // Hold a reference to the task queue for later use. this.taskQueue = taskQueue; // Add a promise property. this.resolveDocumentPending; // Add a fingerprint property to uniquely identify our DOM nodes. // This allows us to create multiple viewers without issues. this.fingerprint = generateUniqueDomId(); this.pages = []; this.currentPage = null; } urlChanged (newValue, oldValue) { if (newValue === oldValue) return; // Load our document and store a reference to PDF.js' loading promise. var promise = this.documentPending || Promise.resolve(); this.documentPending = new Promise((resolve, reject) => { this.resolveDocumentPending = resolve.bind(this); }); return promise .then((pdf) => { if (pdf) { pdf.destroy(); } return PDFJS.getDocument({ url: newValue, worker: this.worker }); }) .then((pdf) => { this.lastpage = pdf.numPages; pdf.cleanupAfterRender = true; // Queue loading of all of our PDF pages so that we can scroll through them later. for (var i = 0; i < pdf.numPages; i++) { this.pages[i] = pdf.getPage(Number(i + 1)) .then((page) => { var viewport = page.getViewport(this.scale); var element = document.getElementById(`${this.fingerprint}-page${page.pageNumber}`); // Update page canvas elements to match viewport dimensions. // Use Aurelia's TaskQueue to batch the DOM changes. this.taskQueue.queueMicroTask(() => { element.height = viewport.height; element.width = viewport.width; }); return { element: element, page: page, rendered: false, clean: false }; }); } // For the initial render, check to see which pages are currently visible, and render them. /* Not implemented yet. */ this.resolveDocumentPending(pdf); }); } detached () { // Destroy our PDF worker asynchronously to avoid any race conditions. return this.documentPending .then((pdf) => { if (pdf) { pdf.destroy(); } this.worker.destroy(); }) .catch(() => { this.worker.destroy(); }); } } // Generate unique ID values to avoid any DOM conflicts and allow multiple PDF element instances. var generateUniqueDomId = function () { var S4 = function() { return (((1 + Math.random()) * 0x10000) | 0) .toString(16) .substring(1); }; return `_${S4()}${S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`; } 

Сохраните свою работу, и Gulp должен перерисовать страницу. Вы заметите, что контейнер показывает правильное количество страниц для соответствующих PDF-файлов. Единственная проблема в том, что они пустые. Давайте это исправим!

Рендеринг наших страниц

Теперь, когда мы загрузили наши страницы, нам нужно отобразить их в элемент DOM. Для этого мы будем полагаться на функциональность рендеринга PDF.js. Библиотека просмотра PDF.js имеет асинхронный API, предназначенный для отображения страниц; На их сайте есть отличный пример, который показывает, как создать объект renderContext и передать его в метод рендеринга renderContext Мы возьмем этот код из примера и обернем его внутри функции рендеринга:

SRC / ресурсы / элементы / PDF-document.js

 ... export class PdfDocument { ... } var generateUniqueDomId = function () { ... } var render = function (renderPromise, scale) { return Promise.resolve(renderPromise) .then((renderObject) => { if (renderObject.rendered) return Promise.resolve(renderObject); renderObject.rendered = true; var viewport = renderObject.page.getViewport(scale); var context = renderObject.element.getContext('2d'); return renderObject.page.render({ canvasContext: context, viewport: viewport }) .promise.then(() => { return renderObject; }); }); }; 

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

 // src/resources/elements/pdf-document.js export class PdfDocument { ... } var generateUniqueDomId = function () { ... } var render = function (...) { ... } var checkIfElementVisible = function (container, element) { var containerBounds = { top: container.scrollTop, bottom: container.scrollTop + container.clientHeight }; var elementBounds = { top: element.offsetTop, bottom: element.offsetTop + element.clientHeight }; return (!((elementBounds.bottom < containerBounds.top && elementBounds.top < containerBounds.top) || (elementBounds.top > containerBounds.bottom && elementBounds.bottom > containerBounds.bottom))); } 

Когда мы впервые загружаем документ и когда выполняем прокрутку, мы запускаем эти проверки области просмотра. Теперь, при загрузке, мы просто визуализируем то, что видимо, вот так.

 // src/resources/elements/pdf-document.js export class PdfDocument { ... urlChanged (newValue, oldValue) { ... // For the initial render, check to see which pages are currently visible, and render them. this.pages.forEach((page) => { page.then((renderObject) => { if (checkIfElementVisible(this.container, renderObject.element)) { if (renderObject.rendered) return; render(page, this.scale); } }); }); this.resolveDocumentPending(pdf); }); } 

Перезагрузите приложение, и вы увидите, что отображается первая страница каждого файла PDF.

Реализация прокрутки

Чтобы обеспечить знакомый и понятный интерфейс, наш компонент должен отображать страницы в виде отдельных частей полностью прокручиваемого документа. Мы можем добиться этого, сделав наш контейнер фиксированной высоты с прокруткой через CSS.

Чтобы максимизировать производительность с большими документами, мы сделаем несколько вещей. Во-первых, мы будем использовать TaskQueue Aurelia для пакетных изменений в DOM. Во-вторых, мы будем отслеживать страницы, которые PDF.js уже отредактировал, поэтому он не должен повторять работу, которая уже сделана. Наконец, мы будем отображать видимые страницы только после остановки прокрутки, используя поведение привязки debounce Aurelia. Это метод, который мы будем запускать при прокрутке:

 // src/resources/elements/pdf-document.js export class PdfDocument { ... renderHandler () { Promise.all(this.pages) .then((values) => { values.forEach((renderObject) => { if (!renderObject) return; if (!checkIfElementVisible(this.container, renderObject.element)) { if (renderObject.rendered && renderObject.clean) { renderObject.page.cleanup(); renderObject.clean = true; } return; } this.taskQueue.queueMicroTask(() => { if (renderObject.rendered) return; render(renderObject, this.scale); }); }); }); } ... } 

И вот наш взгляд; мы используем привязку событий Aurelia в scroll.trigger , используя метод, который мы определили, вместе с поведением привязки debounce.

 // src/resources/elements/pdf-document.html <template> <require from="./pdf-document.css"></require> <div ref="container" id.bind="fingerprint" class="pdf-container" scroll.trigger="pageHandler()" scroll.trigger2="renderHandler() & debounce:100"> <div repeat.for="page of lastpage" class="text-center"> <canvas id="${fingerprint}-page${(page + 1)}"></canvas> </div> </div> </template> 

Мы связываем свойство page в средстве просмотра. Когда он меняется, мы хотим обновить позицию прокрутки, чтобы отобразить текущую страницу. Мы также хотим, чтобы это работало по-другому; когда мы прокручиваем документ, мы хотим, чтобы номер текущей страницы обновлялся до той страницы, которую мы сейчас просматриваем. Таким образом, мы добавим следующие два метода в нашу ViewModel:

 export class PdfDocument { ... // If the page changes, scroll to the associated element. pageChanged (newValue, oldValue) { if (newValue === oldValue || isNaN(Number(newValue)) || Number(newValue) > this.lastpage || Number(newValue) < 0) { this.page = oldValue; return; } // Prevent scroll update collisions with the pageHandler method. if (Math.abs(newValue - oldValue) <= 1) return; this.pages[newValue - 1] .then((renderObject) => { this.container.scrollTop = renderObject.element.offsetTop; render(this.pages[newValue - 1], this.scale); }); } ... // Change the current page number as we scroll. pageHandler () { this.pages.forEach((page) => { page.then((renderObject) => { if ((this.container.scrollTop + this.container.clientHeight) >= renderObject.element.offsetTop && (this.container.scrollTop <= renderObject.element.offsetTop)) { this.page = renderObject.page.pageNumber; } }); }); } ... } 

Мы будем вызывать наш метод scroll.trigger в нашем событии scroll.trigger в нашем контейнере.

Примечание. Из-за существующего ограничения в шаблонах Aurelia невозможно объявить несколько методов в обработчике событий с отдельным поведением привязки. Мы работаем над этим, добавляя эти строки в верхнюю часть нашей ViewModel…

 import {SyntaxInterpreter} from 'aurelia-templating-binding'; SyntaxInterpreter.prototype.trigger2 = SyntaxInterpreter.prototype.trigger; 

… И scroll.trigger2 нового метода в событие scroll.trigger2 .

Gulp должен перезагрузить приложение, и вы увидите, что новые страницы PDF будут отображаться по мере их просмотра. Ура!

Реализация масштабирования

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

 // src/resources/elements/pdf-document.js export class PdfDocument { ... scaleChanged (newValue, oldValue) { if (newValue === oldValue || isNaN(Number(newValue))) return; Promise.all(this.pages) .then((values) => { values.forEach((renderObject) => { if (!renderObject) return; var viewport = renderObject.page.getViewport(newValue); renderObject.rendered = false; this.taskQueue.queueMicroTask(() => { renderObject.element.height = viewport.height; renderObject.element.width = viewport.width; if (renderObject.page.pageNumber === this.page) { this.container.scrollTop = renderObject.element.offsetTop; } }); }); return values; }) .then((values) => { this.pages.forEach((page) => { page.then((renderObject) => { this.taskQueue.queueMicroTask(() => { if (checkIfElementVisible(this.container, renderObject.element)) { render(page, this.scale); } }); }); }); }); } ... } 

Конечный результат

Давайте рассмотрим наши целевые цели:

  1. Мы хотим, чтобы пользователь мог загружать документ, прокручивать и увеличивать и уменьшать его, с приличной производительностью.
  2. Мы хотим иметь возможность двустороннего связывания свойств средства просмотра (таких как текущая страница и текущий уровень масштабирования) со свойствами в нашем приложении.
  3. Мы хотим, чтобы этот просмотрщик был повторно используемым компонентом; мы хотим, чтобы в наше приложение одновременно можно было добавлять несколько зрителей без каких-либо конфликтов и небольших усилий.

Окончательный код можно найти в нашем репозитории GitHub , а также демонстрацию готового кода здесь . Хотя есть возможности для улучшения, мы достигли нашей цели!

Послепроектный анализ и улучшения

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

Отдельные компоненты страницы

В настоящее время это доказательство концепции допускает только прокручиваемое окно просмотра. В идеале мы могли бы отображать любую страницу в любом месте, даже вне программы просмотра, например, создавая эскизы PDF в виде отдельных элементов. Создание пользовательского элемента <pdf-page> или чего-то подобного может обеспечить эту функциональность, в то время как зритель может просто использовать эти элементы посредством композиции.

Оптимизация API

PDF.js имеет обширный API. Хотя есть хорошие примеры использования PDF.js, его интерфейс отображения может использовать больше документации. Могут быть более чистые, более оптимальные способы достижения наших целей с помощью API зрителя.

Виртуальная прокрутка и оптимизация производительности

В настоящее время количество элементов canvas внутри средства просмотра документов равно числу страниц в документе. Все полотна существуют внутри DOM, что может быть очень дорого для больших документов.

Существует плагин Aurelia — плагин ui-virtualization ( demo ), который значительно повышает производительность для очень больших наборов данных, динамически добавляя и удаляя элементы в DOM, чтобы соответствовать активному окну просмотра. В идеале программа просмотра PDF могла бы включить это для повышения производительности (чтобы избежать наличия тысяч полотен в DOM, что действительно снижает производительность). Эта оптимизация в сочетании с отдельными компонентами страницы может иметь огромное значение для больших документов.

Создание плагина

Aurelia предоставляет систему плагинов. Преобразование этого проверочного концепта в плагин Aurelia сделает его ресурсом для любого приложения Aurelia. Хранилище Aurelia Github представляет собой плагин-скелетный проект , который послужит хорошей отправной точкой для разработки. Таким образом, другие могут использовать эту функцию без необходимости перестраивать ее!

Идти вперед

Обработка PDF-файлов в веб-приложении всегда была трудной задачей. Но с доступными сегодня ресурсами мы можем сделать гораздо больше, чем раньше, составляя библиотеки и их функциональные возможности. Сегодня мы увидели пример базового средства просмотра PDF, которое можно расширить с помощью пользовательских функций, поскольку мы имеем полный контроль над ним. Возможности безграничны! Вы готовы что-то построить? Позвольте мне знать в комментариях ниже.