PDFTron is a commercial PDF generation and manipulation library. PDFTron can be used to generate, manipulate, optimise, print, view, and markup PDF's. In this article we're going to setup PDFTron on a server and create a small Symfony3 application for displaying PDF's using the PDFTron WebViewer. We'll also take a look at how to perform some other basic functions of the the PDFTron library.

Getting started

Get the code here and checkout the "tutorial-start" branch. Once you've got the code, install dependencies with composer.


composer install

We're going to keep the app as small as possible. To do that we'll be implementing Symfony's MicroKernelTrait. That means we can build our entire application in one single file. We'll call that file MicroKernel.php.

MicroKernel.php

In our micro kernel we're going to do three things:

  1. Register bundles
  2. Define routes, MicroKernel::configureBundles()
  3. Load configuration and services into the dependency inject container

Here's what our MicroKernel looks like to start with:


// app/MicroKernel.php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Bundle\TwigBundle\TwigBundle;

/**
 * Class MicroKernel
 */
class MicroKernel extends Kernel
{
    use MicroKernelTrait;
    /**
     * @return array
     */
    public function registerBundles()
    {
        return [
            new FrameworkBundle(),
            new TwigBundle(),
        ];
    }

    /**
     * @param \Symfony\Component\Routing\RouteCollectionBuilder $routes
     */
    protected function configureRoutes(\Symfony\Component\Routing\RouteCollectionBuilder $routes)
    {
        $routes->add('/', 'kernel:PDFTronWebViewer');
    }

    /**
     * @param ContainerBuilder $c
     * @param LoaderInterface $loader
     */
    protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
    {
        $loader->load(__DIR__ . '/config/config_micro.yml');
        $loader->load(__DIR__ . '/config/services.yml');
        $c->loadFromExtension('framework', [
            'secret' => 'GoodToGoSecret',
        ]);
    }
    public function PDFTronWebViewer()
    {
        return new JsonResponse([
            'Good to go!'
        ]);
    }
}

Registering bundles

In the MicroKernel::registerBundles() we register two bundles, the Symfony Framework bundle and the twig bundle, by returning an array and bundle objects.

Defining routes

In the MicroKernel::configureRoutes() method there is one route defined which maps to the MicroKernel::PDFTronWebViewer method. Load the site in your browser and you should see "Good to go".

Loading configuration and services

In MicroKernel::configureContainer() we are loading a configuration file and a service definition file. The Framework bundle also requires us to set a 'secret' parameter

Installing PDFTron

Now that the basic application is set up we install install PDFtron/PDFNet. The PDFNet library can be downloaded here. As you can see there isn't actually any link to download a PHP version!

If you require different binaries for a specific programming environment, it is possible to make your own PDFNet bindings

To the use the library with PHP we need to create out own PDFNet bindings! It's actually super simple to do. There are however issues creating said 'bindings' on OSx though. Which means developing on localhost is not possible. You'll have to use vagrant or docker instead.

I have successfully installed it on linux vagrant boxes and docker containers running PHP versions 5.5 and 5.6. If you're using PHP7 good luck to you!

In the root of your project, run these commands:


# Assume cmake and swig, wget are not installed on the server already.
apt-get update
apt-get install -y swig cmake wget git

This installs CMake and Swig, which are needed to create the "bindings"!


# get the wrappers repo
git clone https://github.com/irishdan/PDFNetWrappers

# Move to where we download PDFNet.
cd PDFNetWrappers/PDFNetC

This downloads wrappers needed for "binding" PDFNet (a library built in C) to PHP. And moves inside the downloaded library


# Download PDFNet.
wget http://www.pdftron.com/downloads/PDFNetC64.tar.gz

# Is better Unpack PDFNet.
tar xzvf PDFNetC64.tar.gz

# Move PDFNet Headers/ into place.
mv PDFNetC64/Headers/ .

# Move PDFNet Lib/ into place.
mv PDFNetC64/Lib/ .

# Go back up.
cd ..

# Create a directory to create the Makefiles in.
mkdir Build

# Move to that directory.
cd Build

This downloads the PDFNetC library to the correct location and moves some directories and creates a new "Build" directory. This is where we shall create our "bindings"


# Create the Makefiles with CMake.
cmake -D BUILD_PDFNetPHP=ON ..

# Build the PHP wrappers with SWIG.
make

# Copy the PHP wrappers to suitable location
make install

Thats it. PDFTron is now installed and ready to use on your server!

Gotchas

Have a look at the generated Library here PDFNetWrappers/PDFNetC/Lib/PDFNetPHP.php.

The generated PHP library is not properly name spaced, which might cause issues. For example, if you are using Laravel the 'Config' facade will conlfict the PDFtron's Config class!!

Creating our Mobile friendly PDF reader application

In order to use the PDFNet WebViewer, we need to convert our PDF's into PDFTron's XOD format. When a user visits the application a couple of things will happen:

  1. We'll determine which PDF the user is trying to view
  2. Then convert that PDF into XOD format, if it hasn't already been converted.
  3. We'll also create a thumbnail image from the first page of the PDF just because we can ;).
  4. Finally load the XOD file into the PDFNet WebViewer and display it to the user

Where are all these files anyway?

We need a place to store PDF files, converted XOD files and thumbnail images, so lets define some directories first. The PDF directory should not be publicly accessible so lets just put it at the root or the project. The XOD and Image directory need to be publicly accessible so lets put them in the web directory. Lets just define them as constants on our MicroKernel class.


// app/MicroKernel.php

class MicroKernel extends Kernel
{
    use MicroKernelTrait;
    
    const PDF_DIRECTORY = 'pdf';
    const XOD_DIRECTORY = 'web/xod';
    const IMAGE_DIRECTORY = 'web/images’;

..

Which PDF is being viewed

In our PDF reader application we need a route which maps to a PDF document. In our MicroKernel class configureRoutes method, alter the route definition to accept the PDF name as a parameter, and add the parameter to the PDFTronWebViewer method


// app/MicroKernel.php
...

protected function configureRoutes(\Symfony\Component\Routing\RouteCollectionBuilder $routes)
{
        $routes->add('/{filename}', 'kernel:PDFTronWebViewer');
}

...
public function PDFTronWebViewer($filename)
{
...

Now in the PDFTronWebViewer method we have a the PDF filename as a paramater. We need to locate that file on system. At this stage you should probably add a PDF, or a couple pf PDFs to the PDF directory we defined earlier. When dealing with files we can use Symfony's Filesystem service.

Create two new methods called getPDFSystemPath and appendExtensionIfMissing.


// app/MicroKernel.php
...

protected function getPDFSystemPath($filename)
{
    // 
    $this->appendExtensionIfMissing($filename, 'pdf');

    $rootDirectory = $this->getContainer()->getParameter('kernel.root_dir');
    $PDFDirectory = $rootDirectory . '/../' . self::PDF_DIRECTORY;
    $PDFPath = $PDFDirectory . '/' . $filename;

    if (!$this->fileSystem->exists($PDFPath)) {
        return $PDFPath;
    }

    return false;
}

protected function appendExtensionIfMissing(&$filename, $extension = 'pdf')
{
    if (!preg_match('/(\.' . $extension . ')$/i', $filename)) {
        $filename .= '.' . $extension;
    }
}

Now, use the Filesystem service to try an locate the PDF file in the system. If the File doesn't exist, notify the user.


// app/MicroKernel.php
..
class MicroKernel extends Kernel
{
    ...
    protected $fileSystem;
    ...
    public function PDFTronWebViewer($filename)
    {
        $this->fileSystem = $this->getContainer()->get('filesystem');

        // Convert the filename to system filepath if the PDF exists
        $PDFSystemPath = $this->getPDFSystemPath($filename);
        if (!empty($PDFSystemPath)) {
            return new Response('No such PDF file exists ' . $filename);
        }


Try and locate a converted XOD file exists. Add a new method called getXODSystemPath


// app/MicroKernel.php
..
class MicroKernel extends Kernel
{
    ...
    protected function getXODSystemPath($filename)
    {
        $this->appendExtensionIfMissing($filename, 'xod');
        $rootDirectory = $this->getContainer()->getParameter('kernel.root_dir');
        return $rootDirectory . '/../' . self::XOD_DIRECTORY . '/' . $filename;
    }

And use it in the PDFTronWebViewer method to determine if we need to convert the PDF into XOD version or not.


// app/MicroKernel.php
..
class MicroKernel extends Kernel
{
    ...
    protected $fileSystem;
    ...
    public function PDFTronWebViewer($filename)
    {
        ...
        // Get the XOD system path.
        $XODSystemPath = $this->getXODSystemPath($filename);
        if (!$this->fileSystem->exists($XODSystemPath)) {
             ...

        }


Now we're need to convert the PDF into the XOD using the PDFTron library. To use the library you need to first load the PHP library that we generated during the PDFNet install process.


// app/MicroKernel.php
..
class MicroKernel extends Kernel
{
    ...
    protected $fileSystem;
    ...
    public function PDFTronWebViewer($filename)
    {
        ...
       require_once(__DIR__ . '/../PDFNetWrappers/PDFNetC/Lib/PDFNetPHP.php’);

The code to convert PDF's to XOD pretty straight forward. PDFTron needs the location of the PDF and the location where the converted XOD file should be stored. We already have both of these values.

We can also define options to control certain properties of the output XOD file using an XODOutputOptions object. Available options include JPG quality and "dots per inch".


// app/MicroKernel.php
..
class MicroKernel extends Kernel
{
    ...
    protected $fileSystem;
    ...
    public function PDFTronWebViewer($filename)
    {
        ...
        \PDFNet::Initialize();

        // Add some options.
        // Not required
        $xodOptions = new \XODOutputOptions();

        try {
            \Convert::ToXOD($PDFSystemPath, $XODSystemPath, $xodOptions);
        }
        catch (\Exception $e) {
            return new Response('Ooops unable to create XOD file!!');
        }


We should now have am XOD file ready to be displayed in the WebViewer!

3: We can create a thumbnail image from the first page of the PDF easily.


// Generate a thumbnail from the first page of the PDF.
$imageSystemPath = '';
$dpi = 72;
$imageType = 'JPEG';
$page = 1; // We can select any page in the PDF.

$doc = new \PDFDoc($PDFSystemPath);

// should be called immediately after an encrypted document is opened
$doc->InitSecurityHandler();

// Get the page.
$PDFPage = $doc->GetPage($page);

// Create an image from the PDF.
try {
    $draw = new \PDFDraw();
    $draw->SetDPI($dpi);
    $draw->Export($PDFPage, $imageSystemPath, $imageType);
} catch (\Exception $e) {
    return new Response('Ooops unable to create the image file!!');
}

$doc->Close();

The WebViewer

Now that we have a XOD file we shall use PDTron's Webviewer to view it.

Download the the webviewer, unzip and place it in the publicly accessible "web" directory. The webviewer requires a url to the XOD file, so first we need to find that. Earlier we set our XOD directory to "web/xod". Lets create a method to convert that into a relative url.


protected function getXODWebPath($filename) {
    $this->appendExtensionIfMissing($filename, 'xod');
    
    return 'xod/' . $filename;
}

Once we have our web path, we simply pass it into a twig template and render it


// Web path to the XOD file.
$path = $this->getXODWebPath($filename);

// Render the Web-viewer.
return new Response($this->getContainer()->get('templating')->render('webviewer.html.twig', [
        'xodPath' => $path]
));

The twig template is straight forward


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>{% block title %}PDFTron Symfony app{% endblock %}
    <style type="text/css">
        #viewer {
            height: 768px;
            border: 1px solid grey;
        }
    </style>
    <!-- The jQuery path may need to be updated if when WebViewer updates to new version -->
    <script type="text/javascript" src="/WebViewer/jquery-3.2.1.min.js">
    <script type="text/javascript" src="/WebViewer/lib/WebViewer.min.js">
    <script type="text/javascript">
        var xodPath = '{{ xodPath }}';
        $(function () {
            var viewerElement = document.getElementById('viewer');
            function initPreconverted() {
                myWebViewer = new PDFTron.WebViewer({
                    path: "/WebViewer",
                    initialDoc: xodPath,
                    streaming: "false"
                }, viewerElement);
            }
            initPreconverted();
        });
    </script>
</head>
<body>
<div id="viewer"></div>
</body>
</html>

And thats it. You know have PDFTron powered mobile friendly PDF reader.

Conclusion

As you can see, it's fairly easy to get set up with PDFTron. This code is obviously for demo purposes. In reality there is quite a lot going on in the PDFTronWebViewer method. This should all be refactored into different services so that all of the functionality is reusable

I have already refactored the code into its own Symfony bundle which also includes some services and commands which utilise more of PDFTron's features. Download the bundle here

Tags:

PHP Symfony

Comments:

Get in touch

Contact form

Please enter your name
Please enter a valid email address

I'll be in touch shortly.

Contact details

Contact Info

Feel free to get in touch about your next project

Office address

If you have any questions or queries, please send me an email or give me a call!

Email

hi@danbyrne.me