Unified photo stack library for Epson FastFoto repositories.
Epson FastFoto scanners produce multiple files per scanned photo:
| File Pattern | Description |
|---|---|
<Prefix>_NNNN.jpg/.tif |
Original front scan |
<Prefix>_NNNN_a.jpg/.tif |
Enhanced (color-corrected) version |
<Prefix>_NNNN_b.jpg/.tif |
Back of photo scan |
photostax groups these into a single PhotoStack, managing metadata that is embedded directly in the image files (EXIF/XMP) so any photo application can access it.
- JPEG (
.jpg,.jpeg) - TIFF (
.tif,.tiff)
| Language | Package | Install | Docs |
|---|---|---|---|
| Rust | photostax-core |
cargo add photostax-core |
core/README.md |
| .NET/C# | Photostax |
dotnet add package Photostax |
bindings/dotnet/README.md |
| TypeScript/Node.js | @photostax/core |
npm install @photostax/core |
bindings/typescript/README.md |
| CLI | photostax-cli |
cargo install photostax-cli |
cli/README.md |
use photostax_core::backends::local::LocalRepository;
use photostax_core::stack_manager::StackManager;
use photostax_core::photo_stack::ScannerProfile;
use photostax_core::search::SearchQuery;
let repo = LocalRepository::new("/path/to/photos");
let mut mgr = StackManager::single(Box::new(repo), ScannerProfile::Auto).unwrap();
// Query all stacks — query() auto-scans on first call
let mut result = mgr.query(None, Some(20), None, None).unwrap();
for stack in result.current_page() {
println!("Photo: {} ({})", stack.name(), stack.id());
if stack.has_original() {
println!(" Has original image");
}
}
// Search with pagination
let query = SearchQuery::new().with_has_back(true);
let mut result = mgr.query(Some(&query), Some(20), None).unwrap();
println!("Page 1: {} stacks of {} total", result.current_page().len(), result.total_count());
// Navigate pages
while let Some(page) = result.next_page() {
println!("Next page: {} stacks", page.len());
}using Photostax;
using var repo = new PhotostaxRepository("/path/to/photos");
// Query all stacks — Query() auto-scans on first call
var result = repo.Query(pageSize: 20);
foreach (var stack in result.CurrentPage)
{
Console.WriteLine($"Photo: {stack.Name} ({stack.Id})");
if (stack.HasOriginal)
Console.WriteLine($" Has original image");
}
// Search with page navigation
var filtered = repo.Query(new SearchQuery().WithHasBack(true), pageSize: 20);
Console.WriteLine($"Page 1: {filtered.CurrentPage.Count} of {filtered.TotalCount} total");
// Navigate pages
while (filtered.NextPage() is { } page)
{
Console.WriteLine($"Next page: {page.Count} stacks");
}import { PhotostaxRepository } from '@photostax/core';
const repo = new PhotostaxRepository('/path/to/photos');
// Query all stacks — query() auto-scans on first call
const result = repo.query(undefined, 20);
for (const stack of result.currentPage()) {
console.log(`Photo: ${stack.name} (${stack.id})`);
if (stack.hasOriginal) {
console.log(` Has original image`);
}
}
// Search with page navigation
const filtered = repo.query({ text: 'birthday', hasBack: true }, 20);
console.log(`Page 1: ${filtered.currentPage().length} of ${filtered.totalCount} total`);
// Navigate pages
let page;
while ((page = filtered.nextPage()) !== null) {
console.log(`Next page: ${page.length} stacks`);
}# Scan a directory and list all photo stacks
photostax-cli scan /photos
# Search for stacks containing "birthday"
photostax-cli search /photos birthday
# Paginate results (20 items starting at offset 40)
photostax-cli scan /photos --limit 20 --offset 40
photostax-cli search /photos birthday --limit 10 --offset 0
# Show detailed info about a specific stack
photostax-cli info /photos IMG_0001
# Export all stacks as JSON
photostax-cli export /photos --output stacks.json
# Manage metadata
photostax-cli metadata write /photos IMG_0001 --tag album="Family Photos"
photostax-cli metadata read /photos IMG_0001 --format jsonSee cli/README.md for complete CLI documentation.
┌─────────────────────────────────────────────────────────────────┐
│ Applications │
├──────────────┬─────────────────┬─────────────────┬──────────────┤
│ .NET │ TypeScript │ CLI │ (Future) │
│ (C#) │ (Node.js) │ (photostax-cli)│ │
├──────────────┴─────────────────┴─────────────────┴──────────────┤
│ photostax-ffi │
│ (C-compatible FFI layer) │
├─────────────────────────────────────────────────────────────────┤
│ photostax-core │
│ ┌─────────────┐ ┌────────────┐ ┌──────────────────────────┐ │
│ │StackManager │ │ FileAccess │ │ Content Hashing │ │
│ │(multi-repo │ │ (I/O trait)│ │ (SHA-256, Merkle, lazy) │ │
│ │ cache, │ │ │ │ │ │
│ │ query()) │ │ │ │ │ │
│ └─────────────┘ └────────────┘ └──────────────────────────┘ │
│ ┌─────────────┐ ┌────────────┐ ┌──────────────────────────┐ │
│ │ FS Watching │ │ Opaque IDs │ │ Scanner & Metadata │ │
│ │ (notify rx) │ │ (hash-based│ │ (EXIF, XMP, sidecars) │ │
│ │ │ │ globally │ │ │ │
│ │ │ │ unique) │ │ │ │
│ └─────────────┘ └────────────┘ └──────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Storage Backends │
│ ┌──────────────┬──────────────┬──────────────┐ │
│ │ Local FS │ OneDrive │ Google Drive │ │
│ │ (implemented)│ (planned) │ (planned) │ │
│ └──────────────┴──────────────┴──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- StackManager — primary API; unified multi-repository cache with
query()as the sole entry point, overlap detection, and filesystem watch integration - FileAccess trait — backend-polymorphic file I/O with
open_read()/open_write()andHashingReaderfor zero-overhead content hashing - Opaque stack IDs — deterministic SHA-256 hashes (16 hex chars) that are globally unique across subfolders;
stack.nameandstack.folderfor display - Content hashing — lazy per-file SHA-256 via
ImageFile::content_hash()and Merkle-stylePhotoStack::content_hash()for duplicate detection - Filesystem watching —
Repository::watch()returns aReceiver<StackEvent>for reactive cache updates viaCacheEvent(StackAdded/StackUpdated/StackRemoved) - Rust core (
photostax-core) — scanning, metadata, storage backend support - FFI layer (
photostax-ffi) — C-compatible interface for language bindings - Client bindings — idiomatic libraries for C# (.NET), TypeScript (Node.js), and more (constructors wrap
StackManagerautomatically) - Storage backends — pluggable
Repository: FileAccesstrait (local filesystem now; OneDrive, Google Drive planned)
Metadata strategy: EXIF tags are read from images, XMP tags are read/written for interoperability with other photo apps, and XMP sidecar files (.xmp) store custom tags and EXIF overrides alongside your images. See docs/metadata-strategy.md for details.
- Rust toolchain (1.70+)
- For .NET binding: .NET SDK 8.0+
- For TypeScript binding: Node.js 18+ with npm
# Build all crates
cargo build --workspace
# Run all tests
cargo test --workspace
# Build release binaries
cargo build --release --workspace# Build native library first
cargo build --release -p photostax-ffi
# Build .NET library
cd bindings/dotnet
dotnet build
# Run tests
dotnet test# Install dependencies and build
cd bindings/typescript
npm install
npm run build
# Run tests
npm test# Rust tests
cargo test --workspace
# .NET tests
cd bindings/dotnet && dotnet test
# TypeScript tests
cd bindings/typescript && npm test# Install cargo-llvm-cov
cargo install cargo-llvm-cov
# Generate coverage report
cargo llvm-cov --workspace --htmlphotostax/
├── core/ # Rust core library (photostax-core)
│ ├── src/ # Library source code
│ └── tests/ # Integration tests
├── ffi/ # C FFI bindings (photostax-ffi)
│ ├── src/ # FFI implementation
│ └── photostax.h # C header file
├── cli/ # CLI tool (photostax-cli)
│ ├── src/ # CLI implementation
│ └── tests/ # CLI tests
├── bindings/
│ ├── dotnet/ # C# client (P/Invoke)
│ │ ├── src/ # .NET source
│ │ └── tests/ # .NET tests
│ └── typescript/ # TypeScript client (napi-rs)
│ ├── src/ # TypeScript source
│ └── __tests__/ # TypeScript tests
├── docs/ # Documentation
│ ├── architecture.md # System architecture
│ ├── fastfoto-convention.md # File naming patterns
│ ├── metadata-strategy.md # EXIF/XMP/sidecar handling
│ └── bindings-guide.md # How to create new bindings
├── LICENSE-MIT
├── LICENSE-APACHE
├── CHANGELOG.md
└── README.md
- Migration Guide (v0.5.x → v0.6.0) — Breaking changes and upgrade instructions
- Architecture Overview — System design and component responsibilities
- FastFoto Naming Convention — How scanner files are named and grouped
- Metadata Strategy — EXIF, XMP, and sidecar database handling
- Bindings Guide — How to create new language bindings
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.