# Pdfstudio Core Tauri PDF Filesystem

> Use when modifying file open/save operations in open-pdf-studio. Prevents the common mistake of using the wrong IPC mechanism (Tauri plugin vs custom invoke) or breaking the file locking system that prevents data loss. Covers dual IPC patterns, Uint8Array transfer, file locking via Rust File handles, and the complete file open/edit/save pipeline through Tauri. Keywords: Tauri, filesystem, invoke, readBinaryFile, writeBinaryFile, file lock, Uint8Array, IPC, Rust commands, save pipeline, file open fails, save not working, file locked, Tauri file operations.

- Skill: `impertio-studio/pdfstudio-core-tauri-pdf-filesystem` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add impertio-studio/pdfstudio-core-tauri-pdf-filesystem`
- Raw SKILL.md: https://api.skillmd.com/api/skills/impertio-studio/pdfstudio-core-tauri-pdf-filesystem/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: Impertio-Studio (https://skillmd.com/u/impertio-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/impertio-studio/pdfstudio-core-tauri-pdf-filesystem

---


# Tauri PDF Filesystem: IPC, File Locking & Binary Transfer

## Purpose

This skill documents how open-pdf-studio transfers PDF binary data between the Rust backend and the WebView frontend, how file locking prevents data loss, and when to use Tauri plugins vs custom invoke commands. EVERY file operation in the app passes through this boundary.

## Architecture Overview

```
User Action (open/save)
    │
    ▼
js/core/platform.js          ◄── Unified API wrapper
    │
    ├── Tauri FS Plugin       ◄── window.__TAURI__.fs.readFile / writeFile
    │   (Uint8Array, fast)        Direct binary transfer, preferred path
    │
    └── Custom Invoke          ◄── window.__TAURI__.core.invoke('cmd', args)
        (JSON serialized)          Used for lock_file, unlock_file, print, etc.
    │
    ▼
src-tauri/src/lib.rs          ◄── 28 Rust #[tauri::command] handlers
```

## Dual IPC Pattern

### Rule: ALWAYS Use the Correct IPC Mechanism

open-pdf-studio uses TWO distinct IPC mechanisms. Using the wrong one causes silent failures or performance degradation.

| Mechanism | When to Use | Data Format | Performance |
|-----------|-------------|-------------|-------------|
| **Tauri FS plugin** (`__TAURI__.fs`) | Reading/writing file bytes | Direct `Uint8Array` | Fast (zero-copy transfer) |
| **Custom invoke** (`__TAURI__.core.invoke`) | All other Rust commands | JSON-serialized arguments | Varies |

### Tauri FS Plugin (Binary Operations)

```javascript
// js/core/platform.js — preferred path for file I/O
export async function readBinaryFile(path) {
  return await window.__TAURI__.fs.readFile(path);  // Returns Uint8Array
}

export async function writeBinaryFile(path, contents) {
  await window.__TAURI__.fs.writeFile(path, contents);  // Accepts Uint8Array
}
```

ALWAYS use the FS plugin for PDF byte transfer. It transfers `Uint8Array` directly without serialization overhead.

### Custom Invoke (Non-Binary Operations)

```javascript
// js/core/platform.js — for commands not covered by plugins
export async function invoke(cmd, args = {}) {
  return await window.__TAURI__.core.invoke(cmd, args);
}
```

Used for: `lock_file`, `unlock_file`, `save_session`, `load_session`, `get_printers`, `print_pdf`, `write_temp_pdf`, and 20+ other commands.

### Binary Fallback via Invoke

When the FS plugin is unavailable (rare), the fallback encodes PDF bytes as base64:

```javascript
// invoke('write_file', { path, data: base64String })
// This is SLOWER — avoid unless FS plugin is genuinely unavailable
```

For `write_temp_pdf` (printing workflow), bytes are transferred as `Array.from(pdfBytes)` (a JSON number array). This is intentional — the temp file path is generated server-side.

## File Locking System

### Rule: ALWAYS Follow the Lock/Unlock Protocol

The app uses Rust `File` handles with Windows shared-read-only access to prevent concurrent writes.

### Lock Lifecycle

```
OPEN:   invoke('lock_file', { path })
        → Rust opens File with FILE_SHARE_READ (others can read, nobody else can write)
        → File handle stored in global HashMap<String, File>

SAVE:   invoke('unlock_file', { path })    // Release lock
        → writeFile(path, newBytes)         // Write new content
        → invoke('lock_file', { path })     // Re-acquire lock

CLOSE:  invoke('unlock_file', { path })
        → File handle dropped, lock released
```

### Lock Rules

1. ALWAYS call `unlock_file` BEFORE `writeFile` — writing to a locked file fails silently on Windows
2. ALWAYS call `lock_file` AFTER `writeFile` — re-lock protects against external modifications
3. NEVER leave a file locked when closing a document — this blocks other applications
4. The lock is per-process. Other processes CAN read the file but CANNOT write to it

### Rust Lock Implementation

```rust
// src-tauri/src/lib.rs (simplified)
static FILE_LOCKS: Lazy<Mutex<HashMap<String, File>>> = ...;

#[tauri::command]
fn lock_file(path: String) -> Result<(), String> {
    let file = OpenOptions::new()
        .read(true)
        .share_mode(FILE_SHARE_READ)  // Windows: shared read, exclusive write
        .open(&path)?;
    FILE_LOCKS.lock().unwrap().insert(path, file);
    Ok(())
}
```

## Complete File Pipeline

### Open Pipeline

```
1. User picks file via dialog plugin or CLI argument
   → filePath: string

2. readBinaryFile(filePath)                    [Tauri FS plugin]
   → typedArray: Uint8Array

3. originalBytesCache.set(filePath, typedArray.slice())
   → CRITICAL: .slice() creates a copy because PDF.js transfers
     the ArrayBuffer to its web worker, detaching the original

4. invoke('lock_file', { path: filePath })     [Custom invoke]
   → Rust holds File handle with shared-read lock

5. pdfjsLib.getDocument({ data: typedArray })  [PDF.js]
   → doc.pdfDoc (rendering, text, annotations)

6. PDFDocument.load(cachedBytes)               [pdf-lib, background]
   → doc._sharedPdfLibDoc (color extraction)
```

### Save Pipeline

```
1. Retrieve original bytes
   → originalBytesCache.get(filePath): Uint8Array

2. PDFDocument.load(originalBytes)             [pdf-lib]
   → Fresh pdf-lib document from original bytes

3. Apply modifications
   → Strip handled annotations from /Annots arrays
   → Convert app annotations to PDF annotation dicts
   → Persist form field values
   → Apply page rotations

4. pdfDocLib.save()
   → newBytes: Uint8Array

5. invoke('unlock_file', { path: filePath })   [Custom invoke]
   → Release lock

6. writeBinaryFile(filePath, newBytes)          [Tauri FS plugin]
   → Write to disk

7. invoke('lock_file', { path: filePath })     [Custom invoke]
   → Re-acquire lock with new content

8. originalBytesCache.set(filePath, newBytes)
   → Update cache for next save
```

## The 28 Rust Commands

| Category | Commands | IPC Type |
|----------|----------|----------|
| **File I/O** | `read_file`, `write_file`, `file_exists`, `delete_file`, `rename_file` | Custom invoke |
| **File Locking** | `lock_file`, `unlock_file` | Custom invoke |
| **Session** | `save_session`, `load_session` | Custom invoke |
| **Preferences** | `save_preferences`, `load_preferences` | Custom invoke |
| **Printing** | `get_printers`, `print_pdf`, `open_printer_properties`, `write_temp_pdf`, `get_temp_dir` | Custom invoke |
| **Virtual Printer** | `install_virtual_printer`, `remove_virtual_printer`, `is_virtual_printer_installed` | Custom invoke |
| **Plugins** | `list_plugins`, `install_plugin`, `uninstall_plugin`, `read_plugin_file` | Custom invoke |
| **System** | `get_username`, `is_dev_mode`, `is_default_pdf_app`, `open_default_apps_settings`, `open_url`, `download_pdf_from_url`, `list_pdf_files`, `play_alert_sound`, `get_opened_file` | Custom invoke |

NEVER add new file I/O commands to `lib.rs` when the Tauri FS plugin already handles the operation. Custom commands exist only for functionality the plugin does not provide (locking, session, printing, etc.).

## Binary Data Transfer Formats

| Operation | Method | Format | Notes |
|-----------|--------|--------|-------|
| Read PDF | `__TAURI__.fs.readFile()` | `Uint8Array` | Zero-copy, fast |
| Write PDF | `__TAURI__.fs.writeFile()` | `Uint8Array` | Zero-copy, fast |
| Write (fallback) | `invoke('write_file')` | Base64 string | Slow, avoid |
| Print temp | `invoke('write_temp_pdf')` | `Array.from(bytes)` | JSON number array |

## Web Fallback Mode

`platform.js` provides browser fallbacks when Tauri is not available:

| Operation | Desktop (Tauri) | Web (Browser) |
|-----------|----------------|---------------|
| Read file | FS plugin → `Uint8Array` | `<input type="file">` → `FileReader` |
| Write file | FS plugin → disk | `<a download>` → browser download |
| File lock | Rust `File` handle | No-op (not supported) |
| Session | Rust file I/O | `localStorage` |

NEVER assume Tauri APIs are available without checking. `platform.js` ALWAYS provides the correct abstraction.

## Buffer Detachment Warning

PDF.js transfers the `ArrayBuffer` to its web worker using the Structured Clone algorithm with transfer. After transfer, the original `Uint8Array` becomes zero-length.

```javascript
// WRONG — bytes will be detached after getDocument()
const bytes = await readBinaryFile(path);
pdfjsLib.getDocument({ data: bytes });
// bytes.length === 0 here — save will fail!

// CORRECT — clone before passing to PDF.js
const bytes = await readBinaryFile(path);
originalBytesCache.set(path, bytes.slice());  // Clone for later use
pdfjsLib.getDocument({ data: bytes });         // PDF.js takes ownership
```

ALWAYS clone `Uint8Array` before passing to `pdfjsLib.getDocument()`. Failure to do so causes silent data loss on save.

## Key Files

| File | Lines | Purpose |
|------|-------|---------|
| `src-tauri/src/lib.rs` | ~903 | All 28 Rust commands, Tauri builder setup |
| `js/core/platform.js` | ~508 | Unified Tauri API wrapper with web fallbacks |
| `js/pdf/loader.js` | — | PDF loading orchestrator, calls readBinaryFile + lock_file |
| `js/pdf/saver.js` | — | Save orchestrator, calls unlock → write → lock sequence |
| `src-tauri/Cargo.toml` | — | Rust dependencies including 8 Tauri plugins |
| `src-tauri/tauri.conf.json` | — | Plugin permissions and window configuration |

