PDF viewer journey

Recently we needed to implement a PDF viewer.

Our first attempt was to use a React package that used PDF.js under the hood. For some reason, it was not easy, because we couldn’t package the worker easily. There were errors on the build stage. We ended up referencing the worker from a CDN. It worked, but the UX was shitty.

For some time it was like that, but a couple of weeks later there was a request to fix the UX. Remembering the integration shitshow from the past, I was looking for something that was easier to integrate. That’s when I found PDFObject.js. It was really quick to integrate and personally felt like the right thing to do. Because you get the native experience. Feels intuitive. And it worked well. The only issue was that, this way, we couldn’t control the viewer the moment it loaded. No way to jump between pages, no way to highlight stuff. But it worked much smoother than the previous implementation.

About a month later two requests appeared: make the PDF zoomable via the touchpad gestures, add search functionality to the viewer and add the ability to highlight specific areas in the PDF. The zoom part was weird, because it worked with the PDFObject.js, but the search and highlight just were not possible with the native viewer. Because it completely depended on the browser’s implementation. So I continued my search.

I found these enterprise products: Apryse and Nutrient. They were relatively easy to integrate, the API is convoluted, but nothing critical. The issues we had with them were the crowded UI and the price. While the UI is customisable, the price is not. Apryse offered a yearly subscription for $5K. So we decided to look for something else.

The next two were also commercial products, but on a much smaller scale. React PDF Viewer and React PDF. Their pricing was much lower, $200–250 one-time payment. The integration was kind of OK. The UX was not great. Zooming was very choppy, and we were hesitant about going with them.

Then our CEO posted a link to the original PDF.js viewer, which is created by Mozilla itself. And it worked surprisingly well. So I had to return to the origins and start exploring the PDF.js library again. Creating an in-house viewer based on this library wasn’t the way I wanted to go. The library offered a low-level API that I had to build on top of. I had already spent about a week researching different options, so I just wanted to find a solution and be done with it. So the obvious way was to look at Mozilla’s viewer and try to make use of it.

Which ended up being not a very straightforward task. Long story short, I had to create a build step where the whole PDF.js build output is copied to the Next.js public folder and then in the React app it’s used via an iframe. All the commands to jump between pages and highlight areas are sent via message events. It was a bit of a nightmare, because you deal with an async thing, so you can't just shoot the events. You need to wait for the iframe to be ready, then for the doc to be ready and while these are in the process, everything else needs to be put in a queue. But after a lot of debugging, it finally worked.

For me, the outcome of this journey was a better understanding that sometimes you just need to try different approaches to the same thing. If this approach doesn’t really work or is a mess, try another. Even a hacky approach is good if it does the thing.