This article was peer-reviewed by Vildan Softic. Thanks to all the peer reviewers at SitePoint for getting SitePoint content to its best!
Processing PDF files in web applications has always been very tricky. If you are lucky, your users just need to download the file. But sometimes, users need more features. I was lucky in the past, but this time, our users need the app to display PDF documents so that they can save metadata related to each page. Previously, people might have used expensive PDF plugins (such as Adobe Reader) to run in the browser to achieve this. However, after some time and experimentation, I found a better way to integrate a PDF viewer in my web application. Today, we will learn how to simplify PDF processing using Aurelia and PDF.js.
Core points
- Use Aurelia and PDF.js to create a custom, efficient PDF viewer with features such as scaling and scrolling to enhance user interaction and performance.
- Implement two-way data binding for attributes such as current page and zoom level in Aurelia, allowing seamless integration and dynamic updates in the application.
- Develop PDF viewer into reusable Aurelia custom elements that allow you to add multiple viewers to your application without conflict.
- Use PDF.js to process PDF rendering, support asynchronous operations and Web Worker to uninstall processing and improve UI response speed.
- Address potential performance issues by considering virtual scrolling and other optimizations, especially in effectively handling large documents.
- Explore the possibility of converting a custom PDF viewer to an Aurelia plugin to make it easy to integrate into other projects and applications.
Overview: Goal
Our goal today is to build a PDF viewer component in Aurelia that allows for two-way data flow between the viewer and our application. We have three main requirements:
- We want users to be able to load documents, scroll and zoom in/out with good performance.
- We want to be able to bidirectionally bind viewer properties (such as the current page and the current zoom level) to properties in the application.
- We want this viewer to be a reusable component; we want to be able to easily place multiple viewers in the application without conflict.
You can find the code for this tutorial in our GitHub repository, as well as a demo of the completed code here.
Introduction PDF.js
PDF.js is a JavaScript library written by the Mozilla Foundation. It loads PDF documents, parses files and related metadata, and renders the page output to the DOM node (usually <canvas></canvas>
element). The default viewer included in the project provides support for embedded PDF viewers in Chrome and Firefox and can be used as standalone pages or resources (embedded in iframes).
This is really cool. The problem here is that the default viewer, while it has many features, is designed as a standalone webpage. This means that while it can be integrated into a web application, it basically has to run inside an iframe sandbox. The default viewer is designed to get configuration input through its query string, but we cannot easily change the configuration after initial loading, nor can we easily get information and events from the viewer. In order to integrate it with the Aurelia web application—including event handling and two-way binding—we need to create an Aurelia custom component.
Note: If you need a review about PDF.js, please check out our tutorial: Custom PDF rendering in JavaScript using Mozilla's PDF.js
Implementation
To achieve our goal, we will create an Aurelia custom element. However, we will not put the default viewer into our components. Instead, we will create our own viewer that connects to the PDF.js core and viewer library so that we can maximize control of our bindable properties and renderings. For our initial proof of concept, we will start with the Aurelia Skeleton application.
Photoscope code
As you can see from the link above, the skeleton application has many files, many of which we don't need. To simplify operations, we prepared a streamlined version of the skeleton and added some content to it:
- A Gulp task for copying our PDF files to the dist folder (Aurelia is used for bundling).
- PDF.js dependency has been added to package.json.
- In the application root directory, index.html and index.css have undergone some initial style settings.
- The empty copy of the file we are going to use has been added.
- The file src/resources/elements/pdf-document.css contains some CSS styles for custom elements.
So let's get the application up and running.
First, make sure gulp and jspm are installed globally:
npm install -g gulp jspm
Then clone the skeleton and enter it:
git clone git@github.com:sitepoint-editors/aurelia-pdfjs.git -b skeleton cd aurelia-pdfjs
Then install the necessary dependencies:
npm install jspm install -y
Finally run gulp watch and navigate to http://localhost:9000. If everything goes according to plan, you should see a welcome message.
More settings
The next thing to do is find a few PDF files and put them in src/documents. Name them one.pdf and two.pdf. To maximize testing of our custom components, it is best that one of the PDF files is very long, such as War and Peace that can be found in the Gutenberg Project.
After putting the PDF file in place, open src/app.html and src/app.js (As agreed, the App component is the root component of the Aurelia application), and replace the code in it with these two files Code: src/app.html and src/app.js. In this tutorial, we will not discuss these files, but there are good comments in the code.
Gulp will automatically detect these changes and you should see our application UI rendering. That's the setting. Now start showing...
Create Aurelia custom element
We want to create a component that can be used directly for any Aurelia view. Since the Aurelia view is just an HTML snippet included in the HTML5 template tag, an example might look like this:
npm install -g gulp jspm
<pdf-document>
tags are an example of custom elements. It and its properties (such as scale and page) are not native properties of HTML, but we can use Aurelia custom elements to create it. Custom elements are easy to create, using Aurelia's basic building blocks: views and ViewModel. Therefore, we will first build our ViewModel, named pdf-document.js, as shown below:
git clone git@github.com:sitepoint-editors/aurelia-pdfjs.git -b skeleton cd aurelia-pdfjs
The main content to note here is the @bindable
decorator; by creating binding properties with configuration defaultBindingMode: bindingMode.twoWay
, and by creating handler methods (urlChanged, pageChanged, etc.) in our ViewModel, we can monitor and respond to changes to related attributes we place on custom elements. This will allow us to control our PDF viewer simply by changing the properties on the element.
We will then create the initial view paired with our ViewModel.
npm install jspm install -y
(The following content is basically consistent with the original text, but some sentences have been adjusted in detail to maintain fluency and readability and avoid duplication.)
Integrated PDF.js
PDF.js is divided into three parts: core library (processing the parsing and interpretation of PDF documents), display library (building available APIs on top of the core layer), and web viewer plug-in (pre-built we mentioned earlier). Web page). For our purposes, we will use the core library through the display API; we will build our own viewer.
Show API exports a library object named PDFJS, which allows us to set some configuration variables and load our document using PDFJS.getDocument(url). The API is completely asynchronous - it sends and receives messages to the Web Worker, so it relies heavily on JavaScript Promise. We will mainly use the PDFDocumentProxy object asynchronously returned from the PDFJS.getDocument() method and the PDFPageProxy object asynchronously returned from the PDFDocumentProxy.getPage().
Although the documentation is a bit sparse, PDF.js has some examples of creating a basic viewer, here and here. We will build our custom components based on this.
Web Worker Integration
PDF.js uses Web Worker to uninstall its rendering tasks. Because of how Web Workers run in a browser environment (they are actually sandboxed), we are forced to load Web Workers using the direct file path to JavaScript files instead of the usual module loaders. Fortunately, Aurelia provides a loader abstraction, so we don't have to reference static file paths (this may change when we bundle the application).
If you are following our version of the repository, you may have installed the pdfjs-dist package, otherwise you will need to do this now (for example, using jspm jspm install npm:pdfjs-dist@^1.5.391). We will then inject Aurelia's loader abstraction using Aurelia's dependency injection module and use the loader to load the Web Worker file in our constructor as follows:
Loading page
PDF.js library handles the loading, parsing and display of PDF documents. It has built-in support for partial downloads and authentication. All we have to do is provide the URI of the relevant document. PDF.js will return a Promise object that resolves to a JavaScript object representing the PDF document and its metadata.
The loading and display of the PDF will be driven by our bindable attribute; in this case it will be the url attribute. Basically, when the URL changes, the custom element should ask PDF.js to issue a request to the file. We will do this in the urlChanged handler and make some changes to our constructor to initialize some properties and some changes to our detached method to clean up.
For each page of the document, we will create a <canvas></canvas>
element in the DOM that is located in a scrollable container with a fixed height. To do this, we will use Aurelia's basic template functionality, using a repeater. Because each PDF page can have its own size and orientation, we will set the width and height of each canvas element according to the PDF page viewport.
Rendering page
Now that we have loaded the pages, we need to be able to render them to the DOM elements. To do this, we will rely on the rendering functionality of PDF.js. The PDF.js viewer library has an asynchronous API dedicated to rendering pages; their website has a great example showing how to create a renderContext object and pass it to the PDF.js render method. We extract this code from the example and wrap it in a render function:
Implement scrolling
To provide a familiar and seamless experience, our components should display the page as parts of a fully scrollable document. We can do this by having our container have a fixed height with scroll overflow, which can be achieved through CSS.
To maximize the performance of large documents, we will do the following things. First, we will use Aurelia's TaskQueue to batch change the DOM. Second, we will track the page that PDF.js has rendered so it doesn't have to redo the work it has done. Finally, we will render the visible page only after the scrolling stops by using Aurelia's debounce binding behavior. Here is how we will run when scrolling:
Achieve scaling
When we scale, we want to update the current zoom level. We will do this in the scaleChanged property handler. Basically, we will resize all the canvas elements to reflect the new viewport size for each page of a given scale. We will then re-render what is displayed in the current viewport and restart the loop.
The final result
Let's review our goals:
- We want users to be able to load documents, scroll and zoom in/out with good performance.
- We want to be able to bidirectionally bind viewer properties (such as the current page and the current zoom level) to properties in the application.
- We want this viewer to be a reusable component; we want to be able to easily place multiple viewers in the application without conflict.
The final code can be found in our GitHub repository, as well as a demo of the completed code here. Although there is still room for improvement, we have reached our goal!
(The following content is basically consistent with the original text, but some sentences have been adjusted in detail to maintain fluency and readability and avoid duplication.)
Post-project analysis and improvement
There is always room for improvement. It is always a good habit to conduct post-project analysis and determine areas that need to be solved in future iterations. Here are some things I want to upgrade in PDF viewer implementation:
Create plugin
Aurelia provides a plug-in system. Converting this proof of concept to an Aurelia plugin will make it an out-of-use resource for any Aurelia application. Aurelia Github repository provides a plugin skeleton project which will be a good starting point. This way, others can use this feature without rebuilding it!
Optimal
Processing PDF files in web applications has always been very tricky. But with the resources available today, we can achieve more than ever by combining libraries and their capabilities. Today, we have seen an example of a basic PDF viewer – a viewer that can be extended with custom features, because we have full control over it. The possibilities are endless! Are you ready to build something? Please let me know in the comments below.
FAQs (FAQs) about Aurelia Custom PDF Viewer Components
(The following content is basically consistent with the original text, but some sentences have been adjusted in detail to maintain fluency and readability and avoid duplication.)
The above is the detailed content of Adventures in Aurelia: Creating a Custom PDF Viewer. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

There are three common ways to initiate HTTP requests in Node.js: use built-in modules, axios, and node-fetch. 1. Use the built-in http/https module without dependencies, which is suitable for basic scenarios, but requires manual processing of data stitching and error monitoring, such as using https.get() to obtain data or send POST requests through .write(); 2.axios is a third-party library based on Promise. It has concise syntax and powerful functions, supports async/await, automatic JSON conversion, interceptor, etc. It is recommended to simplify asynchronous request operations; 3.node-fetch provides a style similar to browser fetch, based on Promise and simple syntax

JavaScript data types are divided into primitive types and reference types. Primitive types include string, number, boolean, null, undefined, and symbol. The values are immutable and copies are copied when assigning values, so they do not affect each other; reference types such as objects, arrays and functions store memory addresses, and variables pointing to the same object will affect each other. Typeof and instanceof can be used to determine types, but pay attention to the historical issues of typeofnull. Understanding these two types of differences can help write more stable and reliable code.

Hello, JavaScript developers! Welcome to this week's JavaScript news! This week we will focus on: Oracle's trademark dispute with Deno, new JavaScript time objects are supported by browsers, Google Chrome updates, and some powerful developer tools. Let's get started! Oracle's trademark dispute with Deno Oracle's attempt to register a "JavaScript" trademark has caused controversy. Ryan Dahl, the creator of Node.js and Deno, has filed a petition to cancel the trademark, and he believes that JavaScript is an open standard and should not be used by Oracle

CacheAPI is a tool provided by the browser to cache network requests, which is often used in conjunction with ServiceWorker to improve website performance and offline experience. 1. It allows developers to manually store resources such as scripts, style sheets, pictures, etc.; 2. It can match cache responses according to requests; 3. It supports deleting specific caches or clearing the entire cache; 4. It can implement cache priority or network priority strategies through ServiceWorker listening to fetch events; 5. It is often used for offline support, speed up repeated access speed, preloading key resources and background update content; 6. When using it, you need to pay attention to cache version control, storage restrictions and the difference from HTTP caching mechanism.

Promise is the core mechanism for handling asynchronous operations in JavaScript. Understanding chain calls, error handling and combiners is the key to mastering their applications. 1. The chain call returns a new Promise through .then() to realize asynchronous process concatenation. Each .then() receives the previous result and can return a value or a Promise; 2. Error handling should use .catch() to catch exceptions to avoid silent failures, and can return the default value in catch to continue the process; 3. Combinators such as Promise.all() (successfully successful only after all success), Promise.race() (the first completion is returned) and Promise.allSettled() (waiting for all completions)

JavaScript array built-in methods such as .map(), .filter() and .reduce() can simplify data processing; 1) .map() is used to convert elements one to one to generate new arrays; 2) .filter() is used to filter elements by condition; 3) .reduce() is used to aggregate data as a single value; misuse should be avoided when used, resulting in side effects or performance problems.

JavaScript's event loop manages asynchronous operations by coordinating call stacks, WebAPIs, and task queues. 1. The call stack executes synchronous code, and when encountering asynchronous tasks, it is handed over to WebAPI for processing; 2. After the WebAPI completes the task in the background, it puts the callback into the corresponding queue (macro task or micro task); 3. The event loop checks whether the call stack is empty. If it is empty, the callback is taken out from the queue and pushed into the call stack for execution; 4. Micro tasks (such as Promise.then) take precedence over macro tasks (such as setTimeout); 5. Understanding the event loop helps to avoid blocking the main thread and optimize the code execution order.

Event bubbles propagate from the target element outward to the ancestor node, while event capture propagates from the outer layer inward to the target element. 1. Event bubbles: After clicking the child element, the event triggers the listener of the parent element upwards in turn. For example, after clicking the button, it outputs Childclicked first, and then Parentclicked. 2. Event capture: Set the third parameter to true, so that the listener is executed in the capture stage, such as triggering the capture listener of the parent element before clicking the button. 3. Practical uses include unified management of child element events, interception preprocessing and performance optimization. 4. The DOM event stream is divided into three stages: capture, target and bubble, and the default listener is executed in the bubble stage.
