PDF Reader

Episode #413 by Teacher's Avatar David Kimura

Summary

When previewing PDF files that were uploaded through Active Storage, we can only get a simple image of the first page. In this episode, we'll look at taking the uploaded PDFs and interacting with them.
rails uploads hotwire stimulusjs 18:49

Chapters

  • Introduction (0:00)
  • Setting up the application (2:01)
  • Setting up the view (3:11)
  • Setting up the stimulus controller (7:09)
  • Testing. the PDF reader (16:16)
  • Fixing a bug (16:52)
  • Demo (17:05)
  • Final Thoughts (17:41)

Resources

This episode is sponsored by Honeybadger
Download Source Code

Summary

# Terminal
rails g stimulus pdf_reader
yarn add pdfjs-dist
npm install -g node-gyp

# app/views/books/_book.html.erb
<div data-controller="pdf-reader" data-pdf-reader-url-value="<%= url_for(book.pdf) %>">
  <div class="row justify-content-center">
    <div class="col-12 col-md-8 d-flex justify-content-center">
      <canvas data-pdf-reader-target="canvas" class="w-100"></canvas>
    </div>
  </div>
  <div class="row justify-content-center mt-3">
    <div class="col-12 col-md-8 d-flex flex-column flex-md-row justify-content-between">
      <button
        data-action="click->pdf-reader#prevPage"
        class="btn btn-primary text-nowrap mb-2 mx-1 mb-md-0">
        Previous Page
      </button>

      <input type="number" min="1"
        data-pdf-reader-target="pageNumber"
        data-action="change->pdf-reader#changePage"
        class="form-control mb-2 mx-1 mb-md-0" />

      <button
        data-action="click->pdf-reader#nextPage"
        class="btn btn-primary text-nowrap mb-2 mx-1 mb-md-0">
        Next Page
      </button>
    </div>
  </div>
</div>

# app/javascript/controllers/pdf_reader_controller.js
import { Controller } from "@hotwired/stimulus"
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist/build/pdf'
import pdfjsWorker from "pdfjs-dist/build/pdf.worker.entry"

// Connects to data-controller="pdf-reader"
export default class extends Controller {
  static targets = ["canvas", "pageNumber"]
  static values = { url: String }

  currentPage = 1
  // this.currentPage
  pdf = null

  connect() {
    GlobalWorkerOptions.workerSrc = pdfjsWorker
    this.loadPdf()
  }

  async loadPdf() {
    const loadingTask = getDocument(this.urlValue)
    this.pdf = await loadingTask.promise
    this.pageNumberTarget.value = this.currentPage
    this.renderPage()
  }

  async renderPage() {
    const page = await this.pdf.getPage(this.currentPage)
    const viewport = page.getViewport({ scale: 1.75 })
    const canvas = this.canvasTarget
    canvas.width = viewport.width
    canvas.height = viewport.height

    const context = canvas.getContext("2d")
    const renderContext = {
      canvasContext: context,
      viewport: viewport
    }
    await page.render(renderContext)
  }

  prevPage() {
    if (this.currentPage > 1) {
      this.currentPage -= 1
      this.renderPage()
      this.pageNumberTarget.value = this.currentPage
    }
  }

  nextPage() {
    if (this.currentPage < this.pdf.numPages) {
      this.currentPage += 1
      this.renderPage()
      this.pageNumberTarget.value = this.currentPage
    }
  }

  changePage() {
    let requestedPage = Number(this.pageNumberTarget.value)
    if (requestedPage > 0 && requestedPage <= this.pdf.numPages) {
      this.currentPage = requestedPage
      this.renderPage()
    } else {
      alert(`Invalid page number (Max: ${this.pdf.numPages})`)
      this.pageNumberTarget.value = this.currentPage
    }
  }
}