In-browser document viewer

Read Word, spreadsheet, presentation and PDF files in the browser without downloading them, in an accessible viewer built on pdf.js.

tech stack
  • pdf.js
  • React
  • TypeScript
my role
Front-end engineer (apprentice)
timeline
2023

I built this for my employer, so there’s no public demo or repo. I’m happy to walk through it in more detail in an interview.

The problem

On a learning platform, reading a document someone had uploaded meant downloading it and opening it in another app. That’s a lot of friction for something as ordinary as reading a file, and on a tablet or a locked-down machine it might not work at all. Viewing also depended on commercially licensed libraries, which came with a recurring cost.

My approach

We built the viewer from scratch on Mozilla’s open-source pdf.js. Word, spreadsheet and presentation files are converted to PDF before they reach the browser, so the viewer only has to render one format.

I set up the shared front-end package the viewer lives in and built its first component. On top of the rendering sit the controls people expect: zoom, paging and fullscreen. pdf.js draws a page onto a canvas at whatever scale you give it. We size the canvas for the screen’s pixel density as well, so text stays sharp at every zoom level on a high-resolution display:

const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale: zoom });
const ratio = window.devicePixelRatio || 1;

canvas.width = Math.floor(viewport.width * ratio);
canvas.height = Math.floor(viewport.height * ratio);
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;

await page.render({
    canvasContext: canvas.getContext("2d")!,
    viewport,
    transform: ratio === 1 ? undefined : [ratio, 0, 0, ratio, 0, 0],
}).promise;

Documents open fitted to the width of their container, so they’re readable straight away. pdf.js is loaded only when someone opens a document, which keeps a heavy library off the pages that never need it.

Accessibility

We make features fully accessible before they’re released, and anything we miss gets fixed on the feature branch first. A canvas is just pixels, though, so a viewer that only paints pages would be useless to a screen reader.

I added the pdf.js text layer, which is transparent text positioned exactly over the canvas. With it a screen reader can read the page, browser find works, and people can select and copy text. When pdf.js can’t extract the text from a page, the viewer still shows the page and tells people its text can’t be read.

I also added live announcements naming the file, the page and the zoom level as they change. Support for forced-colours (high-contrast) mode was my work too: the page is drawn in the colours the user has chosen. The fullscreen button checks the browser can actually go fullscreen before offering itself. All the toolbar controls work from the keyboard.

Challenges

Converting a document takes a few seconds, sometimes more, and an empty box for that long looks broken even when everything is fine. So the viewer reports where it’s up to. While a document is being prepared it shows waiting messages that change as time passes, so a long wait doesn’t look frozen. When the document is ready it renders straight away. If conversion fails or takes too long, people get a message saying so.

The errors are specific, too. A password-protected file, a file the viewer can’t open and a single page that fails each get their own message, telling people what they can do next. We also made the viewer cancel any rendering it hasn’t finished when someone leaves mid-conversion or switches document, so no stray renders or requests carry on in the background.

lesson learnt

Show progress, honestly

Naming each stage left conversion just as slow and made the wait look deliberate. The same goes for failure: saying a page’s text can’t be read is better than a viewer that looks accessible and isn’t.

Outcome

The viewer replaced the licensed libraries and their recurring cost, and nobody has to download a file just to read it.

Back to top