Embedding a PDF viewer in a Wails application

I am enjoying using Wails, a Go UI framework which allows you to build desktop applications using Go & Web Technologies. So I thought it would be interesting to try to embed PDF.js in a Wails application. After spending some time working with PDF.js directly and getting it to work, I decided to try React-PDF since I am most likely to build something with React or Vue anyways.

This article will go over how to embed a basic PDF viewer in a Wails application starting from the Wails' React template. It is not meant to be a comprehensive tutorial so I won't go into styling the PDF reader or anything.

I have put the complete code in a repository here

NOTE: this article is using Wails v2.0.0-beta.35

Setup Wails Project

$ wails init -n pdf-in-wails -t react

Install React and React-PDF

$ cd pdf-in-wails

$ cd frontend

$ npm install --save react react-pdf fast-base64

$ cd ..

Read a Document from the File System with Go

We are going to use the Go backend to read the PDF from the file system. In order to do so we will use the Wails' runtime package which provides a function to Open a file dialog and then use the standard ioutil.ReadAll function to read the file into a byte slice.

In the app.go file in the project add this content

package main

import (
    "context"
    "fmt"
    "io/ioutil"
    "github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
    ctx context.Context
}

// NewApp creates a new App application struct
func NewApp() *App {
    return &App{}
}

// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
    return fmt.Sprintf("Hello %s, It's show time!", name)
}

// startup is called at application startup
func (a *App) startup(ctx context.Context) {
    // Perform your setup here
    a.ctx = ctx
}

func (a *App) OpenAndGetPDFData() ([]byte, error) {
    filters := []runtime.FileFilter{
        {DisplayName: "PDF Documents (*.pdf)", Pattern: "*.pdf"},
    }
    filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
        Filters:         filters,
        ShowHiddenFiles: false,
    })
    if err != nil {
        return nil, err
    }

    return ioutil.ReadFile(filePath)
}

Register the OnStartup function in main.go

 // ... other lines ignored
func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        Title:     "pdf-in-wails",
        Width:     1024,
        Height:    768,
        Assets:    assets,
        OnStartup: app.startup,
        Bind: []interface{}{
            app,
        },
    })

    if err != nil {
        println("Error:", err)
    }
}

Adding a button to trigger the action with React

Since Wails User Interfaces are driven by JavaScript, we need to add some way to triggger the function we just created.

You will notice the import for OpenAndGetPDFData - this function binding is generated automatically by Wails when you run wails build or wails dev or you can run it directly with wails generate module.

Replace the App.jsx with the following.

import './App.css';
import {useState} from 'react';
import {Document, Page, pdfjs} from 'react-pdf';
import {toBytes} from 'fast-base64';
import {OpenAndGetPDFData} from "../wailsjs/go/main/App";

function App() {
    pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.js`;
    const [pdfDocument, setPdfDocument] = useState(null)
    const [numPages, setNumPages] = useState(null);
    const [pageNumber, setPageNumber] = useState(1);

    function onDocumentLoadSuccess({ numPages }) {
      setNumPages(numPages);
    }

    async function openPDF() {
        try {
            const pdfFile = await OpenAndGetPDFData()
            const pdfBytes = await toBytes(pdfFile)
            setPdfDocument({data: pdfBytes })
        } catch(e) {
            console.log("Failed to open pdf", e)
        }
    }

    return (
        <div id="App">
            <button className="btn" onClick={openPDF}>Open PDF</button>
            <Document file={pdfDocument} 
                    onLoadSuccess={onDocumentLoadSuccess} 
                    onLoadError={(error) => alert('Error while loading document! ' + error.message)}
                    onSourceError={(error) => alert('Error while retrieving document source! ' + error.message)}>
                <Page pageNumber={pageNumber} />
            </Document>
            <p>
                Page {pageNumber} of {numPages}
            </p>
        </div>
    )
}

export default App

Wails sends []byte as a Base64 encoded string

One thing you will note is that we are using the toBytes function from the fast-base64 library to get the data of the PDF file on the JavaScript side. This is because Wails serializes the byte slice as a Base64 encoded string when sending the data from the Go side. Other data structures are serialized as JavaScript objects appropriately.

Run it

You can then run the application using Wails dev which will startup a webserver with hot reloading and open a New Window with a running instance of the application.

In order to build the application as a binary you need only to run wails build

$ wails dev

Here are screenshots from my build of the same

Screenshot 2022-05-22 163122.png

With a PDF opened

Screenshot 2022-05-22 162817.png

Conclusion

Wails is an interesting technology for building desktop applications which is worth a look if you want to keep using web technologies to build desktop applications and as an alternative to something like Electron. We have seen how we can embed a PDF viewer in Wails easily using React PDF library (though you can do it without React).

Thanks for reading.