Nerdy Creativity

Adding PDF viewer to Eleventy

I wanted to add a PDF viewer to Eleventy using PDF.js.

At first, I installed PDF.js from npm. However, the npm version only works on Firefox and Chrome. To get a version that works on Safari, I needed to use the legacy build version.

I cloned PDF.js repo and installed dependencies.

git clone https://github.com/mozilla/pdf.js.git
cd pdf.js
npm install

Then I built the legacy version.

npx gulp generic-legacy

This generated pdf.js and pdf.worker.js in the build/generic-legacy/build/ directory. I copied pdf.js and pdf.worker.js to /public/lib/pdfjs/generic-legacy/build.

Then I put the code to create a viewer in public/lib/pdfviewer.js. The code is based on PDF.js Prev/Next example and mobile viewer example.

// set workerSrc property
pdfjsLib.GlobalWorkerOptions.workerSrc =
  "/lib/pdfjs/generic-legacy/build/pdf.worker.mjs";

let pdfDoc = null;
let pageNum = 1;
let pageRendering = false;
let pageNumPending = null;
let scale = 1;
let canvas = document.getElementById("the-canvas");
let ctx = canvas.getContext("2d");
const DEFAULT_SCALE_DELTA = 1.1;
const MIN_SCALE = 0.25;
const MAX_SCALE = 10.0;

/**
 * Get page info from document, resize canvas accordingly, and render page.
 */
function renderPage(num) {
  if (canvas == null) return;

  pageRendering = true;
  // Using promise to fetch the page
  pdfDoc.getPage(num).then(function (page) {
    var viewport = page.getViewport({ scale: scale });
    // Support HiDPI-screens.
    var outputScale = window.devicePixelRatio || 1;

    canvas.width = Math.floor(viewport.width * outputScale);
    canvas.height = Math.floor(viewport.height * outputScale);
    canvas.style.width = Math.floor(viewport.width) + "px";
    canvas.style.height = Math.floor(viewport.height) + "px";

    var transform =
      outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null;

    // Render PDF page into canvas context
    var renderContext = {
      canvasContext: ctx,
      transform: transform,
      viewport: viewport,
    };
    var renderTask = page.render(renderContext);

    // Wait for rendering to finish
    renderTask.promise.then(function () {
      pageRendering = false;
      if (pageNumPending !== null) {
        // New page rendering is pending
        renderPage(pageNumPending);
        pageNumPending = null;
      }
    });
  });

  // Update page counters
  document.getElementById("page_num").textContent = num;
}

/**
 * If another page rendering in progress, waits until the rendering is
 * finished. Otherwise, executes rendering immediately.
 */
function queueRenderPage(num) {
  if (pageRendering) {
    pageNumPending = num;
  } else {
    renderPage(num);
  }
}

/**
 * Displays previous page.
 */
function onPrevPage() {
  if (pageNum <= 1) {
    return;
  }
  pageNum--;
  queueRenderPage(pageNum);
}

/**
 * Displays next page.
 */
function onNextPage() {
  if (pageNum >= pdfDoc.numPages) {
    return;
  }
  pageNum++;
  queueRenderPage(pageNum);
}

/**
 * Zoom in
 */
function pdfViewZoomIn() {
  let newScale = scale;
  newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
  newScale = Math.ceil(newScale * 10) / 10;
  newScale = Math.min(MAX_SCALE, newScale);
  scale = newScale;

  queueRenderPage(pageNum);
}

/**
 * Zoom out
 */
function pdfViewZoomOut() {
  let newScale = scale;
  newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
  newScale = Math.floor(newScale * 10) / 10;
  newScale = Math.max(MIN_SCALE, newScale);
  scale = newScale;

  queueRenderPage(pageNum);
}

/**
 * Setup event handlers
 */
function setupEventHandlers() {
  let prevEl = document.getElementById("prev");
  if (prevEl) prevEl.addEventListener("click", onPrevPage);

  let nextEl = document.getElementById("next");
  if (nextEl) nextEl.addEventListener("click", onNextPage);

  let zoomInEl = document.getElementById("zoomIn");
  if (zoomInEl) zoomInEl.addEventListener("click", pdfViewZoomIn);

  let zoomOutEl = document.getElementById("zoomOut");
  if (zoomOutEl) zoomOutEl.addEventListener("click", pdfViewZoomOut);
}

export async function init(url) {
  setupEventHandlers();

  // download pdf
  var loadingTask = pdfjsLib.getDocument(url);
  pdfDoc = await loadingTask.promise;

  // set page count
  let countEl = document.getElementById("page_count");
  if (countEl) countEl.textContent = pdfDoc.numPages;

  // render first page
  renderPage(pageNum);
}

I added a template file in _includes/pdfviewer.njk. pdfviewer.njk calls the init() from pdfviewer.js with pdfUrl and pdfScale arguments.


<style>
  {% include "css/pdfviewer.css" %}
</style>

<section class="pdfviewer">
  <div>
    <button id="prev" type="button" title="previous page">Previous</button>
    <button id="next" type="button" title="next page">Next</button>
    <button id="zoomIn" type="button" title="zoom in">+</button>
    <button id="zoomOut" type="button" title="zoom out">-</button>
    <span
      >Page: <span id="page_num"></span> / <span id="page_count"></span
    ></span>
  </div>

  <canvas id="the-canvas" style="border: 1px solid black;"></canvas>
</section>

<script src="/lib/pdfjs/generic-legacy/build/pdf.mjs" type="module"></script>
<script type="module">
  import {init} from '/pdfviewer.js'
  init('{{pdfUrl}}', {{pdfScale}})
</script>

Then in the file I want to add a pdf, I included pdfviewer.njk and passed in the pdfUrl.


{% set pdfUrl = '/assets/pdfs/pcc-portfolio/inat_la_river.pdf' %}
{% include "pdfviewer.njk" %}

To adjust the size of the pdf, I passed in pdfScale argument. If it is not set, the value defaults to 1.


{% set pdfUrl = '/assets/pdfs/pcc-portfolio/inat_la_river.pdf' %}
{% set pdfScale = '.4' %}
{% include "pdfviewer.njk" %}

Here's a PDF.

Page: /