Preview PDF files from Base64 content in Power Pages using the PDFJs library

Natively, there is no out-of-the-box component for Power Pages to handle displaying PDF file content from Base64 values on a page (from cloud flows or Web API for example). This post brings a sample implementation using the PDFJs library.

This implementation also has the functionality to go to a specific page by number and zoom in/out.

The component can be used as a modal (using bootstrap modal) as below:

Or as an embedded viewer in the page as below:

Demonstration of the component working

See below the following functionality in action:

  • Page scrolling
  • Go to page
  • Zooming in/Zooming out

Component overview

In order to use this sample, you will need to create:

  • A Web File with the JavaScript custom code
  • A Web Template with the HTML/Liquid code

See below the details of each file and the contents.

JavaScript code

Create a new WebFile with the partial URL as pdfviewer.js, the parent page to be the home page of your site and the contents below:

function RenderPdfFromBase64(base64Content) {
  $("#txtPageNumber").val("");
  const byteCharacters = atob(base64Content);
  const byteNumbers = new Array(byteCharacters.length);
  for (let i = 0; i < byteCharacters.length; i++) {
    byteNumbers[i] = byteCharacters.charCodeAt(i);
  }
  const byteArray = new Uint8Array(byteNumbers);
  // Render PDF
  pdfjsLib.getDocument(byteArray).promise.then(pdf => {
    RenderPages(pdf);
  }).catch(err => {
    console.error('Error file contents:', err);
  });
}

// Function to render pages and setup pagination
function RenderPages(pdf) {
  const container = document.getElementById('pdfViewer');
  $("#pdfViewer").empty();
  $("#zoomOut").prop("disabled", true); 
  zoomScale = 1;
  $("#spnCurrentPage").text(`Page 1`);
  UpdateZoomLabel();

  function renderPage(pageNumber) {
    pdf.getPage(pageNumber).then(page => {
      const scale = 4;
      const viewport = page.getViewport({ scale });

      const canvas = document.createElement('canvas');
      canvas.id = `pdf-canvas-${pageNumber}`;

      canvas.style.width = "98%";
      const context = canvas.getContext('2d');
      canvas.height = viewport.height;
      canvas.width = viewport.width;

      const renderContext = {
        canvasContext: context,
        viewport: viewport
      };

      page.render(renderContext);

      //add label with page number
      const label = document.createElement('div');
      label.className = "page-label";
      label.textContent = `Page ${pageNumber}`;
      container.appendChild(label);

      container.appendChild(canvas);
    });

  }

  for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
    renderPage(pageNum);
  }

  $("#spnCurrentPage").text(`Page 1 of ${pdf.numPages}`)
  $("#pdfViewerWrapper").show();
  if (displayPDFInModal) {
    $("#pdfModal").modal("show");
  }

}
var zoomScale = 1;

function UpdateZoomLabel() {
  $("#zoomValue").text(`${Math.round(zoomScale * 100)}%`);
}

function UpdateCanvasWidth() {
  $("canvas[id^='pdf-canvas']").css("width", `${100 * 0.98 * zoomScale}%`);
}

$(document).ready(function () {
  const zoomInBtn = $("#zoomIn");
  const zoomOutBtn = $("#zoomOut");
  let container = $("#pdfViewer");

  const btnGoToPage = $("#btnGoToPage");
  //on scroll, when a page is in view, update the current page number
  container.on("scroll", () => {
    let pageCanvas = $("canvas[id^='pdf-canvas']");
    pageCanvas.each((index, canvas) => {
      let canvasTop = $(canvas).offset().top;
      let containerTop = container.offset().top;
      let containerBottom = containerTop + container.height();

      if (canvasTop >= containerTop && canvasTop <= containerBottom) {
        $("#spnCurrentPage").text(`Page ${index + 1} of ${pageCanvas.length}`);
      }
    });
  });

  btnGoToPage.on("click", () => {
    let pageNumber = parseInt($("#txtPageNumber").val());
    if (pageNumber > 0) {
      //scroll to the page
      let pageCanvas = $(`#pdf-canvas-${pageNumber}`);
      if (pageCanvas.length > 0) {
        pageCanvas[0].scrollIntoView();
        $("#spnCurrentPage").text(`Page ${pageNumber} of ${$("canvas[id^='pdf-canvas']").length}`);
      }
      else {
        alert("Page number is not valid");
      }
    }
    else {
      alert("Page number is not valid");
    }
  });

  zoomInBtn.on("click", () => {
    zoomScale += 0.5;
    if (zoomScale >= 3) {
      zoomInBtn.prop("disabled", true);
    }
    zoomOutBtn.prop("disabled", false);
    UpdateCanvasWidth();
    UpdateZoomLabel();
  });

  zoomOutBtn.on("click", () => {
    zoomScale -= 0.5;
    if (zoomScale <= 1) {
      zoomOutBtn.prop("disabled", true);
    }

    UpdateCanvasWidth()
    UpdateZoomLabel();
  });
});

Explanation of the JavaScript Code

The renderPdfFromBase64 function receives and renders base64-encoded PDF content. The base64 content is then decoded into a byte array and passed to the PDF.js library to load the PDF document.

The renderPages function handles the rendering of all pages in the PDF document. It first clears the PDF viewer container and resets the zoom scale and current page display. It then defines an inner function renderPage to render individual pages. This inner function retrieves a page from the PDF document, creates a canvas element for it, and renders the page onto the canvas. Each canvas is appended to the PDF viewer container together with a label indicating the page number.

The UpdateZoomLabel and UpdateCanvasWidth functions are functions used to update the zoom label and adjust the canvas width based on the current zoom scale.

The $(document).ready function sets up event handlers for all the UI elements linked to the functions specified above.

The WebTemplate

The code is made available as a single WebTemplate containing the liquid code and the CSS styles for simplicity, but ideally, we’d have the CSS contents as separate web files.

<style>
    #pdfViewer {
        width: 100%;
        height: 600px;
        background-color: #525659;
        overflow: auto;   
    }

    #pdfViewer canvas {        
        box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1);
        margin-left: 0.5rem;
        margin-right:0.5rem;
    }

    #pdfViewerWrapper {
        width: 100%;
        height: 650px;
        max-width: 880px; 
    }

    #pdfToolBox {
        background-color: #323639;
        display: flex;
        justify-content: space-between;
        gap: 0.5rem;
        align-items: center;
        padding: 0.5rem
    }

    #pdfToolBox span {
        color: white;
    }

    .page-label{
        color: white;
        display: flex;
    justify-content: center;
    }

    #pdfToolBox button {
        color: white;
    }
    .pdfViewerWrapperNoModal {
        width: 90vw;
        height: 650px !important;
        max-width: 1024px;       
        margin-left: auto;
        margin-right: auto;      
    }
    .pdfViewerNoModal{
        height: 600px !important;
    }
</style>    
{%comment%}Import PDF Js library{%endcomment%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> 
<script src="/pdfviewer.js"></script>

<script>
var displayPDFInModal = "{{displayPDFInModal}}"==="true";
</script>


{% if displayPDFInModal %}
<div class="modal" tabindex="-1" role="dialog" id="pdfModal">
    <div class="modal-dialog  modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"><span id="spnModalTitle"></h5>

                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
{%endif %}
                <div id="pdfViewerWrapper" style="display: none;" class="{% unless displayPDFInModal %}pdfViewerWrapperNoModal{%endunless%}">
                    <div id="pdfToolBox">
                        <div>
                            <span id="spnCurrentPage"></span>                          
                        </div>
                        
                        <div>
                            <input type="number" id="txtPageNumber" placeholder="Go to page" style="width: 120px;" />
                            <button id="btnGoToPage" class="btn btn-default">Go</button>
                        </div>
                        <div>
                            <button id="zoomIn" class="btn btn-default">
                                <span class="glyphicon glyphicon-zoom-in">
                                    
                                </span>
                            </button>
                            <span id="zoomValue"></span>
                            <button id="zoomOut" class="btn btn-default">
                                <span class="glyphicon glyphicon-zoom-in">
                                  
                                </span>
                            </button>
                        </div>
                    </div>
                    <div id="pdfViewer" class="{% unless displayPDFInModal%} pdfViewerNoModal{%endunless%}"></div>
                </div>               
{%if displayPDFInModal%} 
  {%comment%} If modal, render divs that close the modal {%endcomment%}

            </div>          
        </div>
    </div>
    </div>
{%endif%}

Explanation of the Liquid Code

The WebTemplate takes a parameter saying whether it should render the PDF in a modal or not. If rendered in a modal, we use a bootstrap modal to display the PDF file content, otherwise it displays it embedded in a page (this is handled by liquid logic, and also setting a javascript variable based on the parameter).

How to use it

You can use it on any page where you can pass a PDF file content as Base64 content to this function.

To follow the sample above, you need to:

1) Create a new WebTemplate using the sample code, and name it “PDF Viewer” (either via VS code or Power Pages Management App) + the related web file for the pdfviewer.js code.

2) Insert the component in the pages you want to use below as below (use the param to display PDF in modal accordingly as you wish):

{% include 'PDF Viewer',displayPDFInModal:true%}

Example – Using with Power Pages Web API

Considering you have enabled the Rich Text Attachments table for Web API, the user has permissions on it and you have the Web API Wrapper code in the page, you could use this function to load the a PDF file in the page by ID:

  function PreviewDataverseRTFFile(fileId){
    webapi.safeAjax({
        type: "GET",
        url: `/_api/msdyn_richtextfiles(${fileId})/msdyn_fileblob`,
        contentType: "application/json",
        headers: {
          "Prefer": "odata.include-annotations=*"
        },
        success: function (data, textStatus, xhr) {
          var result = data["value"];      
          RenderPdfFromBase64(result);
        },
        error: function (xhr, textStatus, errorThrown) {
          console.log(xhr);
        }
      });
  }

Example – Using it with Power Automate flow outputs

If we use this component in my previous sample:

Download SharePoint library files in Power Pages with JavaScript and Cloud flows – Improved code using jQuery & option to open PDF file in a new tab

On the function to open a PDF file from my previous sample, replace this function call:

 OpenSharePointPDFInNewTab(fileData);

By this:

RenderPdfFromBase64(fileData.filecontents)

This will make the PDF file to be displayed in the component instead of being opened on a tab as in the previous post.

Note: As the code uses multiple canvases to render the PDF content, for large PDFs the performance is not ideal. Maybe an implementation rendering one page at a time would be better, but this is subject for a future post😊

There also can be CSS improvements on the page styling, as this post is just a proof of concept to illustrate the functionality, and the styling was created in a basic manner.

Conclusion

Using PDF Js we can preview PDF documents from their Base64 values in Power Pages sites. This post only illustrates the concept, there are a few points that might be improved for ideal usage.

Be aware of licensing usage of the PDFJs library whenever you use it.

Leave a Reply

Your email address will not be published. Required fields are marked *