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-String
- parseFlatJson(text)- Parse legacy JSON format
- toObsidianExcalidrawMd(data, frontMatter?)- Convert to Obsidian format
- extractFrontMatter(md)- Extract YAML front matter
- parseExcalidrawAny(text)- Auto-detect and parse any format
- isValidExcalidrawScene(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 format
- parseFlatJson(text)- Parse legacy format
- toObsidianMd(data, frontMatter?)- Convert to Obsidian format
- parseAny(text)- Auto-detect format
- isValidScene(data)- Validate scene
drawings-file.service.ts
HTTP service for file operations:
- get(path)- Load Excalidraw file
- put(path, scene)- Save with conflict detection
- putForce(path, scene)- Force overwrite
- putBinary(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