Build a Web Component to Embed PDF in an HTML Website
Dynamic Web TWAIN is an SDK which enables document scanning and viewing from browsers. It can display PDF files in a customizable viewer.
In this article, we are going to build a web component with Stencil.js to wrap Dynamic Web TWAIN as a PDF viewer so that we can embed a PDF file in an HTML website easily using the following code:
<pdf-viewer
url="https://www.dynamsoft.com/codepool/assets/patch-code-sample.pdf"
></pdf-viewer>
The following is the PDF viewer embedded in this blog.
Build a PDF Viewer Web Component
Let’s build the component in steps.
New Project
Run the following command to create a new Stencil Component project:
npm init stencil
Generate a New Component
Generate a new component named pdf-viewer
.
npx stencil g pdf-viewer
To test the component, we can modify the src\index.html
file:
- <my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>
+ <pdf-viewer>
+ <p>Inner elements</p>
+ </pdf-viewer>
Then run the following command to test it:
npm run start
Install Dynamic Web TWAIN
Run the following to install Dynamic Web TWAIN.
npm install dwt@18.0.0
Initialize Dynamic Web TWAIN
Next, we are going to initialize an instance of Dynamic Web TWAIN and bind the viewer to a container.
-
In the render function, add a container.
containerID:string = "dwtcontrolContainer"; container:HTMLDivElement; render() { return ( <Host> <div id={this.containerID} class="container" ref={(el) => this.container = el as HTMLDivElement}> </div> <slot></slot> </Host> ); }
-
In the
componentDidLoad
lifecycle event, initialize Dynamic Web TWAIN.DWObject:WebTwain; componentDidLoad() { this.initDWT(); } initDWT() { Dynamsoft.DWT.ResourcesPath = "https://unpkg.com/dwt@18.0.0/dist"; let pThis = this; Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => { Dynamsoft.DWT.CreateDWTObjectEx( { WebTwainId: 'dwtcontrol' }, function(obj) { pThis.DWObject = obj; pThis.DWObject.Viewer.bind(pThis.container); pThis.DWObject.Viewer.show(); }, function(err) { console.log(err); } ); }); Dynamsoft.DWT.Containers = [{ WebTwainId: 'dwtObject' }]; Dynamsoft.DWT.Load(); }
CSS:
.container { position: relative; }
Because web component uses shadowDom, we need to use the
CreateDWTObjectEx
function to create a new Web TWAIN object. -
Add a license prop to set the license for Dynamic Web TWAIN. You can apply for a license here.
@Prop() license?: string; initDWT() { if (this.license) { Dynamsoft.DWT.ProductKey = this.license; } }
Add Props to Manipulate the Viewer
-
Create a thumbnail viewer and add a prop to set whether to enable it by default.
thumbnailShown:boolean = true; initDWT() { //... pThis.thumbnailViewer = pThis.DWObject.Viewer.createThumbnailViewer(); if (pThis.showthumbnailviewer === "true") { pThis.thumbnailViewer.show(); pThis.thumbnailShown = true; }else{ pThis.thumbnailShown = false; } //... }
-
Add a
url
prop. If it is set, load the PDF file via the URL after the component is mounted.@State() status: string = ""; async loadPDF() { if (this.url) { this.status = "Loading PDF..."; try { let response = await fetch(this.url); let blob = await response.blob(); let pThis = this; this.DWObject.LoadImageFromBinary(blob,function(){ pThis.DWObject.Viewer.singlePageMode=true; pThis.DWObject.SelectImages([0]); },function(){}); this.status = ""; } catch (error) { this.status = "Failed to load the PDF"; } } }
A status container is used to indicate the status.
JSX:
renderStatus(){ if (this.status) { return (<div class="status">{this.status}</div>); } } render() { return ( <Host> <div id={this.containerID} class="container" ref={(el) => this.container = el as HTMLDivElement}> {this.renderStatus()} </div> <slot></slot> </Host> ) }
CSS:
.status { position: absolute; left: 0; top: 0; z-index: 99; }
-
Now, we can update
index.html
to use the PDF viewer to load a PDF file.<div class="app" style="width:800px;height:600px;"> <pdf-viewer style="width:100%;height:100%;" showthumbnailviewer="true" url="./sample.pdf"></pdf-viewer> </div>
Add a Toolbar for the Viewer
Next, we can add a toolbar for the viewer. It has the following functions:
- Toggle the thumbnail viewer.
- Display the current page number and total page number.
- Set the display percentage (zoom).
- Scan documents.
- Load local PDF or image files.
- Save the document as a PDF file.
- Enter fullscreen.
Add the toolbar in JSX (the toolbar and the viewer of Dynamic Web TWAIN are wrapped in a parent container):
render() {
return (
<Host>
<div class="container" ref={(el) => this.parentContainer = el as HTMLDivElement}>
<div class="toolbar">
</div>
<div class="viewer-container">
<div id={this.containerID} class="dwt-container" ref={(el) => this.dwtContainer = el as HTMLDivElement}>
{this.renderStatus()}
</div>
</div>
</div>
<slot></slot>
</Host>
);
}
CSS:
.toolbar {
height: 30px;
border: 1px solid rgb(204, 204, 204);
display: flex;
align-items: center;
background: white;
}
.container {
position: relative;
width: 100%;
height: 100%;
}
.viewer-container {
position: relative;
width: 100%;
height: calc(100% - 30px);
}
.dwt-container {
width: 100%;
height: 100%;
}
Add a Button to Toggle Thumbnail Viewer
-
Add the button in JSX.
<div class="toolbar"> <div class="toolbar-item"> <img title="Toggle thumbnail viewer" class="Icon" src={sidebar} onClick={()=>this.toggleThumbnailViewer()}/> </div> </div>
CSS:
.toolbar-item { display: flex; align-items: center; height: 100%; padding-right: 5px; } .Icon { width: 22px; height: 22px; cursor: pointer; }
-
Convert the icon’s SVG file to base64 and save it in the component’s
assets/base64.tsx
file so that we can use it in the component.export const sidebar = "data:image/svg+xml;base64,PD94b...";
-
The
toggleThumbnailViewer
function.@Method() async toggleThumbnailViewer() { if (this.thumbnailShown) { this.thumbnailViewer.hide(); }else{ this.thumbnailViewer.show(); } this.thumbnailShown = !this.thumbnailShown; }
The method decorator is used here so that we can also call it like this:
const PDFViewer = document.querySelector("pdf-viewer"); PDFViewer.toggleThumbnailViewer();
Add an Input for Zoom
Next, add an Input for setting zoom (display percent).
JSX:
<div class="zoom toolbar-item">
<input type="number" id="percent-input"
value={this.percent}
title="Percent"
onChange={(e) => this.updateZoom(e)}
/><label htmlFor="percent-input">%</label>
</div>
CSS:
#percent-input {
width: 4em;
}
The update zoom function:
@State() percent: number = 100;
updateZoom(e:any){
this.percent = e.target.value;
const zoom = this.percent/100;
this.DWObject.Viewer.zoom = zoom;
}
Add a Button to Quickly Set a Proper Zoom Percentage
Next, let’s add a toggle button to set the zoom percentage to 100% or make it fit the window.
JSX:
<div class="quicksize toolbar-item">
{this.showFitWindow
? <img title="Fit window" class="Icon" src={fitWindow} onClick={()=>this.quicksize()}/>
: <img title="Original size" class="Icon" src={origSize} onClick={()=>this.quicksize()}/>
}
</div>
The quicksize
function:
@State() showFitWindow: boolean = true;
quicksize(){
if (this.showFitWindow) {
this.DWObject.Viewer.fitWindow("width");
this.percent = this.DWObject.Viewer.zoom*100;
}else{
this.DWObject.Viewer.zoom = 1.0;
this.percent = 100;
}
this.showFitWindow = !this.showFitWindow;
}
Add Controls for Page Number
Add controls to display/set the page number and display the total page number.
JSX:
<div class="page toolbar-item">
<input type="number" id="page-input"
value={this.selectedPageNumber}
title="Page number"
onChange={(e) => this.updateSelectedPageNumber((e as any).target.value)}
/>/{this.totalPageNumber}
</div>
CSS:
#page-input {
width: 3em;
}
The updateSelectedPageNumber
and updateTotalPage
functions:
@State() totalPageNumber: number = 0;
@State() selectedPageNumber: number = 0;
updateSelectedPageNumber(num:number){
if (num <= 0 || num > this.totalPageNumber) {
this.selectedPageNumber = this.DWObject.CurrentImageIndexInBuffer + 1;
return;
}
this.selectedPageNumber = num;
let index = this.selectedPageNumber - 1;
if (this.DWObject) {
this.DWObject.SelectImages([index]);
}
}
updateTotalPage(){
if (this.DWObject) {
this.totalPageNumber = this.DWObject.HowManyImagesInBuffer;
}
}
Then, call the above functions in the OnBufferChanged
event.
let pThis = this;
pThis.DWObject.RegisterEvent('OnBufferChanged',function (bufferChangeInfo) {
if (bufferChangeInfo.action === "add" || bufferChangeInfo.action === "remove") {
pThis.updateTotalPage();
}
pThis.updateSelectedPageNumber(pThis.DWObject.CurrentImageIndexInBuffer + 1);
});
Add a Button to Show Extra Actions
There are some extra actions we can add. Here, we use a button to toggle an overlay which contains buttons for these actions.
JSX for the button (a container is appended to make it right-aligned):
<div class="toolbar">
<div class="toolbar-container toolbar-item"></div>
<div class="action toolbar-item">
<img class="Icon" src={settings} onClick={()=>this.toggleActionOverlay()}/>
</div>
</div>
CSS:
.toolbar-container {
flex: 1;
}
The overlay for actions is appended to the container of Web TWAIN.
<div id={this.containerID} class="dwt-container" ref={(el) => this.dwtContainer = el as HTMLDivElement}>
{this.renderStatus()}
{this.renderActionOverlay()}
</div>
The toggleActionOverlay
and renderActionOverlay
function:
toggleActionOverlay(){
this.showActionOverlay = !this.showActionOverlay;
}
renderActionOverlay(){
let className:string;
if (this.showActionOverlay) {
className = "overlay";
}else{
className = "overlay hidden";
}
return (
<div class={className}>
{this.fullscreen
? <img title="Exit fullscreen" class="Icon" src={exitFullscreen} onClick={()=>this.toggleFullscreen()}/>
: <img title="Enter fullscreen" class="Icon" src={fullscreen} onClick={()=>this.toggleFullscreen()}/>
}
<img title="Scan documents" class="Icon" src={scanner} onClick={()=>this.scan()}/>
<img title="Load local file" class="Icon" src={openFile} onClick={()=>this.loadFile()}/>
<img title="Save to PDF" class="Icon" src={download} onClick={()=>this.saveFile()}/>
</div>
);
}
CSS for the overlay:
.overlay {
position: absolute;
display: flex;
flex-direction: column;
right: 5px;
top: 5px;
width: 22px;
height: 125px;
z-index: 999;
padding: 5px;
border-radius: 5%;
border: 1px solid rgb(204, 204, 204);
align-items: flex-start;
justify-content: space-between;
background: white;
}
.overlay.hidden {
visibility: hidden;
}
Next, let’s talk about the JavaScript functions for actions.
-
toggleFullscreen
which makes the PDF viewer fullscreen. Since iPhone’s Safari does not support the fullscren API, we set the container to a fixed fullscreen element for Safari.async toggleFullscreen(){ let isSafari = Dynamsoft.Lib.env.bSafari; if (isSafari) { if (this.parentContainer.classList.contains("fullscreen")) { this.parentContainer.classList.remove("fullscreen"); this.fullscreen = false; this.resizeViewer(); }else{ this.parentContainer.classList.add("fullscreen"); this.fullscreen = true; this.resizeViewer(); } }else{ if (document.fullscreenElement) { document.exitFullscreen(); }else{ let ele = this.parentContainer.parentNode["host"]; await ele.requestFullscreen(); } } } resizeViewer(){ this.DWObject.Viewer.singlePageMode = true; }
CSS:
.container.fullscreen { position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 990; }
-
loadFile
which loads local PDF or image files.loadFile(){ const success = () => { this.updateTotalPage(); } this.DWObject.LoadImageEx("",Dynamsoft.DWT.EnumDWT_ImageType.IT_ALL,success); }
-
scan
which scans documents from scanners or cameras (for mobile devices).scan(){ let pThis = this; if (Dynamsoft.Lib.env.bMobile) { pThis.DWObject.Addon.Camera.scanDocument(); }else{ pThis.DWObject.SelectSource(function () { pThis.DWObject.OpenSource(); pThis.DWObject.AcquireImage(); }, function () { console.log("SelectSource failed!"); } ); } }
-
saveFile
which saves the document as a PDF file.saveFile(){ this.DWObject.SaveAllAsPDF("scanned.pdf"); }
The final result is like the following:
Source Code
Get the source code of the PDF viewer web component to have a try: