6.7 KiB
Excalidraw Implementation - Obsidian Format Support
Overview
ObsiViewer now fully supports Obsidian's Excalidraw plugin format, including:
- ✅ Reading
.excalidraw.mdfiles with LZ-String compressed data - ✅ Writing files in Obsidian-compatible format
- ✅ Preserving front matter and metadata
- ✅ Round-trip compatibility (ObsiViewer ⇄ Obsidian)
- ✅ Backward compatibility with legacy flat JSON format
- ✅ Migration tool for converting old files
File Format
Obsidian Format (.excalidraw.md)
---
excalidraw-plugin: parsed
tags: [excalidraw]
---
==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==
# Excalidraw Data
## Text Elements
%%
## Drawing
```compressed-json
<LZ-STRING_BASE64_COMPRESSED_DATA>
%%
The `compressed-json` block contains the Excalidraw scene data compressed using **LZ-String** (`compressToBase64`/`decompressFromBase64`).
### Legacy Format (`.excalidraw`, `.json`)
Plain JSON format:
```json
{
"elements": [...],
"appState": {...},
"files": {...}
}
Architecture
Backend (server/)
excalidraw-obsidian.mjs
Core utilities for parsing and serializing Excalidraw files:
parseObsidianExcalidrawMd(md)- Parse Obsidian format, decompress LZ-StringparseFlatJson(text)- Parse legacy JSON formattoObsidianExcalidrawMd(data, frontMatter?)- Convert to Obsidian formatextractFrontMatter(md)- Extract YAML front matterparseExcalidrawAny(text)- Auto-detect and parse any formatisValidExcalidrawScene(data)- Validate scene structure
API Routes (server/index.mjs)
GET /api/files?path=<file>
- Accepts
pathas query parameter (properly URL-encoded) - Returns parsed Excalidraw scene as JSON
- Supports
.excalidraw.md,.excalidraw,.json - Sets
ETagheader for conflict detection
PUT /api/files?path=<file>
- Accepts
pathas query parameter - Content-Type:
application/json(Excalidraw scene) - Automatically converts to Obsidian format for
.excalidraw.mdfiles - Preserves existing front matter
- Supports
If-Matchheader for conflict detection - Atomic write with backup (
.bak)
PUT /api/files/blob?path=<file>
- For binary sidecars (PNG/SVG exports)
- Accepts
pathas query parameter
Frontend (src/app/features/drawings/)
excalidraw-io.service.ts
Frontend parsing and serialization service:
parseObsidianMd(md)- Parse Obsidian formatparseFlatJson(text)- Parse legacy formattoObsidianMd(data, frontMatter?)- Convert to Obsidian formatparseAny(text)- Auto-detect formatisValidScene(data)- Validate scene
drawings-file.service.ts
HTTP service for file operations:
get(path)- Load Excalidraw fileput(path, scene)- Save with conflict detectionputForce(path, scene)- Force overwriteputBinary(path, blob, mime)- Save binary sidecar
All methods use query parameters for proper URL encoding.
drawings-editor.component.ts
Editor component with:
- Auto-save (debounced)
- Conflict detection and resolution
- Manual save (Ctrl+S)
- Export to PNG/SVG
- Theme synchronization
Usage
Opening Files
Files are automatically detected and parsed:
// Backend automatically detects format
GET /api/files?path=drawing.excalidraw.md
// Returns: { elements: [...], appState: {...}, files: {...} }
Saving Files
The backend automatically converts to Obsidian format:
// Frontend sends JSON
PUT /api/files?path=drawing.excalidraw.md
Content-Type: application/json
{ elements: [...], appState: {...}, files: {...} }
// Backend writes Obsidian format with LZ-String compression
Migration
Convert old flat JSON files to Obsidian format:
# Dry run (preview changes)
npm run migrate:excalidraw:dry
# Apply migration
npm run migrate:excalidraw
# Custom vault path
node server/migrate-excalidraw.mjs --vault-path=/path/to/vault
The migration script:
- Scans for
.excalidrawand.jsonfiles - Validates Excalidraw structure
- Converts to
.excalidraw.mdwith Obsidian format - Creates
.bakbackups - Removes original files
Testing
Unit Tests
# Run backend utility tests
npm run test:excalidraw
Tests cover:
- ✅ Front matter extraction
- ✅ Obsidian format parsing
- ✅ LZ-String compression/decompression
- ✅ Round-trip conversion
- ✅ Legacy JSON parsing
- ✅ Edge cases (empty scenes, special characters, large files)
E2E Tests
# Run Playwright tests
npm run test:e2e -- excalidraw.spec.ts
Tests cover:
- ✅ Editor loading
- ✅ API endpoints with query params
- ✅ Obsidian format parsing
- ✅ File structure validation
Compatibility
Obsidian → ObsiViewer
✅ Open .excalidraw.md files created in Obsidian
✅ Render drawings correctly
✅ Preserve all metadata and front matter
ObsiViewer → Obsidian
✅ Save in Obsidian-compatible format
✅ Files open correctly in Obsidian
✅ No warnings or data loss
✅ Front matter preserved
Legacy Support
✅ Open old flat JSON files
✅ Automatically convert on save
✅ Migration tool available
Key Changes from Previous Implementation
Backend
- ❌ Removed
zlibdecompression (wrong algorithm) - ✅ Added LZ-String support (
lz-stringpackage) - ❌ Removed splat routes (
/api/files/*splat) - ✅ Added query param routes (
/api/files?path=...) - ✅ Proper URL encoding/decoding
- ✅ Front matter preservation
- ✅ Atomic writes with backups
Frontend
- ✅ Created
ExcalidrawIoServicefor parsing - ✅ Updated all HTTP calls to use query params
- ✅ Proper
encodeURIComponentusage - ✅ No more 400 errors on special characters
Troubleshooting
400 Bad Request
Cause: Path not properly encoded or missing query parameter
Fix: Ensure using ?path= query param, not URL path
Invalid Excalidraw Format
Cause: Corrupted compressed data or wrong compression algorithm
Fix: Check file was created with LZ-String, not zlib
Conflict Detected (409)
Cause: File modified externally while editing
Fix: Use "Reload from disk" or "Overwrite" buttons in UI
File Opens in Obsidian but Not ObsiViewer
Cause: Unsupported Excalidraw version or corrupted data
Fix: Check console for parsing errors, validate JSON structure
Dependencies
- Backend:
lz-string(^1.5.0) - Frontend:
lz-string(^1.5.0)
Future Enhancements
- Support for Excalidraw libraries
- Embedded images optimization
- Collaborative editing
- Version history
- Template system