# How Pi-Web File Preview Supports Images, PDFs, and Audio: Implementation Guide

> Learn how pi-web file preview supports images, PDFs, and audio. Explore the implementation guide for viewer components, file streaming, and live synchronization.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-07-23

---

**Pi-Web routes image, audio, and document files to specialized viewer components by detecting file extensions in [`components/FileViewer.tsx`](https://github.com/agegr/pi-web/blob/main/components/FileViewer.tsx), streaming content via `/api/files` endpoints with live synchronization through EventSource watchers.**

Pi-Web is an open-source file management interface that provides real-time previews for multimedia and document formats. The preview system in the `agegr/pi-web` repository intelligently selects rendering strategies based on file extensions, utilizing a modular architecture that separates detection logic from presentation components.

## File Type Detection and Component Routing

The entry point for all file previews is the `FileViewer` function in [`components/FileViewer.tsx`](https://github.com/agegr/pi-web/blob/main/components/FileViewer.tsx). This dispatcher delegates rendering to specialized components by evaluating boolean predicates imported from [`lib/file-types.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-types.ts).

Three primary detection functions determine the rendering path:

- **`isImagePath`** – Returns `true` for supported image extensions (png, jpg, svg)
- **`isAudioPath`** – Matches audio extensions (mp3, wav, ogg)
- **`isDocumentPreviewPath`** – Identifies `.pdf` and `.docx` files

When a user selects a file, `FileViewer` evaluates these conditions in sequence and returns the appropriate viewer component, falling back to `TextFileViewer` for unsupported formats.

```tsx
export function FileViewer({ filePath, cwd, sourceSessionId }: Props) {
  if (isImagePath(filePath)) {
    return <ImageViewer filePath={filePath} cwd={cwd} sourceSessionId={sourceSessionId} />;
  }
  if (isAudioPath(filePath)) {
    return <AudioViewer filePath={filePath} cwd={cwd} sourceSessionId={sourceSessionId} />;
  }
  if (isDocumentPreviewPath(filePath)) {
    return <DocumentViewer filePath={filePath} cwd={cwd} sourceSessionId={sourceSessionId} />;
  }
  return <TextFileViewer {...} />;
}

```

## Image Preview with Live Sync

When `isImagePath` returns `true`, `FileViewer` renders the **`ImageViewer`** component. This viewer streams the binary file through the `/api/files/...` endpoint using the `read` operation type.

The implementation constructs a cache-busting URL via `getFileApiUrl` from [`lib/file-paths.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-paths.ts), appending a version parameter (`v`) when file changes occur. The image loads in a standard `<img>` tag with `objectFit: "contain"` styling, and the component captures natural dimensions through the `onLoad` event to report image width and height.

Live synchronization works via EventSource connection to `/api/files/<encoded>?type=watch`, listening for `change` events that trigger URL updates and force browser reloads.

```tsx
function ImageViewer({ filePath, cwd, sourceSessionId }: Props) {
  const src = getFileApiUrl(filePath, "read", sourceSessionId, bust ? { v: bust } : undefined);
  return (
    <img
      src={src}
      alt={filePath}
      onLoad={e => setNaturalSize({ w: e.currentTarget.naturalWidth, h: e.currentTarget.naturalHeight })}
      onError={() => setError("Failed to load image")}
      style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }}
    />
  );
}

```

## Audio Preview and Metadata Extraction

Audio files trigger the **`AudioViewer`** component when `isAudioPath` identifies supported formats. Like images, audio streams through the `read` endpoint, but renders using the HTML5 `<audio>` element with native controls enabled.

The component sets `preload="metadata"` to optimize loading performance and listens for `onLoadedMetadata` to extract duration information. File size displays alongside the player, and the same EventSource infrastructure watches for file modifications, applying cache-busting parameters to the `src` attribute when changes are detected.

```tsx
function AudioViewer({ filePath, cwd, sourceSessionId }: Props) {
  const src = getFileApiUrl(filePath, "read", sourceSessionId, bust ? { v: bust } : undefined);
  return (
    <audio
      controls
      preload="metadata"
      src={src}
      onLoadedMetadata={e => setDuration(e.currentTarget.duration)}
      onError={() => setError("Failed to load audio")}
    />
  );
}

```

## PDF and DOCX Document Preview

Document previews route to **`DocumentViewer`**, which handles both formats through `<iframe>` embedding but with distinct data strategies.

### PDF Rendering Strategy

PDF files render using the browser's native viewer by streaming raw bytes (`type="read"`) into an **unsandboxed** `<iframe>`. This allows full access to the PDF's interactive features while displaying file size metadata.

### DOCX Conversion and Sandboxing

DOCX files require server-side conversion. The viewer requests `type="preview"` from the API, which returns HTML representation of the document content. For security, this HTML loads inside a **sandboxed** `<iframe>` (sandbox attribute set to empty string), preventing script execution while preserving formatting.

Both document types use EventSource watchers to refresh the iframe source when underlying files change.

```tsx
function DocumentViewer({ filePath, cwd, sourceSessionId }: Props) {
  const isPdf = getFileExt(filePath) === "pdf";
  const previewUrl = isPdf
    ? getFileApiUrl(filePath, "read", sourceSessionId, bust ? { v: bust } : undefined)
    : getFileApiUrl(filePath, "preview", sourceSessionId, bust ? { v: bust } : undefined);
  return <iframe src={previewUrl} sandbox={isPdf ? undefined : ""} title={`Preview ${getFileName(filePath)}`} />;
}

```

## Shared Live Synchronization Infrastructure

All three viewer types implement a common watching mechanism defined in [`components/FileViewer.tsx`](https://github.com/agegr/pi-web/blob/main/components/FileViewer.tsx). Each component opens an `EventSource` connection to `/api/files/<encoded>?type=watch` immediately upon mounting.

The EventSource emits `connected`, `change`, and `error` events. When a `change` event fires, the component increments a cache-busting counter and appends it as the `v` query parameter to the resource URL. This forces the browser to bypass cached versions and reload the updated asset without requiring a full page refresh.

## Summary

- Pi-Web detects file types using `isImagePath`, `isAudioPath`, and `isDocumentPreviewPath` in [`lib/file-types.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-types.ts)
- **Images** render via `<img>` tags streaming from `/api/files` with natural dimension reporting and `objectFit: "contain"` styling
- **Audio** uses HTML5 `<audio>` controls with metadata extraction for duration and file size information
- **PDFs** embed raw bytes in unsandboxed iframes; **DOCX** files receive server-converted HTML in sandboxed iframes via the `preview` endpoint
- All viewers support real-time synchronization through EventSource watchers and cache-busting URL parameters that force reloads on file changes

## Frequently Asked Questions

### How does Pi-Web determine which preview component to use?

The `FileViewer` component in [`components/FileViewer.tsx`](https://github.com/agegr/pi-web/blob/main/components/FileViewer.tsx) evaluates file extensions using helper functions from [`lib/file-types.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-types.ts). It checks for images first, then audio, then documents, returning the appropriate specialized viewer or falling back to text view for unsupported formats.

### Why are PDFs displayed differently from DOCX files?

PDFs render using the browser's native viewer by streaming raw bytes (`type="read"`) into an unsandboxed iframe, allowing full PDF functionality. DOCX files require server-side conversion to HTML (`type="preview"`) and load in sandboxed iframes to prevent potential security issues from converted markup.

### How does live file synchronization work in Pi-Web previews?

Each viewer opens an EventSource connection to `/api/files/<encoded>?type=watch` that monitors file system changes. When the server detects modifications, it sends a `change` event that triggers a cache-busting parameter update, forcing the browser to reload the asset without refreshing the entire page.

### Can Pi-Web preview other document formats besides PDF and DOCX?

Currently, `isDocumentPreviewPath` only matches `.pdf` and `.docx` extensions. Other formats fall back to `TextFileViewer` or binary download options, though the modular architecture in [`components/FileViewer.tsx`](https://github.com/agegr/pi-web/blob/main/components/FileViewer.tsx) allows for easy extension to support additional document types by adding new detection predicates and viewer components.