# Summary - Adds desktop file tracking: local paths are preserved and save buttons now work as expcted (doing Save/Save As as appropriate) - Adds logic to track whether files are 'dirty' (they've been modified by some tool, and not saved to disk yet). - Improves file state UX (dirty vs saved) and close warnings - Web behaviour should be unaffected by these changes ## Indicators Files now have indicators in desktop mode to tell you their state. ### File up-to-date with disk <img width="318" height="393" alt="image" src="https://github.com/user-attachments/assets/06325f9a-afd7-4c2f-8a5b-6d11e3093115" /> ### File modified by a tool but not saved to disk yet <img width="357" height="385" alt="image" src="https://github.com/user-attachments/assets/1a7716d9-c6f7-4d13-be0d-c1de6493954b" /> ### File not tracked on disk <img width="312" height="379" alt="image" src="https://github.com/user-attachments/assets/9cffe300-bd9a-4e19-97c7-9b98bebefacc" /> # Limitations - It's a bit weird that we still have files stored in indexeddb in the app, which are still loadable. We might want to change this behaviour in the future - Viewer's Save doesn't persist to disk. I've left that out here because it'd need a lot of testing to make sure the logic's right with making sure you can leave the Viewer with applying the changes to the PDF _without_ saving to disk - There's no current way to do Save As on a file that has already been persisted to disk - it's only ever Save. Similarly, there's no way to duplicate a file. --------- Co-authored-by: James Brunton <jbrunton96@gmail.com> Co-authored-by: James Brunton <james@stirlingpdf.com>
15 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Common Development Commands
Build and Test
- Build project:
./gradlew clean build - Run locally:
./gradlew bootRun - Full test suite:
./test.sh(builds all Docker variants and runs comprehensive tests) - Code formatting:
./gradlew spotlessApply(runs automatically before compilation)
Docker Development
- Build ultra-lite:
docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite . - Build standard:
docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile . - Build fat version:
docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat . - Example compose files: Located in
exampleYmlFiles/directory
Security Mode Development
Set DOCKER_ENABLE_SECURITY=true environment variable to enable security features during development. This is required for testing the full version locally.
Frontend Development
- Frontend dev server:
cd frontend && npm run dev(requires backend on localhost:8080) - Tech Stack: Vite + React + TypeScript + Mantine UI + TailwindCSS
- Proxy Configuration: Vite proxies
/api/*calls to backend (localhost:8080) - Build Process: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
- Package Installation: DO NOT run npm install commands - package management handled separately
- Deployment Options:
- Desktop App:
npm run tauri-build(native desktop application) - Web Server:
npm run buildthen serve dist/ folder - Development:
npm run tauri-devfor desktop dev mode
- Desktop App:
Import Paths - CRITICAL
ALWAYS use @app/* for imports. Do not use @core/* or @proprietary/* unless explicitly wrapping/extending a lower layer implementation.
// ✅ CORRECT - Use @app/* for all imports
import { AppLayout } from "@app/components/AppLayout";
import { useFileContext } from "@app/contexts/FileContext";
import { FileContext } from "@app/contexts/FileContext";
// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code
import { AppLayout } from "@core/components/AppLayout";
import { useFileContext } from "@proprietary/contexts/FileContext";
Only use explicit aliases when:
- Building layer-specific override that wraps a lower layer's component
- Example:
import { AppProviders as CoreAppProviders } from "@core/components/AppProviders"when creating proprietary/AppProviders.tsx that extends the core version
The @app/* alias automatically resolves to the correct layer based on build target (core/proprietary/desktop) and handles the fallback cascade.
Component Override Pattern (Stub/Shadow)
Use this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.
How it works:
- Core defines stub component (returns null or no-op)
- Desktop/proprietary overrides with same path/name
- Core imports via
@app/*- higher layer "shadows" core in those builds - No
@ts-ignore, noisTauri()checks, no runtime conditionals!
Example - Desktop-specific footer:
// core/components/rightRail/RightRailFooterExtensions.tsx (stub)
interface RightRailFooterExtensionsProps {
className?: string;
}
export function RightRailFooterExtensions(_props: RightRailFooterExtensionsProps) {
return null; // Stub - does nothing in web builds
}
// desktop/components/rightRail/RightRailFooterExtensions.tsx (real implementation)
import { Box } from '@mantine/core';
import { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';
interface RightRailFooterExtensionsProps {
className?: string;
}
export function RightRailFooterExtensions({ className }: RightRailFooterExtensionsProps) {
return (
<Box className={className}>
<BackendHealthIndicator />
</Box>
);
}
// core/components/shared/RightRail.tsx (usage - works in ALL builds)
import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions';
export function RightRail() {
return (
<div>
{/* In web builds: renders nothing (stub returns null) */}
{/* In desktop builds: renders BackendHealthIndicator */}
<RightRailFooterExtensions className="right-rail-footer" />
</div>
);
}
Build resolution:
- Core build:
@app/*→core/*→ Gets stub (returns null) - Desktop build:
@app/*→desktop/*→ Gets real implementation (shadows core)
Benefits:
- No runtime checks or feature flags
- Type-safe across all builds
- Clean, readable code
- Build-time optimization (dead code elimination)
Multi-Tool Workflow Architecture
Frontend designed for stateful document processing:
- Users upload PDFs once, then chain tools (split → merge → compress → view)
- File state and processing results persist across tool switches
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
FileContext - Central State Management
Location: frontend/src/core/contexts/FileContext.tsx
- Active files: Currently loaded PDFs and their variants
- Tool navigation: Current mode (viewer/pageEditor/fileEditor/toolName)
- Memory management: PDF document cleanup, blob URL lifecycle, Web Worker management
- IndexedDB persistence: File storage with thumbnail caching
- Preview system: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
Critical: All file operations go through FileContext. Don't bypass with direct file handling.
Processing Services
- enhancedPDFProcessingService: Background PDF parsing and manipulation
- thumbnailGenerationService: Web Worker-based with main-thread fallback
- fileStorage: IndexedDB with LRU cache management
Memory Management Strategy
Why manual cleanup exists: Large PDFs (up to 100GB+) through multiple tools accumulate:
- PDF.js documents that need explicit .destroy() calls
- Blob URLs from tool outputs that need revocation
- Web Workers that need termination Without cleanup: browser crashes with memory leaks.
Tool Development
Architecture: Modular hook-based system with clear separation of concerns:
-
useToolOperation (
frontend/src/core/hooks/tools/shared/useToolOperation.ts): Main orchestrator hook- Coordinates all tool operations with consistent interface
- Integrates with FileContext for operation tracking
- Handles validation, error handling, and UI state management
-
Supporting Hooks:
- useToolState: UI state management (loading, progress, error, files)
- useToolApiCalls: HTTP requests and file processing
- useToolResources: Blob URLs, thumbnails, ZIP downloads
-
Utilities:
- toolErrorHandler: Standardized error extraction and i18n support
- toolResponseProcessor: API response handling (single/zip/custom)
- toolOperationTracker: FileContext integration utilities
Three Tool Patterns:
Pattern 1: Single-File Tools (Individual processing)
- Backend processes one file per API call
- Set
multiFileEndpoint: false - Examples: Compress, Rotate
return useToolOperation({
operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf',
buildFormData: (params, file: File) => { /* single file */ },
multiFileEndpoint: false,
});
Pattern 2: Multi-File Tools (Batch processing)
- Backend accepts
MultipartFile[]arrays in single API call - Set
multiFileEndpoint: true - Examples: Split, Merge, Overlay
return useToolOperation({
operationType: 'split',
endpoint: '/api/v1/general/split-pages',
buildFormData: (params, files: File[]) => { /* all files */ },
multiFileEndpoint: true,
filePrefix: 'split_',
});
Pattern 3: Complex Tools (Custom processing)
- Tools with complex routing logic or non-standard processing
- Provide
customProcessorfor full control - Examples: Convert, OCR
return useToolOperation({
operationType: 'convert',
customProcessor: async (params, files) => { /* custom logic */ },
});
Benefits:
- No Timeouts: Operations run until completion (supports 100GB+ files)
- Consistent: All tools follow same pattern and interface
- Maintainable: Single responsibility hooks, easy to test and modify
- i18n Ready: Built-in internationalization support
- Type Safe: Full TypeScript support with generic interfaces
- Memory Safe: Automatic resource cleanup and blob URL management
Architecture Overview
Project Structure
- Backend: Spring Boot application
- Frontend: React-based SPA in
/frontenddirectory- File Storage: IndexedDB for client-side file persistence and thumbnails
- Internationalization: JSON-based translations (converted from backend .properties)
- PDF Processing: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
- Security: Spring Security with optional authentication (controlled by
DOCKER_ENABLE_SECURITY) - Configuration: YAML-based configuration with environment variable overrides
Controller Architecture
- API Controllers (
src/main/java/.../controller/api/): REST endpoints for PDF operations- Organized by function: converters, security, misc, pipeline
- Follow pattern:
@RestController+@RequestMapping("/api/v1/...")
Key Components
- SPDFApplication.java: Main application class with desktop UI and browser launching logic
- ConfigInitializer: Handles runtime configuration and settings files
- Pipeline System: Automated PDF processing workflows via
PipelineController - Security Layer: Authentication, authorization, and user management (when enabled)
Frontend Directory Structure
The frontend is organized with a clear separation of concerns:
-
frontend/src/core/: Main application code (shared, production-ready components)core/components/: React components organized by featurecore/components/tools/: Individual PDF tool implementationscore/components/viewer/: PDF viewer componentscore/components/pageEditor/: Page manipulation UIcore/components/tooltips/: Help tooltips for toolscore/components/shared/: Reusable UI components
core/contexts/: React Context providersFileContext.tsx: Central file state managementfile/: File reducer and selectorstoolWorkflow/: Tool workflow state
core/hooks/: Custom React hookshooks/tools/: Tool-specific operation hooks (one directory per tool)hooks/tools/shared/: Shared hook utilities (useToolOperation, etc.)
core/constants/: Application constants and configurationcore/data/: Static data (tool taxonomy, etc.)core/services/: Business logic services (PDF processing, storage, etc.)
-
frontend/src/desktop/: Desktop-specific (Tauri) code -
frontend/src/proprietary/: Proprietary/licensed features -
frontend/src-tauri/: Tauri (Rust) native desktop application code -
frontend/public/: Static assets served directlypublic/locales/: Translation JSON files
Component Architecture
- Static Assets: CSS, JS, and resources in
src/main/resources/static/(legacy) +frontend/public/(modern) - Internationalization:
- Backend:
messages_*.propertiesfiles - Frontend: JSON files in
frontend/public/locales/(converted from .properties) - Conversion Script:
scripts/convert_properties_to_json.py
- Backend:
Configuration Modes
- Ultra-lite: Basic PDF operations only
- Standard: Full feature set
- Fat: Pre-downloaded dependencies for air-gapped environments
- Security Mode: Adds authentication, user management, and enterprise features
Testing Strategy
- Integration Tests: Cucumber tests in
testing/cucumber/ - Docker Testing:
test.shvalidates all Docker variants - Manual Testing: No unit tests currently - relies on UI and API testing
Development Workflow
- Local Development:
- Backend:
./gradlew bootRun(runs on localhost:8080) - Frontend:
cd frontend && npm run dev(runs on localhost:5173, proxies to backend)
- Backend:
- Docker Testing: Use
./test.shbefore submitting PRs - Code Style: Spotless enforces Google Java Format automatically
- Translations:
- Backend: Use helper scripts in
/scriptsfor multi-language updates - Frontend: Update JSON files in
frontend/public/locales/or use conversion script
- Backend: Use helper scripts in
- Documentation: API docs auto-generated and available at
/swagger-ui/index.html
Frontend Architecture Status
- Core Status: React SPA architecture complete with multi-tool workflow support
- State Management: FileContext handles all file operations and tool navigation
- File Processing: Production-ready with memory management for large PDF workflows (up to 100GB+)
- Tool Integration: Modular hook architecture with
useToolOperationorchestrator- Individual hooks:
useToolState,useToolApiCalls,useToolResources - Utilities:
toolErrorHandler,toolResponseProcessor,toolOperationTracker - Pattern: Each tool creates focused operation hook, UI consumes state/actions
- Individual hooks:
- Preview System: Tool results can be previewed without polluting file context (Split tool example)
- Performance: Web Worker thumbnails, IndexedDB persistence, background processing
Translation Rules
- CRITICAL: Always update translations in
en-GBonly, neveren-US - Translation files are located in
frontend/public/locales/
Important Notes
- Java Version: Minimum JDK 17, supports and recommends JDK 21
- Lombok: Used extensively - ensure IDE plugin is installed
- File Persistence:
- Backend: Designed to be stateless - files are processed in memory/temp locations only
- Frontend: Uses IndexedDB for client-side file storage and caching (with thumbnails)
- Security: When
DOCKER_ENABLE_SECURITY=false, security-related classes are excluded from compilation - Import Paths: ALWAYS use
@app/*for imports - never use@core/*or@proprietary/*unless explicitly wrapping/extending a lower layer - FileContext: All file operations MUST go through FileContext - never bypass with direct File handling
- Memory Management: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
- Tool Development: New tools should follow
useToolOperationhook pattern (seeuseCompressOperation.ts) - Performance Target: Must handle PDFs up to 100GB+ without browser crashes
- Preview System: Tools can preview results without polluting main file context (see Split tool implementation)
- Adding Tools: See
ADDING_TOOLS.mdfor complete guide to creating new PDF tools
Communication Style
- Be direct and to the point
- No apologies or conversational filler
- Answer questions directly without preamble
- Explain reasoning concisely when asked
- Avoid unnecessary elaboration
Decision Making
- Ask clarifying questions before making assumptions
- Stop and ask when uncertain about project-specific details
- Confirm approach before making structural changes
- Request guidance on preferences (cross-platform vs specific tools, etc.)
- Verify understanding of requirements before proceeding