Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

Building a browser-only file viewer for 100+ formats: lazy parsers, no uploads, and honest Office rendering

I'm the solo developer behind MakeQuickWork, a small collection of free browser tools. This post is about how I built one of them, the File Viewer, and the trade-offs I hit along the way. The problem is boring and unive

Building a browser-only file viewer for 100+ formats: lazy parsers, no uploads, and honest Office rendering


I'm the solo developer behind MakeQuickWork, a small collection of free browser tools. This post is about how I built one of them, the File Viewer, and the trade-offs I hit along the way.

The problem is boring and universal: someone sends you a .docx, a .pptx, a .heic photo or a .msg email, and the machine in front of you can't open it. A Chromebook. A borrowed laptop. A work PC without Office. A phone. The usual fix is an "online viewer" that uploads your file to a server, converts it there and sends back a preview. That works, but it means a copy of your contract or payslip now lives on someone else's disk.

I wanted the opposite: drop a file onto a web page, and have the page itself parse and render it, with nothing leaving the browser.

What it opens

The current list is a bit over 100 extensions:

  • Documents: PDF, Word (doc, docx, rtf, odt), Excel (xls, xlsx, csv, ods), PowerPoint (ppt, pptx, odp), EPUB
  • Media: images including HEIC and TIFF, audio, video
  • Text and code files
  • Email: eml, msg, mbox
  • Diagrams: draw.io, Excalidraw, XMind
  • 3D: glTF, OBJ, STL, FBX
  • GIS: GeoJSON, KML, GPX, Shapefile
  • Basic CAD: DXF, STEP, IFC
  • Archives: ZIP, TAR, GZ/TGZ can be browsed and their files previewed

There's one toolbar for all of them: zoom, rotate, search, print, full screen, download.

Architecture: a static page and a router

The site is built with Astro as a fully static site and hosted on Cloudflare Pages. There's no backend for the file tools at all. The page is HTML, CSS and a small amount of JS for the drop zone. When you pick a file, the client:

  1. Reads it as a File / ArrayBuffer locally.
  2. Works out the format (worth being careful here: a .docx, an .xlsx and a .zip are all ZIP containers underneath).
  3. Hands it to the matching renderer.

Each renderer is a module that turns bytes into something the browser can display. PDFs go through PDF.js (Mozilla's renderer). ZIP browsing uses fflate, which is small and fast enough to list a large archive and inflate a single entry on demand without unpacking everything.

Lazy-loading the heavy parsers

This is the part that made it practical. A PDF renderer, a spreadsheet parser, a 3D engine, a HEIC decoder and a CAD reader together come to several megabytes of JavaScript. If the landing page shipped all of that, it would be slow on exactly the devices people use it on: phones and cheap Chromebooks.

So the page ships almost nothing up front, and each parser is pulled in with a dynamic import() only after the user chooses a file of that type. Simplified, the shape is:

// Map a detected format to a renderer module.
// Nothing here is downloaded until load() is called.
const renderers = {
  pdf:   () => import("./renderers/pdf.js"),
  docx:  () => import("./renderers/docx.js"),
  xlsx:  () => import("./renderers/sheet.js"),
  zip:   () => import("./renderers/archive.js"),
  stl:   () => import("./renderers/model3d.js"),
  // ...many extensions share one renderer module
};

export async function openFile(file, container) {
  const kind = detectKind(file);              // e.g. from the extension
  const load = renderers[kind] ?? renderers.text;
  const { render } = await load();            // code fetched now, file stays local
  const bytes = new Uint8Array(await file.arrayBuffer());
  await render(bytes, container, { name: file.name });
}

The bundler splits each renderer into its own chunk, so opening a PDF never downloads the 3D engine. The network request you see at that moment fetches code from the same origin. The file itself is never sent anywhere.

One note on that: the page still needs an internet connection to load the tool and those chunks. I don't claim it works offline, and I won't until it actually does.

Why Office rendering is approximate

This is the honest part. A .docx is a ZIP of XML files. Parsing it is straightforward. Laying it out the way Word does is not. Word's layout engine handles line breaking, kerning, fonts embedded or substituted, floating images, section breaks, headers and footers, and many compatibility quirks. None of that is in the file. It's in Word.

A browser renderer rebuilds the document from its structure: paragraphs, runs, styles, tables, images. It then lets the browser's own layout engine place them. In practice:

  • Simple and medium documents (headings, paragraphs, lists, tables, inline images) look very close to the original.
  • Complex layouts (text boxes over images, multi-column sections, heavy use of fonts that aren't installed) can differ: different line breaks, a shifted image, a substitute font.

Spreadsheets and slides are similar. The data and text come through reliably. Pixel-perfect layout does not always.

I say this on the site too. For "what does this attachment say", it's enough. For checking final print layout, use the real application.

Archives: what's real and what isn't

ZIP, TAR and GZ/TGZ are fully browsable: you see the tree and can click any entry to preview it with the matching renderer. That's handy for checking what's inside a download before you extract it.

RAR and 7z are another story. Older RAR4 archives show the file list only. RAR5, 7Z, BZ2 and XZ currently show basic format info and nothing more. I'd rather say that plainly than promise "RAR support" and have someone find out the hard way.

Making "no uploads" checkable

"Your files are never uploaded" is easy to write and hard to trust. Two things back it up:

  1. You can check it yourself. Open DevTools, go to the Network tab, drop a file. You'll see same-origin script chunks load and, if you allowed analytics, a few small Google Analytics requests (the site uses GA4 for anonymous visit counts), but no request carrying your file.
  2. Automated tests. The test suite opens files in every tool and fails if any tool sends a request to a third-party origin other than anonymous analytics, or makes any POST, while processing a file. It also fails if an analytics event ever contains the file name.

The site's How it works page walks through the same check in more detail.

Lessons so far

  • Format detection deserves care. The OOXML formats all look like ZIPs from the outside.
  • Split renderers by weight. Dynamic imports did more for perceived speed than anything else I tried.
  • Write down the limits. "Looks very close for simple documents" is a promise I can keep. "Perfect fidelity" isn't.

Try it

It's free, has no sign-up or watermark, and runs in your browser: https://makequickwork.com/files/file-viewer/

If you have a file type that renders badly, or one that's missing, I'd like to hear about it in the comments. Odd real-world files are the most useful bug reports I get.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.