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.

  1. 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>
      );
    }
    
  2. 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.

  3. 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

  1. 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;
      }
      //...
    }
    
  2. 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;
    }
    
  3. 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>
    

viewer only

Add a Toolbar for the Viewer

Next, we can add a toolbar for the viewer. It has the following functions:

  1. Toggle the thumbnail viewer.
  2. Display the current page number and total page number.
  3. Set the display percentage (zoom).
  4. Scan documents.
  5. Load local PDF or image files.
  6. Save the document as a PDF file.
  7. 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

  1. 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;
    }
    
  2. 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...";
    
  3. 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:

viewer

Source Code

Get the source code of the PDF viewer web component to have a try:

https://github.com/tony-xlh/pdfviewer-component