Skip to content

Project Architecture

Marc Jones edited this page Dec 30, 2025 · 2 revisions

Project Architecture

This page documents the overall architecture of PdfEditor, including MVVM structure, service layer design, and key design decisions.

Technology Stack

Component Technology Purpose
UI Framework Avalonia 11.1.3 Cross-platform XAML UI
MVVM Framework ReactiveUI 20.1.1 Reactive bindings, commands
PDF Reading PdfPig 0.1.11 Text extraction, letter positions
PDF Manipulation PDFsharp 6.2.2 Document modification
PDF Rendering PDFtoImage 4.0.2 Page rendering via PDFium
Graphics SkiaSharp 2.88.8 2D rendering
Scripting Microsoft.CodeAnalysis.CSharp.Scripting 5.0.0 Roslyn C# scripting
Testing xUnit, FluentAssertions Test framework

All dependencies use permissive licenses (MIT, Apache 2.0, BSD-3).

MVVM Architecture

┌─────────────────────────────────────────────────────────────┐
│                         VIEW LAYER                          │
│  MainWindow.axaml          MainWindow.axaml.cs              │
│  ┌─────────────────┐       ┌─────────────────────────────┐  │
│  │    XAML UI      │       │    Code-behind (minimal)    │  │
│  │  - Bindings     │       │  - Event handlers only      │  │
│  │  - Triggers     │       │  - Forward to ViewModel     │  │
│  │  - Styles       │       │  - No business logic        │  │
│  └────────┬────────┘       └───────────────┬─────────────┘  │
│           │ Data Binding                   │                │
└───────────┼────────────────────────────────┼────────────────┘
            │                                │
┌───────────▼────────────────────────────────▼────────────────┐
│                      VIEWMODEL LAYER                         │
│  MainWindowViewModel.cs                                      │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  - Observable properties (current page, zoom, etc.)   │   │
│  │  - Commands (load, save, redact, etc.)               │   │
│  │  - Application state management                       │   │
│  │  - Orchestrates service calls                         │   │
│  └────────┬─────────────────────────────────────────────┘   │
│           │ Service calls                                    │
└───────────┼─────────────────────────────────────────────────┘
            │
┌───────────▼─────────────────────────────────────────────────┐
│                      SERVICE LAYER                           │
│  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐   │
│  │ PdfDocument  │  │ PdfRender    │  │ Redaction       │   │
│  │ Service      │  │ Service      │  │ Service         │   │
│  └──────────────┘  └──────────────┘  └─────────────────┘   │
│  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐   │
│  │ PdfTextEx    │  │ PdfSearch    │  │ Coordinate      │   │
│  │ traction     │  │ Service      │  │ Converter       │   │
│  └──────────────┘  └──────────────┘  └─────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Directory Structure

PdfEditor/                          # GUI Application
├── Program.cs                      # Entry point
├── App.axaml                       # Application resources
├── Views/
│   ├── MainWindow.axaml            # Main window UI
│   └── MainWindow.axaml.cs         # Code-behind
├── ViewModels/
│   ├── MainWindowViewModel.cs      # Main ViewModel
│   └── MainWindowViewModel.Scripting.cs # Scripting API
├── Models/
│   └── PageThumbnail.cs            # Page thumbnail data
└── Services/
    ├── PdfDocumentService.cs       # Load, save, page manipulation
    ├── PdfRenderService.cs         # Render pages to images (with cache)
    ├── PdfTextExtractionService.cs # Text extraction
    ├── PdfSearchService.cs         # Search functionality
    ├── CoordinateConverter.cs      # Coordinate system conversions
    ├── RedactionService.cs         # Redaction orchestration
    └── ScriptingService.cs         # Roslyn C# scripting

PdfEditor.Redaction/                # Redaction Library
├── ITextRedactor.cs                # Main interface
├── TextRedactor.cs                 # Implementation
├── ContentStream/                  # Content stream handling
├── GlyphLevel/                     # Glyph-level removal
├── Operators/                      # PDF operator handlers
├── PdfPig/                         # PdfPig integration
├── PdfADetector.cs                 # PDF/A detection
└── PdfAMetadataPreserver.cs        # PDF/A metadata

PdfEditor.Redaction.Cli/            # CLI Tool
├── Program.cs                      # Entry point
└── Commands/                       # redact, search, verify, info

automation-scripts/                 # Roslyn C# scripts
├── test-birth-certificate.csx
└── test-verapdf-corpus-sample.csx

Key Components

MainWindowViewModel

Central application state and command handlers.

Observable Properties:

[Reactive] public int CurrentPageIndex { get; set; }
[Reactive] public double ZoomLevel { get; set; }
[Reactive] public bool IsDocumentLoaded { get; set; }
[Reactive] public string SearchText { get; set; }

Commands:

public ReactiveCommand<Unit, Unit> OpenCommand { get; }
public ReactiveCommand<Unit, Unit> SaveCommand { get; }
public ReactiveCommand<Unit, Unit> RedactSelectionCommand { get; }
public ReactiveCommand<Unit, Unit> SearchCommand { get; }

Service Layer Design

Services are stateless and focused:

// Each service has a single responsibility
public class PdfDocumentService
{
    public PdfDocument Open(string path) { }
    public void Save(PdfDocument doc, string path) { }
    public void AddPage(PdfDocument doc, int afterIndex) { }
    public void RemovePage(PdfDocument doc, int index) { }
    public void RotatePage(PdfDocument doc, int index, int degrees) { }
}

public class RedactionService
{
    public void RedactArea(PdfPage page, Rect area) { }
    public void RedactText(PdfDocument doc, string text) { }
}

Dependency Injection Pattern

Services are injected into ViewModel:

public class MainWindowViewModel
{
    private readonly PdfDocumentService _documentService;
    private readonly PdfRenderService _renderService;
    private readonly RedactionService _redactionService;

    public MainWindowViewModel(
        PdfDocumentService documentService,
        PdfRenderService renderService,
        RedactionService redactionService)
    {
        _documentService = documentService;
        _renderService = renderService;
        _redactionService = redactionService;
    }
}

Data Flow Examples

Loading a Document

User clicks "Open"
    ↓
MainWindow raises OpenCommand
    ↓
MainWindowViewModel.OpenCommand.Execute()
    ↓
Shows file dialog, gets path
    ↓
_documentService.Open(path)
    ↓
PdfPig opens document
    ↓
ViewModel sets IsDocumentLoaded = true
    ↓
ReactiveUI notifies View
    ↓
View updates (enables controls, shows first page)

Performing Redaction

User drags selection rectangle
    ↓
MainWindow captures mouse events
    ↓
MainWindow calls ViewModel.SetSelection(rect)
    ↓
User clicks "Redact"
    ↓
ViewModel.RedactSelectionCommand.Execute()
    ↓
Convert selection to PDF coordinates
    ↓
_redactionService.RedactArea(page, area)
    ↓
ContentStreamParser.Parse()
    ↓
Filter operations in area
    ↓
ContentStreamBuilder.Build()
    ↓
Replace page content
    ↓
_renderService.RenderPage() for refresh
    ↓
View updates with new image

Redaction Engine Architecture

The redaction engine is the most complex component:

┌─────────────────────────────────────────────────────────────┐
│                     RedactionService                         │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  RedactArea(page, area)                                  │ │
│  │    1. Get page content bytes                            │ │
│  │    2. Parse with ContentStreamParser                    │ │
│  │    3. Filter operations (remove those in area)          │ │
│  │    4. Rebuild with ContentStreamBuilder                 │ │
│  │    5. Replace page content                              │ │
│  │    6. Draw black rectangle (visual confirmation)        │ │
│  └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
            │                           │
            ▼                           ▼
┌───────────────────────┐   ┌───────────────────────┐
│  ContentStreamParser  │   │ ContentStreamBuilder  │
│  - Tokenize stream    │   │ - Serialize operations│
│  - Track graphics     │   │ - Escape strings      │
│    state (q/Q/cm)     │   │ - Handle TJ arrays    │
│  - Track text state   │   │                       │
│    (Tf/Td/Tm)         │   │                       │
│  - Calculate bboxes   │   │                       │
└───────────────────────┘   └───────────────────────┘
            │
            ▼
┌───────────────────────┐
│  TextBoundsCalculator │
│  - Apply text matrix  │
│  - Apply CTM          │
│  - Convert to Avalonia│
│    coordinates        │
└───────────────────────┘

Key Design Decisions

Decision 1: Two PDF Libraries

Problem: No single library does everything we need.

Solution: Use two complementary libraries:

  • PdfPig: Excellent for reading/parsing (accurate text extraction)
  • PdfSharpCore: Good for writing/modifying (easy content manipulation)

Trade-off: More complexity but better capabilities.

Decision 2: Parse-Filter-Rebuild

Problem: How to remove content from PDF?

Options Considered:

  1. String replacement in content stream (fragile)
  2. Parse to AST, modify, serialize (robust)
  3. Use PDF library's editing API (limited)

Decision: Parse to operations, filter, rebuild. More work but reliable.

Decision 3: Avalonia (not WPF)

Problem: Need cross-platform support.

Decision: Avalonia provides WPF-like XAML with cross-platform support.

Benefits: Single codebase for Windows, Linux, macOS.

Decision 4: Trust PdfPig for Positions

Problem: Calculating accurate text positions is complex.

Decision: Use PdfPig's Letter positions as source of truth (text selection works reliably).

Approach: Match PdfPig letters back to content stream operations.

Error Handling Strategy

Service Layer

Services throw exceptions for unrecoverable errors:

public PdfDocument Open(string path)
{
    if (!File.Exists(path))
        throw new FileNotFoundException($"PDF not found: {path}");

    try
    {
        return PdfDocument.Open(path);
    }
    catch (PdfDocumentFormatException ex)
    {
        throw new InvalidOperationException($"Invalid PDF: {ex.Message}", ex);
    }
}

ViewModel Layer

ViewModel catches and presents to user:

public async Task LoadDocumentAsync(string path)
{
    try
    {
        _document = _documentService.Open(path);
        IsDocumentLoaded = true;
    }
    catch (Exception ex)
    {
        await ShowErrorDialog($"Failed to open PDF: {ex.Message}");
        IsDocumentLoaded = false;
    }
}

Logging Strategy

Use Microsoft.Extensions.Logging:

public class ContentStreamParser
{
    private readonly ILogger<ContentStreamParser> _logger;

    public ContentStreamParser(ILogger<ContentStreamParser> logger)
    {
        _logger = logger;
    }

    public List<PdfOperation> Parse(byte[] content, double pageHeight)
    {
        _logger.LogDebug("Parsing content stream: {Length} bytes", content.Length);

        // ... parsing ...

        _logger.LogInformation("Parsed {Count} operations", operations.Count);
        return operations;
    }
}

Threading Model

UI Thread

All UI operations on main thread (Avalonia requirement).

Background Operations

Long operations use Task:

public async Task LoadDocumentAsync(string path)
{
    IsLoading = true;

    // Run on thread pool
    var document = await Task.Run(() => _documentService.Open(path));

    // Back on UI thread
    _document = document;
    IsLoading = false;
}

Rendering

Page rendering is CPU-intensive; do off UI thread:

public async Task<Bitmap> RenderPageAsync(int pageIndex, int dpi)
{
    return await Task.Run(() => {
        return _renderService.RenderPage(_document, pageIndex, dpi);
    });
}

Future Considerations

Planned Refactoring

  1. Independent Redaction Library: Extract PdfEditor.Redaction as standalone library
  2. Plugin Architecture: Allow extensibility for new features
  3. OCR Integration: Built-in OCR for scanned documents

Scalability

Current architecture handles:

  • PDFs up to ~1000 pages
  • Pages with ~500 operations each
  • Real-time rendering at 150 DPI

For larger documents, consider:

  • Virtual scrolling for thumbnails
  • On-demand page loading
  • Background pre-rendering

Last updated: 2024-12-29

Clone this wiki locally