diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md deleted file mode 100644 index 0e58f08..0000000 --- a/.agents/AGENTS.md +++ /dev/null @@ -1,431 +0,0 @@ -# FryPDF Agent Operating Guide (AGENTS.md) - -Welcome to the **FryPDF** repository. This guide provides AI agents and human contributors with the architectural guidelines, core workflows, conventions, and debugging procedures needed to work effectively in this codebase. - ---- - -## 1. Project Overview & Tech Stack - -**FryPDF** is a high-performance, cross-platform PDF creation, editing, and document analysis studio. - -- **Framework**: .NET 10 / C# 13+ -- **Architecture**: Microkernel Plugin System ("Everything is a Plugin") inspired by Cordis architecture -- **UI Toolkit**: Avalonia UI (v12.x) with Google Material Design 3 (M3) Expressive styling (macOS, Windows, Linux) -- **MVVM**: `CommunityToolkit.Mvvm` (Observable Objects, Relay Commands, Source Generators) -- **PDF Generation & Export**: `QuestPDF` -- **PDF Parsing & Deconstruction**: `UglyToad.PdfPig` & `PdfPig.DocumentLayoutAnalysis` -- **Raster & Graphics Engine**: `SkiaSharp` -- **Testing**: `xUnit` + Skia visual verification testing suite (770+ unit and integration tests) - ---- - -## 2. Essential Commands - -### Build Solution -```bash -dotnet build -``` - -### Run Full Test Suite (770+ Unit & Integration Tests) -```bash -dotnet test -``` - -### Run Specific / Filtered Tests -```bash -dotnet test --filter "FullyQualifiedName~GestureAndNavigationTests" -dotnet test --filter "FullyQualifiedName~PluginKernelTests" -dotnet test --filter "FullyQualifiedName~PluginsStudioUiTests" -dotnet test --filter "FullyQualifiedName~Material3ExpressiveThemeTests" -dotnet test --filter "FullyQualifiedName~GenerateVisualComparison_SideBySide" -``` - -### Run Desktop App Locally -```bash -dotnet run --project src/PdfEditorApp/PdfEditorApp.csproj -``` - ---- - -## 3. Project Structure - -``` -PDFCreator/ -├── src/ -│ ├── PdfEditorApp.Core/ # Pure headless core engine (cross-platform, zero UI dependencies) -│ │ ├── Analysis/ # Layout analysis, typography, Unicode script detection -│ │ ├── Deconstruction/ # PDF deconstruction engine (PDF stream to live editable model) -│ │ ├── Models/ # Domain element POCOs (Text, Image, Shape, Table, Chart, Math, Form) -│ │ └── Plugins/ # Microkernel plugin infrastructure (IFryPluginContext, DAG, Pipelines) -│ │ ├── Descriptors/ # Extension descriptors (Tools, Ribbon, Sidebars, Inspector, Navigation) -│ │ ├── Pipelines/ # Typed dispatch pipelines (Waterfall, Bail, Parallel, Serial, Emit) -│ │ ├── Profiles/ # App composition profiles (desktop, headless-cli, sdk) and bundle contracts -│ │ └── Settings/ # Typed plugin settings schema and persistent file store -│ └── PdfEditorApp/ # Main Avalonia desktop application -│ ├── Assets/ # Embedded fonts (Noto Sans, Roboto, Inter, etc.) and SVG icons -│ ├── Plugins/ # Desktop plugin implementations and modular bundles -│ │ ├── Bundles/ # 16 standard bundles (Tools, Pages, Sidebars, AI, CanvasElements, etc.) -│ │ └── Loader/ # Collectible AssemblyLoadContext and .fryplugin package extractor -│ ├── Services/ # Dynamic registries (Tools, Ribbon, Sidebars, Inspector, Navigation, AI) -│ ├── ViewModels/ # MVVM ViewModels (MainViewModel, HomeViewModel, ElementViewModels) -│ └── Views/ # Avalonia XAML Views, canvas controls, and M3 dialogs -├── tests/ -│ └── PdfEditorApp.Tests/ # Comprehensive xUnit test suite (770+ tests) & visual verification suite -├── docs/ # Architecture, contributing, M3 guidelines, and plugin manuals -├── AGENTS.md # Root link to this operating guide -├── GEMINI.md # Immediate critical agent mandate instructions -└── README.md # Repository overview -``` - ---- - -## 4. Architectural Rules & Subsystem Guidelines - -### A. PDF Deconstruction Engine ([`PdfDeconstructionEngine.cs`](src/PdfEditorApp.Core/Deconstruction/PdfDeconstructionEngine.cs)) - -When importing 3rd-party PDFs for live editing: - -1. **Strict Layered Z-Index Allocation**: - - **`ZIndex = 0..99`**: Background container shapes ($W \ge 120\text{ pt}, H \ge 80\text{ pt}$) and faint watermarks. - - **`ZIndex = 100..499`**: Content images (photos, QR codes, emblems, government banners, logos). - - **`ZIndex = 500..599`**: Structured tables and data grids. - - **`ZIndex = 600..999`**: Foreground shapes, badges, and divider lines. - - **`ZIndex = 1000..1999`**: Text blocks, headings, labels, and rotated vertical marginalia. - - **`ZIndex = 2000+`**: Interactive form fields, checkboxes, and digital signatures. - > **CRITICAL RULE**: Background shapes must NEVER be placed at a higher Z-index than content images. Otherwise, white container cards will occlude images underneath. - -2. **Multi-Format Skia Image Extraction**: - - Always use SkiaSharp multi-format decoding (`ExtractImageBytes`) to decode raw DCT/JPEG, JPEG2000, uncompressed CMYK/RGB, and monochrome 1-bit samples before re-encoding to standardized PNG bytes. - -3. **Watermark Classification**: - - Only classify an image as a background watermark if it spans $\ge 65\%$ page width and $\ge 55\%$ page height. Sub-page content images (such as information cards) must retain $100\%$ opacity. - ---- - -### B. Layout Analysis & Typography Engine ([`PdfLayoutAnalyzer.cs`](src/PdfEditorApp.Core/Analysis/PdfLayoutAnalyzer.cs)) - -1. **Script-Aware Token Joining**: - - For Indic scripts (Devanagari, Tamil, Telugu, etc.) and CJK, evaluate horizontal bounding-box gaps. Sub-threshold gaps must be joined without artificial spaces to preserve natural words. - -2. **Paragraph Indent & Clustering**: - - In `ShouldClusterLines`, check vertical line pitch ($\le 0.95 \times \text{FontSize}$). - - If the next line has a positive left indent ($>8\text{ pt}$), break to start a new paragraph. - -3. **Line Height Multipliers**: - - `LineHeight` is a proportional multiplier ($1.25\times$ to $1.4\times\text{FontSize}$), not fixed points. - -4. **Script-Aware Font Fallback ([`UnicodeScriptDetector.cs`](src/PdfEditorApp.Core/Analysis/UnicodeScriptDetector.cs))**: - - Match detected Unicode script ranges to system/embedded font families (e.g. `Noto Sans Devanagari`, `Kohinoor Devanagari`, `Noto Sans SC`, `PingFang SC`) to eliminate tofu boxes (`□□□`). - ---- - -### C. MVVM Architecture & Maximum CommunityToolkit.Mvvm Utilization - -Always take maximum advantage of **`CommunityToolkit.Mvvm`** features and source generators across all ViewModels: - -1. **Source Generators Over Boilerplate**: - - **`[ObservableProperty]`**: Always prefer `[ObservableProperty]` attributes on private fields (`[ObservableProperty] private string _title;`) to let Roslyn source generators generate clean, type-safe public properties and partial change methods (`OnTitleChanged`). - - **`[NotifyPropertyChangedFor]`**: Use `[NotifyPropertyChangedFor(nameof(DependentProperty))]` on backing fields to automatically cascade dependent property notifications instead of manually calling `OnPropertyChanged` in property bodies. - - **`[NotifyCanExecuteChangedFor]`**: Use `[NotifyCanExecuteChangedFor(nameof(MyCommand))]` on state-driving properties so buttons bound to commands automatically refresh their enabled/disabled visual state without manual polling. - -2. **Relay Commands & Async Operations**: - - **`[RelayCommand]`**: Always use `[RelayCommand]` on methods instead of declaring manual `IRelayCommand` fields and instantiating `new RelayCommand(...)`. - - **Async Relay Commands**: Declare asynchronous operations directly as `[RelayCommand] private async Task LoadDocumentAsync(...)`. CommunityToolkit automatically manages `IsRunning`, concurrent execution locking, and cancellation token propagation. - - **CanExecute Wiring**: Use `[RelayCommand(CanExecute = nameof(CanPerformAction))]` for clean condition-based execution guards. - -3. **Loose Coupling & Memory-Safe Messaging (`WeakReferenceMessenger`)**: - - Use `WeakReferenceMessenger.Default.Send(new Message(...))` and `Register(this, ...)` for decoupled communication between services, ribbons, sidebars, and dialogs. - - Never pass strong references to ViewModels or Views to prevent memory retention cycles. - - For ViewModels requiring lifetime-bound messaging, inherit from `ObservableRecipient` and activate/deactivate with `IsActive = true/false`. - -4. **Input Validation with `ObservableValidator`**: - - Inherit from `ObservableValidator` for dialog forms and data entry ViewModels. - - Use standard DataAnnotations (`[Required]`, `[Range]`, `[RegularExpression]`, `[CustomValidation]`) and call `ValidateProperty(value, nameof(...))` or `ValidateAllProperties()`. - -5. **Canvas Element ViewModels**: - - Every canvas element inherits from `ElementViewModelBase` and implements `LoadFromModel(PdfElementModelBase)` and `ToModel()`. - - Property changes must notify to support live canvas rendering, property sidebar binding, and atomic undo/redo records. - - Keep business logic and coordinate conversions inside `Core` and `Services`, leaving ViewModels focused on presentation state and commands. - ---- - -### D. Avalonia UI Data Binding & Value Converters - -When binding ViewModels to Avalonia XAML Views: - -1. **Verify Converter Requirement for Data Types & Transforms**: - - Always check if a ViewModel property type differs from the View property type (e.g. `double` $\to$ `ITransform`, `string` hex $\to$ `IBrush`/`Color`, `long` bytes $\to$ formatted size, `enum` $\to$ display title, `double` points $\to$ mm/inches). - - **Critical Avalonia Rule**: Never instantiate an `AvaloniaObject` transform directly inside a Style/ControlTheme Setter (e.g. ``). Avalonia `Transform` instances do **not** inherit `DataContext` inside Setter values, causing `{Binding}` to fail silently to 0. Always bind through an `IValueConverter`: - ```xml - - - ``` - -2. **Check Existing Converter Registrations**: - - Inspect [`CommonConverters.cs`](src/PdfEditorApp/Converters/CommonConverters.cs) and [`ExtendedConverters.cs`](src/PdfEditorApp/Converters/ExtendedConverters.cs) before writing ad-hoc inline bindings. - - When adding a new converter, register it in [`App.axaml`](src/PdfEditorApp/App.axaml) for global application availability. - -3. **Converter Quality & Testing**: - - Implement `ConvertBack` whenever two-way binding is required (e.g. numeric adjustments, unit conversions, color views). - - Handle `null` and invalid input gracefully without throwing unhandled exceptions. - - Add deterministic unit test assertions in [`tests/PdfEditorApp.Tests/ConverterTests.cs`](tests/PdfEditorApp.Tests/ConverterTests.cs). - ---- - -### E. Memory Leak Prevention, Resource Disposal & LOH Management - -To maintain high responsiveness and prevent memory leaks during long editing sessions and large PDF processing: - -1. **Large Object Heap (LOH) & Base64 Bloat Prevention**: - - **Never** store Base64 strings for raw image or vector assets inside document models (`PdfDocumentModel`, `PdfPageModel`, `PdfImageElement`) or ViewModels. - - Store raw `byte[] ImageData` directly. Any object $\ge 85,000\text{ bytes}$ is allocated on the LOH, which is not compacted in standard GC cycles and causes memory fragmentation. - - `Base64Data` must only be computed lazily on demand for JSON serialization. - -2. **Unmanaged Graphics Handle & Bitmap Disposal**: - - `SKBitmap`, `SKImage`, `SKSurface`, `SKPixmap`, `SKData`, `SKCodec`, and Avalonia `Bitmap`/`WriteableBitmap` wrap native unmanaged graphics handles (`IntPtr`). - - Always wrap short-lived Skia graphics objects in `using` statements. - - In ViewModels ([`ImageElementViewModel.cs`](src/PdfEditorApp/ViewModels/ElementViewModels/ImageElementViewModel.cs), [`ChartElementViewModel.cs`](src/PdfEditorApp/ViewModels/ElementViewModels/ChartElementViewModel.cs)), always dispose previous bitmap instances before assigning a new `PreviewBitmap` or `ChartBitmap`: - ```csharp - var oldBitmap = _previewBitmap; - _previewBitmap = newBitmap; - OnPropertyChanged(nameof(PreviewBitmap)); - oldBitmap?.Dispose(); - ``` - - When closing pages or clearing canvas items, ensure child element view models dispose their bitmaps. - -3. **Event Handler & Subscription Lifecycle**: - - Do not attach event listeners (`PropertyChanged`, `CollectionChanged`, timers) to singleton services from transient ViewModels without an unsubscription mechanism. - - Prefer `CommunityToolkit.Mvvm` `WeakReferenceMessenger` for cross-component communication to avoid strong reference cycles that prevent garbage collection. - -4. **PDF Streams & UglyToad.PdfPig Lifecycle**: - - `PdfDocument.Open(...)` parses internal object tables and font dictionaries. Always wrap `PdfDocument` instances in `using` blocks in [`PdfDeconstructionEngine.cs`](src/PdfEditorApp.Core/Deconstruction/PdfDeconstructionEngine.cs) and services. - - Always dispose input/output `MemoryStream` and `FileStream` objects promptly. - -5. **Bounded Undo/Redo History**: - - The undo/redo command stack must remain bounded (e.g. max 50 actions) to prevent unlimited memory growth from document state snapshots. - ---- - -### F. Google Material Design 3 (M3) Expressive UI Mandate (STRICT) - -**CRITICAL MANDATE**: ALL future UI changes, new views, dialogs, sidebars, controls, or visual enhancements across FryPDF **MUST strictly adhere to Google Material Design 3 (M3) Expressive** design principles. Flat, rigid, or rectangular legacy styles are strictly forbidden. - -1. **Expressive Shape Scale Hierarchy**: - - Never hardcode arbitrary small corner radii (`CornerRadius="2"`, `"4"`, etc.) on major interactive elements. - - Always reference the centralized M3 shape scale tokens defined in [`Material3ExpressiveTokens.axaml`](src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml): - - **`M3ShapeCornerNone` (0px)**: Edge-to-edge full-bleed panels and splitters. - - **`M3ShapeCornerExtraSmall` (4px)**: Micro badges, code spans, keyboard shortcut badges (`kbd-badge`). - - **`M3ShapeCornerSmall` (8px)**: Badges, thumbnail cards, compact chips, small status pills. - - **`M3ShapeCornerMedium` (12px)**: Context menus, tooltips, list items, card inner sections. - - **`M3ShapeCornerLarge` (16px)**: Content cards (`m3-card-elevated`), `TextBox.m3-outlined`, `ComboBox`, `NumericUpDown`, inspector section groups. - - **`M3ShapeCornerExtraLarge` (28px)**: Modal dialog cards (`Border.m3-dialog-card`, `AboutDialog`, `CommandPaletteDialog`, `AiAssistantDialog`), hero promo banners. - - **`M3ShapeCornerFull` (9999px)**: Tactile pill buttons (`primary-btn`, `accent-btn`, `action-pill-btn`, `m3-filled-btn`, `m3-tonal-btn`), segmented button capsules (`m3-segmented-container`), global search bars (`TextBox.m3-search`), Floating Action Buttons (`m3-fab`), and chubby slider thumbs. - -2. **Tonal Palette & Dynamic Color Roles**: - - Never hardcode light/dark hex colors (e.g. `#FFFFFF`, `#000000`, `#F1F5F9`) in templates or hover triggers that break dark mode adaptability. - - Always reference semantic M3 color roles via `{DynamicResource ...}`: - - **Primary Actions**: `M3PrimaryBrush`, `M3OnPrimaryBrush`, `M3PrimaryContainerBrush`, `M3OnPrimaryContainerBrush`. - - **Secondary / Active Accents**: `M3SecondaryBrush`, `M3SecondaryContainerBrush`, `M3OnSecondaryContainerBrush`. - - **Surfaces & Layers**: `M3SurfaceBrush`, `M3SurfaceDimBrush`, `M3SurfaceContainerLowestBrush` through `M3SurfaceContainerHighestBrush`. - - **Outlines & Dividers**: `M3OutlineBrush`, `M3OutlineVariantBrush`. - - **Legacy Aliases**: When maintaining existing views, legacy aliases (`WinBgBrush`, `WinPanelBrush`, `WinBorderBrush`, `WinAccentBrush`, `WinTextBrush`, `WinMutedBrush`, `WinHoverBrush`, `WinActiveBrush`, `WinInputBgBrush`) are supported and automatically map to M3 Expressive tokens. - -3. **Standard Component Classes ([`Material3ExpressiveStyles.axaml`](src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml))**: - - **Buttons**: Use standard M3 hierarchy (`m3-filled-btn`, `m3-tonal-btn`, `m3-elevated-btn`, `m3-outlined-btn`, `m3-text-btn`, `m3-fab`, `m3-fab-extended`, `m3-icon-btn`). - - **Segmented Capsules**: Wrap segmented buttons in `Border.m3-segmented-container` with child `Button.m3-segment-btn` or `RadioButton.m3-segment-btn`. - - **Chips**: Use `Button.m3-chip`, `Button.m3-filter-chip`, or `ToggleButton.m3-filter-chip` with `M3ShapeCornerSmall` or pill geometry. - - **Cards & Dialogs**: Use `Border.m3-card-elevated`, `Border.m3-card-filled`, `Border.m3-card-outlined`, and `Border.m3-dialog-card`. - - **Inputs & Search**: Use `TextBox.m3-outlined` for forms/inspector and `TextBox.m3-search` for pill search fields. - - **Tactile Chubby Sliders**: Retain chubby tactile sliders with `SliderTrackThemeHeight` ($\ge 8\text{px}$), `SliderHorizontalThumbWidth` ($\ge 20\text{px}$), and smooth hover scale transitions. - -4. **Elevation Shadows & Motion Physics**: - - Apply elevation on `Border` elements using multi-layered diffuse shadows: `{StaticResource M3ElevationLevel0}` through `{StaticResource M3ElevationLevel5}`. (Note: `BoxShadow` is an Avalonia `Border` property; do not apply it directly to `Button` or `ContextMenu` selectors). - - Use subtle, springy micro-transitions (`BrushTransition` 150ms, `TransformOperationsTransition` 150ms) on hover and pressed states. - -5. **Regression Verification**: - - Every new token or UI component style must be covered in [`tests/PdfEditorApp.Tests/Material3ExpressiveThemeTests.cs`](tests/PdfEditorApp.Tests/Material3ExpressiveThemeTests.cs) and pass `dotnet test`. - - Build must always remain 100% warning-free (`TreatWarningsAsErrors=true`). - ---- - -### G. Modular Microkernel Plugin Architecture ("Everything is a Plugin") (STRICT) - -**CRITICAL ARCHITECTURAL MANDATE**: -FryPDF implements an **"Everything is a Plugin"** microkernel architecture inspired by the **Cordis** framework. -**Monolithic additions, hardcoded switch-case statements, god viewmodels, and hardwired view instantiations are strictly forbidden.** -Whenever you create new tools, add document operations, contribute ribbon actions, add sidebar panels, introduce inspector sections, or add new canvas element types, you **MUST implement them as modular plugins registered through dynamic capability registries**. - -#### 1. The 5 Core Architectural Pillars -- **A. Inverted Capability Context (`IFryPluginContext`)**: Plugins register and consume capabilities via a thread-safe context (`ctx.RegisterService`, `ctx.GetService`, `ctx.TryGetService`). -- **B. Directed Acyclic Graph (DAG) Dependency Solver**: Plugins declare dependencies via `RequiredServices` or `[DependsOn]`. The kernel sorts and mounts dependencies in topological order. -- **C. Reversible Effects (`PluginScope` & LIFO Rollback)**: Every registration (listeners, commands, event handlers) is tracked as an effect. Unloading a plugin unwinds its effect stack in reverse order, ensuring 100% clean teardown with zero dangling subscriptions. -- **D. Typed Dispatch Pipelines**: - - `Waterfall`: Around-middleware with `next()` delegation (e.g. Export pipelines, document watermarking, audit trails). - - `Bail`: Short-circuits on first handled result (e.g. multi-format document converters, OCR engines). - - `Parallel`: Concurrent asynchronous dispatch via `Task.WhenAll` (e.g. analytics, autosave). - - `Serial`: Sequential asynchronous dispatch (e.g. validation chains, signature checks). - - `Emit`: Decoupled broadcast notifications. -- **E. Composable Profiles & Bundles**: Capabilities are grouped into self-contained `IFryPluginBundle` instances and composed into profiles (`desktop`, `headless-cli`, `sdk`). - -#### 2. The 12 Dynamic Registry Pillars -Never write hardcoded UI wiring. Mount capabilities through the designated registry: - -| Pillar / Registry | Descriptor | Typical Usage | -| :--- | :--- | :--- | -| **1. PDF Tools** | `IPdfToolRegistry` / `PdfToolDescriptor` | New tools (Merge, Split, Watermark, Compress, etc.). Inherit from `ToolPluginBase`. | -| **2. Dynamic Ribbon** | `IRibbonRegistry` / `RibbonContribution` | Custom ribbon tabs (`RibbonTabDescriptor`), groups, and action pill buttons. | -| **3. Extensible Sidebars** | `ISidebarRegistry` / `SidebarTabDescriptor` | Custom sidebars (signatures, bookmarks, AI search, metadata trees). | -| **4. Contextual Inspector** | `IInspectorRegistry` / `InspectorSectionDescriptor` | Custom property sections targeting specific canvas element types. | -| **5. Workspace Pages** | `INavigationRegistry` / `NavigationItemDescriptor` | New workspace pages and navigation sections. | -| **6. Canvas Elements** | `ICanvasElementRegistry` / `CanvasElementDescriptor` | Custom element types (Barcode, LaTeX Math, Form, Stamp, Ink). | -| **7. Document Importers** | `IDocumentImporterRegistry` / `DocumentImporterDescriptor` | Born-digital PDF, DOCX, XLSX, Markdown, HTML ingestion. | -| **8. Document Exporters** | `IDocumentExporterRegistry` / `DocumentExporterDescriptor` | QuestPDF static PDF, PDF/A, image sequences, CSV data export. | -| **9. AI Providers** | `IAiProviderRegistry` / `AiProviderDescriptor` | Local Ollama, OpenAI, Groq, Anthropic, custom LLM endpoints. | -| **10. OCR Engines** | `IOcrEngineRegistry` / `OcrEngineDescriptor` | Tesseract, Apple Vision OCR, Windows OCR, cloud vision engines. | -| **11. Data Connectors** | `IDataConnectorRegistry` / `DataConnectorDescriptor` | Excel, CSV, JSON REST APIs, SQL database ingestion. | -| **12. Status & Dialogs** | `IStatusBarRegistry`, `IDialogRegistry` | Contextual status indicators, modals, and command palette actions. | - -#### 3. Standard Tool Plugin Pattern Example -When implementing a new tool, inherit from `ToolPluginBase` and register it inside a bundle: - -```csharp -public class CustomWatermarkToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.watermark"; - public override string Name => "Watermark PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - Name = Name, - Description = "Apply custom text and image stamps with opacity control.", - Category = "OptimizeAndSecurity", - IconKind = "Watermark", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} -``` - -#### 4. Reversible Effects Rule -Any plugin subscribing to events or registering UI elements must use `ctx.RegisterEffect`: -```csharp -public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) -{ - var subscription = WeakReferenceMessenger.Default.Register(this, HandleDocumentChanged); - ctx.RegisterEffect(() => WeakReferenceMessenger.Default.Unregister(this)); - return Task.CompletedTask; -} -``` - -#### 5. Declarative Settings Schema -Plugins with configurable settings declare typed schemas via `IFryPlugin.SettingsSchema`. The UI automatically generates an M3 Expressive configurator in `PluginsDialog.axaml`: -```csharp -public IReadOnlyDictionary? SettingsSchema => new Dictionary -{ - ["ApiKey"] = new() { Key = "ApiKey", Label = "API Key", Type = PluginSettingType.Secret, IsRequired = true }, - ["Model"] = new() { Key = "Model", Label = "Model", Type = PluginSettingType.Select, Options = ["gpt-4o", "claude-3-5-sonnet"] } -}; -``` - ---- - -### H. High-Performance, Zero-Lag Responsiveness & Resource Lifecycle (STRICT) - -**CRITICAL PERFORMANCE MANDATE**: -FryPDF is designed to feel instantaneous, smooth, and lightweight. **UI lag, dropped frames, stuttering animations, and memory leaks are completely unacceptable.** -Always think in terms of performance optimization when designing and writing code: - -#### 1. Zero UI Thread Blocking -- **Never Run Heavy Work on the UI Thread**: PDF parsing, deconstruction, Skia rasterization, QuestPDF compilation, OCR recognition, AI completions, JSON serialization, and disk/network I/O MUST run on background threads via `Task.Run` or asynchronous pipelines. -- **Never Block Asynchronously**: Never call `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` on the Avalonia UI thread. -- **Cancellation Token Propagation**: Always accept and respect `CancellationToken` in long-running jobs to allow instant aborts when the user switches pages or cancels operations. - -#### 2. Instant Navigation & View Caching (0ms Latency) -- **Plugin View Caching**: In view models managing navigation (e.g. [`HomeViewModel.cs`](src/PdfEditorApp/ViewModels/HomeViewModel.cs)), always cache dynamically instantiated plugin views in an instance dictionary (`_dynamicViewCache`). Never destroy and re-instantiate heavy visual trees on every tab switch: - ```csharp - if (!_dynamicViewCache.TryGetValue(sectionName, out var cachedView)) - { - cachedView = desc.ViewFactory(serviceProvider); - _dynamicViewCache[sectionName] = cachedView; - } - DynamicPageView = cachedView; - ``` -- **Pre-Mounted Built-In Trees**: For primary screens (Dashboard, Reader Landing, PDF Tools Studios), pre-mount compiled XAML templates in the parent view (`HomeView.axaml`) so navigation occurs in 0ms with zero layout recalculation overhead. - -#### 3. UI Virtualization & Recycling -- **Virtualizing Panels Only**: Always use virtualized layout containers (`ItemsRepeater`, `VirtualizingStackPanel` with `ScrollUnit="Pixel"`) for document page thumbnails, tool cards, audit logs, and tabular data rows. -- **Never use unvirtualized `StackPanel`** inside a `ScrollViewer` for collections with more than 10 items. -- **Element Recycling**: Ensure templates in `ItemsRepeater` reuse container elements rather than continually allocating visual tree nodes during scrolling. - -#### 4. Gesture & Continuous Input Smoothing -- **Proportional Clamped Zooming**: For trackpad pinch-to-zoom and gestures, calculate zoom multiplicatively with bounds clamping to prevent exponential explosion: - ```csharp - double newZoom = Math.Clamp(Math.Round(currentZoom * (1.0 + delta), 3), 0.1, 5.0); - ``` -- **Reactive Debounce & Throttling**: - - Debounce search queries and text filters (150–250ms) before filtering in-memory collections. - - Throttle slider scrubbing events during interactive drag; do not trigger expensive Skia canvas repaints on every micro-pixel increment. - - Throttle canvas drag and snap-line calculation loops. - -#### 5. SkiaSharp Rendering & Frame Budgets -- **Frame Budgets**: Maintain $\le 16\text{ms}$ (60 FPS) and $\le 8\text{ms}$ (120 FPS) frame render times. -- **RenderTransform Over Layout Mutation**: Animate transformations using `RenderTransform` (GPU-accelerated compositing). Never animate `Margin`, `Width`, or `Height`, as mutating layout properties triggers full synchronous measure/arrange passes across the visual tree. -- **Minimize Visual Tree Depth**: Avoid unnecessary nested `Border`, `Grid`, and `Panel` elements. Keep visual hierarchy shallow. - -#### 6. Large Object Heap (LOH) & GC Hygiene -- **Never allocate $\ge 85\text{ KB}$ byte arrays in loops**: Reuse memory buffers via `ArrayPool.Shared` and `MemoryStream`. -- **Zero Base64 in Models**: Store raw `byte[]` for image and vector payloads. Compute Base64 only lazily during JSON persistence export. -- **Immediate Unmanaged Disposal**: Wrap short-lived `SKBitmap`, `SKImage`, `SKSurface`, `SKData`, and `SKCodec` instances in `using` blocks. Dispose previous bitmaps before assigning replacement preview bitmaps. -- **Weak Subscriptions**: Use `WeakReferenceMessenger` for pub/sub messaging to ensure garbage collection is never blocked by forgotten event subscriptions. - ---- - -## 5. Continuous PDF Scenario Development Workflow - -When adding support for a new or complex PDF type: - -1. **Place Sample PDF Locally**: Save the test PDF in the project root or test directory. Ensure it matches `.gitignore` patterns (`*.pdf`, `sample*.pdf`) and is **never** staged or committed to Git. -2. **Run Visual Verification Test**: - ```bash - dotnet test --filter "FullyQualifiedName~GenerateVisualComparison_SideBySide" - ``` -3. **Inspect Output Bitmaps**: Open the generated side-by-side PNG in `VisualArtifacts/` (automatically ignored by Git) to compare original PDF ground truth against the deconstructed canvas. -4. **Tune Algorithms**: Adjust clustering, Z-indexing, or shape heuristics in `PdfDeconstructionEngine` or `PdfLayoutAnalyzer`. -5. **Enforce Regression Invariance**: Run `dotnet test` and confirm all 770+ unit tests pass. - ---- - -## 6. Coding & Quality Standards - -- **Nullable Reference Types**: Enabled across all projects (`enable`). -- **Treat Warnings As Errors**: All builds must be 100% warning-free (`TreatWarningsAsErrors=true`). -- **Plugin-First Architecture**: All new tools, elements, sidebars, and panels must implement `IFryPlugin` and register into the appropriate registry. -- **Zero-Lag Compliance**: Zero UI-thread blocking, view caching on navigation, and virtualized collections are mandatory. -- **Unit Testing**: Every new service, converter, plugin, or deconstruction heuristic must include unit tests with deterministic assertions. -- **Documentation**: Keep [`docs/PLUGIN_BASED_ARCHITECTURE.md`](docs/PLUGIN_BASED_ARCHITECTURE.md), [`docs/PDF_DECONSTRUCTION_AND_EDITING.md`](docs/PDF_DECONSTRUCTION_AND_EDITING.md), and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) updated when making structural changes. - ---- - -## 7. Security, Privacy & Zero PII Policy (STRICT) - -**CRITICAL RULE**: Under no circumstances should sensitive data, credentials, or Personally Identifiable Information (PII) ever be written, hardcoded, or committed to this repository. - -1. **Zero PII in Source Code and Tests**: - - **Never** hardcode real personal names, private addresses, real phone numbers, real email addresses, or personal biometric/signature records in source code, unit test assertions, docstrings, or template defaults. - - **Never** write real government identification numbers (e.g. Aadhaar numbers, SSNs, PANs, Passport numbers, Driver's License IDs). - - In test cases, always use **structural assertions** (e.g. element count, dimension bounds, non-empty text) or **generic regular expressions** (e.g. `\b\d{4}\s\d{4}\s\d{4}\b`) rather than asserting specific personal data. - -2. **No Credentials, Keys, or Secrets**: - - **Never** commit API keys, access tokens, passwords, private certificates, or user credentials. - - Use environment variables or configuration providers for external services where needed. - -3. **Synthetic & Dummy Data Only**: - - When building mock templates, demonstrations, or test fixtures, use only standard synthetic dummy data (e.g. `Jane Doe`, `555-0199`, `user@example.com`, `ACME Corp`, generic sample logos). - -4. **Local Artifact & PDF Exclusion**: - - All real-world test PDFs, generated side-by-side visual comparison bitmaps, and test exports must be excluded via `.gitignore` (`*.pdf`, `VisualArtifacts/`, `*_side_by_side.png`). - - Prior to making commits or pushing to remote, always verify with `git status` and `git diff` that no untracked PDFs or sensitive data are being introduced. diff --git a/.agents/rules/m3_expressive_ui_mandate.md b/.agents/rules/m3_expressive_ui_mandate.md deleted file mode 100644 index 1acf3ef..0000000 --- a/.agents/rules/m3_expressive_ui_mandate.md +++ /dev/null @@ -1,65 +0,0 @@ -# STRICT MANDATE: Google Material Design 3 (M3) Expressive UI - -**APPLIES TO ALL AI AGENTS AND DEVELOPERS**: -Whenever you create new UI, modify existing UI, add dialogs, sidebars, buttons, controls, or adjust styles/themes in FryPDF, you **MUST STRICTLY FOLLOW GOOGLE MATERIAL DESIGN 3 (M3) EXPRESSIVE** guidelines. -Never use sharp, rigid, boxy, or flat legacy styling. - ---- - -## 1. Shape Scale Hierarchy (NEVER HARDCODE SMALL CORNER RADII) -Never write arbitrary `CornerRadius="2"`, `"4"`, `"5"` on interactive controls or containers. Always reference the tokens in `Material3ExpressiveTokens.axaml`: - -- **Pill Contours (`CornerRadius="{StaticResource M3ShapeCornerFull}"` or `9999`)**: - - Primary, accent, tonal, and action buttons (`primary-btn`, `accent-btn`, `action-pill-btn`, `m3-filled-btn`, `m3-tonal-btn`) - - Segmented button capsules (`Border.m3-segmented-container` + `Button.m3-segment-btn`) - - Global search fields (`TextBox.m3-search`, Command Palette search bar) - - Floating Action Buttons (`m3-fab`, `m3-fab-extended`) - - Chubby slider thumbs -- **28px Extra-Large Contours (`CornerRadius="{StaticResource M3ShapeCornerExtraLarge}"`)**: - - Modal dialog cards (`Border.m3-dialog-card`, `AboutDialog`, `CommandPaletteDialog`, `AiAssistantDialog`) - - Large hero promo cards and dashboard banners -- **16px Large Contours (`CornerRadius="{StaticResource M3ShapeCornerLarge}"`)**: - - Content cards (`m3-card-elevated`, `m3-card-filled`, `m3-card-outlined`) - - Form fields and inputs (`TextBox.m3-outlined`, `ComboBox`, `NumericUpDown`) - - Inspector section cards -- **12px Medium Contours (`CornerRadius="{StaticResource M3ShapeCornerMedium}"`)**: - - Context menus, dropdown menus, flyouts, tooltips, list items -- **8px Small Contours (`CornerRadius="{StaticResource M3ShapeCornerSmall}"`)**: - - Chips (`m3-chip`, `m3-filter-chip`), thumbnail cards, status badges - ---- - -## 2. Color Roles (NO HARDCODED LIGHT/DARK HEX COLORS) -Never hardcode hex colors (like `#FFFFFF`, `#000000`, `#F1F5F9`) in templates or hover triggers that break dark mode adaptability. Always use `{DynamicResource ...}` tokens: - -- **Primary Actions**: `M3PrimaryBrush`, `M3OnPrimaryBrush`, `M3PrimaryContainerBrush`, `M3OnPrimaryContainerBrush` -- **Secondary / Active Accents**: `M3SecondaryBrush`, `M3SecondaryContainerBrush`, `M3OnSecondaryContainerBrush` -- **Surfaces & Layers**: `M3SurfaceBrush`, `M3SurfaceDimBrush`, `M3SurfaceContainerLowestBrush` through `M3SurfaceContainerHighestBrush` -- **Borders & Dividers**: `M3OutlineBrush`, `M3OutlineVariantBrush` -- **Legacy Aliases**: When maintaining existing views, `WinBgBrush`, `WinPanelBrush`, `WinBorderBrush`, `WinAccentBrush`, `WinTextBrush`, `WinMutedBrush`, `WinHoverBrush`, `WinActiveBrush`, `WinInputBgBrush` map automatically to M3 tokens. - ---- - -## 3. Standard Component Classes (`Material3ExpressiveStyles.axaml`) -- **Buttons**: `m3-filled-btn`, `m3-tonal-btn`, `m3-elevated-btn`, `m3-outlined-btn`, `m3-text-btn`, `m3-fab`, `m3-icon-btn` -- **Capsules & Chips**: `Border.m3-segmented-container`, `Button.m3-segment-btn`, `Button.m3-chip`, `ToggleButton.m3-filter-chip` -- **Cards & Dialogs**: `Border.m3-card-elevated`, `Border.m3-dialog-card` -- **Inputs & Search**: `TextBox.m3-outlined`, `TextBox.m3-search` -- **Tactile Sliders**: Chubby 8-10px tracks with 20-22px thumbs and interactive scale transitions. - ---- - -## 4. MVVM: Maximum CommunityToolkit.Mvvm Utilization -Take full advantage of **`CommunityToolkit.Mvvm`** across all ViewModels: -- **`[ObservableProperty]`**: Generate observable properties from private fields (`[ObservableProperty] private string _title;`). -- **`[NotifyPropertyChangedFor]` & `[NotifyCanExecuteChangedFor]`**: Cascade dependent updates and automatically refresh button commands without manual boilerplate. -- **`[RelayCommand]` & Async Commands**: Decorate methods directly with `[RelayCommand]`. Asynchronous tasks (`async Task DoWorkAsync()`) automatically handle `IsRunning`, cancellation, and locking. -- **`WeakReferenceMessenger`**: Use decoupled pub/sub messaging (`WeakReferenceMessenger.Default.Send(...)` / `Register(...)`) or inherit from `ObservableRecipient` to prevent memory leaks and strong references. -- **`ObservableValidator`**: Use for forms, dialogs, and validated inputs with standard DataAnnotations (`[Required]`, `[Range]`, etc.). - ---- - -## 5. Verification -After making any UI changes: -1. `dotnet build` must succeed with **0 warnings and 0 errors** (`TreatWarningsAsErrors=true`). -2. Run `dotnet test --filter "FullyQualifiedName~Material3ExpressiveThemeTests"` to ensure no token regressions. diff --git a/.agents/rules/performance_and_zero_lag_mandate.md b/.agents/rules/performance_and_zero_lag_mandate.md deleted file mode 100644 index 6a76c9e..0000000 --- a/.agents/rules/performance_and_zero_lag_mandate.md +++ /dev/null @@ -1,73 +0,0 @@ -# STRICT PERFORMANCE MANDATE: Zero-Lag, 60+ FPS & Responsive Architecture - -**APPLIES TO ALL AI AGENTS AND DEVELOPERS**: -FryPDF is an interactive, real-time desktop document studio. **UI lag, frame drops, blocking operations, stuttering transitions, and high memory retention are strictly forbidden.** -Whenever you write or modify code in FryPDF, you **MUST ALWAYS THINK IN TERMS OF PERFORMANCE OPTIMIZATION**. - ---- - -## 1. Zero UI Thread Blocking -- **Never Run Heavy Operations on the UI Thread**: - - PDF deconstruction, parsing, Skia rendering, QuestPDF generation, OCR execution, AI inference, and disk/network I/O **must always be offloaded** via `Task.Run` or asynchronous commands. -- **Never Block Asynchronously**: - - Calling `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` on the Avalonia UI thread is strictly forbidden. -- **Always Pass `CancellationToken`**: - - Accept and propagate cancellation tokens throughout long-running operations so users can instantly cancel actions or switch pages without background worker orphan threads. - ---- - -## 2. Instant Navigation & View Caching (0ms Latency) -- **Cache Contributed Plugin Views**: - - In navigation controllers (such as `HomeViewModel`), always cache dynamically created views in an instance dictionary (`_dynamicViewCache`). Never destroy and re-instantiate heavy visual trees when navigating between tabs. -- **Pre-Mounted Static Views**: - - Built-in primary pages (Overview Dashboard, Tools Studio, PDF Reader Landing) should have pre-mounted XAML elements in `HomeView.axaml` to guarantee 0ms instantaneous switching without layout compilation overhead. - ---- - -## 3. Virtualization & Recycling -- **Always Virtualize Dynamic Collections**: - - Always use `ItemsRepeater` or `VirtualizingStackPanel` with `ScrollUnit="Pixel"` for lists with variable or large items (document thumbnails, tool card grids, audit logs, tabular data). - - Never use an unvirtualized `StackPanel` inside a `ScrollViewer` for collections larger than 10 items. - ---- - -## 4. Continuous Input Throttling & Debouncing -- **Pinch-to-Zoom Math**: - - Calculate trackpad pinch gestures multiplicatively and clamp strictly: - `Math.Clamp(Math.Round(currentZoom * (1.0 + delta), 3), 0.1, 5.0)` to eliminate exponential explosion. -- **Debounced Filters**: - - Debounce search queries and text filters (150–250ms) to avoid executing filter passes on every keystroke. -- **Throttled Sliders**: - - Do not trigger expensive Skia repaints or document recompilations on every slider micro-tick; throttle updates during drag. - ---- - -## 5. Memory Management & Unmanaged Resource Disposal -- **Large Object Heap (LOH) Avoidance**: - - Any byte array $\ge 85\text{ KB}$ is allocated on the LOH and causes GC fragmentation. Use `ArrayPool.Shared` and `MemoryStream` buffers. - - Never store Base64 strings in memory for image or vector assets; store raw `byte[] ImageData` directly and compute Base64 only lazily for JSON export. -- **Dispose Unmanaged Skia Objects Immediately**: - - `SKBitmap`, `SKImage`, `SKSurface`, `SKData`, and Avalonia `WriteableBitmap` hold native unmanaged pointers. - - Always wrap temporary Skia objects in `using` blocks. - - Always dispose old bitmaps before assigning replacement preview bitmaps. -- **Weak Subscriptions**: - - Use `WeakReferenceMessenger` for pub/sub messaging across ViewModels to avoid memory retention cycles that prevent garbage collection. - ---- - -## 6. Rendering & Frame Budgets -- **Frame Budgets**: - - Maintain 16ms per frame (60 FPS) and 8ms per frame (120 FPS). -- **Favor `RenderTransform` Over Layout Mutations**: - - Use GPU-composited `RenderTransform` rather than mutating layout properties (`Margin`, `Width`, `Height`) during animations or gesture interactions to avoid triggering costly synchronous layout reflows. -- **Keep Visual Trees Shallow**: - - Avoid deeply nested `Border` and `Grid` hierarchies. - ---- - -## 7. Verification -- Run navigation and gesture performance tests: - ```bash - dotnet test --filter "FullyQualifiedName~GestureAndNavigationTests" - ``` -- Verify zero warnings (`TreatWarningsAsErrors=true`). diff --git a/.agents/rules/plugin_pattern_mandate.md b/.agents/rules/plugin_pattern_mandate.md deleted file mode 100644 index d7ad794..0000000 --- a/.agents/rules/plugin_pattern_mandate.md +++ /dev/null @@ -1,57 +0,0 @@ -# STRICT ARCHITECTURE MANDATE: Modular Microkernel Plugin Pattern ("Everything is a Plugin") - -**APPLIES TO ALL AI AGENTS AND DEVELOPERS**: -Whenever you add features, create tools, contribute UI components, introduce file formats, integrate AI or OCR models, or refactor existing subsystems in FryPDF, you **MUST STRICTLY FOLLOW THE MODULAR MICROKERNEL PLUGIN PATTERN**. -Never write monolithic switch-case statements, god classes, or hardcoded UI component instantiations. - ---- - -## 1. The "Everything is a Plugin" Principle -FryPDF's architecture is inspired by **DeepSeek Harness (`deepseek-harness`)** and the **Cordis** framework: -- The core runtime does not privilege any single subsystem. -- All tools, workspace pages, sidebar panels, ribbon buttons, inspector property sections, canvas elements, AI providers, OCR engines, data connectors, and file importers/exporters are **modular plugins mounted on `IFryPluginContext`**. - ---- - -## 2. Core Implementation Rules - -### A. Inherit from `IFryPlugin` or `ToolPluginBase` -- For PDF tools, inherit from `ToolPluginBase` (which provides automated descriptor registration into `IPdfToolRegistry`). -- For other capabilities, implement `IFryPlugin` directly (`Id`, `Name`, `Version`, `RequiredServices`, `ApplyAsync`). - -### B. Mount Through the 12 Dynamic Registry Pillars -Never instantiate or route components via hardcoded `switch` statements. Always register through the dynamic registry corresponding to the capability: -1. **Tools**: `IPdfToolRegistry` via `ctx.RegisterTool(...)` / `PdfToolDescriptor` -2. **Ribbon Tabs & Groups**: `IRibbonRegistry` via `ctx.RegisterRibbonContribution(...)` -3. **Sidebar Panels**: `ISidebarRegistry` via `ctx.RegisterSidebarTab(...)` -4. **Inspector Sections**: `IInspectorRegistry` via `ctx.RegisterInspectorSection(...)` -5. **Workspace Pages**: `INavigationRegistry` via `ctx.RegisterNavigationItem(...)` -6. **Canvas Elements**: `ICanvasElementRegistry` via `ctx.RegisterCanvasElement(...)` -7. **Document Importers**: `IDocumentImporterRegistry` -8. **Document Exporters**: `IDocumentExporterRegistry` -9. **AI Providers**: `IAiProviderRegistry` -10. **OCR Engines**: `IOcrEngineRegistry` -11. **Data Connectors**: `IDataConnectorRegistry` -12. **Status Bar & Modals**: `IStatusBarRegistry`, `IDialogRegistry` - -### C. Reversible Effects (`PluginScope` & LIFO Rollback) -Every registration—whether an event listener, a message broker subscription, a status bar widget, or an unmanaged cache—**MUST be registered through `PluginScope` (`ctx.RegisterEffect(...)`)**. -When a plugin is unmounted or reloaded, its effects unwind in reverse order to ensure **100% clean teardown and zero memory leaks**. - -### D. Group Capabilities into Bundles (`IFryPluginBundle`) -Group related plugins into a bundle (e.g. `ToolsOrganizeBundle`, `CanvasElementsBundle`, `WorkspacePagesBundle`). Bundles allow composition into runtime profiles (`desktop.profile.json`, `headless.profile.json`). - -### E. Declarative Settings Schema -If a plugin requires configuration (API keys, URLs, model names, options), declare them via `IFryPlugin.SettingsSchema` using `PluginSettingDefinition`. The UI automatically renders an M3 Expressive configuration card in `PluginsDialog.axaml` without manual XAML forms. - ---- - -## 3. Verification -- `dotnet build` must succeed with **0 warnings and 0 errors** (`TreatWarningsAsErrors=true`). -- Unit tests must be written for the plugin and descriptor registration in `tests/PdfEditorApp.Tests/`. -- Run: - ```bash - dotnet test --filter "FullyQualifiedName~PluginKernelTests" - dotnet test --filter "FullyQualifiedName~PluginsStudioUiTests" - dotnet test --filter "FullyQualifiedName~TotalMicrokernelTests" - ``` diff --git a/.agents/skills/avalonia/DETAILED_SUMMARY.md b/.agents/skills/avalonia/DETAILED_SUMMARY.md deleted file mode 100644 index 200c5bc..0000000 --- a/.agents/skills/avalonia/DETAILED_SUMMARY.md +++ /dev/null @@ -1,479 +0,0 @@ -# Avalonia Skill Refactoring - Detailed Summary - -## Executive Summary - -The **avalonia skill** has been successfully refactored from a monolithic ~650-line document into a modular orchestration hub following the proven pattern from **thought-patterns** skill. - -**Result**: A 314-line orchestration hub + 6 focused, self-contained resource files (3,852 lines total) that provide comprehensive, navigable guidance for cross-platform Avalonia development. - ---- - -## Before & After Comparison - -### Before Refactoring -``` -SKILL.md (650 lines) -├── Project structure -├── MVVM architecture -├── XAML best practices -├── Data binding -├── Value converters -├── Styling and theming -├── Common controls -├── Custom controls -├── Reactive programming -├── Cross-platform -├── Performance optimization -├── Common patterns -├── Testing -└── Debugging tips - -resources/ (3 files - basic, underutilized) -├── controls-reference.md (677 lines) -├── platform-specific.md (743 lines) -└── styling-guide.md (552 lines) -``` - -### After Refactoring -``` -SKILL.md (314 lines) - ORCHESTRATION HUB -├── Quick reference table (6 resources) -├── Framework overview -├── Getting started -├── Core patterns (MVVM, Reactive, Platform-adaptive) -├── Navigation by task (5 scenarios) -├── Resource organization guide -├── Common workflows (4 patterns) -├── Best practices summary -└── Cross-platform deployment info - -resources/ (6 files - comprehensive, well-organized) -├── mvvm-databinding.md (622 lines) - PRIMARY -├── controls-reference.md (677 lines) - PRIMARY -├── reactive-animations.md (665 lines) - ADVANCED -├── custom-controls-advanced.md (593 lines) - ADVANCED -├── styling-guide.md (552 lines) - PRIMARY -└── platform-specific.md (743 lines) - ADVANCED - -REFACTORING_SUMMARY.md (documentation) -``` - ---- - -## Content Breakdown by Resource - -### 1. MVVM Data Binding (622 lines) -**Purpose**: Foundation for understanding Avalonia's MVVM architecture and data binding system - -**Sections**: -- MVVM Architecture overview and benefits -- ViewModel base classes (ReactiveObject vs INotifyPropertyChanged) -- Data binding fundamentals (modes, paths, syntax) -- Binding paths (simple, nested, indexed, relative source) -- Binding to commands (parameter passing, multi-binding) -- Multi-binding (combining multiple values) -- Binding validation -- Value converters (single and multi-value) -- Dependency injection setup with ServiceCollection -- Collections and list binding (ObservableCollection) -- ListBox and DataGrid binding patterns -- Design-time data (for XAML preview) -- Common patterns (master-detail, tab navigation, loading state) -- Best practices - -**Key Features**: -- Comprehensive DI examples -- ReactiveUI integration -- Real-world validation patterns -- Service registration walkthrough - -### 2. Controls Reference (677 lines) -**Purpose**: Complete documentation of all Avalonia controls - -**Sections**: -- **Layout Controls**: Grid, StackPanel, DockPanel, WrapPanel, UniformGrid, Canvas, Panel, ScrollViewer, Border, Viewbox -- **Input Controls**: TextBox, Button, CheckBox, RadioButton, ComboBox, Slider, NumericUpDown, DatePicker, TimePicker, CalendarDatePicker, ToggleSwitch -- **Display Controls**: TextBlock, Label, Image, ProgressBar, Separator -- **Collection Controls**: ListBox, TreeView, DataGrid, ItemsControl, Carousel -- **Menu & Navigation**: Menu, ContextMenu, TabControl, Expander, SplitView -- **Dialogs & Popups**: Window, Dialog, ToolTip, Flyout -- **Drawing & Shapes**: Rectangle, Ellipse, Line, Polyline, Polygon, Path -- **Advanced Controls**: AutoCompleteBox, Calendar, MaskedTextBox, ColorPicker, PathIcon - -**Key Features**: -- XAML examples for every control -- Common property bindings -- Template customization -- Real-world usage patterns - -### 3. Reactive Programming & Animations (665 lines) -**Purpose**: Advanced reactive patterns and animation techniques - -**Sections**: -- ReactiveUI integration and installation -- Reactive properties with RaiseAndSetIfChanged -- Reactive commands (sync and async) -- Observable sequences (filtering, transformation) -- Combining observables (CombineLatest, Merge, Switch) -- Buffering and grouping -- Basic animations and transitions -- Complex multi-step animations -- Easing functions (10+ options with examples) -- Programmatic animations in code-behind -- Observable patterns (search with debounce, form validation, auto-complete) -- Performance optimization (debouncing, throttling, sampling) -- Memory management and disposal -- Best practices - -**Key Features**: -- Complete observable operator reference -- Easing function gallery -- Real-world search/validation patterns -- Performance tuning techniques - -### 4. Custom Controls & Advanced Techniques (593 lines) -**Purpose**: Building custom controls and optimizing complex UIs - -**Sections**: -- Creating custom TemplatedControl -- Control properties (StyledProperty) -- Template application (OnApplyTemplate) -- User control composition -- Control templates -- Advanced layouts (adaptive panels, virtualized) -- Performance optimization: - - Compiled bindings - - Virtualization (Simple, Item modes) - - Lazy loading and pagination - - Image optimization -- Render transforms (Translate, Scale, Rotate, Skew) -- Drawing and graphics: - - Shapes (Rectangle, Ellipse, Path) - - Drawing context API - - SVG-like path data -- Styling custom controls (complex selectors) -- Testing with Avalonia.Headless - -**Key Features**: -- Full custom control lifecycle -- Virtualization implementation -- Graphics rendering examples -- Custom control testing - -### 5. Styling Guide (552 lines) -**Purpose**: Advanced styling, theming, and animations - -**Sections**: -- Style basics (selectors, pseudo-classes, combinators) -- Resource dictionaries and theming -- Control templates with template parts -- Data templates (simple and hierarchical) -- Animations: - - Basic keyframe animations - - Transitions and timing - - Easing functions - - Complex multi-step animations - - Infinite loops -- Theme variants (light/dark mode) -- Theme-aware resources -- Runtime theme switching -- Advanced patterns: - - Button styles (primary, danger, icon) - - Card styles - - Input field styling - - List item styling -- Custom theme example (Material Design) -- Performance tips - -**Key Features**: -- Comprehensive selector reference -- Complete animation gallery -- Theme variant patterns -- Material Design example - -### 6. Platform-Specific Implementation (743 lines) -**Purpose**: Cross-platform considerations and platform-specific features - -**Sections**: -- Platform detection (Windows, macOS, Linux, Android, iOS, Browser) -- Multi-platform project structure -- Service abstraction pattern -- Platform-specific service implementations: - - Desktop (FileService example) - - Android (with Context) - - iOS (with NSFileManager) -- Service registration per platform -- Platform-specific UI (OnPlatform markup) -- Platform-specific resources -- Platform-specific views (view locator pattern) -- Window management per platform -- Mobile activity setup (Android) -- iOS AppDelegate -- File system access (cross-platform paths) -- Native dialogs -- Platform-specific features: - - Windows (UWP APIs, DLLs) - - macOS (Cocoa, Dock menu, Touch Bar) - - Linux (D-Bus, system tray) - - Android (Toast, permissions, sharing) - - iOS (activities, alerts, sharing) -- Input handling (touch vs mouse, keyboard shortcuts) -- Performance considerations per platform -- Testing platform-specific code - -**Key Features**: -- Complete service abstraction examples -- Platform detection patterns -- Conditional compilation examples -- All 6 platforms covered - ---- - -## Navigation Features - -### Quick Reference Table (Main SKILL.md) - -Immediate access to the right resource: - -| Task/Goal | Resource | -|-----------|----------| -| MVVM patterns, data binding, dependency injection, value converters | mvvm-databinding.md | -| UI controls reference | controls-reference.md | -| Custom controls, advanced layouts, performance optimization | custom-controls-advanced.md | -| Styling, themes, animations, control templates | styling-guide.md | -| Reactive patterns, commands, observables, animations | reactive-animations.md | -| Platform-specific implementation | platform-specific.md | - -### Task-Based Navigation (Main SKILL.md) - -Five common scenarios with explicit resource paths: - -``` -Task: "I need to build a form with validation" - 1. Load mvvm-databinding.md → Implement ViewModel - 2. Load controls-reference.md → Find form controls - 3. Load reactive-animations.md → Add reactive validation - -Task: "I'm seeing poor performance with large lists" - 1. Load custom-controls-advanced.md → Enable virtualization - 2. Load mvvm-databinding.md → Use compiled bindings - 3. Load reactive-animations.md → Debounce updates - -Task: "I need platform-specific behavior" - 1. Load platform-specific.md → Implement service interfaces - 2. Load mvvm-databinding.md → Register via DI - 3. Platform-specific projects → Implement per-platform - -Task: "I want custom styling and animations" - 1. Load styling-guide.md → Define styles/themes - 2. Load reactive-animations.md → Add animations - 3. Load custom-controls-advanced.md → Custom templates - -Task: "I'm building a complex control" - 1. Load custom-controls-advanced.md → TemplatedControl pattern - 2. Load mvvm-databinding.md → Attached properties/binding - 3. Load styling-guide.md → Control templates -``` - -### Common Workflows (Main SKILL.md) - -Four detailed workflow patterns: - -1. **Desktop App (Windows/macOS/Linux)** - - Standard project setup - - Create Views and ViewModels - - Use controls for UI - - Add styles - - Implement services with DI - - Add animations - - Test on each platform - -2. **Cross-Platform Mobile+Desktop** - - Shared + platform-specific projects - - Service interface abstraction - - Per-platform implementations - - OnPlatform adaptive UI - - DI registration - - Comprehensive testing - -3. **Real-Time Search** - - SearchViewModel design - - Observable collections - - Reactive search pattern - - Input debouncing - - ListBox display - - CSS styling - -4. **Complex Data-Driven UI** - - ViewModel hierarchy - - Master-detail views - - DataGrid for tables - - Sorting/filtering with observables - - Virtualization optimization - - Custom controls - ---- - -## Quality Metrics - -### Organization -- ✓ 6 focused resource files (vs. 1 monolithic file) -- ✓ Clear decision table for navigation -- ✓ Self-contained modules -- ✓ Zero redundancy across files -- ✓ Hierarchical section structure - -### Coverage -- ✓ All 20+ major controls documented with examples -- ✓ All 6 platforms (Windows, macOS, Linux, iOS, Android, Web) -- ✓ MVVM, reactive, styling, platform patterns -- ✓ Beginner to advanced topics -- ✓ Testing, debugging, optimization included - -### Usability -- ✓ Quick reference table (seconds to find resource) -- ✓ Task-based navigation (5 common scenarios) -- ✓ Workflow examples (4 detailed patterns) -- ✓ Code examples for every pattern -- ✓ Best practices in every resource - -### Content -- ✓ 3,852 total lines (vs. 650 original) -- ✓ 314-line orchestration hub -- ✓ 6x more comprehensive content -- ✓ All existing content preserved and enhanced -- ✓ Improved organization and clarity - -### Consistency -- ✓ Follows thought-patterns orchestration pattern -- ✓ Consistent formatting and structure -- ✓ Unified code example style -- ✓ Consistent best practices sections -- ✓ Aligned metadata and descriptions - ---- - -## File Statistics - -``` -File Lines Purpose -───────────────────────────────────────────────────────────── -SKILL.md 314 Orchestration hub -mvvm-databinding.md 622 MVVM + data binding -controls-reference.md 677 UI controls -reactive-animations.md 665 Reactive patterns -custom-controls-advanced.md 593 Advanced techniques -styling-guide.md 552 Styling + animations -platform-specific.md 743 Cross-platform -───────────────────────────────────────────────────────────── -TOTAL 4,166 (without REFACTORING_SUMMARY) -``` - ---- - -## Validation Checklist - -✓ All controls documented with XAML examples -✓ All 6 platforms covered (Windows, macOS, Linux, iOS, Android, Web) -✓ Styling and theming guidance complete (selectors, templates, animations) -✓ Data binding patterns fully documented (modes, converters, validation) -✓ Reactive programming comprehensive (properties, commands, observables) -✓ Performance optimization detailed (virtualization, compiled bindings) -✓ Testing guidance included (unit tests, UI tests, headless) -✓ MVVM architecture clearly explained -✓ Platform separation achieved (dedicated platform-specific.md) -✓ Controls organization improved (45+ controls across 8 categories) -✓ Navigation structure enhanced (quick reference + task-based routing) -✓ Design pattern examples provided (master-detail, validation, search) -✓ Best practices throughout (10+ per resource) -✓ Code examples all tested and verified format - ---- - -## Key Improvements Achieved - -### 1. Accessibility -- **Before**: Scroll through 650 lines to find information -- **After**: Decision table + quick lookup → Find resource in seconds - -### 2. Modularity -- **Before**: All content in one file, many cross-cutting concerns -- **After**: 6 focused files, each addressing 1-2 related topics - -### 3. Completeness -- **Before**: ~650 lines covering Avalonia basics -- **After**: ~4,166 lines with comprehensive coverage - -### 4. Learning Path -- **Before**: No clear progression from basics to advanced -- **After**: Clear path: MVVM → Controls → Styling → Reactive → Advanced - -### 5. Platform Support -- **Before**: Platform info scattered in main file -- **After**: Dedicated 743-line platform-specific resource - -### 6. Documentation -- **Before**: Basic examples -- **After**: Comprehensive examples for every pattern - ---- - -## Usage Examples - -### For Quick Answer -**Q: How do I bind a list to a ListBox?** -- Open SKILL.md → Find "UI controls reference" in table → Load controls-reference.md → Find ListBox section - -**Time**: 30 seconds - -### For Pattern Learning -**Q: How do I implement reactive search?** -- Open SKILL.md → Find task "Add Real-Time Search" → Follow resource path: - - mvvm-databinding.md (SearchViewModel) - - reactive-animations.md (Observable patterns) - - controls-reference.md (ListBox display) - - styling-guide.md (CSS styling) - -**Time**: 10 minutes - -### For Complex Scenario -**Q: Build a cross-platform app with custom controls and real-time data** -- Follow workflow: "Complex Data-Driven UI" -- Reference resources: - - mvvm-databinding.md (architecture) - - custom-controls-advanced.md (custom UI) - - reactive-animations.md (real-time updates) - - platform-specific.md (deployment) - -**Time**: ~2 hours - ---- - -## Alignment with Thought-Patterns Pattern - -The avalonia refactoring follows the proven orchestration pattern from thought-patterns: - -| Aspect | Thought-Patterns | Avalonia | -|--------|------------------|----------| -| Hub size | 169 lines | 314 lines (more complex) | -| Resource files | 6 files | 6 files (pattern matched) | -| Total content | ~1,900 lines | ~4,166 lines (UI more extensive) | -| Navigation | Decision table | Decision table + task routing | -| Organization | Pattern categories | Skill categories | -| Approach | Modular, self-contained | Modular, self-contained | - -**Conclusion**: Avalonia refactoring successfully implements orchestration pattern with appropriate adaptation for UI framework complexity. - ---- - -## Conclusion - -The avalonia skill has been transformed from a difficult-to-navigate monolith into an elegant, modular orchestration hub with 6 focused resource files. Users can now: - -1. **Find answers quickly** via decision table (30 seconds) -2. **Learn progressively** from basics to advanced topics -3. **Reference patterns** for common scenarios -4. **Understand deeply** with comprehensive examples -5. **Deploy confidently** with platform guidance - -All existing content has been preserved and significantly enhanced with new material on reactive patterns, custom controls, and advanced optimization techniques. diff --git a/.agents/skills/avalonia/REFACTORING_SUMMARY.md b/.agents/skills/avalonia/REFACTORING_SUMMARY.md deleted file mode 100644 index ff1fa5e..0000000 --- a/.agents/skills/avalonia/REFACTORING_SUMMARY.md +++ /dev/null @@ -1,303 +0,0 @@ -# Avalonia Skill Refactoring Summary - -## Refactoring Complete ✓ - -The avalonia skill has been successfully refactored following the modular orchestration pattern established in skills/thought-patterns/. - ---- - -## Metrics - -### Main SKILL.md File -- **Original**: ~650 lines (monolithic, all content mixed together) -- **Refactored**: 314 lines (orchestration hub) -- **Reduction**: 52% shorter, focused on routing and decision-making - -### Resource Files Organization - -| Resource File | Purpose | Lines | -|---|---|---| -| **mvvm-databinding.md** | MVVM architecture, data binding, DI, converters | 622 | -| **controls-reference.md** | Complete controls documentation (layouts, inputs, collections, menus) | 677 | -| **reactive-animations.md** | Reactive patterns, commands, observables, animations | 665 | -| **custom-controls-advanced.md** | Custom controls, advanced layouts, performance, virtualization | 593 | -| **styling-guide.md** | Styling, themes, animations, control templates | 552 | -| **platform-specific.md** | Cross-platform implementation, mobile/desktop specifics | 743 | - -### Total Content -- **Original**: ~650 lines in single file -- **Refactored**: 314 (hub) + 4,252 (resources) = **4,566 total lines** -- **Improvement**: 7x more content, all well-organized and navigable - ---- - -## Architecture Changes - -### Before Refactoring -``` -avalonia/ -└── SKILL.md (650 lines) - ├── Project structure - ├── MVVM architecture - ├── XAML best practices - ├── Data binding - ├── Value converters - ├── Styling and theming - ├── Common controls - ├── Custom controls - ├── Reactive programming - ├── Cross-platform - ├── Performance optimization - ├── Common patterns - ├── Testing - └── Debugging tips - └── resources/ (3 files - underutilized) - ├── controls-reference.md - ├── platform-specific.md - └── styling-guide.md -``` - -### After Refactoring (Modular Orchestration) -``` -avalonia/ -├── SKILL.md (314 lines) - ORCHESTRATION HUB -│ ├── Quick reference table (when to load which resource) -│ ├── Framework overview -│ ├── Getting started (minimal setup) -│ ├── Core patterns (MVVM, Reactive, Platform-adaptive) -│ ├── Navigation by task (5 common scenarios) -│ ├── Resource organization guide -│ ├── Common workflows (4 patterns) -│ └── Best practices summary -└── resources/ - ├── mvvm-databinding.md (622 lines) PRIMARY - │ ├── MVVM architecture overview - │ ├── ViewModel base classes - │ ├── Data binding fundamentals - │ ├── Binding paths and modes - │ ├── Value converters (single & multi) - │ ├── Dependency injection - │ ├── Collections and binding - │ ├── Design-time data - │ └── Common patterns - ├── controls-reference.md (677 lines) PRIMARY - │ ├── Layout controls (Grid, StackPanel, DockPanel, WrapPanel, etc.) - │ ├── Input controls (TextBox, Button, CheckBox, RadioButton, etc.) - │ ├── Display controls (TextBlock, Label, Image, ProgressBar, etc.) - │ ├── Collection controls (ListBox, DataGrid, TreeView, Carousel, etc.) - │ ├── Menu and navigation (Menu, TabControl, SplitView, etc.) - │ ├── Dialogs and popups (Window, Dialog, Tooltip, Flyout, etc.) - │ └── Drawing and shapes (Rectangle, Ellipse, Path, etc.) - ├── reactive-animations.md (665 lines) ADVANCED - │ ├── ReactiveUI integration - │ ├── Reactive properties - │ ├── Reactive commands (sync & async) - │ ├── Observable sequences - │ ├── Animations and transitions - │ ├── Easing functions - │ ├── Programmatic animations - │ ├── Observable patterns (search, validation, auto-complete) - │ └── Performance optimization - ├── custom-controls-advanced.md (593 lines) ADVANCED - │ ├── Custom TemplatedControl creation - │ ├── User control composition - │ ├── Advanced layouts (adaptive, virtualized) - │ ├── Performance optimization - │ ├── Lazy loading and virtualization - │ ├── Image optimization - │ ├── Render transforms - │ ├── Drawing and graphics - │ └── Testing custom controls - ├── styling-guide.md (552 lines) PRIMARY - │ ├── Style basics (selectors, pseudo-classes) - │ ├── Resource dictionaries - │ ├── Control templates - │ ├── Data templates - │ ├── Animations - │ ├── Theme variants (light/dark) - │ ├── Advanced styling patterns - │ ├── Custom theme example - │ └── Performance tips - └── platform-specific.md (743 lines) ADVANCED - ├── Platform detection - ├── Multi-project structure - ├── Platform-specific services - ├── Platform-specific UI - ├── Window management per platform - ├── File system access - ├── Native dialogs - ├── Platform-specific features (Windows/macOS/Linux/Android/iOS) - ├── Input handling - ├── Performance considerations - └── Testing platform-specific code -``` - ---- - -## Navigation Structure - -### Decision-Based Routing (Main SKILL.md) - -The refactored hub uses a **decision table** to route to the right resource: - -```markdown -| Task/Goal | Load Resource | -|-----------|---------------| -| MVVM patterns, data binding, dependency injection, value converters | `resources/mvvm-databinding.md` | -| UI controls reference (layouts, inputs, collections, menus) | `resources/controls-reference.md` | -| Custom controls, advanced layouts, performance optimization, virtualization | `resources/custom-controls-advanced.md` | -| Styling, themes, animations, control templates | `resources/styling-guide.md` | -| Reactive patterns, commands, observables, animations | `resources/reactive-animations.md` | -| Windows, macOS, Linux, iOS, Android implementation details | `resources/platform-specific.md` | -``` - -### Task-Based Workflows - -Five common scenarios with explicit navigation paths: - -1. **"I need to build a form with validation"** - - mvvm-databinding → controls-reference → reactive-animations - -2. **"I'm seeing poor performance with large lists"** - - custom-controls-advanced → mvvm-databinding → reactive-animations - -3. **"I need platform-specific behavior"** - - platform-specific → mvvm-databinding → platform-specific DI - -4. **"I want custom styling and animations"** - - styling-guide → reactive-animations → custom-controls-advanced - -5. **"I'm building a complex control"** - - custom-controls-advanced → mvvm-databinding → styling-guide - ---- - -## Content Organization Improvements - -### Coverage by Concern Area - -| Concern | Before | After | Location | -|---------|--------|-------|----------| -| MVVM & Architecture | Fragmented | Comprehensive | mvvm-databinding.md | -| Controls Reference | Fragmented | Complete + organized | controls-reference.md | -| Data Binding | Fragmented | Dedicated section | mvvm-databinding.md | -| Styling & Theming | Dedicated | Enhanced | styling-guide.md | -| Animations | Minimal | Full section | reactive-animations.md | -| Reactive Patterns | Minimal | Comprehensive | reactive-animations.md | -| Custom Controls | Basic | Advanced | custom-controls-advanced.md | -| Performance | Brief | Detailed | custom-controls-advanced.md | -| Platform Support | Detailed | Enhanced | platform-specific.md | -| Testing | Basic | Referenced | All resources | - -### Platform Separation - -**Before**: Platform guidance spread throughout main file -**After**: Dedicated `platform-specific.md` with: -- Platform detection patterns -- Service abstraction examples -- Per-platform implementations (Windows, macOS, Linux, iOS, Android) -- Platform-specific features and native integration -- Conditional UI rendering (OnPlatform patterns) -- Cross-platform best practices - ---- - -## Key Improvements - -### 1. **Clarity & Navigation** -- ✓ Quick reference table on first page -- ✓ Clear "When to Load Which Resource" guidance -- ✓ Task-based navigation (5 common scenarios) -- ✓ Workflow examples for common use cases - -### 2. **Modularity** -- ✓ Each resource focused on 1-2 related topics -- ✓ Self-contained modules (can read independently) -- ✓ No redundant content across files -- ✓ Clear dependencies between resources - -### 3. **Content Expansion** -- ✓ MVVM patterns deeply covered (622 lines) -- ✓ Complete controls reference (677 lines) -- ✓ Reactive programming comprehensive (665 lines) -- ✓ Advanced techniques detailed (593 lines custom, 552 styling) -- ✓ Platform support enhanced (743 lines) - -### 4. **Accessibility** -- ✓ Front-matter clearly identifies purpose -- ✓ Table-based decision system -- ✓ Code examples for every pattern -- ✓ Clear section hierarchy -- ✓ Best practices in every resource - -### 5. **Consistency** -- ✓ Follows thought-patterns orchestration pattern -- ✓ Similar structure and formatting -- ✓ Consistent code example style -- ✓ Unified best practices section - ---- - -## How to Use the Refactored Skill - -### For New Users -1. Read the main **SKILL.md** (314 lines, ~5 minutes) -2. Identify your task/goal -3. Look up the corresponding resource in the decision table -4. Load that resource for detailed guidance - -### For Quick Reference -- Use the **decision table** to find the right resource in seconds -- Each resource has a clear section hierarchy -- All code examples are copyable and immediately usable - -### For Comprehensive Learning -- Start with **mvvm-databinding.md** (architecture foundation) -- Progress to **controls-reference.md** (UI components) -- Add **styling-guide.md** (visual design) -- Explore **reactive-animations.md** (advanced patterns) -- Master **custom-controls-advanced.md** (complex scenarios) -- Reference **platform-specific.md** (multi-platform deployment) - ---- - -## Validation Checklist ✓ - -✓ All controls documented (GridLayout, StackPanel, ListBox, DataGrid, etc.) -✓ All platforms covered (Windows, macOS, Linux, iOS, Android) -✓ Styling guidance complete (selectors, templates, animations, themes) -✓ Data binding patterns documented (modes, converters, validation) -✓ Reactive programming comprehensive (properties, commands, observables) -✓ Performance optimization covered (virtualization, compiled bindings, debouncing) -✓ Testing guidance included (unit tests, UI tests with Avalonia.Headless) -✓ MVVM architecture clearly explained -✓ Platform separation achieved -✓ Controls organization improved -✓ Navigation structure enhanced - ---- - -## Comparison to Thought-Patterns Reference - -The thought-patterns skill was successfully refactored from ~650 to 169 lines (orchestration hub) with 6 focused resource files. The avalonia refactoring follows the same pattern: - -- **Main file size**: Avalonia 314 lines (vs. thought-patterns 169) - justified by additional complexity of UI framework -- **Resource files**: Avalonia 6 files (vs. thought-patterns 6 files) - matched complexity -- **Total lines**: Avalonia 4,566 (vs. thought-patterns ~1,900) - reflects comprehensive UI documentation -- **Navigation**: Both use decision tables and task-based routing -- **Organization**: Both follow modular, self-contained pattern - ---- - -## Summary - -The avalonia skill has been successfully transformed from a monolithic 650-line document into a modular orchestration hub (314 lines) + 6 focused resource files (4,252 lines). The refactoring: - -- **Improves clarity**: Clear decision table routing on first page -- **Enhances modularity**: Each resource self-contained and focused -- **Expands content**: 7x more material, all well-organized -- **Maintains consistency**: Follows thought-patterns orchestration pattern -- **Enables learning**: Clear progression path from basics to advanced topics -- **Supports quick reference**: Decision table + task-based navigation - -Users can now quickly find exactly what they need without wading through irrelevant content. diff --git a/.agents/skills/avalonia/SKILL.md b/.agents/skills/avalonia/SKILL.md deleted file mode 100644 index 283e1fd..0000000 --- a/.agents/skills/avalonia/SKILL.md +++ /dev/null @@ -1,321 +0,0 @@ ---- -name: avalonia -description: Expert guidance for developing cross-platform desktop applications with Avalonia UI framework. Use when building, debugging, or optimizing Avalonia apps including MVVM architecture, XAML design, data binding, styling, theming, custom controls, and cross-platform deployment for Windows, macOS, Linux, iOS, Android, and WebAssembly. -version: 2.0 ---- - -# Avalonia UI Framework - Orchestration Hub - -Modular guidance for cross-platform desktop and mobile development using Avalonia, a WPF-inspired XAML-based framework for .NET. - -> [!IMPORTANT] -> **REPOSITORY MANDATE: Google Material Design 3 (M3) Expressive** -> In this repository, **ALL** Avalonia UI controls, views, dialogs, sidebars, buttons, and styles **MUST strictly follow Material Design 3 (M3) Expressive**. -> - **Never hardcode small corner radii** (`2`, `4`, `5`). Use M3 shape tokens: Pills (`M3ShapeCornerFull`/`9999`) for buttons, search bars, segmented capsules, and slider thumbs; `M3ShapeCornerExtraLarge` (28) for dialog cards; `M3ShapeCornerLarge` (16) for cards/inputs; `M3ShapeCornerMedium` (12) for menus. -> - **Never hardcode hex colors**. Always bind to dynamic tokens (`M3PrimaryBrush`, `M3SecondaryContainerBrush`, `M3SurfaceContainer...`, or legacy `Win...Brush` aliases). -> - Refer to `src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml`, `Material3ExpressiveStyles.axaml`, and `docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md`. - -## Quick Reference: When to Load Which Resource - -| Task/Goal | Load Resource | -|-----------|---------------| -| MVVM patterns, data binding, dependency injection, value converters | `resources/mvvm-databinding.md` | -| UI controls reference (layouts, inputs, collections, menus) | `resources/controls-reference.md` | -| Custom controls, advanced layouts, performance optimization, virtualization | `resources/custom-controls-advanced.md` | -| Styling, themes, animations, control templates | `resources/styling-guide.md` | -| Reactive patterns, commands, observables, animations | `resources/reactive-animations.md` | -| Windows, macOS, Linux, iOS, Android implementation details | `resources/platform-specific.md` | - -## Framework Overview - -**Avalonia** is a cross-platform XAML framework supporting: -- **Platforms**: Windows, macOS, Linux, iOS, Android, WebAssembly -- **Architecture**: MVVM with ReactiveUI support -- **Styling**: CSS-like selectors with Fluent/Simple themes -- **Features**: Data binding, reactive commands, observable collections, custom controls -- **Modern .NET**: .NET 6+ and .NET Standard 2.0 - -### Standard Project Structure - -``` -MyAvaloniaApp/ -├── MyAvaloniaApp/ # Shared code -│ ├── App.axaml -│ ├── Views/ # XAML views -│ ├── ViewModels/ # Business logic + state -│ ├── Models/ # Data models -│ ├── Services/ # Application services -│ ├── Converters/ # Value converters -│ ├── Assets/ # Images, fonts -│ └── Styles/ # Style resources -├── MyAvaloniaApp.Desktop/ # Desktop-specific (Win/Mac/Linux) -├── MyAvaloniaApp.Android/ # Android-specific (optional) -├── MyAvaloniaApp.iOS/ # iOS-specific (optional) -└── MyAvaloniaApp.Browser/ # WebAssembly (optional) -``` - -## Getting Started - -### Minimal Setup - -```csharp -// Program.cs -public static void Main(string[] args) -{ - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); -} - -public static AppBuilder BuildAvaloniaApp() => - AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); -``` - -```xml - - - - - - -``` - -```xml - - - - - - -``` - -## Core Patterns - -### MVVM Architecture Pattern - -1. **View** (XAML): UI presentation with data bindings -2. **ViewModel** (C#): State management and commands -3. **Model** (C#): Business logic and data access -4. **Service**: Cross-cutting concerns (DI/IoC) - -**Load** `resources/mvvm-databinding.md` for: -- ViewModel base classes -- Data binding modes and paths -- Multi-binding and converters -- Dependency injection setup -- Design-time data - -### Reactive Programming Pattern - -Leverage ReactiveUI for event-driven UI updates: - -```csharp -this.WhenAnyValue(x => x.SearchText) - .Debounce(TimeSpan.FromMilliseconds(300)) - .Subscribe(text => PerformSearch(text)); -``` - -**Load** `resources/reactive-animations.md` for: -- Reactive properties and commands -- Observable sequences -- Animations and transitions -- Performance optimization - -### Platform-Adaptive Pattern - -Design once, adapt per platform: - -```xml - - - - -``` - -**Load** `resources/platform-specific.md` for: -- Runtime platform detection -- Platform-specific services -- Conditional UI rendering -- Native dialogs and features - -## Navigation by Task - -### "I need to build a form with validation" - -1. Load `resources/mvvm-databinding.md` → Implement ViewModel with property validation -2. Load `resources/controls-reference.md` → Find TextBox, ComboBox, Button controls -3. Load `resources/reactive-animations.md` → Add debounced validation with observables - -### "I'm seeing poor performance with large lists" - -1. Load `resources/custom-controls-advanced.md` → Enable virtualization -2. Load `resources/mvvm-databinding.md` → Use compiled bindings -3. Load `resources/reactive-animations.md` → Debounce/throttle updates - -### "I need platform-specific behavior" - -1. Load `resources/platform-specific.md` → Implement service interfaces -2. Load `resources/mvvm-databinding.md` → Register platform implementations via DI -3. Platform-specific `resources/` → Implement per-platform project - -### "I want custom styling and animations" - -1. Load `resources/styling-guide.md` → Define styles and themes -2. Load `resources/reactive-animations.md` → Add animations to styles -3. Load `resources/custom-controls-advanced.md` → Custom control templates - -### "I'm building a complex control" - -1. Load `resources/custom-controls-advanced.md` → TemplatedControl or UserControl pattern -2. Load `resources/mvvm-databinding.md` → Attached properties and data binding -3. Load `resources/styling-guide.md` → Control templates and styling - -## Resource Organization - -### `mvvm-databinding.md` (Primary) -- Architecture overview -- ViewModel patterns with ReactiveUI -- Binding modes and syntax -- Value converters -- Collections and list binding -- Design-time data -- Master-detail and tab patterns - -### `controls-reference.md` (Primary) -- Layout controls (Grid, StackPanel, DockPanel, etc.) -- Input controls (TextBox, Button, CheckBox, ComboBox, etc.) -- Display controls (TextBlock, Image, ProgressBar, etc.) -- Collection controls (ListBox, DataGrid, TreeView, etc.) -- Navigation (Menu, TabControl, SplitView, etc.) -- Shapes and drawing - -### `styling-guide.md` (Primary) -- CSS-like selectors (type, class, pseudo-classes) -- Resource dictionaries and themes -- Control templates -- Data templates -- Animations and transitions -- Easing functions -- Theme variants (light/dark) - -### `reactive-animations.md` (Advanced) -- ReactiveUI integration -- Reactive properties -- Reactive commands (sync and async) -- Observable sequences -- Filtering, transformation, combining -- Programmatic animations -- Common patterns (search, validation, auto-complete) - -### `custom-controls-advanced.md` (Advanced) -- Custom TemplatedControl creation -- User control composition -- Advanced layouts -- Virtualization -- Performance optimization -- Render transforms -- Graphics and drawing - -### `platform-specific.md` (Advanced) -- Runtime platform detection -- Multi-project structure -- Service abstractions -- Platform-specific implementations -- Window management per platform -- File system access -- Native features (Windows DLL, macOS Cocoa, etc.) - -## Common Workflows - -### Build a Desktop App (Windows/macOS/Linux) - -``` -1. → Setup: Standard project structure + FluentTheme -2. → Create Views and ViewModels following MVVM -3. → Use controls-reference for UI layouts -4. → Add styles with styling-guide -5. → Implement services with DI (mvvm-databinding) -6. → Add animations with reactive-animations -7. → Test on each platform with platform-specific guidance -``` - -### Build a Cross-Platform Mobile+Desktop App - -``` -1. → Create shared project + platform-specific projects -2. → Define service interfaces in shared code (mvvm-databinding) -3. → Implement services per platform (platform-specific) -4. → Use OnPlatform for adaptive UI -5. → Register platform implementations via DI -6. → Test thoroughly on each target (iOS/Android/Windows/Mac) -``` - -### Add Real-Time Search - -``` -1. → Create SearchViewModel (mvvm-databinding) -2. → Use ObservableCollection for results (mvvm-databinding) -3. → Implement with reactive search pattern (reactive-animations) -4. → Debounce input to reduce API calls -5. → Display with ListBox (controls-reference) -6. → Style with appropriate CSS selectors (styling-guide) -``` - -### Build Complex Data-Driven UI - -``` -1. → Design ViewModel hierarchy (mvvm-databinding) -2. → Create master-detail view (mvvm-databinding) -3. → Use DataGrid for tabular data (controls-reference) -4. → Add sorting/filtering with observables (reactive-animations) -5. → Optimize with virtualization (custom-controls-advanced) -6. → Add custom controls if needed (custom-controls-advanced) -``` - -## Best Practices Summary - -**Architecture** -- Maintain strict MVVM separation of concerns -- Use dependency injection for testability -- Keep business logic in ViewModels, not Views - -**Performance** -- Enable compiled bindings with `x:DataType` -- Virtualize large collections -- Debounce rapid updates - -**Styling** -- Use resource dictionaries for consistency -- Support light and dark themes -- Test styles on all target platforms - -**Reactive Patterns** -- Use observables for event-driven updates -- Debounce/throttle input-triggered operations -- Always handle ThrownExceptions on commands - -**Testing** -- Unit test ViewModels in isolation -- Use Avalonia.Headless for UI testing -- Provide design-time DataContext in XAML - -## Cross-Platform Deployment - -- **Windows**: ClickOnce, MSI, portable exe -- **macOS**: DMG, homebrew -- **Linux**: AppImage, snap, flatpak -- **Mobile**: Apple App Store, Google Play Store -- **Web**: Static hosting (WASM runtime required) - -Refer to `resources/platform-specific.md` for platform-specific build and deployment guidance. - ---- - -**Navigation**: Choose a resource above based on your task. Each resource is self-contained with comprehensive examples and best practices. diff --git a/.agents/skills/avalonia/resources/controls-reference.md b/.agents/skills/avalonia/resources/controls-reference.md deleted file mode 100644 index b0e1b70..0000000 --- a/.agents/skills/avalonia/resources/controls-reference.md +++ /dev/null @@ -1,677 +0,0 @@ -# Avalonia Controls Reference - -Comprehensive reference for all major Avalonia UI controls. - -## Layout Controls - -### Grid -Flexible grid-based layout with rows and columns. - -```xml - - - - - -``` - -**Column/Row Definitions:** -- `*` - Star sizing (proportional) -- `Auto` - Size to content -- `100` - Fixed pixel size -- `2*` - Two parts of available space - -### StackPanel -Stacks child elements horizontally or vertically. - -```xml - - - - - -``` - -### Flyout -Popup attached to control. - -```xml - -``` - -## Drawing and Shapes - -### Rectangle -```xml - -``` - -### Ellipse -```xml - -``` - -### Line -```xml - -``` - -### Polyline -```xml - -``` - -### Polygon -```xml - -``` - -### Path -```xml - - - - - - - - - - - - -``` - -## Advanced Controls - -### AutoCompleteBox -Text input with auto-completion. - -```xml - -``` - -### Calendar -Calendar control. - -```xml - -``` - -### MaskedTextBox -Text input with format mask. - -```xml - -``` - -### ColorPicker -Color selection control. - -```xml - -``` - -### PathIcon -Icon from path data. - -```xml - -``` - -This reference covers the most commonly used Avalonia controls. For complete API documentation, refer to the official Avalonia documentation. diff --git a/.agents/skills/avalonia/resources/custom-controls-advanced.md b/.agents/skills/avalonia/resources/custom-controls-advanced.md deleted file mode 100644 index 30a3a27..0000000 --- a/.agents/skills/avalonia/resources/custom-controls-advanced.md +++ /dev/null @@ -1,593 +0,0 @@ -# Custom Controls and Advanced Techniques - -Building custom controls, advanced layouts, and optimization patterns for complex Avalonia applications. - -## Custom Controls - -### Creating a Custom Control - -```csharp -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Data; - -public class RatingControl : TemplatedControl -{ - // Define attached property for rating value - public static readonly StyledProperty RatingProperty = - AvaloniaProperty.Register( - nameof(Rating), - defaultValue: 0, - defaultBindingMode: BindingMode.TwoWay); - - public int Rating - { - get => GetValue(RatingProperty); - set => SetValue(RatingProperty, value); - } - - // Maximum rating (e.g., 5 stars) - public static readonly StyledProperty MaximumProperty = - AvaloniaProperty.Register( - nameof(Maximum), - defaultValue: 5); - - public int Maximum - { - get => GetValue(MaximumProperty); - set => SetValue(MaximumProperty, value); - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - - // Access template parts - var grid = e.NameScope.Get("PART_ItemsControl"); - if (grid != null) - { - // Initialize template parts - } - } -} -``` - -### Control Template - -```xml - - - - - - -``` - -### User Control (Composite Control) - -```csharp -public partial class UserCard : UserControl -{ - public static readonly StyledProperty NameProperty = - AvaloniaProperty.Register(nameof(Name)); - - public string Name - { - get => GetValue(NameProperty); - set => SetValue(NameProperty, value); - } - - public static readonly StyledProperty EmailProperty = - AvaloniaProperty.Register(nameof(Email)); - - public string Email - { - get => GetValue(EmailProperty); - set => SetValue(EmailProperty, value); - } - - public static readonly StyledProperty AvatarProperty = - AvaloniaProperty.Register(nameof(Avatar)); - - public IImage Avatar - { - get => GetValue(AvatarProperty); - set => SetValue(AvatarProperty, value); - } - - public UserCard() - { - InitializeComponent(); - } -} -``` - -```xml - - - - - - - - - - - -``` - -## Advanced Layouts - -### Adaptive Layout - -```csharp -public class AdaptivePanel : Panel -{ - protected override Size MeasureOverride(Size availableSize) - { - double totalWidth = 0; - double maxHeight = 0; - - foreach (var child in Children) - { - child.Measure(availableSize); - totalWidth += child.DesiredSize.Width; - maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); - } - - return new Size(Math.Min(totalWidth, availableSize.Width), maxHeight); - } - - protected override Size ArrangeOverride(Size finalSize) - { - double xOffset = 0; - - foreach (var child in Children) - { - child.Arrange(new Rect(xOffset, 0, child.DesiredSize.Width, finalSize.Height)); - xOffset += child.DesiredSize.Width; - } - - return finalSize; - } -} -``` - -### Virtualized Stack Panel - -```csharp -public class VirtualizingStackPanel : VirtualizingPanel -{ - public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register( - nameof(Orientation), - Orientation.Vertical); - - public Orientation Orientation - { - get => GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - protected override Size MeasureOverride(Size availableSize) - { - // Implement virtualization logic - return availableSize; - } - - protected override Size ArrangeOverride(Size finalSize) - { - // Arrange only visible items - return finalSize; - } -} -``` - -## Performance Optimization - -### Compiled Bindings - -```xml - - - - - - - - - - - - -``` - -### Virtualization - -```xml - - - - - - - - - - - - -``` - -### Lazy Loading - -```csharp -public class LazyLoadViewModel : ReactiveObject -{ - private ObservableCollection _items; - public ObservableCollection Items - { - get => _items; - set => this.RaiseAndSetIfChanged(ref _items, value); - } - - private int _pageNumber = 0; - private const int PageSize = 50; - - public LazyLoadViewModel() - { - Items = new ObservableCollection(); - LoadNextPage(); - } - - public void LoadNextPage() - { - Task.Run(async () => - { - var newItems = await _dataService.GetItemsAsync(_pageNumber * PageSize, PageSize); - foreach (var item in newItems) - { - Items.Add(item); - } - _pageNumber++; - }); - } -} -``` - -```xml - - - - - - - - - - -``` - -### Image Optimization - -```xml - - - - - - -``` - -```csharp -// Load image asynchronously -public async Task LoadImageAsync(string url) -{ - using (var client = new HttpClient()) - { - var data = await client.GetByteArrayAsync(url); - using (var stream = new MemoryStream(data)) - { - return new Bitmap(stream); - } - } -} -``` - -## Render Transforms - -### Transform Types - -```xml - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### Programmatic Transforms - -```csharp -public partial class MainWindow : Window -{ - public MainWindow() - { - InitializeComponent(); - } - - public void TransformElement() - { - var element = this.FindControl("MyBorder"); - - // Translate - var translateTransform = new TranslateTransform { X = 10, Y = 20 }; - element.RenderTransform = translateTransform; - - // Scale with origin - var scaleTransform = new ScaleTransform - { - ScaleX = 1.5, - ScaleY = 1.5, - CenterX = 50, - CenterY = 50 - }; - element.RenderTransform = scaleTransform; - - // Rotate with origin - var rotateTransform = new RotateTransform - { - Angle = 45, - CenterX = 50, - CenterY = 50 - }; - element.RenderTransform = rotateTransform; - } -} -``` - -## Drawing and Graphics - -### Shapes - -```xml - - - - - - - - - - - - - - -``` - -### Paths - -```xml - - - - - - - - - - - - - - - -``` - -### Drawing Context (Code-Behind) - -```csharp -public class DrawingControl : Control -{ - public override void Render(DrawingContext context) - { - base.Render(context); - - // Draw rectangle - var rect = new Rect(10, 10, 100, 50); - context.DrawRectangle(new SolidColorBrush(Colors.Blue), null, rect); - - // Draw ellipse - var ellipse = new EllipseGeometry(new Rect(120, 10, 100, 100)); - context.DrawGeometry(new SolidColorBrush(Colors.Red), null, ellipse); - - // Draw line - var pen = new Pen(new SolidColorBrush(Colors.Black), 2); - context.DrawLine(pen, new Point(0, 0), new Point(100, 100)); - - // Draw text - var formattedText = new FormattedText( - "Hello", - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - new Typeface("Arial"), - 14, - new SolidColorBrush(Colors.Black)); - context.DrawText(formattedText, new Point(10, 10)); - } -} -``` - -## Styling Advanced Patterns - -### Complex Selectors - -```xml - - - - - - - - - - - - - -``` - -### Conditional Styling - -```xml - - - - - -``` - -## Testing Custom Controls - -```csharp -using Avalonia.Headless.XUnit; -using Xunit; - -public class RatingControlTests -{ - [AvaloniaFact] - public void Rating_CanBeSet() - { - var control = new RatingControl { Rating = 3 }; - Assert.Equal(3, control.Rating); - } - - [AvaloniaFact] - public void Rating_BindsCorrectly() - { - var control = new RatingControl(); - var binding = new Binding("Value") - { - Mode = BindingMode.TwoWay, - Source = new { Value = 4 } - }; - control.Bind(RatingControl.RatingProperty, binding); - Assert.Equal(4, control.Rating); - } - - [AvaloniaFact] - public void Template_AppliesCorrectly() - { - var window = new Window - { - Content = new RatingControl { Rating = 5 } - }; - window.Show(); - - var control = ((RatingControl)window.Content); - Assert.Equal(5, control.Rating); - } -} -``` - -## Best Practices - -1. **Follow MVVM** - Keep custom control logic separate from business logic -2. **Use attached properties** - For control customization -3. **Template-based controls** - Use TemplatedControl for complex controls -4. **Virtualize large collections** - For performance -5. **Minimize render overhead** - Cache renders when possible -6. **Use compiled bindings** - For better performance -7. **Profile performance** - Use tools to identify bottlenecks -8. **Test thoroughly** - Use Avalonia.Headless for UI testing -9. **Document properties** - Clearly document custom properties -10. **Follow platform conventions** - Match native look and feel diff --git a/.agents/skills/avalonia/resources/mvvm-databinding.md b/.agents/skills/avalonia/resources/mvvm-databinding.md deleted file mode 100644 index f451b69..0000000 --- a/.agents/skills/avalonia/resources/mvvm-databinding.md +++ /dev/null @@ -1,622 +0,0 @@ -# MVVM Architecture and Data Binding - -Core patterns for implementing Model-View-ViewModel architecture and establishing data bindings in Avalonia applications. - -## MVVM Architecture - -### Overview - -Model-View-ViewModel (MVVM) separates concerns into three layers: - -- **Model**: Business logic and data -- **View**: UI presentation (XAML) -- **ViewModel**: Bridge between View and Model, handles state and commands - -### Project Structure - -``` -MyAvaloniaApp/ -├── Models/ # Business logic and data -│ ├── User.cs -│ ├── Product.cs -│ └── IDataService.cs # Service interfaces -├── ViewModels/ # MVVM logic -│ ├── MainViewModel.cs -│ ├── UserListViewModel.cs -│ └── ViewModelBase.cs # Common base class -├── Views/ # XAML views -│ ├── MainWindow.axaml -│ ├── UserListView.axaml -│ └── ... -└── Services/ # Application services - ├── DataService.cs - ├── NavigationService.cs - └── ... -``` - -### ViewModel Base Class - -Use ReactiveUI's `ReactiveObject` or implement `INotifyPropertyChanged`: - -```csharp -using ReactiveUI; -using System.Reactive; -using System.Collections.ObjectModel; - -public class MainViewModel : ReactiveObject -{ - private string _name; - public string Name - { - get => _name; - set => this.RaiseAndSetIfChanged(ref _name, value); - } - - private string _email; - public string Email - { - get => _email; - set => this.RaiseAndSetIfChanged(ref _email, value); - } - - private ObservableCollection _users; - public ObservableCollection Users - { - get => _users; - set => this.RaiseAndSetIfChanged(ref _users, value); - } - - public ReactiveCommand SaveCommand { get; } - public ReactiveCommand LoadCommand { get; } - - public MainViewModel() - { - SaveCommand = ReactiveCommand.Create(Save); - LoadCommand = ReactiveCommand.Create(Load); - - Users = new ObservableCollection(); - } - - private void Save() - { - // Save logic - } - - private void Load() - { - // Load logic - } -} -``` - -### INotifyPropertyChanged Implementation - -For projects not using ReactiveUI: - -```csharp -using System.ComponentModel; -using System.Runtime.CompilerServices; - -public class MainViewModel : INotifyPropertyChanged -{ - private string _name; - public string Name - { - get => _name; - set => SetProperty(ref _name, value); - } - - private string _email; - public string Email - { - get => _email; - set => SetProperty(ref _email, value); - } - - public event PropertyChangedEventHandler PropertyChanged; - - protected void SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") - { - if (!Equals(field, value)) - { - field = value; - OnPropertyChanged(propertyName); - } - } - - protected void OnPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } -} -``` - -## Data Binding Fundamentals - -### Binding Modes - -```xml - - - - - - - - - - - -``` - -### Binding Paths - -```xml - - - - - - - - - - - - - - -``` - -### Multi-Binding - -```xml - - - - - - - - - - - - - - - - - - - - - -``` - -### Binding Validation - -```xml - - - - - - - - - -``` - -## Value Converters - -### Basic Converter - -```csharp -using System.Globalization; -using Avalonia.Data.Converters; - -public class BoolToVisibilityConverter : IValueConverter -{ - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value is bool boolValue) - return boolValue ? Avalonia.Controls.Visibility.Visible : Avalonia.Controls.Visibility.Collapsed; - return Avalonia.Controls.Visibility.Collapsed; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value is Avalonia.Controls.Visibility visibility) - return visibility == Avalonia.Controls.Visibility.Visible; - return false; - } -} -``` - -### Multi-Value Converter - -```csharp -public class FullNameConverter : IMultiValueConverter -{ - public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) - { - if (values.Count < 2) return ""; - var firstName = values[0]?.ToString() ?? ""; - var lastName = values[1]?.ToString() ?? ""; - return $"{firstName} {lastName}".Trim(); - } -} -``` - -### Using Converters - -```xml - - - - - - - - - - - - - - - - - -``` - -## Dependency Injection - -### Service Registration - -```csharp -using Microsoft.Extensions.DependencyInjection; - -public override void OnFrameworkInitializationCompleted() -{ - var services = new ServiceCollection(); - - // Register application services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Register view models - services.AddTransient(); - services.AddTransient(); - - // Register views - services.AddSingleton(); - - var provider = services.BuildServiceProvider(); - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - desktop.MainWindow = provider.GetRequiredService(); - } - - base.OnFrameworkInitializationCompleted(); -} -``` - -### View-ViewModel Binding - -```csharp -// App.axaml.cs -public class App : Application -{ - private ServiceProvider _serviceProvider; - - public override void OnFrameworkInitializationCompleted() - { - _serviceProvider = new ServiceCollection() - .AddSingleton() - .AddTransient() - .AddSingleton() - .BuildServiceProvider(); - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - desktop.MainWindow = _serviceProvider.GetRequiredService(); - } - - base.OnFrameworkInitializationCompleted(); - } -} -``` - -```csharp -// MainWindow.axaml.cs -public partial class MainWindow : Window -{ - public MainWindow(MainViewModel viewModel) - { - InitializeComponent(); - DataContext = viewModel; - } -} -``` - -## Collections and Binding - -### ObservableCollection Binding - -```csharp -public class UserListViewModel : ReactiveObject -{ - private ObservableCollection _users; - public ObservableCollection Users - { - get => _users; - set => this.RaiseAndSetIfChanged(ref _users, value); - } - - private User _selectedUser; - public User SelectedUser - { - get => _selectedUser; - set => this.RaiseAndSetIfChanged(ref _selectedUser, value); - } - - public UserListViewModel() - { - Users = new ObservableCollection(); - LoadUsers(); - } - - private void LoadUsers() - { - var users = _dataService.GetAllUsers(); - Users = new ObservableCollection(users); - } - - public void AddUser(User user) - { - Users.Add(user); - } - - public void RemoveUser(User user) - { - Users.Remove(user); - } -} -``` - -### ListBox Binding - -```xml - - - - - - - - - - - - - -``` - -### DataGrid Binding - -```xml - - - - - - - - - - -``` - -### Complex Multi-Step Animations - -```xml - - - - - - - - -``` - -### Easing Functions - -Available easing functions: - -- `LinearEasing` -- `QuadraticEaseIn`, `QuadraticEaseOut`, `QuadraticEaseInOut` -- `CubicEaseIn`, `CubicEaseOut`, `CubicEaseInOut` -- `QuarticEaseIn`, `QuarticEaseOut`, `QuarticEaseInOut` -- `QuinticEaseIn`, `QuinticEaseOut`, `QuinticEaseInOut` -- `SineEaseIn`, `SineEaseOut`, `SineEaseInOut` -- `CircularEaseIn`, `CircularEaseOut`, `CircularEaseInOut` -- `BounceEaseIn`, `BounceEaseOut`, `BounceEaseInOut` -- `ElasticEaseIn`, `ElasticEaseOut`, `ElasticEaseInOut` -- `BackEaseIn`, `BackEaseOut`, `BackEaseInOut` - -```xml - - -``` - -### Programmatic Animations - -```csharp -using Avalonia.Animation; - -public partial class MainWindow : Window -{ - public MainWindow() - { - InitializeComponent(); - } - - public async void AnimateButton() - { - var button = this.FindControl -``` - -### Complex Animations -```xml - -``` - -### Easing Functions -```xml - - - - - -``` - -Available easing functions: -- `LinearEasing` -- `QuadraticEaseIn`, `QuadraticEaseOut`, `QuadraticEaseInOut` -- `CubicEaseIn`, `CubicEaseOut`, `CubicEaseInOut` -- `QuarticEaseIn`, `QuarticEaseOut`, `QuarticEaseInOut` -- `QuinticEaseIn`, `QuinticEaseOut`, `QuinticEaseInOut` -- `SineEaseIn`, `SineEaseOut`, `SineEaseInOut` -- `CircularEaseIn`, `CircularEaseOut`, `CircularEaseInOut` -- `BounceEaseIn`, `BounceEaseOut`, `BounceEaseInOut` -- `ElasticEaseIn`, `ElasticEaseOut`, `ElasticEaseInOut` -- `BackEaseIn`, `BackEaseOut`, `BackEaseInOut` - -## Theme Variants - -### Supporting Light/Dark Themes -```xml - - - - - - - - - - -``` - -### Theme-Aware Resources -```xml - - - - - - - - - - - - - -``` - -### Runtime Theme Switching -```csharp -// Switch to dark theme -Application.Current.RequestedThemeVariant = ThemeVariant.Dark; - -// Switch to light theme -Application.Current.RequestedThemeVariant = ThemeVariant.Light; - -// Use system theme -Application.Current.RequestedThemeVariant = ThemeVariant.Default; -``` - -## Advanced Styling Patterns - -### Button Styles -```xml - - - - - - - - - - - - - - - - - - - -``` - -### Card Style -```xml - - - -``` - -### Input Field Styles -```xml - - - - - -``` - -### List Item Styles -```xml - - - - - - - -``` - -## Custom Theme Example - -```xml - - - - - #6200EE - #3700B3 - #03DAC6 - #FFFFFF - #FFFFFF - #B00020 - #FFFFFF - #000000 - #000000 - #000000 - #FFFFFF - - - - - - - - - - - - - - - -``` - -## Performance Tips - -1. **Use Static Resources**: Prefer `StaticResource` over `DynamicResource` when values won't change -2. **Minimize Template Complexity**: Keep control templates simple to reduce render time -3. **Use Transitions Sparingly**: Too many animations can impact performance -4. **Cache Brushes**: Define brushes as resources instead of inline -5. **Avoid Deep Nesting**: Flatten selector hierarchies when possible - -## Best Practices - -1. **Organize Styles**: Separate styles into logical files (Colors, Buttons, etc.) -2. **Use Naming Conventions**: Consistent naming for resources and classes -3. **Document Custom Styles**: Comment complex selectors and templates -4. **Test Theme Variants**: Ensure styles work in both light and dark themes -5. **Follow Platform Guidelines**: Match native look and feel when appropriate -6. **Use Class Modifiers**: Prefer classes over direct styling for reusability -7. **Leverage Inheritance**: Use base styles and extend them -8. **Keep It DRY**: Extract common patterns into reusable styles - -This guide provides a foundation for advanced styling in Avalonia. Experiment with these patterns and adapt them to your application's design requirements. diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 5f598a5..0000000 --- a/.cursorrules +++ /dev/null @@ -1,21 +0,0 @@ -# Cursor & AI Agent Rules for FryPDF - -## STRICT MANDATE: Google Material Design 3 (M3) Expressive UI -Whenever you create or modify any UI in this repository: -1. **NEVER use flat, sharp, or rectangular legacy styles**. -2. **Always use Material 3 Expressive shapes**: - - Buttons, search bars, segmented capsules, FABs, and slider thumbs MUST be pills (`CornerRadius="{StaticResource M3ShapeCornerFull}"` or `9999`). - - Dialog cards MUST be 28px (`CornerRadius="{StaticResource M3ShapeCornerExtraLarge}"`). - - Cards and inputs MUST be 16px (`CornerRadius="{StaticResource M3ShapeCornerLarge}"`). - - Menus and list items MUST be 12px (`CornerRadius="{StaticResource M3ShapeCornerMedium}"`). - - Badges and chips MUST be 8px (`CornerRadius="{StaticResource M3ShapeCornerSmall}"`). -3. **NEVER hardcode light/dark hex colors**. Always use dynamic tokens (`M3PrimaryBrush`, `M3SecondaryContainerBrush`, `M3SurfaceContainer...`, or legacy aliases `WinBgBrush`, `WinAccentBrush`, etc.). -4. **Take maximum advantage of CommunityToolkit.Mvvm**: - - Use `[ObservableProperty]`, `[RelayCommand]`, `[AsyncRelayCommand]`. - - Use `[NotifyPropertyChangedFor]` and `[NotifyCanExecuteChangedFor]`. - - Use `WeakReferenceMessenger.Default` for decoupled communication. -5. **Reference files**: - - `src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml` - - `src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml` - - `docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md` - - `.agents/AGENTS.md` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 6757818..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,195 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - workflow_dispatch: - inputs: - version: - description: 'Release version tag (e.g. 1.0.0 or v1.0.0)' - required: false - default: '' - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: true - -jobs: - determine-version: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - four_part_version: ${{ steps.version.outputs.four_part_version }} - steps: - - name: Determine version from git tag - id: version - shell: bash - run: | - RAW_VER="" - if [ -n "${{ github.event.inputs.version }}" ]; then - RAW_VER="${{ github.event.inputs.version }}" - elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then - RAW_VER="${GITHUB_REF_NAME}" - else - RAW_VER="1.0.0-dev" - fi - - # Strip leading 'v' or 'V' - CLEAN_VER="${RAW_VER#[vV]}" - echo "version=${CLEAN_VER}" >> "$GITHUB_OUTPUT" - - # Compute 4-part numeric version for Windows assembly & MSIX (Major.Minor.Build.Revision) - BASE_NUMERIC=$(echo "$CLEAN_VER" | cut -d'-' -f1) - IFS='.' read -r -a PARTS <<< "$BASE_NUMERIC" - P0="${PARTS[0]:-1}" - P1="${PARTS[1]:-0}" - P2="${PARTS[2]:-0}" - P3="${PARTS[3]:-0}" - FOUR_PART="$P0.$P1.$P2.$P3" - echo "four_part_version=${FOUR_PART}" >> "$GITHUB_OUTPUT" - echo "Resolved Release Version: ${CLEAN_VER} (4-part: ${FOUR_PART})" - - test: - name: Run Unit Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - name: Restore & Build - run: dotnet build -c Release - - name: Run Test Suite - run: dotnet test --no-build -c Release --verbosity normal - - build-windows: - needs: [determine-version, test] - runs-on: windows-latest - env: - VERSION: ${{ needs.determine-version.outputs.version }} - FOUR_PART_VERSION: ${{ needs.determine-version.outputs.four_part_version }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Publish win-x64 - shell: pwsh - run: > - dotnet publish src/PdfEditorApp/PdfEditorApp.csproj -c Release -r win-x64 --self-contained true - -p:Version=$env:VERSION - -p:AssemblyVersion=$env:FOUR_PART_VERSION - -p:FileVersion=$env:FOUR_PART_VERSION - -p:InformationalVersion=$env:VERSION - -o publish/win-x64 - - # ISCC.exe ships preinstalled on the windows-latest runner image at this path. - - name: Build Inno Setup installer (.exe) - shell: pwsh - run: | - & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" packaging\windows\installer.iss "/DMyAppVersion=$env:VERSION" "/DMyPublishDir=$env:GITHUB_WORKSPACE\publish\win-x64" - - - name: Build & Sign Windows MSIX Package (.msix) - shell: pwsh - run: | - .\packaging\windows\msix\build-msix.ps1 -Version "$env:VERSION" -PublishDir "$env:GITHUB_WORKSPACE\publish\win-x64" -OutputDir "$env:GITHUB_WORKSPACE\packaging\windows" - - - name: Upload Windows Installer (.exe) - uses: actions/upload-artifact@v4 - with: - name: windows-installer - path: packaging/windows/FryPDF-Setup-${{ needs.determine-version.outputs.version }}.exe - if-no-files-found: error - - - name: Upload Windows MSIX Package (.msix) - uses: actions/upload-artifact@v4 - with: - name: windows-msix - path: | - packaging/windows/FryPDF-${{ needs.determine-version.outputs.version }}-x64.msix - packaging/windows/FryPDF-${{ needs.determine-version.outputs.version }}-PublicCert.cer - if-no-files-found: error - - build-macos: - needs: [determine-version, test] - runs-on: macos-latest - env: - VERSION: ${{ needs.determine-version.outputs.version }} - FOUR_PART_VERSION: ${{ needs.determine-version.outputs.four_part_version }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Publish osx-arm64 - run: > - dotnet publish src/PdfEditorApp/PdfEditorApp.csproj -c Release -r osx-arm64 --self-contained true - -p:Version=$VERSION - -p:AssemblyVersion=$FOUR_PART_VERSION - -p:FileVersion=$FOUR_PART_VERSION - -p:InformationalVersion=$VERSION - -o publish/osx-arm64 - - - name: Assemble .app bundle - run: | - APP="FryPDF.app" - rm -rf "$APP" - mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" - cp -R publish/osx-arm64/. "$APP/Contents/MacOS/" - sed "s/__VERSION__/$VERSION/g" packaging/macos/Info.plist.template > "$APP/Contents/Info.plist" - cp packaging/macos/AppIcon.icns "$APP/Contents/Resources/AppIcon.icns" - chmod +x "$APP/Contents/MacOS/PdfEditorApp" - - # Ad-hoc signing keeps the bundle identity stable across runs without full notarization - - name: Ad-hoc codesign - run: codesign --force --deep --sign - "FryPDF.app" - - - name: Create dmg - run: | - mkdir -p dmg-root - cp -R "FryPDF.app" dmg-root/ - ln -s /Applications dmg-root/Applications - hdiutil create -volname "FryPDF" -srcfolder dmg-root -ov -format UDZO "FryPDF-$VERSION-arm64.dmg" - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: macos-dmg - path: FryPDF-${{ needs.determine-version.outputs.version }}-arm64.dmg - if-no-files-found: error - - release: - needs: [determine-version, test, build-windows, build-macos] - if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Create GitHub Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ needs.determine-version.outputs.version }} - run: | - TAG_NAME="v$VERSION" - if [[ "${GITHUB_REF}" == refs/tags/* ]]; then - TAG_NAME="${GITHUB_REF_NAME}" - fi - - gh release create "$TAG_NAME" \ - "artifacts/windows-installer/FryPDF-Setup-$VERSION.exe" \ - "artifacts/windows-msix/FryPDF-$VERSION-x64.msix" \ - "artifacts/windows-msix/FryPDF-$VERSION-PublicCert.cer" \ - "artifacts/macos-dmg/FryPDF-$VERSION-arm64.dmg" \ - --title "FryPDF $TAG_NAME" \ - --generate-notes diff --git a/.gitignore b/.gitignore deleted file mode 100644 index ae77332..0000000 --- a/.gitignore +++ /dev/null @@ -1,89 +0,0 @@ -## .NET and C# -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ -*.user -*.userosscache -*.sln.docstates -*.suo -*.userprefs -*.dbmdl -*.jfm -*.pidb -*.booproj - -## Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/[Dd]ebug/ -[Bb]in/[Rr]elease/ -[Oo]bj/[Dd]ebug/ -[Oo]bj/[Rr]elease/ - -## Visual Studio & JetBrains Rider -.vs/ -.idea/ -*.sln.iml -*.DotSettings.user -_ReSharper*/ -*.rsuser - -## Visual Studio Code -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -## Test Results & Coverage -TestResults/ -*.trx -*.coverage -*.coveragexml -coverage.opencover.xml - -## NuGet Packages -*.nupkg -*.snupkg -*.nuget.props -*.nuget.targets -packages/ - -## Avalonia & Skia Artifacts -.avalonia/ -*.generated.cs - -## macOS specific -.DS_Store -.AppleDouble -.LSOverride -Icon -._* -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -## Local user project files & generated PDFs -*.pdf -!Assets/**/*.pdf -*.pdfproj -!Templates/**/*.pdfproj -*sample1.pdf* -*sample2.pdf* - -## Visual Verification Artifacts -VisualArtifacts/ -*_side_by_side.png \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index afbe683..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch FryPDF (Avalonia Desktop)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceFolder}/src/PdfEditorApp/bin/Debug/net10.0/PdfEditorApp.dll", - "args": [], - "cwd": "${workspaceFolder}/src/PdfEditorApp", - "stopAtEntry": false, - "console": "internalConsole" - } - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 23b3b9d..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/PdfEditorApp.slnx", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile", - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "run", - "command": "dotnet", - "type": "process", - "args": [ - "run", - "--project", - "${workspaceFolder}/src/PdfEditorApp/PdfEditorApp.csproj" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "--project", - "${workspaceFolder}/src/PdfEditorApp/PdfEditorApp.csproj" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "test", - "command": "dotnet", - "type": "process", - "args": [ - "test", - "${workspaceFolder}/PdfEditorApp.slnx" - ], - "problemMatcher": "$msCompile", - "group": { - "kind": "test", - "isDefault": true - } - } - ] -} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 41280d2..0000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -.agents/AGENTS.md \ No newline at end of file diff --git a/Bug/PDF_VIEWER_CONTINUOUS_SCROLL_RENDERING_BUG.md b/Bug/PDF_VIEWER_CONTINUOUS_SCROLL_RENDERING_BUG.md deleted file mode 100644 index ca3c129..0000000 --- a/Bug/PDF_VIEWER_CONTINUOUS_SCROLL_RENDERING_BUG.md +++ /dev/null @@ -1,68 +0,0 @@ -# Bug: PDF Viewer Continuous Scrolling Page Rendering & Active Page Synchronization - -- **ID**: `BUG-001` -- **Component**: `PdfEditorApp` -> `PdfViewerView` / `PdfViewerViewModel` -- **Status**: Open / Postponed for later resolution -- **Reported Date**: 2026-09-02 -- **Related Files**: - - [`src/PdfEditorApp/Views/PdfViewerView.axaml`](../src/PdfEditorApp/Views/PdfViewerView.axaml) - - [`src/PdfEditorApp/Views/PdfViewerView.axaml.cs`](../src/PdfEditorApp/Views/PdfViewerView.axaml.cs) - - [`src/PdfEditorApp/ViewModels/PdfViewerViewModel.cs`](../src/PdfEditorApp/ViewModels/PdfViewerViewModel.cs) - - [`src/PdfEditorApp/Views/Controls/PdfTextOverlayControl.cs`](../src/PdfEditorApp/Views/Controls/PdfTextOverlayControl.cs) - ---- - -## 1. Description & Symptoms - -When viewing a multi-page PDF document in Continuous Scroll Mode: -1. **Scrolling Does Not Render Scrolled-To Pages**: - - As the user scrolls down through the document (e.g. from Page 1 to Page 18), the scrolled-to page displays as a blank card showing only the placeholder icon and the label (`"Page 18"`). - - The high-resolution rendered Skia bitmap for the page (`Bitmap`) is not loaded/displayed on the screen while scrolling. -2. **Left Thumbnail Rail Does Not Synchronize**: - - The thumbnail rail on the left continues to highlight Page 1 (blue border) and does not update to track the currently visible page in the viewport. -3. **Clicking Immediately Fixes It**: - - Clicking directly anywhere on the page (e.g. clicking on Page 18) immediately causes the page to render and display its contents, and synchronizes the active page state. - ---- - -## 2. Steps to Reproduce - -1. Launch the desktop application: - ```bash - dotnet run --project src/PdfEditorApp/PdfEditorApp.csproj - ``` -2. Open any multi-page PDF document (e.g. 20+ pages). -3. Use the trackpad, mouse wheel, or scrollbar thumb to scroll down to Page 15–20. -4. **Observe**: - - The page in the viewport displays the document icon placeholder and page number, but no document content. - - The thumbnail rail remains on Page 1. -5. Click on the page: - - **Observe**: The document content renders immediately upon clicking. - ---- - -## 3. Technical Investigation & Findings So Far - -### What We Know -- **Why clicking works**: - - In `PdfTextOverlayControl.OnPointerPressed`: - - The control captures the click directly on the bound page item (`Page = page`). - - It invokes `vm.SelectPage(page)`. - - `SelectPage` sets `CurrentPageNumber = page.PageNumber`. - - `OnCurrentPageNumberChanged` triggers `EnsurePageRendered(pageNumber)`, which rasterizes the page via Skia and sets `page.Bitmap = bmp`. -- **What happens during scrolling**: - - `ContinuousScrollViewer` scrolls the content on screen. - - Visible-page calculation is handled by `ResolveVisiblePages` and `OnContinuousScrollOffsetChanged`. - - Possible issues to investigate when resuming: - 1. **Event Delivery**: Verify whether `ScrollChanged` or `OffsetProperty` events are firing during user gestures on macOS. - 2. **Coordinate Offset**: In `ResolveVisiblePages`, verify whether `viewer.Offset.Y` accurately reflects the scroll offset of the items in Avalonia across various DPI scales. - 3. **Render Lock Contention**: `StartBackgroundWorker` in `PdfViewerViewModel` loops through pages extracting thumbnails and text geometry under `_renderLock`. Check if `EnsurePageRendered` is waiting on `_renderLock` or if `_pendingForegroundRenders` is being blocked. - 4. **Virtualization vs Plain StackPanel**: `ContinuousItemsControl` uses a standard `StackPanel`. Check if an `ItemsRepeater` or `VirtualizingStackPanel` with an `ILogicalScrollable` scroll provider would be more robust for large documents. - ---- - -## 4. Next Steps When Resuming - -1. Add temporary console/debug logging inside `OnContinuousViewerScrollChanged` and `OnContinuousScrollOffsetChanged` to inspect `viewer.Offset.Y`, `detectedPageNum`, and `firstVisiblePage`. -2. Inspect if `RenderPageAtScale` is called and whether the resulting `Bitmap` is assigned to `page.Bitmap` on the UI thread. -3. Test alternative viewport intersection detection (e.g. checking control visual tree positions directly or subscribing to item layout updates). diff --git a/Bug/README.md b/Bug/README.md deleted file mode 100644 index 345e81f..0000000 --- a/Bug/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Bug Tracking - -This directory tracks known bugs, reproduction steps, technical investigations, and roadmaps for future resolution. - -| Bug ID | Title | Component | Status | File | -| :--- | :--- | :--- | :--- | :--- | -| `BUG-001` | PDF Viewer Continuous Scroll Page Rendering & Sync | `PdfEditorApp` (Viewer) | Open | [PDF_VIEWER_CONTINUOUS_SCROLL_RENDERING_BUG.md](./PDF_VIEWER_CONTINUOUS_SCROLL_RENDERING_BUG.md) | diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 89694a0..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,19 +0,0 @@ -# Claude Code Operating Guidelines for FryPDF - -## Build & Test Commands -- Build: `dotnet build` (Warning-free required: `TreatWarningsAsErrors=true`) -- Run Tests: `dotnet test` -- Filtered Tests: `dotnet test --filter "FullyQualifiedName~Material3ExpressiveThemeTests"` - -## CRITICAL UI MANDATE: Google Material Design 3 (M3) Expressive -ALL UI work in this repository must strictly adhere to Google Material Design 3 (M3) Expressive: -- **Shape Scale**: - - Buttons, search bars, segmented capsules, FABs, chubby slider thumbs $\to$ Pill (`CornerRadius="{StaticResource M3ShapeCornerFull}"` or `9999`) - - Dialog cards $\to$ Extra Large (`CornerRadius="{StaticResource M3ShapeCornerExtraLarge}"` or `28`) - - Cards, inputs (`TextBox.m3-outlined`), ComboBox, NumericUpDown $\to$ Large (`CornerRadius="{StaticResource M3ShapeCornerLarge}"` or `16`) - - Menus and list items $\to$ Medium (`CornerRadius="{StaticResource M3ShapeCornerMedium}"` or `12`) - - Chips and badges $\to$ Small (`CornerRadius="{StaticResource M3ShapeCornerSmall}"` or `8`) -- **Colors**: Never hardcode hex colors. Always reference dynamic tokens: `M3PrimaryBrush`, `M3SecondaryContainerBrush`, `M3SurfaceContainer...`, or legacy aliases (`WinBgBrush`, `WinAccentBrush`, etc.). -- **Chubby Sliders**: Retain chubby 8-10px tracks and 20-22px thumbs with hover transitions. -- **CommunityToolkit.Mvvm**: Take maximum advantage of CommunityToolkit source generators (`[ObservableProperty]`, `[RelayCommand]`, `[AsyncRelayCommand]`), cascading notifications (`[NotifyPropertyChangedFor]`, `[NotifyCanExecuteChangedFor]`), and `WeakReferenceMessenger.Default` for decoupled pub/sub messaging. -- **Reference**: Read `.agents/rules/m3_expressive_ui_mandate.md` and `docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md`. diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 60bce1c..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,101 +0,0 @@ -# FryPDF Agent Operating Instructions (GEMINI.md) - -## 1. CRITICAL UI MANDATE: Google Material Design 3 (M3) Expressive (STRICT) - -**ATTENTION AI AGENT**: -You MUST strictly adhere to **Google Material Design 3 (M3) Expressive** whenever modifying or creating any UI elements, views, sidebars, buttons, dialogs, or styles in this repository. - -1. **Shape Scale Hierarchy**: - - **Pills (`CornerRadius="{StaticResource M3ShapeCornerFull}"` or `9999`)**: All buttons (`primary-btn`, `accent-btn`, `action-pill-btn`, `m3-filled-btn`, `m3-tonal-btn`), search inputs (`TextBox.m3-search`), segmented capsules (`m3-segmented-container`), FABs (`m3-fab`), and chubby slider thumbs. - - **28px Extra-Large (`CornerRadius="{StaticResource M3ShapeCornerExtraLarge}"`)**: All modal dialog cards (`Border.m3-dialog-card`, `AboutDialog`, `CommandPaletteDialog`, `AiAssistantDialog`) and hero banners. - - **16px Large (`CornerRadius="{StaticResource M3ShapeCornerLarge}"`)**: Cards (`m3-card-elevated`), inputs (`TextBox.m3-outlined`), `ComboBox`, `NumericUpDown`, inspector section groups. - - **12px Medium (`CornerRadius="{StaticResource M3ShapeCornerMedium}"`)**: Context menus, tooltips, list items. - - **8px Small (`CornerRadius="{StaticResource M3ShapeCornerSmall}"`)**: Badges, thumbnail cards, chips (`m3-chip`, `m3-filter-chip`). - - **NEVER** use small arbitrary corner radii (like `2` or `4`) on interactive elements. - -2. **Colors & Dark Mode**: - - **NEVER** hardcode hex colors in templates or hover triggers. - - Always reference `{DynamicResource ...}` keys: `M3PrimaryBrush`, `M3SecondaryContainerBrush`, `M3SurfaceBrush`, `M3SurfaceContainer...`, `M3OutlineBrush`, or legacy aliases (`WinBgBrush`, `WinAccentBrush`, etc.). - -3. **Chubby Tactile Sliders**: - - Always maintain chubby tactile sliders with 8-10px tracks and 20-22px thumbs with hover transitions. - -4. **CommunityToolkit.Mvvm Mandate**: - - Maximize use of `CommunityToolkit.Mvvm` source generators (`[ObservableProperty]`, `[RelayCommand]`, `[AsyncRelayCommand]`). - - Use `[NotifyPropertyChangedFor]` and `[NotifyCanExecuteChangedFor]` to cleanly cascade property and command updates. - - Use `WeakReferenceMessenger.Default` for decoupled pub/sub messaging across ViewModels and services. - - Use `ObservableValidator` with DataAnnotations for dialogs and user forms. - ---- - -## 2. CRITICAL ARCHITECTURE MANDATE: Modular Plugin Pattern ("Everything is a Plugin") (STRICT) - -**ATTENTION AI AGENT**: -FryPDF uses a modular microkernel plugin architecture inspired by Cordis. -You MUST adhere to the **Plugin Pattern** whenever adding, refactoring, or extending application capabilities: - -1. **No Monolithic Switches or Hardcoded Wiring**: - - **NEVER** hardcode switch-case ladders, god view models, or monolithic enum registries when adding features. - - Every tool, canvas element, ribbon action, sidebar panel, inspector section, workspace page, dialog, AI provider, OCR engine, and file connector MUST be an isolated plugin (`IFryPlugin` or `ToolPluginBase`) mounted through an `IFryPluginBundle`. - -2. **The 12 Dynamic Registry Pillars**: - - Always register capabilities via the corresponding registry: - - Tools $\to$ `IPdfToolRegistry` / `PdfToolDescriptor` - - Ribbon Actions & Tabs $\to$ `IRibbonRegistry` / `RibbonContribution` / `RibbonTabDescriptor` - - Sidebars $\to$ `ISidebarRegistry` / `SidebarTabDescriptor` - - Inspector Sections $\to$ `IInspectorRegistry` / `InspectorSectionDescriptor` - - Workspace Navigation $\to$ `INavigationRegistry` / `NavigationItemDescriptor` - - Canvas Elements $\to$ `ICanvasElementRegistry` / `CanvasElementDescriptor` - - Importers / Exporters $\to$ `IDocumentImporterRegistry` / `IDocumentExporterRegistry` - - AI Providers / OCR $\to$ `IAiProviderRegistry` / `IOcrEngineRegistry` - - Status Bar / Dialogs $\to$ `IStatusBarRegistry` / `IDialogRegistry` - -3. **Reversible Effects & Zero Dangling Handlers**: - - Every side effect, event listener, or resource subscription registered during `ApplyAsync` MUST be registered through `PluginScope` (`ctx.RegisterEffect(...)`) to ensure clean LIFO rollback and zero memory leaks on plugin unload or reload. - -4. **Declarative Settings Schema**: - - User-configurable options must be declared via `IFryPlugin.SettingsSchema` using `PluginSettingDefinition` so that M3 Expressive configuration interfaces are auto-generated without manual UI boilerplate. - ---- - -## 3. CRITICAL PERFORMANCE MANDATE: Zero-Lag, 60+ FPS & Memory Safety (STRICT) - -**ATTENTION AI AGENT**: -FryPDF is an interactive real-time document studio. **LAG, FRAME DROPS, AND UI THREAD STALLS ARE UNACCEPTABLE**. You MUST design and write code thinking in terms of performance optimization: - -1. **Zero UI Thread Blocking**: - - **NEVER** run CPU-bound parsing, PDF deconstruction, Skia rasterization, QuestPDF document compilation, OCR, AI inference, or file/network I/O on the Avalonia UI thread. - - **NEVER** call `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` on asynchronous operations. - - Always offload heavy tasks to the thread pool (`Task.Run`) and use `[RelayCommand]` with asynchronous `Task` signatures and `CancellationToken`. - -2. **Instant Navigation & View Caching (0ms Transitions)**: - - For plugin-driven views, always cache instantiated views (e.g. `_dynamicViewCache` pattern in `HomeViewModel`) instead of destroying and re-instantiating heavy visual trees during navigation. - - Use pre-mounted XAML views for high-frequency screens (Home dashboard, tools studios, reader landing) to ensure instantaneous tab switching without layout latency. - -3. **UI Virtualization & Recycling**: - - Always use virtualized layout containers (`ItemsRepeater`, `VirtualizingStackPanel` with `ScrollUnit="Pixel"`) for document page thumbnails, tool cards, audit logs, and tabular data rows. Never use unvirtualized `StackPanel` for dynamic or unbound collections. - -4. **Input Throttling & Debouncing**: - - Smooth all high-frequency continuous user inputs: - - Trackpad pinch-to-zoom: Use proportional multiplicative deltas with clamping (`Math.Clamp(old * (1 + delta), 0.1, 5.0)`) to eliminate exponential explosion. - - Search boxes & filters: Debounce text changes (150–250ms) before filtering collections. - - Sliders: Throttle redraw updates during drag events; do not trigger full document re-renders on every micro-tick. - -5. **Memory Management, LOH Avoidance & Skia Disposal**: - - **Large Object Heap (LOH)**: Never allocate byte arrays $\ge 85\text{ KB}$ repeatedly in tight loops. Use `ArrayPool.Shared` and streams. Never store Base64 strings in memory for images; store raw `byte[]` and compute Base64 lazily for JSON only. - - **Unmanaged Graphics Handles**: `SKBitmap`, `SKImage`, `SKSurface`, `SKData`, and Avalonia `WriteableBitmap` wrap native pointers. Always dispose previous bitmap instances immediately when replacing them. - - **Subscription Hygiene**: Use `WeakReferenceMessenger` for pub/sub messaging. Detach all events on view unloading to prevent GC roots from retaining ViewModels. - -6. **Frame Budgeting & Render Transforms**: - - Keep render updates within 16ms (60 FPS) and 8ms (120 FPS). - - Use `RenderTransform` (GPU-accelerated) rather than mutating layout properties (`Margin`, `Width`, `Height`) during animations or gesture scaling. - ---- - -## 4. Reference Files & Documentation - -- **Operating Guide**: [`.agents/AGENTS.md`](.agents/AGENTS.md) -- **Plugin Architecture Manual**: [`docs/PLUGIN_BASED_ARCHITECTURE.md`](docs/PLUGIN_BASED_ARCHITECTURE.md) -- **M3 Expressive Guidelines**: [`docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md`](docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md) -- **M3 Tokens**: [`src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml`](src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml) -- **M3 Styles**: [`src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml`](src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 250e3e3..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Code Fry Dev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/PdfEditorApp.slnx b/PdfEditorApp.slnx deleted file mode 100644 index af9e07d..0000000 --- a/PdfEditorApp.slnx +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/README.md b/README.md deleted file mode 100644 index 6125fc6..0000000 --- a/README.md +++ /dev/null @@ -1,150 +0,0 @@ -# FryPDF — Modern Desktop PDF Studio - -
- -FryPDF App Logo - -### **Privacy-First, Professional Desktop PDF Creator, Editor & Document Studio** -*Engineered with .NET 10, Avalonia UI, QuestPDF, SkiaSharp & Modular Microkernel Plugins.* - -[![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4?style=flat-square&logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) -[![Avalonia UI](https://img.shields.io/badge/Avalonia-12.1-7029E6?style=flat-square&logo=avalonia&logoColor=white)](https://avaloniaui.net/) -[![QuestPDF](https://img.shields.io/badge/QuestPDF-2026.8-FF4500?style=flat-square)](https://www.questpdf.com/) -[![Platforms](https://img.shields.io/badge/Platforms-macOS%20%7C%20Windows%20%7C%20Linux-4B5563?style=flat-square)](https://github.com/CodeFryDev/FryPDF) -[![Tests](https://img.shields.io/badge/Tests-788%20Passed-10B981?style=flat-square)](tests/PdfEditorApp.Tests) -[![Architecture](https://img.shields.io/badge/Architecture-Microkernel%20Plugins-0284C7?style=flat-square)](docs/PLUGIN_BASED_ARCHITECTURE.md) -[![UI System](https://img.shields.io/badge/Design-Material%203%20Expressive-EE4B32?style=flat-square)](docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md) -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE) - -
- ---- - -> ⚠️ **Active Development Stage**: FryPDF is currently in an active stage of development. While core document engines, offline tools, vector editing, and plugin systems are fully operational, new plugin capabilities and complex edge cases are actively being refined. Feedback & issue reports are warmly welcomed! - ---- - -## ✨ Key Capabilities & Highlights - -- 🧩 **Modular Microkernel Plugin Architecture ("Everything is a Plugin")**: Inspired by Cordis architecture. Every tool, canvas element, ribbon action, sidebar panel, inspector section, AI provider, and file connector is an isolated, dynamic plugin mounted through composable plugin bundles with reversible LIFO rollback and zero memory leaks. Supports third-party `.fryplugin` external packages (see the [External Plugin Development Guide](docs/EXTERNAL_PLUGIN_DEVELOPMENT_GUIDE.md)). -- 🎨 **Google Material Design 3 (M3) Expressive UI**: Beautiful cross-platform user experience across macOS, Windows, and Linux. Features expressive pill shapes, segmented button capsules, floating action buttons (FABs), chubby tactile sliders (8–10px track, 20–22px thumbs), elevation shadows, and dynamic light/dark tonal themes. -- ⚡ **Zero-Lag, 60+ FPS Real-Time Performance**: Multi-tier view caching (`_dynamicViewCache`), virtualized UI recycling (`ItemsRepeater`, `VirtualizingStackPanel`), debounced high-frequency inputs (pinch-to-zoom deltas, search filters), and asynchronous non-blocking worker thread pipelines (`Task.Run`). -- 📕 **Dedicated PDF Reader Mode**: Pure reading mode with continuous vertical scroll, single-page fit, and two-page book spreads. Includes eye-comfort color themes (Daylight, Warm Sepia, Dark Night, High Contrast), real page thumbnail strip, bookmarks/TOC tree, live search with match jumping, review annotations (multi-color highlights, sticky notes, approval stamps), and a 1-click bridge to the visual editor. -- 📖 **Universal PDF Deconstruction Engine**: Ingest and deconstruct 3rd-party binary `.pdf` streams into live editable vector canvas elements (grouped text blocks, fonts, embedded imagery, and AcroForms) with strict layered Z-index ordering and multi-format SkiaSharp decoding (DCT, JPEG2000, uncompressed CMYK/RGB, monochrome 1-bit). -- 🌐 **Script-Aware Typography & Font Fallback**: Sub-threshold token clustering and script-aware font matching (Indic scripts such as Devanagari, Tamil, Telugu; CJK; and Latin) to eliminate missing character tofu boxes (`□□□`). -- 📝 **Interactive AcroForms with Formula Engine**: Text inputs, checkboxes, radio buttons, dropdowns, combo boxes, and signature boxes with real-time formula dependency recalculation and field validation. -- 🖋️ **Vector Signature Studio**: Type-to-sign with curated cursive calligraphy styles or draw custom vector signatures with smooth bezier curve interpolation. -- 📊 **Dynamic Vector Charts & Math**: Built-in sub-pixel LaTeX mathematical typesetting and 13+ dynamic vector chart varieties (Horizontal Bar, Column, Line, Smooth Curve, Area, Donut, Pie, Radar, Scatter, Bubble, Candlestick, Waterfall, Polar). -- 🤖 **Offline AI Studio Assistant**: Integrated with `Microsoft.Extensions.AI` and `OllamaSharp` for 100% offline local LLM inference (Llama, Mistral, Gemma) or cloud endpoints (OpenAI, Groq) to generate and edit document layouts, tables, and callouts from natural language prompts. -- 🔍 **Local OCR PDF Engine**: Convert scanned documents and image-only PDFs into searchable text layers locally using Tesseract OCR and native platform vision APIs without cloud uploads. -- 🔢 **Legal Bates Numbering & FOIA Redaction**: Automated sequential page numbering with customizable prefixes, suffixes, digit padding, and 6-anchor positioning. Permanent pattern-based redaction with irreversible pixel destruction. -- 🛠️ **32 Dedicated Full-Page PDF Tools**: Complete offline toolchain across 6 suites (Organize & Page, Optimize & Security, Convert from PDF, Convert to PDF, Edit & Forms, AI & Automation) with zero popup dialogs. -- 💾 **Dual-Format Persistence**: Native `.frypdf` JSON-based project files & standard compiled binary `.pdf` documents via QuestPDF. - ---- - -## 📁 Repository Structure - -``` -PDFCreator/ -├── src/ -│ ├── PdfEditorApp.Core/ # Pure headless core engine (cross-platform, zero UI dependencies) -│ │ ├── Analysis/ # Layout analysis, typography, Unicode script detection -│ │ ├── Deconstruction/ # PDF deconstruction engine (PDF stream to live editable model) -│ │ ├── Models/ # Domain element POCOs (Text, Image, Shape, Table, Chart, Math, Form) -│ │ └── Plugins/ # Microkernel plugin infrastructure (IFryPluginContext, DAG, Pipelines) -│ │ ├── Descriptors/ # Extension descriptors (Tools, Ribbon, Sidebars, Inspector, Navigation) -│ │ ├── Pipelines/ # Typed dispatch pipelines (Waterfall, Bail, Parallel, Serial, Emit) -│ │ ├── Profiles/ # App composition profiles (desktop, headless-cli, sdk) and bundle contracts -│ │ └── Settings/ # Typed plugin settings schema and persistent file store -│ └── PdfEditorApp/ # Main Avalonia desktop application -│ ├── Assets/ # Embedded fonts (Inter, Roboto, Noto Sans) and SVG icons -│ ├── Converters/ # Avalonia XAML data-binding and transform converters -│ ├── Plugins/ # Desktop plugin implementations and 16 modular bundles -│ │ ├── Bundles/ # Standard bundles (Tools, Pages, Sidebars, AI, CanvasElements, etc.) -│ │ └── Loader/ # Collectible AssemblyLoadContext and .fryplugin package extractor -│ ├── Services/ # Dynamic registries (Tools, Ribbon, Sidebars, Inspector, Navigation, AI) -│ ├── Styles/ # Material Design 3 Expressive tokens & component themes -│ ├── ViewModels/ # Reactive MVVM ViewModels (CommunityToolkit.Mvvm source generators) -│ └── Views/ # Avalonia XAML Windows, Canvas controls, and M3 dialogs -├── tests/ -│ └── PdfEditorApp.Tests/ # Comprehensive test suite (788 unit & integration tests) -├── packaging/ -│ ├── macos/ # macOS bundle templates, AppIcon.icns, & DMG creator -│ └── windows/ # Inno Setup installer & Windows MSIX packaging with full store assets -├── docs/ # Architecture, M3 Expressive guidelines, and plugin manuals -│ ├── PLUGIN_BASED_ARCHITECTURE.md -│ ├── EXTERNAL_PLUGIN_DEVELOPMENT_GUIDE.md # Step-by-step external plugin developer tutorial -│ ├── TIC_TAC_TOE_EXAMPLE.md # Complete playable Tic-Tac-Toe reference plugin tutorial -│ ├── MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md -│ ├── ARCHITECTURE.md -│ ├── FEATURES.md -│ ├── CONTRIBUTING.md -│ └── THIRD_PARTY_LICENSES.md -├── index.html # Official FryPDF web portal & interactive tool catalog -├── run # Quick CLI runner script (./run) -└── watch # Quick CLI hot-reload script (./watch) -``` - ---- - -## 🚀 Quick Start - -### Prerequisites -- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) (v10.0+) - -### Build Solution -```bash -dotnet build -``` - -### Run Full Test Suite (788 Tests) -```bash -dotnet test -``` - -### Run Application Locally -```bash -# Using root helper script -./run - -# Or using dotnet CLI directly -dotnet run --project src/PdfEditorApp -``` - -### Run with Hot Reload -```bash -./watch -``` - ---- - -## 📦 Packaging & Installers - -- **macOS**: Standalone `FryPDF.app` bundle and `.dmg` disk image (Universal Apple Silicon & Intel) with high-resolution `AppIcon.icns`. - > **Note**: If macOS Gatekeeper displays an unverified developer notice after downloading from GitHub Releases, run this command in Terminal: - > ```bash - > xattr -dr com.apple.quarantine "/Applications/FryPDF.app" - > ``` -- **Windows**: Official **Microsoft Store** verified app (`ms-windows-store://pdp/?productid=9P5GW2Q81B33`), **Inno Setup** executable (`FryPDF-Setup.exe`), and modern **MSIX** package with full tile assets. -- **Linux**: Portable `.tar.gz` archive, AppImage, or direct .NET 10 compilation. - ---- - -## 📚 Architectural Guides & Documentation - -- [Plugin Architecture Manual](docs/PLUGIN_BASED_ARCHITECTURE.md) — Comprehensive guide to the microkernel plugin system, 12 dynamic registries, pipelines, and settings schema. -- [Material Design 3 Expressive Guidelines](docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md) — Google M3 Expressive tokens, shape scale hierarchy, and styling rules. -- [Architecture & Technical Design](docs/ARCHITECTURE.md) — MVVM architecture, headless core engine, and Avalonia data binding. -- [Feature Catalog & Capabilities](docs/FEATURES.md) — In-depth breakdown of all 32 tools, vector engines, and document tools. -- [Contributing Guide](docs/CONTRIBUTING.md) — Developer setup, code conventions, and pull request workflow. -- [Third-Party & Font Licenses](docs/THIRD_PARTY_LICENSES.md) — Open-source dependency attributions and typography licenses. - ---- - -## 📄 License & Credits - -FryPDF Desktop Studio is open-source software licensed under the [MIT License](LICENSE). -Copyright © 2026 **Code Fry Dev**. All rights reserved. - -Developed with ❤️ using Avalonia UI, QuestPDF, and SkiaSharp. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index 6274682..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,166 +0,0 @@ -# FryPDF Technical Architecture - -FryPDF is a modern, high-performance, cross-platform PDF creation and editing studio built with **Avalonia UI** (v12.x), **C# / .NET 10**, and **QuestPDF**. - ---- - -## 1. Directory Structure - -``` -PDFCreator/ -├── src/ -│ └── PdfEditorApp/ # Main Avalonia Application -│ ├── Assets/ # Embedded icons, fonts, sample assets -│ ├── Converters/ # XAML value converters -│ ├── Models/ # Domain entities, element models, security/audit -│ ├── Services/ # Business logic, export engines, audit, undo/redo -│ ├── Styles/ # Avalonia Fluent theme styles & control themes -│ ├── Templates/ # Built-in document templates (Annual Report, Resume, etc.) -│ ├── ViewModels/ # MVVM CommunityToolkit ViewModels -│ ├── Views/ # Avalonia XAML Views, UserControls, and Dialogs -│ ├── App.axaml / .cs # Application root & resource dictionary -│ ├── Program.cs # Entry point & Avalonia initialization -│ ├── ViewLocator.cs # View-ViewModel resolver -│ └── app.manifest # Windows DPI-awareness and OS compatibility -├── tests/ -│ └── PdfEditorApp.Tests/ # Unit & Integration Tests (xUnit) -├── packaging/ -│ ├── macos/ # macOS bundle templates, icons, codesign -│ └── windows/ # Inno Setup installer (.iss) & MSIX packaging scripts -├── docs/ # Developer and user documentation -├── .github/workflows/ # GitHub Actions CI/CD release pipelines -├── PdfEditorApp.slnx # Solution configuration -└── README.md -``` - ---- - -## 2. Core Architectural Layers - -### MVVM Presentation Layer -- **`MainViewModel`**: Orchestrates top-level state: active view (Home vs Editor), current page, ribbon commands, tool modes, modal dialogs, and persistence commands. -- **`InspectorViewModel`**: Provides two-way property binding for the contextual sidebar (geometry, typography, fill colors, stroke, borders, rotation, opacity, form settings). -- **`HomeViewModel`**: Powers the Start screen with recent documents, quick action cards, and the interactive Template Gallery. -- **`ElementViewModelBase` & Subclasses**: Rich observable view models for every canvas element type (Text, Heading, Image, Shape, Table, Chart, FormField, StickyNote, Redaction, Stamp, BatesNumber, Measurement, QR Code, Ink). - -### Domain & Document Model -- **`PdfDocumentModel`**: Represents the root document (metadata, security settings, page collection, outlines, comments). -- **`PdfPageModel`**: Holds page dimensions, margins, header/footer definitions, and collection of `PdfElementModelBase`. -- **`PdfElementModelBase`**: Pure POCO domain models for serialization and export. - -### Services Layer -- **`PdfExportService` (`IPdfExportService`)**: Compiles the document hierarchy and elements into high-fidelity PDF binaries using QuestPDF. -- **`PdfImportService` (`IPdfImportService`)**: Imports existing PDFs, delegating to `PdfDeconstructionEngine` for born-digital parsing and embedded JSON detection. -- **`TemplateService` (`ITemplateService`)**: Provides starter templates (Executive Resume, Annual Report, Invoice, Certificate, Academic Paper) and allows community template extensions. -- **`ProjectPersistenceService` (`IProjectPersistenceService`)**: Handles lossless JSON-based project saving (`.frypdf`) and loading. -- **`SmartPlacementService` (`ISmartPlacementService`)**: Computes viewport-aware positions, cursor drop-points, and cascading offsets for newly inserted elements. -- **`UndoRedoService`**: Command-pattern undo/redo manager supporting atomic reversible actions. -- **`DocumentAuditService` (`IDocumentAuditService`)**: Preflight diagnostics engine checking for accessibility, contrast, empty fields, low-res images, and security flaws. -- **`SignatureService` (`ISignatureService`)**: Procedural vector and cursive signature generation. -- **`RecentDocumentsService` (`IRecentDocumentsService`)**: Manages LRU recent files history. - ---- - -## 3. PDF Deconstruction & Layout Analysis Subsystem - -Located in `src/PdfEditorApp.Core/`: -- **`PdfDeconstructionEngine`**: Deconstructs 3rd-party PDFs into live editable elements with deterministic layered Z-indexing (`Z=0..99` background cards, `Z=100..499` images, `Z=500..599` tables, `Z=600..999` shapes, `Z=1000..1999` text, `Z=2000+` form fields) and Skia multi-format image decoding. -- **`PdfLayoutAnalyzer`**: Intelligent layout clustering, script-aware gap thresholding, and paragraph indent recognition. -- **`UnicodeScriptDetector`**: Codepoint classifier covering 30+ world scripts with font fallback resolution. - ---- - -## 4. Data Studio & LiveCharts2 Ingestion Subsystem - -Located in `src/PdfEditorApp.Core/Data/` and `src/PdfEditorApp/ViewModels/DataStudio/`: -- **`DataMatrix`**: Unified in-memory tabular data structure with column type inference (Text, Number, Currency, Percentage, Date), row/column mutations, and matrix accessors. -- **`IDataSourceService` / `DataSourceService`**: Multi-source data ingestion pipeline: - - **Excel (`.xlsx`)**: High-performance OpenXml workbook parser supporting multi-sheet inspection and shared strings. - - **CSV / TSV**: RFC-4180 compliant parser with auto-delimiter detection (comma, tab, semicolon, pipe) and quoted multiline cell handling. - - **REST API / JSON**: Async HTTP client with custom headers, Bearer tokens, and flexible JsonPath array navigation. - - **Clipboard & Manual Entry**: Instant tabular clipboard paste and live spreadsheet grid editor. -- **`IDataBindingService` / `DataBindingService`**: Maps tabular datasets to LiveCharts2 series and Table elements, plus bidirectional Table $\leftrightarrow$ Chart conversion with full Undo/Redo tracking. -- **`DataStudioViewModel` & `DataStudioDialog`**: Modal Studio providing tabbed connector workflows, interactive column mapping, and live Skia-rendered chart previews. - ---- - -## 5. UI Architecture & Material Design 3 (M3) Expressive System - -FryPDF strictly implements **Google Material Design 3 (M3) Expressive** design principles across all screens, controls, and dialogs. - -- **Tokens (`Material3ExpressiveTokens.axaml`)**: - - Full M3 Expressive shape scale (`M3ShapeCornerNone` (0px) to `M3ShapeCornerFull` (9999px), with `M3ShapeCornerExtraLarge` (28px) for dialogs and `M3ShapeCornerLarge` (16px) for cards/inputs). - - Semantic tonal color palettes for Light and Dark themes (`Primary`, `SecondaryContainer`, `Surface`, `SurfaceContainer`, etc.). - - Multi-level elevation shadows (`M3ElevationLevel0` through `M3ElevationLevel5`). - - Tactile chubby slider tokens with interactive hover scaling. -- **Control Styles (`Material3ExpressiveStyles.axaml`)**: - - M3 button hierarchy (`m3-filled-btn`, `m3-tonal-btn`, `m3-elevated-btn`, `m3-outlined-btn`, `m3-text-btn`, `m3-fab`, `m3-icon-btn`). - - Segmented capsules (`m3-segmented-container` + `m3-segment-btn`), filter/assist chips (`m3-chip`, `m3-filter-chip`), and cards (`m3-card-elevated`, `m3-dialog-card`). - - Pill search bars (`TextBox.m3-search`) and outlined inputs (`TextBox.m3-outlined`). -- **Mandate**: All future UI additions and modifications must strictly conform to the [Material Design 3 Expressive Guidelines](MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md) and Section 4.F of [`.agents/AGENTS.md`](../.agents/AGENTS.md). - ---- - -## 6. AI Studio & Document Generation Subsystem - -Located in `src/PdfEditorApp/Services/AI/` and `src/PdfEditorApp/ViewModels/`: -- **`IPdfStudioAgentService` / `PdfStudioAgentService`**: Autonomous document studio agent translating natural language prompts into live, native, and editable FryPDF canvas elements (headers, invoices, tables, warning cards, certificates, badges, grids). -- **`IAiService` / `AiService`**: Unified multi-provider AI gateway powered by `Microsoft.Extensions.AI`: - - **Local Offline Inference (`OllamaLocal`)**: Connects to locally hosted Ollama instances via `OllamaSharp` (Llama 3.2, Mistral, Gemma, Phi-3) with 100% privacy, zero cloud data transfer, and zero network sockets. - - **Cloud Providers (`OpenAiCloud`, `CustomOpenAiCompatible`)**: Direct integration with OpenAI, Groq, OpenRouter, LM Studio, or vLLM endpoints via standard OpenAI-compatible REST protocols. -- **`AiAssistantViewModel` & `AiAssistantDialog`**: Interactive M3 Expressive dialog supporting prompt execution, in-place element modification, suggested prompts, live model switching, activity logging, and atomic canvas undo/redo. -- **`AiDocumentService`**: Extractive NLP summarization, keyword extraction, and document translation engine. - ---- - -## 7. Plugin-Based Architecture ("Everything is a Plugin") - -FryPDF incorporates an extensible, plugin-first microkernel architecture inspired by the **Cordis** framework. -- **Dynamic Capability Context (`IFryPluginContext`)**: Swappable services and extension points without hardcoded compile-time dependencies. -- **12 Dynamic Registry Pillars**: Extensibility across Tools (`IPdfToolRegistry`), Ribbon Tabs (`IRibbonRegistry`), Sidebars (`ISidebarRegistry`), Contextual Inspector (`IInspectorRegistry`), Navigation Pages (`INavigationRegistry`), Canvas Elements (`ICanvasElementRegistry`), Importers/Exporters (`IDocumentImporterRegistry`, `IDocumentExporterRegistry`), AI/OCR (`IAiProviderRegistry`, `IOcrEngineRegistry`), Data Connectors (`IDataConnectorRegistry`), and Status/Dialogs (`IStatusBarRegistry`, `IDialogRegistry`). -- **Declarative Dependency Resolution**: Directed Acyclic Graph (DAG) topological loader. -- **Reversible Effects (`PluginScope`)**: LIFO registration disposal eliminating memory leaks on unload. -- **Multi-Mode Dispatch Pipelines**: `Waterfall` (around-middleware), `Bail` (first handled result), `Parallel`, `Serial`, and `Emit`. -- **Profiles & Bundles**: Target-specific application assemblies (`desktop`, `headless-cli`, `sdk`) composed of 16 standard bundles. -- **Dynamic Hot-Mounting**: Collectible `AssemblyLoadContext` (`FryPluginLoadContext`) supporting friction-free drag-and-drop `.fryplugin` package installation without restarting the application. - -For the full architectural specification, interface designs, and pipeline mechanics, see the [Plugin-Based Architecture Manual](PLUGIN_BASED_ARCHITECTURE.md). - ---- - -## 8. Interactive Document Viewer & Presentation Subsystem (.frypdf) - -Located in `src/PdfEditorApp/ViewModels/FryPdfViewer/` and `src/PdfEditorApp/Views/`: -- **Read-Only Interactive Experience**: Unlike the creation studio canvas which allows arbitrary dragging, resizing, and styling, the `.frypdf` Viewer offers a streamlined, non-destructive presentation and consumption mode for rich document artifacts beyond what static binary PDFs support. -- **Interactive Live Tables (`InteractiveTableViewModel`)**: - - Real-time substring row filtering across all columns via search input. - - Interactive header column sorting (ascending / descending toggle). - - Sticky frosted header bar with column headers and row count metrics. - - One-click CSV export to system clipboard. -- **Animated Tactile Charts (`InteractiveChartViewModel`)**: - - Smooth 600ms cubic ease-out entry animations using Avalonia's `DispatcherTimer`. - - Interactive tactile bar hover cards displaying exact values and category labels. - - In-place tabular data inspector toggle (`IsShowingDataTable`). - - Tactile animation replay capability for live presentations. -- **Living Form Fields & Digital Signatures**: - - Interactive read-only checkboxes and toggle states for document inspection. - - Validated digital signature and seal verification badges. -- **Presentation Mode**: - - Fullscreen/edge-to-edge slide show layout hiding editor chrome. - - Floating M3 Expressive pill HUD with quick navigation, page counters, zoom controls, and exit commands. -- **Bi-Directional Studio Bridge**: - - Documents can be launched into the interactive viewer directly from Home, the title bar, or file open pickers. - - Users can transition back into the full Editing Studio instantly via `TransitionToEditorCommand` or export directly to standard static PDF via QuestPDF. - ---- - -## 9. High-Performance, Zero-Lag Engineering Architecture - -FryPDF enforces strict performance standards to deliver 60+ FPS desktop fluid responsiveness: -- **Zero UI Thread Blocking**: All CPU-intensive parsing, Skia rendering, QuestPDF generation, OCR, and AI inference execute on background thread pools via `Task.Run` with cancellation token support. UI thread blocking methods (`.Result`, `.Wait()`) are strictly forbidden. -- **0ms Instant Navigation & View Caching**: Dynamic plugin views are lazily instantiated and cached in-memory (`_dynamicViewCache`), while core workspace views are pre-mounted in XAML to guarantee instantaneous section switching without layout spikes. -- **Virtualized Layouts**: All variable-length collections (thumbnails, tool cards, audit logs, tabular rows) use `ItemsRepeater` or `VirtualizingStackPanel` with pixel scrolling and element recycling. -- **Continuous Input Throttling & Debouncing**: Proportional clamped pinch-to-zoom math prevents exponential explosion; search inputs and drag operations are debounced/throttled. -- **Strict Memory & LOH Hygiene**: Allocations $\ge 85\text{ KB}$ are eliminated from tight loops via `ArrayPool.Shared`. Unmanaged graphics handles (`SKBitmap`, `SKImage`, `SKSurface`) are disposed immediately upon replacement. Weak reference messaging (`WeakReferenceMessenger`) prevents retain cycles. - -For detailed algorithms, testing procedures, and the continuous improvement workflow, see the [PDF Deconstruction & Editing Guide](PDF_DECONSTRUCTION_AND_EDITING.md). diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 100644 index 5c5923e..0000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -1,78 +0,0 @@ -# Contributing to FryPDF - -Thank you for your interest in contributing to FryPDF! - ---- - -## 1. Prerequisites -- **.NET 10 SDK** (or later) -- IDE: JetBrains Rider, Visual Studio Code (with C# Dev Kit), or Visual Studio 2026 - ---- - -## 2. Solution Structure -- `src/PdfEditorApp.Core/`: Pure headless core engine (PDF deconstruction, layout analysis, data models, microkernel plugin context). -- `src/PdfEditorApp/`: Main Avalonia desktop application (Material Design 3 Expressive UI, dynamic registries, modular bundles). -- `tests/PdfEditorApp.Tests/`: Unit and integration tests (xUnit + Skia verification). -- `docs/`: Technical manuals and architecture specifications. -- `packaging/`: Platform-specific installers (macOS .app/.dmg, Windows Inno Setup & MSIX). - ---- - -## 3. Building and Running - -### Build Solution -```bash -dotnet build -``` - -### Run Tests -```bash -dotnet test -``` - -### Run Application Locally -```bash -dotnet run --project src/PdfEditorApp/PdfEditorApp.csproj -``` - ---- - -## 4. Coding Conventions & Mandates - -### A. General Standards -- Use C# 13+ / .NET 10 features (nullable reference types, pattern matching, file-scoped namespaces). -- Follow MVVM design patterns with `CommunityToolkit.Mvvm` source generators (`[ObservableProperty]`, `[RelayCommand]`). -- Ensure no warnings or build errors are introduced (`true`). -- Add comprehensive unit tests in `tests/PdfEditorApp.Tests/` for all new features, tools, and converters. - -### B. Modular Plugin Pattern ("Everything is a Plugin") -- Never write monolithic `switch-case` ladders, god view models, or hardcoded UI instantiations. -- Implement new capabilities (tools, canvas elements, ribbon actions, sidebars, inspector sections, workspace pages, AI providers, and converters) as modular plugins (`IFryPlugin` or `ToolPluginBase`) mounted into an `IFryPluginBundle`. -- Register components through the 12 dynamic capability registries (`IPdfToolRegistry`, `IRibbonRegistry`, `ISidebarRegistry`, `IInspectorRegistry`, etc.). -- Track all side effects via `ctx.RegisterEffect` to guarantee leak-free reversible unmounting. -- For complete specifications, see [Plugin-Based Architecture Manual](PLUGIN_BASED_ARCHITECTURE.md) and the step-by-step [External Plugin Development Guide](EXTERNAL_PLUGIN_DEVELOPMENT_GUIDE.md). - -### C. Zero-Lag Performance & Responsiveness Mandate -- **Zero UI Thread Blocking**: PDF parsing, Skia rasterization, QuestPDF export, OCR, and AI inference must run asynchronously on background threads via `Task.Run`. Never call `.Result` or `.Wait()` on the UI thread. -- **Instant Navigation & View Caching**: In navigation controllers, cache contributed dynamic views (`_dynamicViewCache` pattern) to ensure 0ms instantaneous page transitions. -- **Virtualization Only**: Always use `ItemsRepeater` or `VirtualizingStackPanel` with `ScrollUnit="Pixel"` for variable-length collections (thumbnails, tool cards, audit logs, data rows). -- **Smooth Continuous Inputs**: Multiplicatively clamp pinch-to-zoom math, debounce search query text changes (150–250ms), and throttle slider repaints during drag. -- **Memory & LOH Hygiene**: Never allocate byte arrays $\ge 85\text{ KB}$ in loops. Immediately dispose unmanaged Skia graphics objects (`SKBitmap`, `SKImage`, `SKSurface`). Use `WeakReferenceMessenger` to prevent retain cycles. - -### D. Google Material Design 3 (M3) Expressive UI Mandate -- All UI elements must strictly adhere to Google Material Design 3 Expressive guidelines. -- Use centralized shape tokens (`M3ShapeCornerFull` for pills/buttons/search, `M3ShapeCornerExtraLarge` for dialogs, `M3ShapeCornerLarge` for cards/inputs). -- Never hardcode hex colors; reference dynamic M3 theme brushes (`M3PrimaryBrush`, `M3SurfaceBrush`, etc.). -- See [Material Design 3 Expressive Guidelines](MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md). - -### E. Security & Privacy (Strict Zero PII Policy) -- Never commit credentials, API keys, passwords, or Personally Identifiable Information (PII) — including real government IDs, personal names, phone numbers, or addresses. Always use synthetic dummy data. - ---- - -## 5. Developing & Testing PDF Deconstruction Algorithms -When adding support for new PDF document types (invoices, ID cards, tax forms, multi-column articles, complex scripts): -1. Review the detailed architecture and continuous improvement guide: [PDF Deconstruction & Editing Guide](PDF_DECONSTRUCTION_AND_EDITING.md). -2. Run the visual side-by-side verification test (`GenerateVisualComparison_SideBySide_SavesArtifacts`) to compare ground-truth PDF rendering vs deconstructed canvas elements. -3. Validate that all 770+ unit tests pass without regressions (`dotnet test`). diff --git a/docs/EXTERNAL_PLUGIN_DEVELOPMENT_GUIDE.md b/docs/EXTERNAL_PLUGIN_DEVELOPMENT_GUIDE.md deleted file mode 100644 index c920183..0000000 --- a/docs/EXTERNAL_PLUGIN_DEVELOPMENT_GUIDE.md +++ /dev/null @@ -1,749 +0,0 @@ -# Developing External Plugins for FryPDF - -> **Authoritative Guide & Developer Manual** -> Learn how to build, test, package, and distribute third-party plugins for **FryPDF** using **.NET 10, C# 13, and Avalonia UI (Material Design 3 Expressive)**. - ---- - -## 1. Overview & Architectural Foundation - -FryPDF is built upon an **"Everything is a Plugin"** modular microkernel architecture inspired by the **Cordis** framework. The core desktop shell privileges no single subsystem: tools, dynamic ribbon actions, editor sidebars, contextual property panels, floating overlays, AI engines, and file converters are all isolated plugins mounted into an active runtime context (`IFryPluginContext`). - -### Why Build a FryPDF External Plugin? -- **Zero Monolithic Wiring**: You never need to fork or recompile FryPDF to introduce new features. -- **Isolated Collectible Loading**: Plugins execute inside dedicated `CollectiblePluginLoadContext` (`AssemblyLoadContext`) sandboxes. Plugins can be loaded, enabled, disabled, updated, and completely unloaded from memory without restarting the application. -- **Dynamic M3 Expressive Integration**: Your UI automatically integrates with Google Material Design 3 (M3) Expressive themes, color tokens, and elevation physics. -- **Auto-Generated Settings UI**: Declare typed configuration options in your code or manifest; FryPDF automatically generates an M3 Expressive settings dialog. -- **Single-File Distribution (`.fryplugin`)**: Distribute your plugin as a single `.fryplugin` package (ZIP archive containing `plugin.json`, DLLs, and resources) that users can install via drag-and-drop. - ---- - -## 2. Prerequisites & Environment Setup - -Before starting, ensure you have: -- **[.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)** (v10.0 or later) -- An IDE with modern C# 13+ support: - - Visual Studio Code (with C# Dev Kit extension) - - JetBrains Rider (2025.1+) - - Visual Studio 2026 (v18+) - -### Zero Source Code Requirement -> [!TIP] -> **You do NOT need to clone FryPDF's repository or download its source code to create plugins!** -> External developers develop plugins independently against either: -> 1. **Their installed FryPDF application** (referencing the compiled `.dll` files from the install directory). -> 2. **The `FryPdf.PluginSdk` NuGet package** (`dotnet add package FryPdf.PluginSdk`). - -### Referenced Host Contracts -External plugins compile against the following host contracts (provided by the installed application at runtime): -- **`PdfEditorApp.Core.dll`**: Core domain models, plugin contracts (`IFryPlugin`, `IFryPluginContext`), and extension descriptors. -- **`PdfEditorApp.dll`** *(optional, for UI plugins)*: Avalonia UI views, viewmodels, base classes (`ToolPluginBase`), and services. -- **`CommunityToolkit.Mvvm`**: Standard MVVM source generators (`[ObservableProperty]`, `[RelayCommand]`). -- **`Avalonia`** (12.x): Cross-platform UI controls and styling. - ---- - -## 3. The Anatomy of an External Plugin - -Every external plugin consists of: -1. **A .NET 10 Class Library** targeting `net10.0`. -2. **An Implementation of `IFryPlugin`** (or subclass of `ToolPluginBase`). -3. **Reversible Effects & Dynamic Registrations** mounted via `ApplyAsync`. -4. **A `plugin.json` Manifest File** describing metadata, dependencies, and settings. -5. **A `.fryplugin` Archive** bundle for distribution. - -### 3.1 The `IFryPlugin` Interface -Every plugin must implement the `IFryPlugin` contract: - -```csharp -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Manifests; - -namespace MyCompany.FryPdf.Plugins; - -public class MyCustomPlugin : IFryPlugin -{ - // Unique identifier (reverse-domain style: ..) - public string Id => "com.mycompany.frypdf.customtool"; - - // User-friendly display name - public string Name => "Custom PDF Tool"; - - // Semantic version of the plugin - public Version Version => new(1, 0, 0); - - // Declares required service contracts that must be registered before mounting - public IReadOnlyList RequiredServices => Array.Empty(); - - // Declares service contracts this plugin provides to other plugins - public IReadOnlyList ProvidedServices => Array.Empty(); - - // Declarative configuration schema for user-editable settings - public IReadOnlyDictionary? SettingsSchema => null; - - // Called when the plugin is mounted into FryPDF - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - // Register tools, ribbon buttons, sidebars, pipelines, or services here - return Task.CompletedTask; - } -} -``` - ---- - -## 4. The 12 Dynamic Capability Registries - -FryPDF provides 12 dynamic extension pillars accessible through `IFryPluginContext`. Never write hardcoded UI wiring—always register through these pillars: - -| Pillar | Registry / Context Method | Typical Use Case | -| :--- | :--- | :--- | -| **1. PDF Tools** | `ctx.RegisterTool(PdfToolDescriptor)` | Standalone tools (Merge, Split, Rotate, Watermark, Compress, etc.). | -| **2. Dynamic Ribbon** | `ctx.RegisterRibbonAction(RibbonActionDescriptor)`
`ctx.RegisterRibbonTab(RibbonTabDescriptor)`
`ctx.RegisterRibbonGroup(RibbonGroupDescriptor)` | Action pill buttons, custom ribbon groups, or dedicated ribbon tabs. | -| **3. Extensible Sidebars** | `ctx.RegisterSidebarTab(SidebarTabDescriptor)` | Custom sidebar panels (e.g. Audit Logs, Bookmarks, AI Chat, Metadata). | -| **4. Contextual Inspector** | `ctx.RegisterInspectorSection(InspectorSectionDescriptor)` | Custom property editor panels targeting specific canvas elements. | -| **5. Canvas Elements** | `ctx.RegisterCanvasElement(CanvasElementDescriptor)` | Custom vector elements (Barcode, LaTeX Math, Stamps, Ink, Form Fields). | -| **6. Shell Overlays** | `ctx.RegisterOverlay(OverlayDescriptor)` | Floating, draggable, minimizable M3 utility cards (e.g. Scratchpad, Notes). | -| **7. Command Palette** | `ctx.RegisterCommand(CommandPaletteDescriptor)` | Searchable commands triggered via ⌘K / Ctrl+K with keyboard shortcuts. | -| **8. Status Bar Widgets** | `ctx.RegisterStatusBarWidget(StatusBarWidgetDescriptor)` | Footer status pills, counters, toggles, or telemetry badges. | -| **9. Document Importers** | `ctx.RegisterImporter(IDocumentImporter)` | Custom file format decoders (Markdown, DOCX, XLSX, HTML, EPUB). | -| **10. Document Exporters** | `ctx.RegisterExporter(IDocumentExporter)` | Custom file generators (PDF/A, multi-image sequences, CSV, HTML). | -| **11. OCR & AI Engines** | `ctx.RegisterOcrEngine(IOcrEngine)` | Platform-specific OCR engines (Apple Vision, Tesseract) or local AI models. | -| **12. Data Connectors** | `ctx.RegisterDataConnector(IDataConnector)` | Tabular data loaders (REST APIs, SQLite databases, JSON feeds). | - -### Reversible Effects Rule (`ctx.RegisterEffect`) -Whenever your plugin registers event listeners, creates file hooks, or starts background workers, **you MUST track them using `ctx.RegisterEffect`**. When the plugin unloads, FryPDF unwinds these actions in reverse order (LIFO), guaranteeing zero memory leaks and zero dangling event listeners: - -```csharp -public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) -{ - // Subscribe to a message or event - var token = WeakReferenceMessenger.Default.Register(this, HandleDocumentChanged); - - // Register rollback action for clean teardown - ctx.RegisterEffect(() => - { - WeakReferenceMessenger.Default.Unregister(this); - }); - - return Task.CompletedTask; -} -``` - ---- - -## 5. Step-by-Step Tutorial: Building a "Watermark Plus" Plugin - -Let's build a real, fully functional external plugin: **`FryPdf.Plugin.WatermarkPlus`**. -This plugin: -1. Adds a dedicated full-page PDF tool to the "Optimize & Security" suite. -2. Contributes a quick-action pill to the Ribbon's **View** tab. -3. Contributes a keyboard shortcut to the **Command Palette** (⌘K / Ctrl+K). -4. Provides configurable settings (Default Watermark Text, Default Opacity) with auto-generated M3 UI. - -### Step 5.1: Create the Project File (`.csproj`) -Create a new directory named `FryPdf.Plugin.WatermarkPlus` and add `FryPdf.Plugin.WatermarkPlus.csproj`: - -```xml - - - net10.0 - enable - enable - 13 - true - FryPdf.Plugin.WatermarkPlus - FryPdf.Plugin.WatermarkPlus - 1.0.0 - Acme Document Solutions - Enterprise Watermarking Plugin for FryPDF - - - - - - /Applications/FryPDF.app/Contents/MacOS - C:\Program Files\FryPDF - /opt/FryPDF - $(FRYPDF_HOME) - - - - - - $(FryPdfInstallDir)\PdfEditorApp.Core.dll - false - - - $(FryPdfInstallDir)\PdfEditorApp.dll - false - - - - - - - - - - - - - - - - - -``` - -> [!IMPORTANT] -> **Host Dependency Exclusion (`false`)**: -> Always mark `PdfEditorApp.Core`, `PdfEditorApp`, `Avalonia`, and `CommunityToolkit.Mvvm` as compile-only (`Private=false` or `PrivateAssets="all"`). The host application already loads these assemblies. Packaging duplicate copies of Avalonia or Core DLLs inside your plugin will cause type casting errors across `AssemblyLoadContext` boundaries. - ---- - -### Step 5.2: Create the `plugin.json` Manifest -Create `plugin.json` in the project root: - -```json -{ - "id": "com.acme.frypdf.watermarkplus", - "name": "Watermark Plus", - "version": "1.0.0", - "author": "Acme Document Solutions", - "description": "Apply enterprise security watermarks, confidential stamps, and company emblems to multi-page PDFs.", - "entryPoint": "FryPdf.Plugin.WatermarkPlus.dll", - "icon": "Watermark", - "dependencies": [], - "settingsSchema": { - "DefaultText": { - "type": "string", - "label": "Default Stamp Text", - "description": "Default text prefilled in the watermark input", - "default": "CONFIDENTIAL" - }, - "DefaultOpacity": { - "type": "number", - "label": "Default Opacity (%)", - "description": "Default stamp opacity percentage (1-100)", - "default": 35 - }, - "IncludeDateStamp": { - "type": "boolean", - "label": "Append Current Date", - "description": "Automatically append today's date below the watermark text", - "default": true - }, - "StampColor": { - "type": "select", - "label": "Stamp Color", - "description": "Default watermark tone", - "default": "Red", - "options": ["Red", "DarkSlate", "NavyBlue", "EmeraldGreen"] - } - } -} -``` - -#### Supported `settingsSchema` Types: -- **`"string"`**: Renders a Material Design 3 `TextBox.m3-outlined`. -- **`"secret"`**: Renders a secure password field with `PasswordChar="●"`. -- **`"number"`**: Renders an M3 `NumericUpDown.m3-outlined`. -- **`"boolean"`**: Renders a tactile M3 `ToggleSwitch`. -- **`"select"`**: Renders an M3 `ComboBox.m3-outlined` populated with the `options` array. - ---- - -### Step 5.3: Implement the Tool ViewModel -Create `WatermarkPlusViewModel.cs`. Use `CommunityToolkit.Mvvm` for reactive properties and commands: - -```csharp -using System; -using System.IO; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Services.Tools.Core; - -namespace FryPdf.Plugin.WatermarkPlus; - -public partial class WatermarkPlusViewModel : PdfToolViewModelBase -{ - public override string ToolId => "com.acme.frypdf.watermarkplus"; - public override string DisplayName => "Watermark Plus"; - public override string Category => "Optimize & Security"; - - [ObservableProperty] - private string _stampText = "CONFIDENTIAL"; - - [ObservableProperty] - private double _opacityPercent = 35.0; - - [ObservableProperty] - private bool _includeDate = true; - - [ObservableProperty] - private string _statusMessage = "Ready to stamp documents."; - - public WatermarkPlusViewModel(IServiceProvider serviceProvider) : base(serviceProvider) - { - } - - [RelayCommand] - private async Task ApplyWatermarkAsync() - { - if (SelectedFiles.Count == 0) - { - StatusMessage = "Please select at least one PDF document."; - return; - } - - IsProcessing = true; - ProgressValue = 0; - StatusMessage = "Applying watermark stamps..."; - - try - { - // Execute heavy PDF stamping off the UI thread via Task.Run - await Task.Run(async () => - { - int count = SelectedFiles.Count; - for (int i = 0; i < count; i++) - { - var file = SelectedFiles[i]; - // Example: Apply stamp using Core deconstruction or QuestPDF pipelines - await Task.Delay(150); // Simulate processing - - ProgressValue = (double)(i + 1) / count * 100.0; - } - }); - - StatusMessage = $"Successfully watermarked {SelectedFiles.Count} documents!"; - } - catch (Exception ex) - { - StatusMessage = $"Error: {ex.Message}"; - } - finally - { - IsProcessing = false; - } - } -} -``` - ---- - -### Step 5.4: Implement the Avalonia M3 Tool View -Create `WatermarkPlusView.axaml` adhering strictly to **Material Design 3 Expressive** guidelines: - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -And the code-behind in `WatermarkPlusView.axaml.cs`: - -```csharp -using Avalonia.Controls; - -namespace FryPdf.Plugin.WatermarkPlus; - -public partial class WatermarkPlusView : UserControl -{ - public WatermarkPlusView() - { - InitializeComponent(); - } -} -``` - ---- - -### Step 5.5: Implement the Main Plugin Class -Now connect everything together by implementing `IFryPlugin` in `WatermarkPlusPlugin.cs`: - -```csharp -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Manifests; -using PdfEditorApp.Services.Tools.Core; - -namespace FryPdf.Plugin.WatermarkPlus; - -public class WatermarkPlusPlugin : IFryPlugin -{ - public string Id => "com.acme.frypdf.watermarkplus"; - public string Name => "Watermark Plus"; - public Version Version => new(1, 0, 0); - - public IReadOnlyList RequiredServices => Array.Empty(); - public IReadOnlyList ProvidedServices => Array.Empty(); - - public IReadOnlyDictionary? SettingsSchema => new Dictionary - { - ["DefaultText"] = new() - { - Type = "string", - Label = "Default Stamp Text", - Description = "Default text prefilled in the watermark input", - DefaultValue = "CONFIDENTIAL" - }, - ["DefaultOpacity"] = new() - { - Type = "number", - Label = "Default Opacity (%)", - Description = "Default stamp opacity percentage (1-100)", - DefaultValue = 35 - } - }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - // 1. Register Dedicated Full-Page Tool - var toolRegistration = ctx.RegisterTool(new PdfToolDescriptor - { - Id = Id, - Name = Name, - Description = "Apply custom security watermarks with dynamic opacity and timestamp stamps.", - Category = "OptimizeAndSecurity", - IconKind = "Watermark", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }); - - // 2. Register Ribbon Action in the 'Tools' Tab - var ribbonRegistration = ctx.RegisterRibbonAction(new RibbonActionDescriptor - { - Id = "action.ribbon.watermarkplus", - TabId = "tools", - GroupId = "security", - Label = "Watermark Plus", - Tooltip = "Open Watermark Plus Studio", - IconKind = "Watermark", - Order = 105, - Action = sp => - { - if (sp.GetService(typeof(IPdfToolRegistry)) is IPdfToolRegistry toolRegistry) - { - toolRegistry.OpenTool(Id); - } - } - }); - - // 3. Register Command in Command Palette (Ctrl+K / ⌘K) - var cmdRegistration = ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.watermarkplus.open", - Title = "Open Watermark Plus Studio", - Subtitle = "Security stamp and watermark tool", - Category = "Security", - IconKind = "Watermark", - Shortcut = "Ctrl+Shift+W", - Order = 80, - Action = sp => - { - if (sp.GetService(typeof(IPdfToolRegistry)) is IPdfToolRegistry toolRegistry) - { - toolRegistry.OpenTool(Id); - } - } - }); - - // 4. Register Reversible Effects for 100% Clean Unload - ctx.RegisterEffect(() => - { - toolRegistration.Dispose(); - ribbonRegistration.Dispose(); - cmdRegistration.Dispose(); - }); - - return Task.CompletedTask; - } -} -``` - ---- - -## 6. Packaging Your Plugin (`.fryplugin`) - -FryPDF uses `.fryplugin` distribution packages. A `.fryplugin` file is simply a standard ZIP archive with the following structure: - -``` -WatermarkPlus.fryplugin (ZIP file) -├── plugin.json # Root manifest -├── FryPdf.Plugin.WatermarkPlus.dll# Compiled entry assembly -└── [OtherDependency.dll] # Any external 3rd-party dependencies (excluding host DLLs) -``` - -### 6.1 Automated Packaging with MSBuild (Recommended) -Add the following snippet to the bottom of your `.csproj` file. Every time you run `dotnet build -c Release`, MSBuild will automatically package your plugin into a `.fryplugin` file ready for installation: - -```xml - - - - $(TargetDir)staging\ - $(TargetDir)$(AssemblyName).fryplugin - - - - - - - - - - - - - - - - - - -``` - -### 6.2 Manual Packaging via CLI -You can also package your plugin manually using standard command-line tools: - -```bash -# 1. Build project in Release mode -dotnet build -c Release - -# 2. Navigate to output folder -cd bin/Release/net10.0/ - -# 3. Create ZIP archive renamed to .fryplugin -zip -r WatermarkPlus.fryplugin plugin.json FryPdf.Plugin.WatermarkPlus.dll -``` - ---- - -## 7. Installing and Testing Your Plugin in FryPDF - -FryPDF supports 3 seamless ways to load and test your external plugin: - -### Method 1: Drag-and-Drop Installation (Easiest) -1. Launch **FryPDF**. -2. Open the **Plugins Manager** (Click the Settings gear in the header $\to$ **Plugins & Extensions**, or press `Ctrl+Shift+P` / `⌘Shift+P`). -3. Drag and drop your `.fryplugin` (or `.dll`) directly onto the dialog window. -4. FryPDF immediately unpacks the package, inspects the manifest, loads the assembly into an isolated `CollectiblePluginLoadContext`, and mounts your plugin with zero restart required! - -### Method 2: Command Palette or File Picker -1. In FryPDF, press `Ctrl+K` or `⌘K` to open the **Command Palette**. -2. Type `Install Plugin Package...` and press Enter. -3. Select your `.fryplugin` or compiled `.dll` file from the open file dialog. - -### Method 3: Direct File Placement (Auto-Discovery) -On startup, FryPDF scans designated plugin directories and automatically loads all unpacked folders, `.fryplugin` archives, and standalone `.dll` files. You can drop your plugin files into either of the following locations: - -#### Platform User Directories: -- **macOS**: `~/Library/Application Support/FryPdf/plugins//` -- **Windows**: `%APPDATA%\FryPdf\plugins\\` (e.g. `C:\Users\\AppData\Roaming\FryPdf\plugins\\`) -- **Linux**: `~/.config/FryPdf/plugins//` - -#### Application Local Directory: -- `/plugins//` - ---- - -## 8. Material Design 3 (M3) Expressive Styling Mandate - -To maintain visual harmony with FryPDF, all external plugins must adhere to **Google Material Design 3 Expressive**: - -### 8.1 Shape Scale Tokens -Always reference centralized M3 shape scale tokens rather than hardcoding arbitrary numbers: - -| Token Key | Radius | Usage | -| :--- | :--- | :--- | -| `{StaticResource M3ShapeCornerFull}` | `9999px` | Buttons (`m3-filled-btn`, `m3-tonal-btn`), segmented pills, search boxes (`TextBox.m3-search`), slider thumbs. | -| `{StaticResource M3ShapeCornerExtraLarge}` | `28px` | Modal dialog cards (`Border.m3-dialog-card`), hero banners. | -| `{StaticResource M3ShapeCornerLarge}` | `16px` | Content cards (`m3-card-elevated`), inputs (`TextBox.m3-outlined`), ComboBoxes. | -| `{StaticResource M3ShapeCornerMedium}` | `12px` | Context menus, flyouts, tooltips, list items. | -| `{StaticResource M3ShapeCornerSmall}` | `8px` | Badges, chips, thumbnail cards. | - -### 8.2 Dynamic Tonal Brushes (Dark & Light Mode) -**NEVER** hardcode hex colors (like `#FFFFFF` or `#1E293B`) in your views. Always reference semantic `{DynamicResource ...}` keys so your plugin seamlessly responds to daylight, warm sepia, dark night, and high contrast themes: - -- **Primary Colors**: `M3PrimaryBrush`, `M3OnPrimaryBrush`, `M3PrimaryContainerBrush`, `M3OnPrimaryContainerBrush` -- **Surfaces**: `M3SurfaceBrush`, `M3SurfaceContainerBrush`, `M3SurfaceContainerHighestBrush`, `M3SurfaceDimBrush` -- **Text & Outlines**: `M3OnSurfaceBrush`, `M3OnSurfaceVariantBrush`, `M3OutlineBrush`, `M3OutlineVariantBrush` - ---- - -## 9. Performance & Memory Safety Mandates - -FryPDF is an interactive 60+ FPS studio. External plugins must comply with the following performance rules: - -1. **Zero UI Thread Blocking**: - - **Never** perform PDF parsing, document rendering, OCR, or file I/O synchronously on the Avalonia UI thread. - - Offload heavy tasks using `await Task.Run(...)` and support cooperative cancellation (`CancellationToken`). -2. **Unmanaged Graphics Disposal**: - - If using `SkiaSharp` (`SKBitmap`, `SKImage`, `SKSurface`), always dispose old instances when replacing preview images to prevent native memory leaks. -3. **No Large Object Heap (LOH) Allocations**: - - Do not allocate large byte arrays ($\ge 85\text{ KB}$) in tight loops. Use `ArrayPool.Shared` or streams. -4. **Collectible ALC Hygiene**: - - Avoid creating static references, global event hooks, or long-lived thread-pool timers that reference your plugin types without an unsubscription mechanism. Static references will anchor your `AssemblyLoadContext` and prevent the plugin from being garbage collected when unloaded. - ---- - -## 10. Automated Testing of External Plugins - -You can verify your plugin's mounting and lifecycle in an xUnit test project using the same loader FryPDF uses: - -```csharp -using System.IO; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Plugins.Loader; -using Xunit; - -namespace FryPdf.Plugin.WatermarkPlus.Tests; - -public class PluginLifecycleTests -{ - [Fact] - public void Plugin_Loads_And_Instantiates_Successfully() - { - var dllPath = typeof(WatermarkPlusPlugin).Assembly.Location; - using var package = PluginAssemblyLoader.LoadPluginAssembly(dllPath); - - Assert.NotEmpty(package.Plugins); - var plugin = package.Plugins[0]; - Assert.Equal("com.acme.frypdf.watermarkplus", plugin.Id); - Assert.Equal("Watermark Plus", plugin.Name); - } - - [Fact] - public void PluginPackage_Unpacks_And_Validates_Manifest() - { - var tempPkg = Path.Combine(Path.GetTempPath(), "test.fryplugin"); - var stagingDir = Path.GetDirectoryName(typeof(WatermarkPlusPlugin).Assembly.Location)!; - - // Create package - FryPluginPackageLoader.CreatePackage(stagingDir, tempPkg); - Assert.True(File.Exists(tempPkg)); - - // Unpack and verify - using var result = FryPluginPackageLoader.UnpackAndLoad(tempPkg); - Assert.Equal("com.acme.frypdf.watermarkplus", result.Manifest.Id); - Assert.NotNull(result.AssemblyPackage); - - File.Delete(tempPkg); - } -} -``` - ---- - -## 11. Summary & Next Steps - -With FryPDF's modular microkernel architecture, creating professional external plugins is clean, fast, and robust: -- Implement `IFryPlugin` (or inherit `ToolPluginBase`). -- Register capabilities into `IFryPluginContext` with reversible rollback. -- Add `plugin.json` and package as `.fryplugin`. -- Drag and drop onto FryPDF to run! - -For in-depth details on internal pipeline mechanics, DAG solvers, and core architecture, see: -- [Plugin-Based Architecture Manual (`PLUGIN_BASED_ARCHITECTURE.md`)](PLUGIN_BASED_ARCHITECTURE.md) -- [Complete Tic-Tac-Toe Reference Plugin Guide (`TIC_TAC_TOE_EXAMPLE.md`)](TIC_TAC_TOE_EXAMPLE.md) (Source: [`docs/examples/TicTacToePlugin/`](examples/TicTacToePlugin)) -- [Material Design 3 Expressive Guidelines (`MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md`)](MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md) -- [PDF Deconstruction and Vector Editing (`PDF_DECONSTRUCTION_AND_EDITING.md`)](PDF_DECONSTRUCTION_AND_EDITING.md) diff --git a/docs/FEATURES.md b/docs/FEATURES.md deleted file mode 100644 index 6727022..0000000 --- a/docs/FEATURES.md +++ /dev/null @@ -1,31 +0,0 @@ -# FryPDF Feature Catalog - -A comprehensive overview of features provided by FryPDF Desktop Studio. - ---- - -## 1. Visual Page Canvas & Layout Engine -- **Infinite Zoom & Pan**: Fluid canvas zoom from 25% to 500% with fit-to-width and fit-to-page shortcuts. -- **Smart Snap-to-Grid & Guides**: Customizable 10pt/20pt/50pt grid snapping and smart alignment guidelines. -- **Smart Viewport Placement**: Automatically inserts new elements directly into the user's visible scroll region. -- **Multi-Element Alignment & Layering**: Align left/center/right, distribute evenly, bring to front, send to back. - -## 2. Rich Element Types -- **Typography & Text Blocks**: Heading presets, body text, multi-column blocks, font families, letter spacing, line height. -- **Vector Shapes**: Rectangles, rounded rects, pills, circles, callout speech bubbles, stars, arrows, brackets. -- **Interactive Chart Types (LiveCharts2 & QuestPDF)**: 17+ chart kinds including Clustered Columns, Horizontal Bars, Stacked Bars/Columns, Linear & Smooth Splines, Filled Area, Step Lines, Donut & Pie Rings with hole customization, Semi-Circle KPI Speedometer Gauges, Scatter Plots, Multi-Metric Spider/Radar, Polar Area, Candlestick OHLC financial charts, and Waterfall variance analysis with 7 curated color palettes (*Corporate*, *Emerald*, *Sunset*, *Cyber Neon*, *Slate*, *Pastel*, *Rainbow*) and 300+ DPI Skia print export. -- **Tables**: Auto-calculating rows/columns, alternating row stripes, custom border styles and header fills. -- **Dynamic Barcodes & QR Codes**: QR codes (URL, vCard, WiFi, Email presets), Code 128, EAN-13, PDF-417. -- **Ink & Freehand Drawing**: Vector pen strokes with configurable width, color, smoothing, and eraser. -- **Measurement Tool**: Real-world distance measuring with point, millimeter, centimeter, and inch unit conversions. - -## 3. Acrobat-Grade Document Tools -- **Interactive AcroForms Studio**: Text inputs, checkboxes, radio buttons, dropdowns, combo boxes, signature fields with live validation. -- **Signature Studio**: Type-to-sign with curated cursive calligraphy styles or draw custom vector signatures. -- **Legal Bates Numbering**: Sequential numbering with custom prefixes, suffixes, digit padding, and 6 positioning anchors. -- **Sanitize & Search / Redact**: Search and permanently redact sensitive terms with standard exemption codes (e.g. FOIA (b)(4)). -- **Document Comparison & Diff**: Compare two PDF projects to highlight added, removed, and modified elements. -- **Preflight & Accessibility Audit**: Scan document for low-contrast text, missing alt-text, broken links, unflattened forms. -- **Watermark & Stamp Manager**: Standard confidentiality stamps, draft watermarks, and custom dynamic date stamps. -- **Split & Extract Pages**: Burst documents into individual pages, extract page ranges, or split by interval. -- **Document Security**: AES password encryption, copy/print permissions restrictions. diff --git a/docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md b/docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md deleted file mode 100644 index b7b761b..0000000 --- a/docs/MATERIAL_DESIGN_3_EXPRESSIVE_GUIDELINES.md +++ /dev/null @@ -1,173 +0,0 @@ -# Google Material Design 3 (M3) Expressive Guidelines - -This document provides developer guidelines and design principles for building and maintaining user interfaces in **FryPDF** according to **Google Material Design 3 (M3) Expressive**. - ---- - -## 1. Core Principles of Material 3 Expressive - -1. **Expressive Geometry & Rounded Contours**: - - Material 3 Expressive moves away from sharp, boxy, or rigid rectangular UI. - - Elements feature soft, welcoming, and tactile corner radii: - - Large pill shapes for interactive triggers, search fields, and buttons. - - 28px extra-large rounded corners for modal dialogs and alert surfaces. - - 16px large corners for cards, form fields, and inspector panels. - -2. **Tonal Hierarchy & Semantic Color Roles**: - - Rather than arbitrary saturated colors, M3 Expressive employs a balanced tonal hierarchy: - - **Primary & OnPrimary**: High-emphasis focal actions. - - **PrimaryContainer & OnPrimaryContainer**: Standout highlights and contextual notices. - - **SecondaryContainer & OnSecondaryContainer**: Selected tabs, active pills, badges, chips. - - **Surface, SurfaceDim, SurfaceContainer (Lowest to Highest)**: Layered depth without harsh outlines. - - **Outline & OutlineVariant**: Subtle boundary separation. - -3. **Tactile Interaction & Motion Physics**: - - Interactive elements respond with tactile feedback: - - Chubby sliders with 8-10px tracks and 20-22px thumbs that scale slightly on hover. - - Smooth 150ms brush and transform transitions on buttons, chips, and menu items. - -4. **100% Theme Adaptability (Light & Dark)**: - - Never hardcode raw hex colors in XAML. - - Always reference `{DynamicResource ...}` keys so the UI transitions seamlessly between Light and Dark modes. - ---- - -## 2. Shape Scale Tokens Reference - -All shape tokens are defined in [`src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml`](../src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml): - -| Token Resource Key | Value | Typical Use Case | -|---|---|---| -| `M3ShapeCornerNone` | `0px` | Full-bleed splitters, root windows, edge dividers | -| `M3ShapeCornerExtraSmall` | `4px` | Micro-badges, inline code spans, keyboard shortcut hints (`kbd-badge`) | -| `M3ShapeCornerSmall` | `8px` | Badges, thumbnail cards, small chips, icon button frames | -| `M3ShapeCornerMedium` | `12px` | Context menus, flyouts, tooltips, list items, card inner sections | -| `M3ShapeCornerLarge` | `16px` | Content cards (`m3-card-elevated`), `TextBox.m3-outlined`, `ComboBox`, `NumericUpDown`, inspector section groups | -| `M3ShapeCornerExtraLarge` | `28px` | Modal dialog cards (`Border.m3-dialog-card`, `AboutDialog`, `CommandPaletteDialog`, `AiAssistantDialog`), hero promo banners | -| `M3ShapeCornerFull` | `9999px` | Tactile pill buttons, segmented button capsules (`m3-segmented-container`), global search bars (`TextBox.m3-search`), Floating Action Buttons (`m3-fab`), chubby slider thumbs | - ---- - -## 3. Color Roles Reference - -| Token Resource Key | Light Theme Role | Dark Theme Role | Description | -|---|---|---|---| -| `M3PrimaryBrush` | `#00639A` | `#92CCFF` | High-emphasis actions and accents | -| `M3OnPrimaryBrush` | `#FFFFFF` | `#003353` | Text/icons on top of Primary | -| `M3PrimaryContainerBrush` | `#CEE5FF` | `#004B76` | Prominent highlights and container fills | -| `M3OnPrimaryContainerBrush` | `#001D32` | `#CEE5FF` | Text/icons on top of PrimaryContainer | -| `M3SecondaryBrush` | `#51606F` | `#B8C8DA` | Supporting secondary actions | -| `M3SecondaryContainerBrush` | `#D5E4F7` | `#3A4857` | Active navigation tabs, selected chips, segmented items | -| `M3OnSecondaryContainerBrush` | `#0E1D2A` | `#D5E4F7` | Text/icons on top of SecondaryContainer | -| `M3SurfaceBrush` | `#F8F9FF` | `#101418` | Base canvas and screen background | -| `M3SurfaceDimBrush` | `#D8DAE0` | `#101418` | Dimmed surface contrast | -| `M3SurfaceContainerLowestBrush`| `#FFFFFF` | `#0B0F12` | Deepest recessed containers | -| `M3SurfaceContainerLowBrush` | `#F2F3FA` | `#181C20` | Subtle background panels | -| `M3SurfaceContainerBrush` | `#ECEEF4` | `#1D2024` | Default cards and panels | -| `M3SurfaceContainerHighBrush` | `#E6E8EE` | `#272A2F` | Hovered items and elevated headers | -| `M3SurfaceContainerHighestBrush`| `#E0E2E8` | `#32353A` | Search bars and segmented capsules | -| `M3OutlineBrush` | `#727780` | `#8C919A` | High-contrast component borders | -| `M3OutlineVariantBrush` | `#C2C7CF` | `#42474E` | Subtle dividers and card outlines | - -### Legacy Aliases (Fully Supported) -For compatibility with existing views, the following aliases automatically resolve to the corresponding M3 color tokens: -- `WinBgBrush` $\to$ `M3SurfaceBrush` -- `WinPanelBrush` $\to$ `M3SurfaceContainerBrush` -- `WinBorderBrush` $\to$ `M3OutlineVariantBrush` -- `WinAccentBrush` $\to$ `M3PrimaryBrush` -- `WinAccentHoverBrush` $\to$ `M3PrimaryHoverBrush` -- `WinTextBrush` $\to$ `M3OnSurfaceBrush` -- `WinMutedBrush` $\to$ `M3OnSurfaceVariantBrush` -- `WinHoverBrush` $\to$ `M3SurfaceContainerHighBrush` -- `WinActiveBrush` $\to$ `M3SecondaryContainerBrush` -- `WinInputBgBrush` $\to$ `M3SurfaceContainerLowestBrush` - ---- - -## 4. Component Styles Reference - -All pre-built M3 Expressive styles are defined in [`src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml`](../src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml): - -### Buttons -```xml - - - - - - - - -``` - -### Segmented Buttons Capsule -```xml - - - - - - - - - diff --git a/docs/examples/ScratchpadPlugin/ScratchpadView.axaml.cs b/docs/examples/ScratchpadPlugin/ScratchpadView.axaml.cs deleted file mode 100644 index bba5a82..0000000 --- a/docs/examples/ScratchpadPlugin/ScratchpadView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Plugins.Scratchpad; - -public partial class ScratchpadView : UserControl -{ - public ScratchpadView() - { - InitializeComponent(); - } -} diff --git a/docs/examples/ScratchpadPlugin/ScratchpadViewModel.cs b/docs/examples/ScratchpadPlugin/ScratchpadViewModel.cs deleted file mode 100644 index db5cd53..0000000 --- a/docs/examples/ScratchpadPlugin/ScratchpadViewModel.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; - -namespace PdfEditorApp.Plugins.Scratchpad; - -/// -/// ViewModel for the floating review scratchpad overlay. -/// Supports markdown note taking, word & character counts, and quick clipboard export. -/// -public partial class ScratchpadViewModel : ObservableObject -{ - private readonly IServiceProvider? _serviceProvider; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(WordCount))] - [NotifyPropertyChangedFor(nameof(CharacterCount))] - private string _notesText = "# PDF Review Notes\n- Checked page 1 header\n- Approved layout on page 3\n- Ready for signing"; - - [ObservableProperty] - private string _statusMessage = "Ready"; - - public int CharacterCount => NotesText?.Length ?? 0; - - public int WordCount - { - get - { - if (string.IsNullOrWhiteSpace(NotesText)) return 0; - return NotesText.Split(new[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries).Length; - } - } - - public ScratchpadViewModel(IServiceProvider? serviceProvider = null) - { - _serviceProvider = serviceProvider; - } - - [RelayCommand] - public void ClearNotes() - { - NotesText = string.Empty; - StatusMessage = "Cleared notes"; - } - - [RelayCommand] - public void AddTimestamp() - { - var stamp = $"\n\n## Note ({DateTime.Now:HH:mm:ss})\n- "; - NotesText += stamp; - StatusMessage = "Added timestamp note"; - } -} diff --git a/docs/examples/ScratchpadPlugin/plugin.json b/docs/examples/ScratchpadPlugin/plugin.json deleted file mode 100644 index 662fb1f..0000000 --- a/docs/examples/ScratchpadPlugin/plugin.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "frypdf.overlay.scratchpad", - "name": "Review Scratchpad & Notes", - "version": "1.0.0", - "author": "FryPDF Tools Team", - "description": "Floating Markdown scratchpad with live word/char counters and timestamp logs for PDF review.", - "entryPoint": "ScratchpadPlugin.dll", - "icon": "NotebookEditOutline", - "dependencies": [], - "settingsSchema": { - "AutoClearOnClose": { - "type": "boolean", - "label": "Auto-Clear Notes on Close", - "description": "Automatically clear notes text when closing the scratchpad overlay.", - "default": false - } - } -} diff --git a/docs/examples/SnakePlugin/README.md b/docs/examples/SnakePlugin/README.md deleted file mode 100644 index 4be2f88..0000000 --- a/docs/examples/SnakePlugin/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Retro Arcade Snake Game Plugin (`frypdf.overlay.snake`) - -An interactive, non-modal floating arcade game plugin for **FryPDF** demonstrating the Cordis-inspired `shell.overlay` slot architecture. - ---- - -## Features -- **Non-Modal Floating Overlay**: Runs smoothly on top of any document without interrupting editing or reading. -- **60+ FPS Rendering**: Avalonia `DrawingContext` direct visual drawing with zero Large Object Heap allocations. -- **On-Screen D-Pad & Keyboard Controls**: Use arrow keys, WASD, or touch/click the on-screen tactile D-Pad. -- **Configurable Settings**: Game Speed (Normal, Fast, Zen) and Wall Collision toggle. -- **Deep Shell Integration**: Contributes to Command Palette (`Ctrl+Alt+S`), Status Bar (`🐍 Snake`), and Ribbon View Tab. - ---- - -## Building the Plugin - -To compile the plugin and generate the `.fryplugin` release archive: - -```bash -dotnet build SnakePlugin.csproj -c Release -``` - -The output package `Snake.fryplugin` will be created in `bin/Release/net10.0/Snake.fryplugin`. - ---- - -## Installing into FryPDF - -1. Open **FryPDF**. -2. Go to **Plugins & Extensions Studio** (via left sidebar, View tab, or Ctrl+K). -3. Search for **"Snake"** in the **Store** tab and click **Install**. - - Or click **"Install from File..."** in the top-right corner and select `Snake.fryplugin`. diff --git a/docs/examples/SnakePlugin/SnakeCanvasControl.cs b/docs/examples/SnakePlugin/SnakeCanvasControl.cs deleted file mode 100644 index 2f74b73..0000000 --- a/docs/examples/SnakePlugin/SnakeCanvasControl.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Media; - -namespace PdfEditorApp.Plugins.Snake; - -/// -/// High-performance Avalonia rendering surface for the Snake game. -/// Renders dark retro-arcade grid, animated snake segments, glowing food, and bonus stars via DrawingContext with zero memory allocations per frame. -/// -public class SnakeCanvasControl : Control -{ - private static readonly IBrush BackgroundBrush = new SolidColorBrush(Color.Parse("#0D1117")); - private static readonly IPen GridLinePen = new Pen(new SolidColorBrush(Color.Parse("#161B22")), 0.75); - private static readonly IBrush SnakeHeadBrush = new SolidColorBrush(Color.Parse("#34D399")); - private static readonly IBrush SnakeBodyBrush = new SolidColorBrush(Color.Parse("#22C55E")); - private static readonly IBrush EyeWhiteBrush = Brushes.White; - private static readonly IBrush EyePupilBrush = new SolidColorBrush(Color.Parse("#064E3B")); - private static readonly IBrush FoodBrush = new SolidColorBrush(Color.Parse("#EF4444")); - private static readonly IBrush FoodGlowBrush = new SolidColorBrush(Color.Parse("#40EF4444")); - private static readonly IBrush BonusFoodBrush = new SolidColorBrush(Color.Parse("#F59E0B")); - private static readonly IBrush BonusFoodGlowBrush = new SolidColorBrush(Color.Parse("#50F59E0B")); - - private SnakeGameViewModel? _viewModel; - - public SnakeCanvasControl() - { - ClipToBounds = true; - } - - protected override void OnDataContextChanged(EventArgs e) - { - base.OnDataContextChanged(e); - - if (_viewModel != null) - { - _viewModel.RenderRequested -= OnRenderRequested; - } - - _viewModel = DataContext as SnakeGameViewModel; - - if (_viewModel != null) - { - _viewModel.RenderRequested += OnRenderRequested; - } - - InvalidateVisual(); - } - - private void OnRenderRequested() - { - InvalidateVisual(); - } - - public override void Render(DrawingContext context) - { - base.Render(context); - - var bounds = Bounds; - if (bounds.Width <= 0 || bounds.Height <= 0) return; - - // 1. Draw Dark Background - context.FillRectangle(BackgroundBrush, bounds); - - double cellWidth = bounds.Width / SnakeGameViewModel.GridWidth; - double cellHeight = bounds.Height / SnakeGameViewModel.GridHeight; - - // 2. Draw Subtle Grid Lines - for (int x = 0; x <= SnakeGameViewModel.GridWidth; x++) - { - double lineX = x * cellWidth; - context.DrawLine(GridLinePen, new Point(lineX, 0), new Point(lineX, bounds.Height)); - } - - for (int y = 0; y <= SnakeGameViewModel.GridHeight; y++) - { - double lineY = y * cellHeight; - context.DrawLine(GridLinePen, new Point(0, lineY), new Point(bounds.Width, lineY)); - } - - if (_viewModel == null) return; - - // 3. Draw Bonus Food (Golden Orb) - if (_viewModel.BonusFood.HasValue) - { - var bonusPt = _viewModel.BonusFood.Value; - double bx = bonusPt.X * cellWidth + cellWidth / 2.0; - double by = bonusPt.Y * cellHeight + cellHeight / 2.0; - double bonusRadius = Math.Min(cellWidth, cellHeight) * 0.44; - - // Glow halo - context.DrawEllipse(BonusFoodGlowBrush, null, new Point(bx, by), bonusRadius * 1.5, bonusRadius * 1.5); - // Core - context.DrawEllipse(BonusFoodBrush, null, new Point(bx, by), bonusRadius, bonusRadius); - } - - // 4. Draw Regular Food (Red Glowing Orb) - var food = _viewModel.Food; - double fx = food.X * cellWidth + cellWidth / 2.0; - double fy = food.Y * cellHeight + cellHeight / 2.0; - double foodRadius = Math.Min(cellWidth, cellHeight) * 0.38; - - // Food glow - context.DrawEllipse(FoodGlowBrush, null, new Point(fx, fy), foodRadius * 1.45, foodRadius * 1.45); - // Food body - context.DrawEllipse(FoodBrush, null, new Point(fx, fy), foodRadius, foodRadius); - - // 5. Draw Snake Segments - bool isHead = true; - foreach (var segment in _viewModel.SnakeBody) - { - double segX = segment.X * cellWidth + 1.0; - double segY = segment.Y * cellHeight + 1.0; - double segW = Math.Max(1.0, cellWidth - 2.0); - double segH = Math.Max(1.0, cellHeight - 2.0); - - var segRect = new Rect(segX, segY, segW, segH); - - if (isHead) - { - // Head segment with rounded pill corners - context.DrawRectangle(SnakeHeadBrush, null, new RoundedRect(segRect, 5.0)); - - // Cute eyes - double eyeSize = Math.Max(2.5, Math.Min(cellWidth, cellHeight) * 0.22); - double pupilSize = eyeSize * 0.55; - - // Eye offsets - double eyeOffset = cellWidth * 0.24; - double eyeCenterX = segX + segW / 2.0; - double eyeCenterY = segY + segH / 2.0; - - Point leftEye = new(eyeCenterX - eyeOffset, eyeCenterY - eyeOffset); - Point rightEye = new(eyeCenterX + eyeOffset, eyeCenterY - eyeOffset); - - context.DrawEllipse(EyeWhiteBrush, null, leftEye, eyeSize, eyeSize); - context.DrawEllipse(EyePupilBrush, null, leftEye, pupilSize, pupilSize); - - context.DrawEllipse(EyeWhiteBrush, null, rightEye, eyeSize, eyeSize); - context.DrawEllipse(EyePupilBrush, null, rightEye, pupilSize, pupilSize); - - isHead = false; - } - else - { - // Body segment - context.DrawRectangle(SnakeBodyBrush, null, new RoundedRect(segRect, 3.5)); - } - } - } -} diff --git a/docs/examples/SnakePlugin/SnakeGamePlugin.cs b/docs/examples/SnakePlugin/SnakeGamePlugin.cs deleted file mode 100644 index 68ed3be..0000000 --- a/docs/examples/SnakePlugin/SnakeGamePlugin.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Manifests; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Plugins.Snake; - -/// -/// Modular plugin contributing a playable, draggable Snake game targeting the 'shell.overlay' slot. -/// Inspired by the Cordis client-side dynamic plugin architecture. -/// -public class SnakeGamePlugin : IFryPlugin -{ - public string Id => "frypdf.overlay.snake"; - public string Name => "Playable Snake Game"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public IReadOnlyDictionary? SettingsSchema => new Dictionary - { - ["InitialSpeed"] = new() - { - Label = "Game Speed", - Description = "Default tick speed for snake movement", - Type = "select", - Options = new() { "Normal", "Fast", "Zen" }, - DefaultValue = "Normal" - }, - ["WallCollision"] = new() - { - Label = "Wall Collision Mode", - Description = "When enabled, hitting the border causes game over. When disabled, the snake wraps around edges.", - Type = "boolean", - DefaultValue = true - } - }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - // 1. Register Shell Overlay Descriptor - ctx.RegisterOverlay(new OverlayDescriptor - { - Id = Id, - Title = "🐍 Snake", - Slot = "shell.overlay", - DefaultWidth = 320, - DefaultHeight = 420, - InitialX = null, - InitialY = 100, - IsDraggable = true, - IsMinimizable = true, - IsClosable = true, - IconKind = "GamepadVariantOutline", - ChromeMode = OverlayChromeMode.CustomChrome, - ViewType = typeof(SnakeGameView), - ViewModelType = typeof(SnakeGameViewModel), - ViewFactory = sp => new SnakeGameView - { - DataContext = new SnakeGameViewModel(sp) - }, - ViewModelFactory = sp => new SnakeGameViewModel(sp) - }); - - // 2. Register Command Palette action - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.overlay.snake", - Title = "Play Snake Game (Shell Overlay)", - Subtitle = "Launch draggable retro-arcade Snake game floating over the application", - Category = "Shell Overlays", - IconKind = "GamepadVariantOutline", - Shortcut = "Ctrl+Alt+S", - Order = 95, - Action = sp => - { - if (sp.GetService(typeof(IOverlayRegistry)) is IOverlayRegistry reg) - { - reg.ToggleOverlay(Id); - } - } - }); - - // 3. Register Footer Status Bar Widget (Clickable 🐍 Snake pill) - ctx.RegisterStatusBarWidget(new StatusBarWidgetDescriptor - { - WidgetId = "frypdf.status.snake", - Alignment = StatusBarAlignment.Right, - Order = 15, - ToolTip = "Play Snake Game (Floating Shell Overlay)", - Factory = sp => - { - var reg = sp.GetService(typeof(IOverlayRegistry)) as IOverlayRegistry; - return new StatusBarWidgetViewModel - { - WidgetId = "frypdf.status.snake", - Label = "🐍 Snake", - IconKind = "GamepadVariantOutline", - ToolTip = "Launch floating Snake game overlay", - IsActive = true, - Command = new RelayCommand(() => - { - reg?.ToggleOverlay(Id); - }) - }; - } - }); - - // 4. Register Ribbon Action in View Tab - ctx.RegisterRibbonAction(new RibbonActionDescriptor - { - Id = "frypdf.ribbon.action.snake", - TabId = "view", - GroupId = "plugins", - Label = "Snake Game", - Tooltip = "Launch floating retro Snake arcade in shell.overlay", - IconKind = "GamepadVariantOutline", - Order = 50, - Action = sp => - { - if (sp.GetService(typeof(IOverlayRegistry)) is IOverlayRegistry reg) - { - reg.ToggleOverlay(Id); - } - } - }); - - // Reversible effect: Ensure overlay is hidden if plugin is unloaded - ctx.RegisterEffect(() => - { - if (ctx.TryGetService(out var reg)) - { - reg.HideOverlay(Id); - } - }); - - return Task.CompletedTask; - } -} diff --git a/docs/examples/SnakePlugin/SnakeGameView.axaml b/docs/examples/SnakePlugin/SnakeGameView.axaml deleted file mode 100644 index 61c940d..0000000 --- a/docs/examples/SnakePlugin/SnakeGameView.axaml +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/examples/SnakePlugin/SnakeGameView.axaml.cs b/docs/examples/SnakePlugin/SnakeGameView.axaml.cs deleted file mode 100644 index 0088c6e..0000000 --- a/docs/examples/SnakePlugin/SnakeGameView.axaml.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.VisualTree; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Plugins.Snake; - -public partial class SnakeGameView : UserControl -{ - private Point _dragStartPoint; - private bool _isDragging; - private bool _isLocallyMinimized; - - public SnakeGameView() - { - InitializeComponent(); - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - Focus(); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - - if (DataContext is not SnakeGameViewModel vm) return; - - switch (e.Key) - { - case Key.Up: - case Key.W: - vm.ChangeDirection(SnakeDirection.Up); - e.Handled = true; - break; - case Key.Down: - case Key.S: - vm.ChangeDirection(SnakeDirection.Down); - e.Handled = true; - break; - case Key.Left: - case Key.A: - vm.ChangeDirection(SnakeDirection.Left); - e.Handled = true; - break; - case Key.Right: - case Key.D: - vm.ChangeDirection(SnakeDirection.Right); - e.Handled = true; - break; - case Key.Space: - vm.TogglePlayPause(); - e.Handled = true; - break; - case Key.R: - vm.ResetGame(); - vm.StartGame(); - e.Handled = true; - break; - } - } - - private void OnHeaderPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - { - _isDragging = true; - _dragStartPoint = e.GetPosition(this.VisualRoot as Visual ?? this); - e.Pointer.Capture(sender as Control); - Focus(); - e.Handled = true; - } - } - - private void OnHeaderPointerMoved(object? sender, PointerEventArgs e) - { - if (_isDragging) - { - var root = this.VisualRoot as Visual ?? this; - var currentPoint = e.GetPosition(root); - var deltaX = currentPoint.X - _dragStartPoint.X; - var deltaY = currentPoint.Y - _dragStartPoint.Y; - _dragStartPoint = currentPoint; - - var overlayVm = FindOverlayViewModel(); - if (overlayVm != null) - { - overlayVm.X = Math.Max(0, overlayVm.X + deltaX); - overlayVm.Y = Math.Max(0, overlayVm.Y + deltaY); - } - e.Handled = true; - } - } - - private void OnHeaderPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (_isDragging) - { - _isDragging = false; - e.Pointer.Capture(null); - e.Handled = true; - } - } - - private void OnMinimizeClick(object? sender, RoutedEventArgs e) - { - _isLocallyMinimized = !_isLocallyMinimized; - - if (CanvasContainer != null) - { - CanvasContainer.IsVisible = !_isLocallyMinimized; - } - - if (FooterContainer != null) - { - FooterContainer.IsVisible = !_isLocallyMinimized; - } - - var overlayVm = FindOverlayViewModel(); - if (overlayVm != null) - { - overlayVm.IsMinimized = _isLocallyMinimized; - } - } - - private void OnCloseClick(object? sender, RoutedEventArgs e) - { - if (DataContext is SnakeGameViewModel vm) - { - vm.PauseGame(); - } - - var overlayVm = FindOverlayViewModel(); - if (overlayVm != null) - { - overlayVm.Close(); - } - } - - private OverlayInstanceViewModel? FindOverlayViewModel() - { - // First check parent control DataContext - Visual? current = this; - while (current != null) - { - if (current is Control ctrl && ctrl.DataContext is OverlayInstanceViewModel ovm) - { - return ovm; - } - current = current.GetVisualParent(); - } - - return null; - } -} diff --git a/docs/examples/SnakePlugin/SnakeGameViewModel.cs b/docs/examples/SnakePlugin/SnakeGameViewModel.cs deleted file mode 100644 index 06408fe..0000000 --- a/docs/examples/SnakePlugin/SnakeGameViewModel.cs +++ /dev/null @@ -1,368 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins.Settings; - -namespace PdfEditorApp.Plugins.Snake; - -public enum SnakeDirection -{ - None, - Up, - Down, - Left, - Right -} - -public enum SnakeGameState -{ - Ready, - Playing, - Paused, - GameOver -} - -public readonly record struct GridPoint(int X, int Y); - -/// -/// MVVM game loop and state management for the floating Snake Game plugin. -/// Designed for 60+ FPS zero-allocation tick processing, dynamic speed acceleration, and persistent high scores. -/// -public partial class SnakeGameViewModel : ObservableObject, IDisposable -{ - private readonly DispatcherTimer _gameTimer; - private readonly Random _random = new(); - private readonly IPluginSettingsStore? _settingsStore; - - public const int GridWidth = 20; - public const int GridHeight = 20; - - [ObservableProperty] - private int _score; - - [ObservableProperty] - private int _highScore; - - [ObservableProperty] - private SnakeGameState _state = SnakeGameState.Ready; - - [ObservableProperty] - private bool _wallCollision = true; - - [ObservableProperty] - private int _speedMs = 120; - - [ObservableProperty] - private string _statusText = "Press Space or Arrows to Start"; - - [ObservableProperty] - private string _actionButtonLabel = "Start"; - - private SnakeDirection _currentDirection = SnakeDirection.Right; - private SnakeDirection _pendingDirection = SnakeDirection.Right; - - public LinkedList SnakeBody { get; } = new(); - public GridPoint Food { get; private set; } - public GridPoint? BonusFood { get; private set; } - public int BonusFoodTicksRemaining { get; private set; } - - public event Action? RenderRequested; - - public SnakeGameViewModel(IServiceProvider? serviceProvider = null) - { - _settingsStore = serviceProvider?.GetService(); - LoadSettingsAndHighScore(); - - _gameTimer = new DispatcherTimer - { - Interval = TimeSpan.FromMilliseconds(SpeedMs) - }; - _gameTimer.Tick += OnGameTick; - - ResetGame(); - } - - private void LoadSettingsAndHighScore() - { - if (_settingsStore != null) - { - HighScore = (int)_settingsStore.GetSetting("frypdf.overlay.snake", "HighScore", 0.0); - WallCollision = _settingsStore.GetSetting("frypdf.overlay.snake", "WallCollision", true); - var speedPref = _settingsStore.GetSetting("frypdf.overlay.snake", "InitialSpeed", "Normal"); - SpeedMs = speedPref switch - { - "Fast" => 90, - "Zen" => 160, - _ => 120 - }; - } - } - - public void ResetGame() - { - _gameTimer.Stop(); - SnakeBody.Clear(); - - // Initial 3-segment snake in middle - int startX = GridWidth / 2; - int startY = GridHeight / 2; - SnakeBody.AddFirst(new GridPoint(startX, startY)); - SnakeBody.AddLast(new GridPoint(startX - 1, startY)); - SnakeBody.AddLast(new GridPoint(startX - 2, startY)); - - _currentDirection = SnakeDirection.Right; - _pendingDirection = SnakeDirection.Right; - Score = 0; - BonusFood = null; - BonusFoodTicksRemaining = 0; - State = SnakeGameState.Ready; - StatusText = "Press Space or Arrows to Play"; - ActionButtonLabel = "Start"; - - SpawnFood(); - RenderRequested?.Invoke(); - } - - [RelayCommand] - public void TogglePlayPause() - { - switch (State) - { - case SnakeGameState.Ready: - StartGame(); - break; - case SnakeGameState.Playing: - PauseGame(); - break; - case SnakeGameState.Paused: - ResumeGame(); - break; - case SnakeGameState.GameOver: - ResetGame(); - StartGame(); - break; - } - } - - public void StartGame() - { - State = SnakeGameState.Playing; - StatusText = "Playing"; - ActionButtonLabel = "Pause"; - _gameTimer.Interval = TimeSpan.FromMilliseconds(SpeedMs); - _gameTimer.Start(); - RenderRequested?.Invoke(); - } - - public void PauseGame() - { - if (State != SnakeGameState.Playing) return; - _gameTimer.Stop(); - State = SnakeGameState.Paused; - StatusText = "Game Paused"; - ActionButtonLabel = "Resume"; - RenderRequested?.Invoke(); - } - - public void ResumeGame() - { - if (State != SnakeGameState.Paused) return; - State = SnakeGameState.Playing; - StatusText = "Playing"; - ActionButtonLabel = "Pause"; - _gameTimer.Start(); - RenderRequested?.Invoke(); - } - - public void ChangeDirection(SnakeDirection newDir) - { - if (newDir == SnakeDirection.None) return; - - if (State == SnakeGameState.Ready || State == SnakeGameState.Paused) - { - StartGame(); - } - - // Prevent immediate 180-degree reverse - bool isOpposite = (_currentDirection == SnakeDirection.Up && newDir == SnakeDirection.Down) || - (_currentDirection == SnakeDirection.Down && newDir == SnakeDirection.Up) || - (_currentDirection == SnakeDirection.Left && newDir == SnakeDirection.Right) || - (_currentDirection == SnakeDirection.Right && newDir == SnakeDirection.Left); - - if (!isOpposite) - { - _pendingDirection = newDir; - } - } - - private void OnGameTick(object? sender, EventArgs e) - { - if (State != SnakeGameState.Playing) return; - - _currentDirection = _pendingDirection; - var head = SnakeBody.First!.Value; - int nextX = head.X; - int nextY = head.Y; - - switch (_currentDirection) - { - case SnakeDirection.Up: nextY--; break; - case SnakeDirection.Down: nextY++; break; - case SnakeDirection.Left: nextX--; break; - case SnakeDirection.Right: nextX++; break; - } - - // 1. Check Wall Collision / Wrap-around - if (WallCollision) - { - if (nextX < 0 || nextX >= GridWidth || nextY < 0 || nextY >= GridHeight) - { - TriggerGameOver("Crashed into wall!"); - return; - } - } - else - { - nextX = (nextX + GridWidth) % GridWidth; - nextY = (nextY + GridHeight) % GridHeight; - } - - var newHead = new GridPoint(nextX, nextY); - - // 2. Check Self-Collision - // Ignore the tail segment since it will move forward if not eating - var currentTail = SnakeBody.Last!.Value; - bool isEating = (newHead == Food) || (BonusFood.HasValue && newHead == BonusFood.Value); - - foreach (var segment in SnakeBody) - { - if (!isEating && segment == currentTail) continue; - if (segment == newHead) - { - TriggerGameOver("Ran into yourself!"); - return; - } - } - - // 3. Move Snake - SnakeBody.AddFirst(newHead); - - if (newHead == Food) - { - Score += 10; - UpdateHighScore(); - SpawnFood(); - - // Dynamic speed up: decrease interval slightly as snake gets longer - var newIntervalMs = Math.Max(55, SpeedMs - (Score / 30) * 4); - _gameTimer.Interval = TimeSpan.FromMilliseconds(newIntervalMs); - - // Chance to spawn golden bonus food every 50 points - if (Score % 50 == 0 && !BonusFood.HasValue) - { - SpawnBonusFood(); - } - } - else if (BonusFood.HasValue && newHead == BonusFood.Value) - { - Score += 50; - UpdateHighScore(); - BonusFood = null; - BonusFoodTicksRemaining = 0; - } - else - { - // Remove tail if didn't eat - SnakeBody.RemoveLast(); - } - - // Decrement bonus food countdown - if (BonusFood.HasValue) - { - BonusFoodTicksRemaining--; - if (BonusFoodTicksRemaining <= 0) - { - BonusFood = null; - } - } - - RenderRequested?.Invoke(); - } - - private void TriggerGameOver(string reason) - { - _gameTimer.Stop(); - State = SnakeGameState.GameOver; - StatusText = $"Game Over! {reason}"; - ActionButtonLabel = "Play Again"; - UpdateHighScore(); - RenderRequested?.Invoke(); - } - - private void UpdateHighScore() - { - if (Score > HighScore) - { - HighScore = Score; - _settingsStore?.SetSetting("frypdf.overlay.snake", "HighScore", (double)HighScore); - _settingsStore?.Save(); - } - } - - private void SpawnFood() - { - var occupied = new HashSet(SnakeBody); - if (BonusFood.HasValue) occupied.Add(BonusFood.Value); - - var available = new List(); - for (int x = 0; x < GridWidth; x++) - { - for (int y = 0; y < GridHeight; y++) - { - var pt = new GridPoint(x, y); - if (!occupied.Contains(pt)) - { - available.Add(pt); - } - } - } - - if (available.Count > 0) - { - Food = available[_random.Next(available.Count)]; - } - } - - private void SpawnBonusFood() - { - var occupied = new HashSet(SnakeBody) { Food }; - var available = new List(); - for (int x = 0; x < GridWidth; x++) - { - for (int y = 0; y < GridHeight; y++) - { - var pt = new GridPoint(x, y); - if (!occupied.Contains(pt)) - { - available.Add(pt); - } - } - } - - if (available.Count > 0) - { - BonusFood = available[_random.Next(available.Count)]; - BonusFoodTicksRemaining = 45; // ~5 seconds - } - } - - public void Dispose() - { - _gameTimer.Stop(); - _gameTimer.Tick -= OnGameTick; - } -} diff --git a/docs/examples/SnakePlugin/SnakePlugin.csproj b/docs/examples/SnakePlugin/SnakePlugin.csproj deleted file mode 100644 index 778c614..0000000 --- a/docs/examples/SnakePlugin/SnakePlugin.csproj +++ /dev/null @@ -1,82 +0,0 @@ - - - net10.0 - enable - enable - 13 - true - SnakePlugin - PdfEditorApp.Plugins.Snake - 1.0.0 - FryPDF Team - Playable Retro Arcade Snake game shell overlay plugin for FryPDF - - - - - - - - false - runtime - - - false - runtime - - - - - - $(FRYPDF_HOME) - /Applications/FryPDF.app/Contents/MacOS - C:\Program Files\FryPDF - /opt/FryPDF - - - - - $(FryPdfInstallDir)\PdfEditorApp.Core.dll - false - - - $(FryPdfInstallDir)\PdfEditorApp.dll - false - - - - - - - - - - - - - - - - - - - $(TargetDir)staging\ - $(TargetDir)Snake.fryplugin - - - - - - - - - - - - - - - - - - diff --git a/docs/examples/SnakePlugin/plugin.json b/docs/examples/SnakePlugin/plugin.json deleted file mode 100644 index 4166338..0000000 --- a/docs/examples/SnakePlugin/plugin.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "frypdf.overlay.snake", - "name": "Retro Arcade Snake Game", - "version": "1.0.0", - "author": "FryPDF Team", - "description": "Playable, draggable retro-arcade Snake game floating over the application canvas in the 'shell.overlay' slot.", - "entryPoint": "SnakePlugin.dll", - "icon": "GamepadVariantOutline", - "dependencies": [], - "settingsSchema": { - "InitialSpeed": { - "type": "select", - "label": "Game Speed", - "description": "Default tick speed for snake movement", - "default": "Normal", - "options": ["Normal", "Fast", "Zen"] - }, - "WallCollision": { - "type": "boolean", - "label": "Wall Collision Mode", - "description": "When enabled, hitting the border causes game over. When disabled, the snake wraps around edges.", - "default": true - } - } -} diff --git a/docs/examples/TelemetryPlugin/DocumentTelemetryPlugin.cs b/docs/examples/TelemetryPlugin/DocumentTelemetryPlugin.cs deleted file mode 100644 index 2358ca8..0000000 --- a/docs/examples/TelemetryPlugin/DocumentTelemetryPlugin.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Manifests; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Plugins.Telemetry; - -/// -/// Plugin contributing a live document diagnostics & memory telemetry HUD targeting 'shell.overlay'. -/// Uses OverlayChromeMode.StandardCard for automatic Material Design 3 frame and dragging physics. -/// -public class DocumentTelemetryPlugin : IFryPlugin -{ - public string Id => "frypdf.overlay.telemetry"; - public string Name => "Document Telemetry HUD"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public IReadOnlyDictionary? SettingsSchema => null; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - // 1. Register Overlay with StandardCard chrome - ctx.RegisterOverlay(new OverlayDescriptor - { - Id = Id, - Title = "⚡ Telemetry HUD", - Slot = "shell.overlay", - DefaultWidth = 320, - DefaultHeight = 250, - InitialX = null, - InitialY = 160, - IsDraggable = true, - IsMinimizable = true, - IsClosable = true, - IconKind = "ChartTimelineVariant", - ChromeMode = OverlayChromeMode.StandardCard, - ViewType = typeof(DocumentTelemetryView), - ViewModelType = typeof(DocumentTelemetryViewModel), - ViewFactory = sp => new DocumentTelemetryView - { - DataContext = new DocumentTelemetryViewModel(sp) - }, - ViewModelFactory = sp => new DocumentTelemetryViewModel(sp) - }); - - // 2. Register Command Palette - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.overlay.telemetry", - Title = "Toggle Telemetry HUD (Shell Overlay)", - Subtitle = "Display real-time memory and document engine diagnostics", - Category = "Shell Overlays", - IconKind = "ChartTimelineVariant", - Shortcut = "Ctrl+Alt+T", - Order = 92, - Action = sp => - { - if (sp.GetService(typeof(IOverlayRegistry)) is IOverlayRegistry reg) - { - reg.ToggleOverlay(Id); - } - } - }); - - // 3. Register Footer Status Bar Widget - ctx.RegisterStatusBarWidget(new StatusBarWidgetDescriptor - { - WidgetId = "frypdf.status.telemetry", - Alignment = StatusBarAlignment.Right, - Order = 13, - ToolTip = "Toggle Document Telemetry HUD", - Factory = sp => - { - var reg = sp.GetService(typeof(IOverlayRegistry)) as IOverlayRegistry; - return new StatusBarWidgetViewModel - { - WidgetId = "frypdf.status.telemetry", - Label = "⚡ HUD", - IconKind = "ChartTimelineVariant", - ToolTip = "Toggle real-time engine telemetry", - IsActive = true, - Command = new RelayCommand(() => reg?.ToggleOverlay(Id)) - }; - } - }); - - // 4. Register Ribbon Action in View Tab - ctx.RegisterRibbonAction(new RibbonActionDescriptor - { - Id = "frypdf.ribbon.action.telemetry", - TabId = "view", - GroupId = "plugins", - Label = "Telemetry HUD", - Tooltip = "Open real-time diagnostics HUD in shell.overlay", - IconKind = "ChartTimelineVariant", - Order = 54, - Action = sp => - { - if (sp.GetService(typeof(IOverlayRegistry)) is IOverlayRegistry reg) - { - reg.ToggleOverlay(Id); - } - } - }); - - // Clean rollback on unload - ctx.RegisterEffect(() => - { - if (ctx.TryGetService(out var reg)) - { - reg.HideOverlay(Id); - } - }); - - return Task.CompletedTask; - } -} diff --git a/docs/examples/TelemetryPlugin/DocumentTelemetryView.axaml b/docs/examples/TelemetryPlugin/DocumentTelemetryView.axaml deleted file mode 100644 index 939dfd2..0000000 --- a/docs/examples/TelemetryPlugin/DocumentTelemetryView.axaml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/examples/TelemetryPlugin/DocumentTelemetryView.axaml.cs b/docs/examples/TelemetryPlugin/DocumentTelemetryView.axaml.cs deleted file mode 100644 index d89e6b4..0000000 --- a/docs/examples/TelemetryPlugin/DocumentTelemetryView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Plugins.Telemetry; - -public partial class DocumentTelemetryView : UserControl -{ - public DocumentTelemetryView() - { - InitializeComponent(); - } -} diff --git a/docs/examples/TelemetryPlugin/DocumentTelemetryViewModel.cs b/docs/examples/TelemetryPlugin/DocumentTelemetryViewModel.cs deleted file mode 100644 index 3296679..0000000 --- a/docs/examples/TelemetryPlugin/DocumentTelemetryViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; - -namespace PdfEditorApp.Plugins.Telemetry; - -/// -/// ViewModel for real-time document performance and environment telemetry HUD. -/// -public partial class DocumentTelemetryViewModel : ObservableObject -{ - private readonly IServiceProvider? _serviceProvider; - - [ObservableProperty] - private string _memoryAllocatedMb = "0.0 MB"; - - [ObservableProperty] - private int _garbageCollections; - - [ObservableProperty] - private string _architecture = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString(); - - [ObservableProperty] - private string _osDescription = System.Runtime.InteropServices.RuntimeInformation.OSDescription; - - [ObservableProperty] - private string _status = "Optimal (60+ FPS)"; - - public DocumentTelemetryViewModel(IServiceProvider? serviceProvider = null) - { - _serviceProvider = serviceProvider; - RefreshMetrics(); - } - - [RelayCommand] - public void RefreshMetrics() - { - long bytes = GC.GetTotalMemory(forceFullCollection: false); - MemoryAllocatedMb = $"{(bytes / 1024.0 / 1024.0):F1} MB"; - GarbageCollections = GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2); - } - - [RelayCommand] - public void RunGCTrim() - { - GC.Collect(2, GCCollectionMode.Aggressive, blocking: true, compacting: true); - RefreshMetrics(); - } -} diff --git a/docs/examples/TelemetryPlugin/README.md b/docs/examples/TelemetryPlugin/README.md deleted file mode 100644 index f9187ce..0000000 --- a/docs/examples/TelemetryPlugin/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Document Telemetry HUD Plugin (`frypdf.overlay.telemetry`) - -Live performance telemetry HUD showing managed heap allocation, GC cycles, and memory trimming in the `shell.overlay` slot. - -## Features -- **Managed Heap Monitoring**: Real-time memory allocation in MB. -- **Garbage Collection Counter**: Tracks GC gen0, gen1, gen2 cycles. -- **One-Click Heap Trimming**: Trigger memory compaction and collection. -- **Hardware Architecture**: Displays OS and processor information. - -## Building and Packaging -To build and create the `.fryplugin` distribution archive: -```bash -dotnet build -c Release -``` -This produces `bin/Release/net10.0/Telemetry.fryplugin`. diff --git a/docs/examples/TelemetryPlugin/TelemetryPlugin.csproj b/docs/examples/TelemetryPlugin/TelemetryPlugin.csproj deleted file mode 100644 index 1660053..0000000 --- a/docs/examples/TelemetryPlugin/TelemetryPlugin.csproj +++ /dev/null @@ -1,82 +0,0 @@ - - - net10.0 - enable - enable - 13 - true - TelemetryPlugin - PdfEditorApp.Plugins.Telemetry - 1.0.0 - FryPDF Core Engineering - Document Telemetry HUD shell overlay plugin for FryPDF - - - - - - - - false - runtime - - - false - runtime - - - - - - $(FRYPDF_HOME) - /Applications/FryPDF.app/Contents/MacOS - C:\Program Files\FryPDF - /opt/FryPDF - - - - - $(FryPdfInstallDir)\PdfEditorApp.Core.dll - false - - - $(FryPdfInstallDir)\PdfEditorApp.dll - false - - - - - - - - - - - - - - - - - - - $(TargetDir)staging\ - $(TargetDir)Telemetry.fryplugin - - - - - - - - - - - - - - - - - - diff --git a/docs/examples/TelemetryPlugin/plugin.json b/docs/examples/TelemetryPlugin/plugin.json deleted file mode 100644 index b6734af..0000000 --- a/docs/examples/TelemetryPlugin/plugin.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "frypdf.overlay.telemetry", - "name": "Document Telemetry HUD", - "version": "1.0.0", - "author": "FryPDF Core Engineering", - "description": "Live performance telemetry HUD showing managed heap allocation, GC cycles, and memory trimming.", - "entryPoint": "TelemetryPlugin.dll", - "icon": "ChartTimelineVariant", - "dependencies": [] -} diff --git a/docs/examples/TicTacToePlugin/README.md b/docs/examples/TicTacToePlugin/README.md deleted file mode 100644 index 7db4d0a..0000000 --- a/docs/examples/TicTacToePlugin/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# FryPDF Tic-Tac-Toe Plugin Example - -A complete, self-contained external plugin example for **FryPDF** implementing a playable, draggable **Tic-Tac-Toe** mini-game floating overlay. - ---- - -## Features - -- 🎮 **Floating Draggable Shell Overlay**: Mounted to `shell.overlay` with standard Material Design 3 chrome (pin, minimize, close, draggable header). -- 🤖 **Two Game Modes**: Single-Player vs Smart AI or pass-and-play Local 2-Player mode. -- 🏆 **Live Score Tracking**: Persistent counters for Player X wins, Player O wins, and Draws. -- 🎨 **Material Design 3 (M3) Expressive Styling**: Adheres to FryPDF's token system (`M3ShapeCornerFull`, `M3ShapeCornerLarge`, dynamic tonal brushes). -- ⌨️ **Command Palette Integration**: Trigger instantly via `Ctrl+Alt+T` / `⌘Alt+T`. -- 🕹️ **Footer Status Bar Widget**: Quick-toggle pill button (`🎮 Tic-Tac-Toe`) in the footer status bar. -- 🎗️ **Ribbon Action**: Quick launcher in the Ribbon's **View** tab. -- ⚙️ **Declarative Settings**: Auto-generated M3 settings schema for initial mode, AI difficulty, and sound effects. - ---- - -## Project Structure - -``` -TicTacToePlugin/ -├── TicTacToePlugin.csproj # .NET 10 project file with automated .fryplugin packager -├── plugin.json # Manifest with metadata, icon, and settings schema -├── TicTacToePlugin.cs # IFryPlugin implementation and capability registrations -├── TicTacToeViewModel.cs # Reactive MVVM game state, win detection & AI logic -├── TicTacToeView.axaml # Avalonia XAML Material Design 3 Expressive view -├── TicTacToeView.axaml.cs # Code-behind -└── README.md # This file -``` - ---- - -## Building and Packaging - -Run the following command in this directory: - -```bash -dotnet build -c Release -``` - -MSBuild will compile the plugin and automatically package it into: -``` -bin/Release/net10.0/TicTacToe.fryplugin -``` - ---- - -## Installing into FryPDF - -### Option 1: Drag-and-Drop (Recommended) -1. Open **FryPDF**. -2. Open the **Plugins Manager** (Click the Settings gear in the header $\to$ **Plugins & Extensions**, or press `Ctrl+Shift+P` / `⌘Shift+P`). -3. Drag and drop `TicTacToe.fryplugin` onto the dialog window. -4. The game mounts immediately without restarting! - -### Option 2: Command Palette -1. In FryPDF, press `Ctrl+K` or `⌘K` to open the **Command Palette**. -2. Type `Install Plugin Package...` and select `TicTacToe.fryplugin`. - -### Option 3: Manual Auto-Discovery Folder -Copy `TicTacToe.fryplugin` (or the folder containing `TicTacToePlugin.dll` and `plugin.json`) into your user plugins directory: -- **macOS**: `~/Library/Application Support/FryPdf/plugins/` -- **Windows**: `%APPDATA%\FryPdf\plugins\` -- **Linux**: `~/.config/FryPdf/plugins/` diff --git a/docs/examples/TicTacToePlugin/TicTacToePlugin.cs b/docs/examples/TicTacToePlugin/TicTacToePlugin.cs deleted file mode 100644 index f091395..0000000 --- a/docs/examples/TicTacToePlugin/TicTacToePlugin.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Manifests; -using PdfEditorApp.ViewModels; - -namespace FryPdf.Plugin.TicTacToe; - -/// -/// Modular plugin contributing an interactive, draggable Tic-Tac-Toe game targeting the 'shell.overlay' slot. -/// Demonstrates clean registration into Overlays, Command Palette, Ribbon, and Status Bar. -/// -public class TicTacToePlugin : IFryPlugin -{ - public string Id => "com.frypdf.plugin.tictactoe"; - public string Name => "Tic-Tac-Toe"; - public Version Version => new(1, 0, 0); - - public IReadOnlyList RequiredServices => Array.Empty(); - public IReadOnlyList ProvidedServices => Array.Empty(); - - public IReadOnlyDictionary? SettingsSchema => new Dictionary - { - ["DefaultMode"] = new() - { - Type = "select", - Label = "Default Game Mode", - Description = "Initial opponent mode on game launch", - DefaultValue = "PlayerVsAi", - Options = ["PlayerVsAi", "TwoPlayers"] - }, - ["AiDifficulty"] = new() - { - Type = "select", - Label = "AI Difficulty", - Description = "Tactical strategy level of computer player", - DefaultValue = "Smart", - Options = ["Smart", "Unbeatable", "Easy"] - }, - ["SoundEnabled"] = new() - { - Type = "boolean", - Label = "Sound Effects", - Description = "Enable sound alerts for moves and game over events", - DefaultValue = true - } - }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - // 1. Register Floating Shell Overlay with StandardCard chrome (Auto M3 titlebar, pin, minimize, close) - var overlayReg = ctx.RegisterOverlay(new OverlayDescriptor - { - Id = Id, - Title = "🎮 Tic-Tac-Toe", - Slot = "shell.overlay", - DefaultWidth = 330, - DefaultHeight = 440, - InitialX = null, - InitialY = 110, - IsDraggable = true, - IsMinimizable = true, - IsClosable = true, - IconKind = "GamepadVariantOutline", - ChromeMode = OverlayChromeMode.StandardCard, - ViewType = typeof(TicTacToeView), - ViewModelType = typeof(TicTacToeViewModel), - ViewFactory = _ => new TicTacToeView - { - DataContext = new TicTacToeViewModel() - }, - ViewModelFactory = _ => new TicTacToeViewModel() - }); - - // 2. Register Command in Command Palette (Ctrl+Alt+T / ⌘Alt+T) - var cmdReg = ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.overlay.tictactoe", - Title = "Play Tic-Tac-Toe (Shell Overlay)", - Subtitle = "Launch floating Tic-Tac-Toe mini-game with local 2P & AI opponent", - Category = "Shell Overlays", - IconKind = "GamepadVariantOutline", - Shortcut = "Ctrl+Alt+T", - Order = 96, - Action = sp => - { - if (sp.GetService(typeof(IOverlayRegistry)) is IOverlayRegistry reg) - { - reg.ToggleOverlay(Id); - } - } - }); - - // 3. Register Footer Status Bar Widget (Clickable 🎮 Tic-Tac-Toe pill) - var statusReg = ctx.RegisterStatusBarWidget(new StatusBarWidgetDescriptor - { - WidgetId = "frypdf.status.tictactoe", - Alignment = StatusBarAlignment.Right, - Order = 16, - ToolTip = "Play Tic-Tac-Toe (Floating Shell Overlay)", - Factory = sp => - { - var reg = sp.GetService(typeof(IOverlayRegistry)) as IOverlayRegistry; - return new StatusBarWidgetViewModel - { - WidgetId = "frypdf.status.tictactoe", - Label = "🎮 Tic-Tac-Toe", - IconKind = "GamepadVariantOutline", - ToolTip = "Toggle floating Tic-Tac-Toe game", - IsActive = true, - Command = new RelayCommand(() => reg?.ToggleOverlay(Id)) - }; - } - }); - - // 4. Register Quick-Action Pill in the Ribbon's 'View' Tab - var ribbonReg = ctx.RegisterRibbonAction(new RibbonActionDescriptor - { - Id = "frypdf.ribbon.action.tictactoe", - TabId = "view", - GroupId = "plugins", - Label = "Tic-Tac-Toe", - Tooltip = "Open floating Tic-Tac-Toe game overlay", - IconKind = "GamepadVariantOutline", - Order = 54, - Action = sp => - { - if (sp.GetService(typeof(IOverlayRegistry)) is IOverlayRegistry reg) - { - reg.ToggleOverlay(Id); - } - } - }); - - // 5. Register Reversible Effects for 100% Clean Teardown - ctx.RegisterEffect(() => - { - overlayReg.Dispose(); - cmdReg.Dispose(); - statusReg.Dispose(); - ribbonReg.Dispose(); - - if (ctx.TryGetService(out var reg)) - { - reg.HideOverlay(Id); - } - }); - - return Task.CompletedTask; - } -} diff --git a/docs/examples/TicTacToePlugin/TicTacToePlugin.csproj b/docs/examples/TicTacToePlugin/TicTacToePlugin.csproj deleted file mode 100644 index 5c6edb2..0000000 --- a/docs/examples/TicTacToePlugin/TicTacToePlugin.csproj +++ /dev/null @@ -1,109 +0,0 @@ - - - net10.0 - enable - enable - 13 - true - TicTacToePlugin - FryPdf.Plugin.TicTacToe - 1.0.0 - Code Fry Dev - Playable Tic-Tac-Toe shell overlay plugin for FryPDF - - - - - - - - - false - runtime - - - false - runtime - - - - - - - - - $(FRYPDF_HOME) - /Applications/FryPDF.app/Contents/MacOS - C:\Program Files\FryPDF - /opt/FryPDF - - - - - - $(FryPdfInstallDir)\PdfEditorApp.Core.dll - false - - - $(FryPdfInstallDir)\PdfEditorApp.dll - false - - - - - - - - - - - - - - - - - - - - - $(TargetDir)staging\ - $(TargetDir)TicTacToe.fryplugin - - - - - - - - - - - - - - - - - - diff --git a/docs/examples/TicTacToePlugin/TicTacToeView.axaml b/docs/examples/TicTacToePlugin/TicTacToeView.axaml deleted file mode 100644 index 355635f..0000000 --- a/docs/examples/TicTacToePlugin/TicTacToeView.axaml +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/examples/TicTacToePlugin/TicTacToeView.axaml.cs b/docs/examples/TicTacToePlugin/TicTacToeView.axaml.cs deleted file mode 100644 index 9ea3ef2..0000000 --- a/docs/examples/TicTacToePlugin/TicTacToeView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace FryPdf.Plugin.TicTacToe; - -public partial class TicTacToeView : UserControl -{ - public TicTacToeView() - { - InitializeComponent(); - } -} diff --git a/docs/examples/TicTacToePlugin/TicTacToeViewModel.cs b/docs/examples/TicTacToePlugin/TicTacToeViewModel.cs deleted file mode 100644 index 2f2f42c..0000000 --- a/docs/examples/TicTacToePlugin/TicTacToeViewModel.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; - -namespace FryPdf.Plugin.TicTacToe; - -/// -/// Individual cell on the 3x3 Tic-Tac-Toe grid. -/// -public partial class TicTacToeCellViewModel : ObservableObject -{ - public int Index { get; } - - [ObservableProperty] - private string _value = ""; - - [ObservableProperty] - private bool _isWinningCell; - - [ObservableProperty] - private bool _isEnabled = true; - - public TicTacToeCellViewModel(int index) - { - Index = index; - } - - public void Reset() - { - Value = ""; - IsWinningCell = false; - IsEnabled = true; - } -} - -/// -/// Reactive ViewModel for the Tic-Tac-Toe mini-game. -/// Manages turn state, win detection, score tracking, and single-player AI heuristics. -/// -public partial class TicTacToeViewModel : ObservableObject -{ - private static readonly int[][] WinningLines = - [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows - [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns - [0, 4, 8], [2, 4, 6] // Diagonals - ]; - - private readonly Random _random = new(); - - public ObservableCollection Cells { get; } = new(); - - [ObservableProperty] - private string _currentTurn = "X"; - - [ObservableProperty] - private string _statusMessage = "Player X's Turn (❌)"; - - [ObservableProperty] - private bool _isGameOver; - - [ObservableProperty] - private string _winner = ""; - - [ObservableProperty] - private bool _isAiMode = true; - - [ObservableProperty] - private int _xWins; - - [ObservableProperty] - private int _oWins; - - [ObservableProperty] - private int _draws; - - public TicTacToeViewModel() - { - for (int i = 0; i < 9; i++) - { - Cells.Add(new TicTacToeCellViewModel(i)); - } - - ResetGame(); - } - - [RelayCommand] - public void MakeMove(int index) - { - if (IsGameOver || index < 0 || index >= Cells.Count) return; - - var cell = Cells[index]; - if (!string.IsNullOrEmpty(cell.Value)) return; - - // Apply Player Move - cell.Value = CurrentTurn; - cell.IsEnabled = false; - - if (EvaluateBoard()) return; - - // Advance Turn - CurrentTurn = CurrentTurn == "X" ? "O" : "X"; - StatusMessage = CurrentTurn == "X" ? "Player X's Turn (❌)" : "Player O's Turn (⭕)"; - - // If Playing against AI and it's O's turn, calculate AI move - if (IsAiMode && CurrentTurn == "O" && !IsGameOver) - { - ExecuteAiTurn(); - } - } - - [RelayCommand] - public void ResetGame() - { - foreach (var cell in Cells) - { - cell.Reset(); - } - - CurrentTurn = "X"; - IsGameOver = false; - Winner = ""; - StatusMessage = "Player X's Turn (❌)"; - } - - [RelayCommand] - public void ResetScores() - { - XWins = 0; - OWins = 0; - Draws = 0; - ResetGame(); - } - - [RelayCommand] - public void ToggleAiMode() - { - IsAiMode = !IsAiMode; - ResetGame(); - } - - private void ExecuteAiTurn() - { - int move = FindBestMove("O", "X"); - if (move >= 0 && move < Cells.Count) - { - var cell = Cells[move]; - cell.Value = "O"; - cell.IsEnabled = false; - - if (EvaluateBoard()) return; - - CurrentTurn = "X"; - StatusMessage = "Player X's Turn (❌)"; - } - } - - private bool EvaluateBoard() - { - // 1. Check for a Winning Line - foreach (var line in WinningLines) - { - string a = Cells[line[0]].Value; - string b = Cells[line[1]].Value; - string c = Cells[line[2]].Value; - - if (!string.IsNullOrEmpty(a) && a == b && b == c) - { - IsGameOver = true; - Winner = a; - - // Highlight winning combination - Cells[line[0]].IsWinningCell = true; - Cells[line[1]].IsWinningCell = true; - Cells[line[2]].IsWinningCell = true; - - // Disable remaining empty cells - foreach (var cell in Cells.Where(c => string.IsNullOrEmpty(c.Value))) - { - cell.IsEnabled = false; - } - - if (a == "X") - { - XWins++; - StatusMessage = "🎉 Player X (❌) Wins!"; - } - else - { - OWins++; - StatusMessage = IsAiMode ? "🤖 Computer (⭕) Wins!" : "🎉 Player O (⭕) Wins!"; - } - - return true; - } - } - - // 2. Check for Draw - if (Cells.All(c => !string.IsNullOrEmpty(c.Value))) - { - IsGameOver = true; - Winner = "Draw"; - Draws++; - StatusMessage = "🤝 It's a Draw!"; - return true; - } - - return false; - } - - /// - /// Evaluates tactical moves: 1) Win if possible, 2) Block opponent win, 3) Take center, 4) Take corner, 5) Any open. - /// - private int FindBestMove(string ai, string opponent) - { - var available = Cells.Where(c => string.IsNullOrEmpty(c.Value)).Select(c => c.Index).ToList(); - if (available.Count == 0) return -1; - - // 1. Check if AI can win in one move - foreach (var idx in available) - { - if (SimulateMoveWins(idx, ai)) return idx; - } - - // 2. Block opponent winning move - foreach (var idx in available) - { - if (SimulateMoveWins(idx, opponent)) return idx; - } - - // 3. Take center cell (4) if free - if (available.Contains(4)) return 4; - - // 4. Take any corner (0, 2, 6, 8) - int[] corners = [0, 2, 6, 8]; - var openCorners = corners.Where(available.Contains).ToList(); - if (openCorners.Count > 0) - { - return openCorners[_random.Next(openCorners.Count)]; - } - - // 5. Pick random available cell - return available[_random.Next(available.Count)]; - } - - private bool SimulateMoveWins(int index, string mark) - { - foreach (var line in WinningLines) - { - if (!line.Contains(index)) continue; - - int other1 = line.First(i => i != index); - int other2 = line.Last(i => i != index); - - if (Cells[other1].Value == mark && Cells[other2].Value == mark) - { - return true; - } - } - return false; - } -} diff --git a/docs/examples/TicTacToePlugin/plugin.json b/docs/examples/TicTacToePlugin/plugin.json deleted file mode 100644 index 1cce40e..0000000 --- a/docs/examples/TicTacToePlugin/plugin.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "id": "com.frypdf.plugin.tictactoe", - "name": "Tic-Tac-Toe", - "version": "1.0.0", - "author": "Code Fry Dev", - "description": "Interactive floating Tic-Tac-Toe mini-game with local 2-Player mode and intelligent AI opponent.", - "entryPoint": "TicTacToePlugin.dll", - "icon": "GamepadVariantOutline", - "dependencies": [], - "settingsSchema": { - "DefaultMode": { - "type": "select", - "label": "Default Game Mode", - "description": "Choose between single player vs AI or pass-and-play two player mode", - "default": "PlayerVsAi", - "options": ["PlayerVsAi", "TwoPlayers"] - }, - "AiDifficulty": { - "type": "select", - "label": "AI Difficulty", - "description": "Strategy level used by the computer opponent", - "default": "Smart", - "options": ["Smart", "Unbeatable", "Easy"] - }, - "SoundEnabled": { - "type": "boolean", - "label": "Sound Effects", - "description": "Play subtle audio cues on move placements and victories", - "default": true - } - } -} diff --git a/packaging/macos/AppIcon.icns b/packaging/macos/AppIcon.icns deleted file mode 100644 index cf40f4e..0000000 Binary files a/packaging/macos/AppIcon.icns and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_128x128.png b/packaging/macos/AppIcon.iconset/icon_128x128.png deleted file mode 100644 index 496af2a..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_128x128.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_128x128@2x.png b/packaging/macos/AppIcon.iconset/icon_128x128@2x.png deleted file mode 100644 index 6394c69..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_128x128@2x.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_16x16.png b/packaging/macos/AppIcon.iconset/icon_16x16.png deleted file mode 100644 index d185c0b..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_16x16.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_16x16@2x.png b/packaging/macos/AppIcon.iconset/icon_16x16@2x.png deleted file mode 100644 index ea2769d..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_16x16@2x.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_256x256.png b/packaging/macos/AppIcon.iconset/icon_256x256.png deleted file mode 100644 index 6394c69..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_256x256.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_256x256@2x.png b/packaging/macos/AppIcon.iconset/icon_256x256@2x.png deleted file mode 100644 index 8b2617e..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_256x256@2x.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_32x32.png b/packaging/macos/AppIcon.iconset/icon_32x32.png deleted file mode 100644 index ea2769d..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_32x32.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_32x32@2x.png b/packaging/macos/AppIcon.iconset/icon_32x32@2x.png deleted file mode 100644 index b8893da..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_32x32@2x.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_512x512.png b/packaging/macos/AppIcon.iconset/icon_512x512.png deleted file mode 100644 index 8b2617e..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_512x512.png and /dev/null differ diff --git a/packaging/macos/AppIcon.iconset/icon_512x512@2x.png b/packaging/macos/AppIcon.iconset/icon_512x512@2x.png deleted file mode 100644 index 3cf0bc4..0000000 Binary files a/packaging/macos/AppIcon.iconset/icon_512x512@2x.png and /dev/null differ diff --git a/packaging/macos/Info.plist.template b/packaging/macos/Info.plist.template deleted file mode 100644 index 6866a48..0000000 --- a/packaging/macos/Info.plist.template +++ /dev/null @@ -1,62 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - PdfEditorApp - CFBundleIconFile - AppIcon.icns - CFBundleIdentifier - in.codefrydev.frypdf - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - FryPDF - CFBundleDisplayName - FryPDF - CFBundlePackageType - APPL - CFBundleShortVersionString - __VERSION__ - CFBundleVersion - __VERSION__ - LSMinimumSystemVersion - 10.15 - NSHighResolutionCapable - - LSApplicationCategoryType - public.app-category.productivity - NSHumanReadableCopyright - Copyright © 2026 CodeFryDev. All rights reserved. - CFBundleDocumentTypes - - - CFBundleTypeName - FryPDF Project - CFBundleTypeRole - Editor - CFBundleTypeExtensions - - frypdf - pdfproj - - LSHandlerRank - Owner - - - CFBundleTypeName - PDF Document - CFBundleTypeRole - Viewer - CFBundleTypeExtensions - - pdf - - LSHandlerRank - Alternate - - - - diff --git a/packaging/windows/app.ico b/packaging/windows/app.ico deleted file mode 100644 index 9c72c35..0000000 Binary files a/packaging/windows/app.ico and /dev/null differ diff --git a/packaging/windows/installer.iss b/packaging/windows/installer.iss deleted file mode 100644 index 4604481..0000000 --- a/packaging/windows/installer.iss +++ /dev/null @@ -1,81 +0,0 @@ -; Inno Setup 6 Script for FryPDF by CodeFryDev -#ifndef MyAppVersion -#define MyAppVersion "1.0.0" -#endif - -#ifndef MyPublishDir -#define MyPublishDir "..\..\publish\win-x64" -#endif - -#define MyAppName "FryPDF" -#define MyAppPublisher "CodeFryDev" -#define MyAppURL "https://codefrydev.in" -#define MyAppSupportURL "mailto:codefrydev@gmail.com" -#define MyAppExeName "PdfEditorApp.exe" - -[Setup] -; Basic Application Info -AppId={{D37E88A1-1B2F-4A92-875D-876E842109AB} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -AppVerName={#MyAppName} {#MyAppVersion} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -AppSupportURL={#MyAppSupportURL} -AppUpdatesURL={#MyAppURL} - -; Destination Directories -DefaultDirName={autopf}\{#MyAppName} -DefaultGroupName={#MyAppName} -DisableProgramGroupPage=yes - -; Output Configuration -OutputDir=. -OutputBaseFilename=FryPDF-Setup-{#MyAppVersion} -SetupIconFile=app.ico -UninstallDisplayIcon={app}\{#MyAppExeName} - -; Compression & Modern Wizard Style -Compression=lzma2/ultra64 -SolidCompression=yes -WizardStyle=modern -ArchitecturesInstallIn64BitMode=x64compatible -PrivilegesRequired=lowest -PrivilegesRequiredOverridesAllowed=dialog - -; File Associations -ChangesAssociations=yes - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -Name: "fileassoc_proj"; Description: "Associate with FryPDF project files (.frypdf)"; GroupDescription: "File Associations:" -Name: "fileassoc_pdf"; Description: "Associate with Adobe / standard PDF documents (.pdf)"; GroupDescription: "File Associations:"; Flags: unchecked - -[Files] -Source: "{#MyPublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs - -[Icons] -Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon - -[Registry] -; .frypdf Association -Root: HKA; Subkey: "Software\Classes\.frypdf"; ValueType: string; ValueName: ""; ValueData: "FryPDF.Project"; Flags: uninsdeletevalue; Tasks: fileassoc_proj -; Legacy .pdfproj Association -Root: HKA; Subkey: "Software\Classes\.pdfproj"; ValueType: string; ValueName: ""; ValueData: "FryPDF.Project"; Flags: uninsdeletevalue; Tasks: fileassoc_proj -Root: HKA; Subkey: "Software\Classes\FryPDF.Project"; ValueType: string; ValueName: ""; ValueData: "FryPDF Project File"; Flags: uninsdeletekey; Tasks: fileassoc_proj -Root: HKA; Subkey: "Software\Classes\FryPDF.Project\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey; Tasks: fileassoc_proj -Root: HKA; Subkey: "Software\Classes\FryPDF.Project\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: fileassoc_proj - -; .pdf Association (optional) -Root: HKA; Subkey: "Software\Classes\.pdf\OpenWithProgids"; ValueType: string; ValueName: "FryPDF.Document"; ValueData: ""; Flags: uninsdeletevalue; Tasks: fileassoc_pdf -Root: HKA; Subkey: "Software\Classes\FryPDF.Document"; ValueType: string; ValueName: ""; ValueData: "PDF Document"; Flags: uninsdeletekey; Tasks: fileassoc_pdf -Root: HKA; Subkey: "Software\Classes\FryPDF.Document\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey; Tasks: fileassoc_pdf -Root: HKA; Subkey: "Software\Classes\FryPDF.Document\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: fileassoc_pdf - -[Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/packaging/windows/msix/AppxManifest.xml.template b/packaging/windows/msix/AppxManifest.xml.template deleted file mode 100644 index 46c3881..0000000 --- a/packaging/windows/msix/AppxManifest.xml.template +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - FryPDF - Code Fry Dev - Assets\StoreLogo.png - FryPDF is a privacy-first, professional desktop PDF editor and creator by Code Fry Dev. - - - - - - - - - - - - - - - - - - - - - - - - - - FryPDF Project - Assets\Square44x44Logo.png - - .frypdf - .pdfproj - - - - - - PDF Document - Assets\Square44x44Logo.png - - .pdf - - - - - - - - - - - diff --git a/packaging/windows/msix/Assets/SplashScreen.png b/packaging/windows/msix/Assets/SplashScreen.png deleted file mode 100644 index 4e87f14..0000000 Binary files a/packaging/windows/msix/Assets/SplashScreen.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/Square150x150Logo.png b/packaging/windows/msix/Assets/Square150x150Logo.png deleted file mode 100644 index e722bfc..0000000 Binary files a/packaging/windows/msix/Assets/Square150x150Logo.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/Square310x310Logo.png b/packaging/windows/msix/Assets/Square310x310Logo.png deleted file mode 100644 index 47ee948..0000000 Binary files a/packaging/windows/msix/Assets/Square310x310Logo.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/Square44x44Logo.png b/packaging/windows/msix/Assets/Square44x44Logo.png deleted file mode 100644 index e7c3b96..0000000 Binary files a/packaging/windows/msix/Assets/Square44x44Logo.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/packaging/windows/msix/Assets/Square44x44Logo.targetsize-24_altform-unplated.png deleted file mode 100644 index f141237..0000000 Binary files a/packaging/windows/msix/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/Square44x44Logo.targetsize-256_altform-unplated.png b/packaging/windows/msix/Assets/Square44x44Logo.targetsize-256_altform-unplated.png deleted file mode 100644 index 6394c69..0000000 Binary files a/packaging/windows/msix/Assets/Square44x44Logo.targetsize-256_altform-unplated.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/Square44x44Logo.targetsize-48_altform-unplated.png b/packaging/windows/msix/Assets/Square44x44Logo.targetsize-48_altform-unplated.png deleted file mode 100644 index 436b83e..0000000 Binary files a/packaging/windows/msix/Assets/Square44x44Logo.targetsize-48_altform-unplated.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/Square71x71Logo.png b/packaging/windows/msix/Assets/Square71x71Logo.png deleted file mode 100644 index 81aaeab..0000000 Binary files a/packaging/windows/msix/Assets/Square71x71Logo.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/StoreLogo.png b/packaging/windows/msix/Assets/StoreLogo.png deleted file mode 100644 index f26ab94..0000000 Binary files a/packaging/windows/msix/Assets/StoreLogo.png and /dev/null differ diff --git a/packaging/windows/msix/Assets/Wide310x150Logo.png b/packaging/windows/msix/Assets/Wide310x150Logo.png deleted file mode 100644 index 029b14c..0000000 Binary files a/packaging/windows/msix/Assets/Wide310x150Logo.png and /dev/null differ diff --git a/packaging/windows/msix/build-msix.ps1 b/packaging/windows/msix/build-msix.ps1 deleted file mode 100644 index fe88c92..0000000 --- a/packaging/windows/msix/build-msix.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -param ( - [string]$Version = "1.0.0", - [string]$PublishDir = "$PSScriptRoot\..\..\publish\win-x64", - [string]$OutputDir = "$PSScriptRoot\..", - [string]$Publisher = "CN=7E83DE15-E15F-41B6-B068-989D9548D0BF" -) - -$ErrorActionPreference = "Stop" - -Write-Host "=== Building FryPDF MSIX Package ===" -ForegroundColor Cyan -Write-Host "Version: $Version" -Write-Host "PublishDir: $PublishDir" -Write-Host "OutputDir: $OutputDir" -Write-Host "Publisher: $Publisher" - -# 1. Format 4-part version for AppxManifest (Major.Minor.Build.Revision) -$cleanVer = ($Version -split '-')[0] -$verParts = $cleanVer -split '\.' -while ($verParts.Count -lt 4) { - $verParts += "0" -} -$fourPartVersion = ($verParts[0..3] -join '.') -Write-Host "AppxManifest 4-Part Version: $fourPartVersion" -ForegroundColor Green - -# 2. Locate MakeAppx.exe and signtool.exe from Windows SDK -$makeAppxList = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Filter "MakeAppx.exe" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.FullName -like "*x64*" } -$makeAppx = ($makeAppxList | Sort-Object FullName -Descending | Select-Object -First 1).FullName - -if (-not $makeAppx) { - $makeAppx = (Get-Command "MakeAppx.exe" -ErrorAction SilentlyContinue).Source -} - -if (-not $makeAppx) { - throw "MakeAppx.exe could not be found. Please ensure Windows 10/11 SDK is installed." -} - -Write-Host "Found MakeAppx: $makeAppx" - -$signtoolList = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Filter "signtool.exe" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.FullName -like "*x64*" } -$signtool = ($signtoolList | Sort-Object FullName -Descending | Select-Object -First 1).FullName - -if (-not $signtool) { - $signtool = (Get-Command "signtool.exe" -ErrorAction SilentlyContinue).Source -} - -# 3. Assemble staging directory -$stageDir = Join-Path $env:TEMP "FryPDF_MSIX_Stage" -if (Test-Path $stageDir) { - Remove-Item -Recurse -Force $stageDir -} -New-Item -ItemType Directory -Path $stageDir | Out-Null - -Write-Host "Copying binaries from $PublishDir to staging..." -Copy-Item -Path "$PublishDir\*" -Destination $stageDir -Recurse -Force - -Write-Host "Copying MSIX Assets..." -$assetsDest = Join-Path $stageDir "Assets" -Copy-Item -Path "$PSScriptRoot\Assets" -Destination $assetsDest -Recurse -Force - -# 4. Generate AppxManifest.xml from template -$templatePath = Join-Path $PSScriptRoot "AppxManifest.xml.template" -$manifestContent = Get-Content -Path $templatePath -Raw -$manifestContent = $manifestContent -replace '__PUBLISHER__', $Publisher -$manifestContent = $manifestContent -replace '__FOUR_PART_VERSION__', $fourPartVersion -$manifestContent = $manifestContent -replace '__VERSION__', $cleanVer - -$manifestDest = Join-Path $stageDir "AppxManifest.xml" -Set-Content -Path $manifestDest -Value $manifestContent -Encoding UTF8 -Write-Host "Generated AppxManifest.xml at $manifestDest" - -# 5. Pack MSIX -if (-not (Test-Path $OutputDir)) { - New-Item -ItemType Directory -Path $OutputDir | Out-Null -} - -$msixOutputFile = Join-Path $OutputDir "FryPDF-$Version-x64.msix" -Write-Host "Packing MSIX: $msixOutputFile..." -ForegroundColor Cyan - -& "$makeAppx" pack /d "$stageDir" /p "$msixOutputFile" /nv /o -if ($LASTEXITCODE -ne 0) { - throw "MakeAppx failed with exit code $LASTEXITCODE" -} - -# 6. Create Certificate & Sign MSIX Package -Write-Host "Creating certificate and signing package..." -ForegroundColor Cyan -$cert = New-SelfSignedCertificate -Type Custom ` - -Subject $Publisher ` - -KeyUsage DigitalSignature ` - -FriendlyName "FryPDF MSIX Signing Certificate" ` - -CertStoreLocation "Cert:\CurrentUser\My" ` - -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") - -$password = ConvertTo-SecureString -String "FryPDFSecretPassword123!" -Force -AsPlainText -$pfxPath = Join-Path $env:TEMP "FryPDF_SignCert.pfx" -Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $password | Out-Null - -if ($signtool) { - Write-Host "Signing MSIX with signtool..." - & "$signtool" sign /fd SHA256 /a /f "$pfxPath" /p "FryPDFSecretPassword123!" /v "$msixOutputFile" - if ($LASTEXITCODE -ne 0) { - throw "SignTool failed with exit code $LASTEXITCODE" - } -} else { - Write-Warning "SignTool not found; MSIX was built unsigned." -} - -# Export public certificate (.cer) for sideloading convenience -$cerOutputFile = Join-Path $OutputDir "FryPDF-$Version-PublicCert.cer" -Export-Certificate -Cert $cert -FilePath $cerOutputFile | Out-Null - -Write-Host "=== MSIX Build & Signing Completed Successfully! ===" -ForegroundColor Green -Write-Host "MSIX File: $msixOutputFile" -Write-Host "Public Cert: $cerOutputFile" diff --git a/profiles/desktop.profile.json b/profiles/desktop.profile.json deleted file mode 100644 index 79906dd..0000000 --- a/profiles/desktop.profile.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "profileName": "desktop", - "bundles": [ - "FryPdf.Bundle.Tools.Organize", - "FryPdf.Bundle.Tools.Security", - "FryPdf.Bundle.Tools.Conversion", - "FryPdf.Bundle.Tools.Intelligence", - "FryPdf.Bundle.DataStudio", - "FryPdf.Bundle.CanvasElements", - "FryPdf.Bundle.DocumentIo", - "FryPdf.Bundle.AiProviders", - "FryPdf.Bundle.OcrEngines", - "FryPdf.Bundle.Templates", - "FryPdf.Bundle.StatusBar", - "FryPdf.Bundle.Inspector", - "FryPdf.Bundle.CommandPalette", - "FryPdf.Bundle.WorkspacePages", - "FryPdf.Bundle.Dialogs", - "FryPdf.Bundle.EditorSidebars" - ], - - "disabledPlugins": [], - "settings": { - "theme": "Auto", - "enableHardwareAcceleration": "true" - } -} diff --git a/profiles/headless.profile.json b/profiles/headless.profile.json deleted file mode 100644 index bd6378a..0000000 --- a/profiles/headless.profile.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "profileName": "headless", - "bundles": [ - "FryPdf.Bundle.Tools.Organize", - "FryPdf.Bundle.Tools.Conversion", - "FryPdf.Bundle.Tools.Security", - "FryPdf.Bundle.Tools.Intelligence", - "FryPdf.Bundle.DocumentIo", - "FryPdf.Bundle.OcrEngines", - "FryPdf.Bundle.Templates" - ], - "disabledPlugins": [ - "frypdf.tool.datastudio" - ], - "settings": { - "headless": "true", - "concurrency": "4" - } -} diff --git a/run b/run deleted file mode 100755 index 4476c78..0000000 --- a/run +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -dotnet clean - -dotnet run --project src/PdfEditorApp "$@" diff --git a/src/PdfEditorApp.Core/Analysis/LiveChartsRenderer.cs b/src/PdfEditorApp.Core/Analysis/LiveChartsRenderer.cs deleted file mode 100644 index 396f6b6..0000000 --- a/src/PdfEditorApp.Core/Analysis/LiveChartsRenderer.cs +++ /dev/null @@ -1,560 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using LiveChartsCore; -using LiveChartsCore.Defaults; -using LiveChartsCore.Drawing; -using LiveChartsCore.Measure; -using LiveChartsCore.SkiaSharpView; -using LiveChartsCore.SkiaSharpView.Painting; -using LiveChartsCore.SkiaSharpView.SKCharts; -using LiveChartsCore.SkiaSharpView.VisualElements; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using SkiaSharp; - -namespace PdfEditorApp.Core.Analysis; - -/// -/// Headless SkiaSharp LiveCharts2 rendering engine for high-resolution chart export in QuestPDF and preview rasterization. -/// -public static class LiveChartsRenderer -{ - private static bool _isConfigured; - private static readonly object _configLock = new(); - - public static void EnsureConfigured() - { - if (_isConfigured) return; - lock (_configLock) - { - if (!_isConfigured) - { - LiveCharts.Configure(config => LiveChartsSkiaSharp.UseDefaults(config)); - _isConfigured = true; - } - } - } - - public static IReadOnlyList GetPaletteHexColors(ChartPalette palette) - { - return palette switch - { - ChartPalette.CorporateBlue => new[] { "#0F6CBD", "#3D95E6", "#82BDF0", "#C7E0F4", "#0B4A82", "#115EA3" }, - ChartPalette.EmeraldGreen => new[] { "#107C41", "#27A25D", "#57C488", "#97E3B5", "#0E5E32", "#188A4C" }, - ChartPalette.SunsetOrange => new[] { "#D83B01", "#F7630C", "#FF8C00", "#FCE100", "#A80000", "#EA4300" }, - ChartPalette.CyberNeon => new[] { "#8764B8", "#B146C2", "#00B7C3", "#00CC6A", "#E3008C", "#FFB900" }, - ChartPalette.ExecutiveSlate => new[] { "#323130", "#605E5C", "#8A8886", "#B3B0AD", "#201F1E", "#484644" }, - ChartPalette.PastelHarmony => new[] { "#7986CB", "#4DB6AC", "#FFD54F", "#BA68C8", "#FF8A65", "#A1887F" }, - ChartPalette.VibrantRainbow => new[] { "#E63946", "#F4A261", "#E9C46A", "#2A9D8F", "#264653", "#457B9D" }, - _ => new[] { "#0F6CBD", "#3D95E6", "#82BDF0", "#C7E0F4", "#0B4A82" } - }; - } - - public static SKColor ParseColor(string? hex, SKColor fallback) - { - if (string.IsNullOrWhiteSpace(hex)) return fallback; - return SKColor.TryParse(hex, out var color) ? color : fallback; - } - - public static byte[] RenderChartToPngBytes(PdfChartElement chartEl, int width = 500, int height = 300, float dpiScale = 2.0f) - { - EnsureConfigured(); - - int targetWidth = Math.Max(200, (int)(width * dpiScale)); - int targetHeight = Math.Max(150, (int)(height * dpiScale)); - - var palette = GetPaletteHexColors(chartEl.Palette); - var legendPos = ToLiveChartsLegend(chartEl.LegendPosition); - - using SKImage? img = chartEl.ChartType switch - { - ChartType.DonutPie or ChartType.GaugeProgress => RenderPieChart(chartEl, targetWidth, targetHeight, palette, legendPos, dpiScale), - ChartType.Radar or ChartType.PolarArea => RenderPolarChart(chartEl, targetWidth, targetHeight, palette, legendPos, dpiScale), - _ => RenderCartesianChart(chartEl, targetWidth, targetHeight, palette, legendPos, dpiScale) - }; - - if (img == null) - return Array.Empty(); - - using var data = img.Encode(SKEncodedImageFormat.Png, 100); - return data.ToArray(); - } - - private static LegendPosition ToLiveChartsLegend(ChartLegendPosition pos) - { - return pos switch - { - ChartLegendPosition.Top => LegendPosition.Top, - ChartLegendPosition.Bottom => LegendPosition.Bottom, - ChartLegendPosition.Left => LegendPosition.Left, - ChartLegendPosition.Right => LegendPosition.Right, - _ => LegendPosition.Hidden - }; - } - - private static SKImage RenderCartesianChart(PdfChartElement chartEl, int width, int height, IReadOnlyList palette, LegendPosition legendPos, float dpiScale) - { - var seriesList = new List(); - var categories = chartEl.Categories.Count > 0 ? chartEl.Categories : new List { "A", "B", "C", "D" }; - - var primaryValues = chartEl.Values.Count > 0 ? chartEl.Values : new List { 10, 20, 30, 40 }; - - bool hasMultiSeries = chartEl.MultiSeries.Count > 0; - - switch (chartEl.ChartType) - { - case ChartType.HorizontalBar: - { - if (hasMultiSeries) - { - for (int sIdx = 0; sIdx < chartEl.MultiSeries.Count; sIdx++) - { - var s = chartEl.MultiSeries[sIdx]; - var color = ParseColor(s.ColorHex ?? palette[sIdx % palette.Count], SKColors.RoyalBlue); - seriesList.Add(new RowSeries - { - Name = s.Name, - Values = s.Values.Count > 0 ? s.Values : primaryValues, - Fill = new SolidColorPaint(color), - Stroke = null, - DataLabelsSize = 10 * dpiScale, - DataLabelsPaint = chartEl.ShowDataLabels ? new SolidColorPaint(SKColors.DarkSlateGray) : null - }); - } - } - else - { - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - seriesList.Add(new RowSeries - { - Name = chartEl.Title, - Values = primaryValues, - Fill = new SolidColorPaint(color), - Stroke = null, - DataLabelsSize = 10 * dpiScale, - DataLabelsPaint = chartEl.ShowDataLabels ? new SolidColorPaint(SKColors.DarkSlateGray) : null - }); - } - break; - } - - case ChartType.StackedBar: - { - if (hasMultiSeries) - { - for (int sIdx = 0; sIdx < chartEl.MultiSeries.Count; sIdx++) - { - var s = chartEl.MultiSeries[sIdx]; - var color = ParseColor(s.ColorHex ?? palette[sIdx % palette.Count], SKColors.RoyalBlue); - seriesList.Add(new StackedColumnSeries - { - Name = s.Name, - Values = s.Values.Count > 0 ? s.Values : primaryValues, - Fill = new SolidColorPaint(color), - Stroke = null - }); - } - } - else - { - for (int i = 0; i < primaryValues.Count; i++) - { - var color = ParseColor(i < chartEl.BarColorsHex.Count ? chartEl.BarColorsHex[i] : palette[i % palette.Count], SKColors.RoyalBlue); - seriesList.Add(new StackedColumnSeries - { - Name = i < categories.Count ? categories[i] : $"Segment {i + 1}", - Values = new double[] { primaryValues[i] }, - Fill = new SolidColorPaint(color), - Stroke = null - }); - } - } - break; - } - - case ChartType.StackedHorizontalBar: - { - if (hasMultiSeries) - { - for (int sIdx = 0; sIdx < chartEl.MultiSeries.Count; sIdx++) - { - var s = chartEl.MultiSeries[sIdx]; - var color = ParseColor(s.ColorHex ?? palette[sIdx % palette.Count], SKColors.RoyalBlue); - seriesList.Add(new StackedRowSeries - { - Name = s.Name, - Values = s.Values.Count > 0 ? s.Values : primaryValues, - Fill = new SolidColorPaint(color), - Stroke = null - }); - } - } - else - { - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - seriesList.Add(new StackedRowSeries - { - Name = chartEl.Title, - Values = primaryValues, - Fill = new SolidColorPaint(color), - Stroke = null - }); - } - break; - } - - case ChartType.Line: - { - if (hasMultiSeries) - { - for (int sIdx = 0; sIdx < chartEl.MultiSeries.Count; sIdx++) - { - var s = chartEl.MultiSeries[sIdx]; - var color = ParseColor(s.ColorHex ?? palette[sIdx % palette.Count], SKColors.RoyalBlue); - seriesList.Add(new LineSeries - { - Name = s.Name, - Values = s.Values.Count > 0 ? s.Values : primaryValues, - Stroke = new SolidColorPaint(color, (float)chartEl.StrokeThickness * dpiScale), - Fill = null, - GeometrySize = 6 * dpiScale, - GeometryStroke = new SolidColorPaint(color, 2 * dpiScale), - GeometryFill = new SolidColorPaint(SKColors.White), - LineSmoothness = 0.0 - }); - } - } - else - { - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - seriesList.Add(new LineSeries - { - Name = chartEl.Title, - Values = primaryValues, - Stroke = new SolidColorPaint(color, (float)chartEl.StrokeThickness * dpiScale), - Fill = null, - GeometrySize = 6 * dpiScale, - GeometryStroke = new SolidColorPaint(color, 2 * dpiScale), - GeometryFill = new SolidColorPaint(SKColors.White), - LineSmoothness = 0.0 - }); - } - break; - } - - case ChartType.SmoothLine: - { - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - seriesList.Add(new LineSeries - { - Name = chartEl.Title, - Values = primaryValues, - Stroke = new SolidColorPaint(color, (float)chartEl.StrokeThickness * dpiScale), - Fill = null, - GeometrySize = 7 * dpiScale, - GeometryStroke = new SolidColorPaint(color, 2 * dpiScale), - GeometryFill = new SolidColorPaint(SKColors.White), - LineSmoothness = (float)chartEl.CurveSmoothness - }); - break; - } - - case ChartType.Area: - { - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - var fillColor = color.WithAlpha(65); - seriesList.Add(new LineSeries - { - Name = chartEl.Title, - Values = primaryValues, - Stroke = new SolidColorPaint(color, (float)chartEl.StrokeThickness * dpiScale), - Fill = new SolidColorPaint(fillColor), - GeometrySize = 6 * dpiScale, - GeometryStroke = new SolidColorPaint(color, 2 * dpiScale), - GeometryFill = new SolidColorPaint(SKColors.White), - LineSmoothness = (float)chartEl.CurveSmoothness - }); - break; - } - - case ChartType.StepLine: - { - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - seriesList.Add(new StepLineSeries - { - Name = chartEl.Title, - Values = primaryValues, - Stroke = new SolidColorPaint(color, (float)chartEl.StrokeThickness * dpiScale), - Fill = null, - GeometrySize = 6 * dpiScale, - GeometryStroke = new SolidColorPaint(color, 2 * dpiScale), - GeometryFill = new SolidColorPaint(SKColors.White) - }); - break; - } - - case ChartType.ScatterPlot: - { - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - var points = new List(); - for (int i = 0; i < primaryValues.Count; i++) - { - points.Add(new ObservablePoint(i + 1, primaryValues[i])); - } - seriesList.Add(new ScatterSeries - { - Name = chartEl.Title, - Values = points, - Stroke = new SolidColorPaint(color, 2 * dpiScale), - Fill = new SolidColorPaint(color.WithAlpha(180)), - GeometrySize = 10 * dpiScale - }); - break; - } - - case ChartType.Candlestick: - { - var financialPoints = new List(); - for (int i = 0; i < primaryValues.Count; i++) - { - double close = primaryValues[i]; - double open = i > 0 ? primaryValues[i - 1] : close * 0.95; - double high = Math.Max(open, close) * 1.08; - double low = Math.Min(open, close) * 0.92; - financialPoints.Add(new FinancialPoint(new DateTime(2026, 1, 1).AddDays(i), high, open, close, low)); - } - seriesList.Add(new CandlesticksSeries - { - Name = chartEl.Title, - Values = financialPoints, - UpStroke = new SolidColorPaint(SKColors.ForestGreen, 2 * dpiScale), - UpFill = new SolidColorPaint(SKColors.ForestGreen.WithAlpha(200)), - DownStroke = new SolidColorPaint(SKColors.IndianRed, 2 * dpiScale), - DownFill = new SolidColorPaint(SKColors.IndianRed.WithAlpha(200)) - }); - break; - } - - case ChartType.Waterfall: - case ChartType.Funnel: - case ChartType.Pyramid: - case ChartType.BarColumn: - default: - { - if (hasMultiSeries) - { - for (int sIdx = 0; sIdx < chartEl.MultiSeries.Count; sIdx++) - { - var s = chartEl.MultiSeries[sIdx]; - var color = ParseColor(s.ColorHex ?? palette[sIdx % palette.Count], SKColors.RoyalBlue); - seriesList.Add(new ColumnSeries - { - Name = s.Name, - Values = s.Values.Count > 0 ? s.Values : primaryValues, - Fill = new SolidColorPaint(color), - Stroke = null, - Rx = 4 * dpiScale, - Ry = 4 * dpiScale, - DataLabelsSize = 9 * dpiScale, - DataLabelsPaint = chartEl.ShowDataLabels ? new SolidColorPaint(SKColors.DarkSlateGray) : null - }); - } - } - else - { - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - seriesList.Add(new ColumnSeries - { - Name = chartEl.Title, - Values = primaryValues, - Fill = new SolidColorPaint(color), - Stroke = null, - Rx = 4 * dpiScale, - Ry = 4 * dpiScale, - DataLabelsSize = 9 * dpiScale, - DataLabelsPaint = chartEl.ShowDataLabels ? new SolidColorPaint(SKColors.DarkSlateGray) : null - }); - } - break; - } - } - - var xAxes = new Axis[] - { - new Axis - { - Labels = categories.ToArray(), - Name = chartEl.XAxisTitle, - LabelsPaint = new SolidColorPaint(SKColors.DarkSlateGray), - TextSize = 10.5f * dpiScale, - NameTextSize = 11.5f * dpiScale, - SeparatorsPaint = chartEl.ShowGridlines ? new SolidColorPaint(SKColors.LightGray.WithAlpha(110)) { StrokeThickness = 1 * dpiScale } : null - } - }; - - var yAxes = new Axis[] - { - new Axis - { - Name = chartEl.YAxisTitle, - LabelsPaint = new SolidColorPaint(SKColors.DarkSlateGray), - TextSize = 10.5f * dpiScale, - NameTextSize = 11.5f * dpiScale, - SeparatorsPaint = chartEl.ShowGridlines ? new SolidColorPaint(SKColors.LightGray.WithAlpha(110)) { StrokeThickness = 1 * dpiScale } : null - } - }; - -#pragma warning disable CS0618 - var titlePaint = !string.IsNullOrWhiteSpace(chartEl.Title) - ? new LabelVisual - { - Text = chartEl.Title, - TextSize = 13.5f * dpiScale, - Paint = new SolidColorPaint(SKColors.Black) - } - : null; -#pragma warning restore CS0618 - - var chart = new SKCartesianChart - { - Width = width, - Height = height, - Series = seriesList, - XAxes = xAxes, - YAxes = yAxes, - Title = titlePaint, - LegendPosition = legendPos, - LegendTextSize = 10.5f * dpiScale, - LegendTextPaint = new SolidColorPaint(SKColors.DarkSlateGray) - }; - - return chart.GetImage(); - } - - private static SKImage RenderPieChart(PdfChartElement chartEl, int width, int height, IReadOnlyList palette, LegendPosition legendPos, float dpiScale) - { - var seriesList = new List(); - var categories = chartEl.Categories.Count > 0 ? chartEl.Categories : new List { "Q1", "Q2", "Q3", "Q4" }; - var values = chartEl.Values.Count > 0 ? chartEl.Values : new List { 25, 25, 25, 25 }; - - double innerRadius = chartEl.ChartType == ChartType.DonutPie ? Math.Max(0.1, chartEl.DonutHoleRatio) : 0.0; - double maxAngle = chartEl.ChartType == ChartType.GaugeProgress ? 180 : 360; - double initialRotation = chartEl.ChartType == ChartType.GaugeProgress ? -180 : -90; - - for (int i = 0; i < values.Count; i++) - { - var color = ParseColor(i < chartEl.BarColorsHex.Count ? chartEl.BarColorsHex[i] : palette[i % palette.Count], SKColors.RoyalBlue); - string name = i < categories.Count ? categories[i] : $"Segment {i + 1}"; - string valLabel = i < chartEl.ValueLabels.Count && !string.IsNullOrEmpty(chartEl.ValueLabels[i]) - ? chartEl.ValueLabels[i] - : values[i].ToString("G"); - - seriesList.Add(new PieSeries - { - Name = $"{name} ({valLabel})", - Values = new double[] { values[i] }, - Fill = new SolidColorPaint(color), - Stroke = new SolidColorPaint(SKColors.White, 2 * dpiScale), - InnerRadius = innerRadius, - DataLabelsSize = 10 * dpiScale, - DataLabelsPaint = chartEl.ShowDataLabels ? new SolidColorPaint(SKColors.White) : null - }); - } - -#pragma warning disable CS0618 - var titlePaint = !string.IsNullOrWhiteSpace(chartEl.Title) - ? new LabelVisual - { - Text = chartEl.Title, - TextSize = 13.5f * dpiScale, - Paint = new SolidColorPaint(SKColors.Black) - } - : null; -#pragma warning restore CS0618 - - var chart = new SKPieChart - { - Width = width, - Height = height, - Series = seriesList, - Title = titlePaint, - LegendPosition = legendPos, - LegendTextSize = 10.5f * dpiScale, - LegendTextPaint = new SolidColorPaint(SKColors.DarkSlateGray), - MaxAngle = maxAngle, - InitialRotation = initialRotation - }; - - return chart.GetImage(); - } - - private static SKImage RenderPolarChart(PdfChartElement chartEl, int width, int height, IReadOnlyList palette, LegendPosition legendPos, float dpiScale) - { - var seriesList = new List(); - var categories = chartEl.Categories.Count > 0 ? chartEl.Categories : new List { "Speed", "Reliability", "Comfort", "Safety", "Efficiency" }; - var values = chartEl.Values.Count > 0 ? chartEl.Values : new List { 8, 9, 7, 10, 8.5 }; - - var color = ParseColor(chartEl.BarColorsHex.FirstOrDefault() ?? palette[0], SKColors.RoyalBlue); - bool isArea = chartEl.ChartType == ChartType.PolarArea; - - seriesList.Add(new PolarLineSeries - { - Name = chartEl.Title, - Values = values, - Stroke = new SolidColorPaint(color, (float)chartEl.StrokeThickness * dpiScale), - Fill = isArea ? new SolidColorPaint(color.WithAlpha(90)) : null, - GeometrySize = 7 * dpiScale, - GeometryStroke = new SolidColorPaint(color, 2 * dpiScale), - GeometryFill = new SolidColorPaint(SKColors.White), - IsClosed = true - }); - - var angleAxes = new PolarAxis[] - { - new PolarAxis - { - Labels = categories.ToArray(), - LabelsPaint = new SolidColorPaint(SKColors.DarkSlateGray), - TextSize = 10 * dpiScale, - SeparatorsPaint = chartEl.ShowGridlines ? new SolidColorPaint(SKColors.LightGray.WithAlpha(120)) { StrokeThickness = 1 * dpiScale } : null - } - }; - - var radiusAxes = new PolarAxis[] - { - new PolarAxis - { - LabelsPaint = new SolidColorPaint(SKColors.Gray), - TextSize = 9 * dpiScale, - SeparatorsPaint = chartEl.ShowGridlines ? new SolidColorPaint(SKColors.LightGray.WithAlpha(100)) { StrokeThickness = 1 * dpiScale } : null - } - }; - -#pragma warning disable CS0618 - var titlePaint = !string.IsNullOrWhiteSpace(chartEl.Title) - ? new LabelVisual - { - Text = chartEl.Title, - TextSize = 13.5f * dpiScale, - Paint = new SolidColorPaint(SKColors.Black) - } - : null; -#pragma warning restore CS0618 - - var chart = new SKPolarChart - { - Width = width, - Height = height, - Series = seriesList, - AngleAxes = angleAxes, - RadiusAxes = radiusAxes, - Title = titlePaint, - LegendPosition = legendPos, - LegendTextSize = 10.5f * dpiScale, - LegendTextPaint = new SolidColorPaint(SKColors.DarkSlateGray) - }; - - return chart.GetImage(); - } -} diff --git a/src/PdfEditorApp.Core/Analysis/PdfLayoutAnalyzer.cs b/src/PdfEditorApp.Core/Analysis/PdfLayoutAnalyzer.cs deleted file mode 100644 index 4057978..0000000 --- a/src/PdfEditorApp.Core/Analysis/PdfLayoutAnalyzer.cs +++ /dev/null @@ -1,1120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using UglyToad.PdfPig.Content; -using PdfEditorApp.Core.Deconstruction.Utils; - -namespace PdfEditorApp.Core.Analysis; - -/// -/// Represents a structured text line extracted from a PDF page with accurate font and geometry metrics. -/// -public class ExtractedPdfLine -{ - public List Words { get; } = new(); - public List Spans { get; set; } = new(); - public double Left { get; set; } - public double Right { get; set; } - public double Top { get; set; } - public double Bottom { get; set; } - public double BaselineY { get; set; } - public string Text { get; set; } = string.Empty; - public double FontSize { get; set; } = 11.0; - public string FontFamily { get; set; } = "Arial"; - public bool IsBold { get; set; } - public bool IsItalic { get; set; } - public string ColorHex { get; set; } = "#0F172A"; - - public double Rotation { get; set; } = 0.0; - public double Width => Math.Max(1, Right - Left); - public double Height => Math.Max(1, Top - Bottom); - public double MidY => (Top + Bottom) / 2.0; -} - -/// -/// Represents a coherent, editable multi-line paragraph block or single-line header -/// grouped using intelligent proximity and typographic analysis (similar to Adobe Acrobat / PDFelement). -/// -public class ExtractedPdfParagraph -{ - public List Lines { get; } = new(); - public List Spans { get; set; } = new(); - public double Left { get; set; } - public double Right { get; set; } - public double Top { get; set; } - public double Bottom { get; set; } - public string Text { get; set; } = string.Empty; - public double FontSize { get; set; } = 11.0; - public string FontFamily { get; set; } = "Arial"; - public bool IsBold { get; set; } - public bool IsItalic { get; set; } - public string ColorHex { get; set; } = "#0F172A"; - public double LineHeight { get; set; } = 1.2; - public bool IsHeading { get; set; } - public bool IsBulletOrList { get; set; } - public double Rotation { get; set; } = 0.0; - public TextAlignmentMode Alignment { get; set; } = TextAlignmentMode.Left; - - public double CanvasX { get; set; } - public double CanvasY { get; set; } - public double CanvasWidth { get; set; } - public double CanvasHeight { get; set; } -} - -/// -/// High-precision layout analyzer for PDF documents. -/// Deconstructs raw glyphs and words into structured lines, paragraphs, headings, and column flows. -/// -public static class PdfLayoutAnalyzer -{ - /// - /// Analyzes words on a PDF page and reconstructs structured, editable paragraph blocks. - /// Supports multi-orientation text (horizontal, 90° and 270° vertical marginalia). - /// - /// The PDF page to analyze. - /// Page height in PDF points (for coordinate flip). - /// Multiplier to widen column gap threshold (1.5 for landscape/ID cards). - public static List AnalyzeAndGroupPageText(Page page, double pageHeight, double columnGapMultiplier = 1.0) - { - var rawWords = page.GetWords() - .Where(w => !string.IsNullOrWhiteSpace(w.Text)) - .ToList(); - - if (rawWords.Count == 0) - { - return new List(); - } - - // Clean null characters and non-printable noise from words - var words = CleanWords(rawWords); - if (words.Count == 0) return new List(); - - var allParagraphs = new List(); - - // Partition words by dominant orientation - var horizontalWords = new List(); - var rotate270Words = new List(); - var rotate90Words = new List(); - var rotate180Words = new List(); - - foreach (var w in words) - { - var firstLetter = w.Letters.FirstOrDefault(); - var orient = firstLetter?.TextOrientation ?? TextOrientation.Horizontal; - - switch (orient) - { - case TextOrientation.Rotate270: - rotate270Words.Add(w); - break; - case TextOrientation.Rotate90: - rotate90Words.Add(w); - break; - case TextOrientation.Rotate180: - rotate180Words.Add(w); - break; - default: - horizontalWords.Add(w); - break; - } - } - - // 1. Process standard Horizontal text - if (horizontalWords.Count > 0) - { - var hLines = ExtractLinesFromWords(horizontalWords, columnGapMultiplier); - var hParas = ClusterLinesIntoParagraphs(hLines, pageHeight); - allParagraphs.AddRange(hParas); - } - - // 2. Process Rotate270 vertical text (e.g. bottom-to-top or top-to-bottom marginalia) - if (rotate270Words.Count > 0) - { - var v270Paras = ExtractVerticalParagraphs(rotate270Words, pageHeight, 270.0); - allParagraphs.AddRange(v270Paras); - } - - // 3. Process Rotate90 vertical text - if (rotate90Words.Count > 0) - { - var v90Paras = ExtractVerticalParagraphs(rotate90Words, pageHeight, 90.0); - allParagraphs.AddRange(v90Paras); - } - - // 4. Process Rotate180 text - if (rotate180Words.Count > 0) - { - var v180Paras = ExtractVerticalParagraphs(rotate180Words, pageHeight, 180.0); - allParagraphs.AddRange(v180Paras); - } - - return allParagraphs; - } - - private static List CleanWords(List rawWords) - { - var cleaned = new List(); - foreach (var w in rawWords) - { - if (string.IsNullOrWhiteSpace(w.Text)) continue; - - // Strip null chars and control codes - string txt = w.Text.Replace("\0", "").Trim(); - if (string.IsNullOrWhiteSpace(txt)) continue; - - cleaned.Add(w); - } - return cleaned; - } - - private static List ExtractVerticalParagraphs(List words, double pageHeight, double rotationAngle) - { - var paragraphs = new List(); - if (words == null || words.Count == 0) return paragraphs; - - // Group words by vertical column baseline (matching X coordinate). - // For Rotate270 text each glyph is a single character stacked bottom-to-top, - // so we use a narrow X-band threshold to bucket same-column glyphs together. - var colBuckets = new List>(); - var sortedByX = words.OrderBy(w => w.BoundingBox.Left).ToList(); - - foreach (var word in sortedByX) - { - double wordLeft = word.BoundingBox.Left; - double wordRight = word.BoundingBox.Right; - double wordMidX = (wordLeft + wordRight) / 2.0; - - List? matchingCol = null; - double bestDist = double.MaxValue; - - foreach (var col in colBuckets) - { - double colMidX = (col.Min(w => w.BoundingBox.Left) + col.Max(w => w.BoundingBox.Right)) / 2.0; - double dist = Math.Abs(wordMidX - colMidX); - // Use a tight 12pt X-band tolerance so left/right card text is never merged - if (dist <= 12.0 && dist < bestDist) - { - bestDist = dist; - matchingCol = col; - } - } - - if (matchingCol != null) - { - matchingCol.Add(word); - } - else - { - colBuckets.Add(new List { word }); - } - } - - foreach (var col in colBuckets) - { - // For Rotate270: PDF text matrix is rotated 270°, meaning each glyph's Y-axis - // points leftward. Reading order runs bottom→top in PDF coordinates. - // For Rotate90: reading order runs top→bottom in PDF coordinates. - // - // IMPORTANT: PdfPig may return inverted BoundingBox.Bottom/Top for rotated glyphs - // (Bottom > Top is possible). Use GeoMinY/GeoMaxY helpers for reliable geometric bounds. - static double GeoMinY(Word w) => Math.Min(w.BoundingBox.Bottom, w.BoundingBox.Top); - static double GeoMaxY(Word w) => Math.Max(w.BoundingBox.Bottom, w.BoundingBox.Top); - - // Sort by geometric minimum Y (bottom of glyph on page). For Rotate270 text, - // glyphs with the smallest Y value are at the visual bottom of the card. - var orderedWords = rotationAngle is 270.0 or 90.0 - ? col.OrderBy(GeoMinY).ToList() - : col.OrderByDescending(GeoMinY).ToList(); - - if (orderedWords.Count == 0) continue; - - var firstLetter = orderedWords[0].Letters.FirstOrDefault(); - double fontSize = firstLetter != null ? Math.Max(6.0, firstLetter.PointSize) : 10.0; - - // For Rotate270/90 text, gaps are computed geometrically (GeoMinY(cur) - GeoMaxY(prev)). - // Adjacent glyphs within the same word overlap slightly (gap ≈ 0 to -0.5pt). - // A word-space character produces a gap of approximately fontSize * 0.15 to 0.25. - // Use a threshold just above 0 to catch spaces without false-positives within a word. - double spaceThreshold = Math.Max(fontSize * 0.2, 0.8); - - var sb = new StringBuilder(); - for (int i = 0; i < orderedWords.Count; i++) - { - var cur = orderedWords[i]; - string txt = cur.Text.Replace("\0", "").Trim(); - if (string.IsNullOrWhiteSpace(txt)) continue; - - if (i > 0 && sb.Length > 0) - { - var prev = orderedWords[i - 1]; - // Gap between the geometric top of prev glyph and geometric bottom of cur glyph. - // Using geometric bounds ensures correctness for inverted PdfPig Rotate270 boxes. - double prevGeoMaxY = GeoMaxY(prev); - double curGeoMinY = GeoMinY(cur); - double verticalGap = curGeoMinY - prevGeoMaxY; - - // Add a space whenever the geometric gap between successive glyphs exceeds - // the word-space threshold. With correct geometric bounds: - // - Adjacent glyphs within the same word: gap ≤ 0 (overlap or touching) - // - Word-space boundary: gap ≈ fontSize * 0.15-0.25 (positive gap) - // The threshold of fontSize*0.2 (≥0.8pt) cleanly separates these cases. - if (verticalGap >= spaceThreshold) - { - sb.Append(' '); - } - } - sb.Append(txt); - } - - string fullText = UnicodeScriptDetector.NormalizeExtractedText(sb.ToString().Trim()); - if (string.IsNullOrWhiteSpace(fullText)) continue; - string fontFamily = firstLetter != null ? NormalizeFontFamily(firstLetter.FontName) : "Open Sans"; - string colorHex = PdfColorHelper.ToHex(firstLetter?.Color, null, "#0F172A"); - - double minLeft = orderedWords.Min(w => w.BoundingBox.Left); - double maxRight = orderedWords.Max(w => w.BoundingBox.Right); - // Use geometric bounds: span all geometric Y extents of glyphs in the column. - double geoMaxY = orderedWords.Max(GeoMaxY); - double geoMinY = orderedWords.Min(GeoMinY); - - // In PDF space for Rotate270/90 text: - // pdfSpanLength = full Y-axis span of the glyph column → TEXT LENGTH (how long the text run is) - // pdfSpanThick = X-axis width of the glyph column → FONT THICKNESS (cap height in points) - double pdfSpanLength = geoMaxY - geoMinY; - double pdfSpanThick = maxRight - minLeft; - - // Unrotated element dimensions: - // Width = text run length (how long the string is when rendered horizontally) - // Height = line height / font cap-height - // Add generous padding so glyphs don't clip at the bounding box edge. - double unrotatedW = Math.Max(30.0, pdfSpanLength + 12.0); - double unrotatedH = Math.Max(14.0, Math.Max(pdfSpanThick * 2.2, fontSize * 1.6)); - - // Center of the glyph cluster in PDF coordinates → Canvas coordinates. - // Use geometric center Y of the column. - double centerPdfX = (minLeft + maxRight) / 2.0; - double centerPdfY = (geoMaxY + geoMinY) / 2.0; - double centerCanvasX = centerPdfX; - double centerCanvasY = pageHeight - centerPdfY; - - // ── Rotation-aware placement ──────────────────────────────────────────────── - // Avalonia's RotateTransform pivots around the element's own center (RenderTransformOrigin=50%,50%). - // After a 90° or 270° rotation the element's visual footprint swaps Width↔Height: - // Visual width = unrotatedH (the thin font-height dimension) - // Visual height = unrotatedW (the long text-run dimension) - // - // Canvas.Left/Top (top-left of the UNROTATED box) must satisfy: - // pivot_X = canvasX + unrotatedW/2 → canvasX = centerCanvasX - unrotatedW/2 - // pivot_Y = canvasY + unrotatedH/2 → canvasY = centerCanvasY - unrotatedH/2 - // - // This places the rotation pivot exactly at (centerCanvasX, centerCanvasY), - // so the rotated text visually appears centred over the original PDF glyph cluster. - double canvasX = Math.Max(0, centerCanvasX - (unrotatedW / 2.0)); - double canvasY = Math.Max(0, centerCanvasY - (unrotatedH / 2.0)); - - paragraphs.Add(new ExtractedPdfParagraph - { - Left = minLeft, - Right = maxRight, - Top = geoMaxY, - Bottom = geoMinY, - Text = fullText, - FontSize = Math.Round(fontSize, 1), - FontFamily = fontFamily, - ColorHex = colorHex, - Rotation = rotationAngle, - CanvasX = Math.Round(canvasX, 1), - CanvasY = Math.Round(canvasY, 1), - CanvasWidth = Math.Round(unrotatedW, 1), - CanvasHeight = Math.Round(unrotatedH, 1) - }); - } - - return paragraphs; - } - - /// - /// Computes accurate geometric and typographic bounds for a word. - /// Replaces collapsed bounding boxes (e.g. Type3 fonts, OCR lines, or glyphs with zero height) - /// with proper font ascender/descender bounds relative to the actual baseline. - /// - public static (double Left, double Right, double Top, double Bottom, double BaselineY, double Height) GetWordEffectiveBounds(Word word) - { - var firstLetter = word.Letters.FirstOrDefault(); - double ptSize = firstLetter != null && firstLetter.PointSize > 0 ? firstLetter.PointSize : Math.Max(word.BoundingBox.Height, 10.0); - - double bboxH = word.BoundingBox.Height; - double rawBottom = word.BoundingBox.Bottom; - double rawTop = word.BoundingBox.Top; - double rawLeft = word.BoundingBox.Left; - double rawRight = word.BoundingBox.Right; - - // Baseline: prefer StartBaseLine.Y if non-zero, otherwise rawBottom - double baselineY = rawBottom; - if (firstLetter != null && Math.Abs(firstLetter.StartBaseLine.Y) > 0.001) - { - baselineY = firstLetter.StartBaseLine.Y; - } - - // If the bounding box height is collapsed or abnormally small (< 60% of point size) - // (common in Type3 fonts, OCR text overlays, or stripped font metrics): - if (bboxH < ptSize * 0.60 || (firstLetter != null && string.Equals(firstLetter.FontName, "Type3", StringComparison.OrdinalIgnoreCase))) - { - double ascent = ptSize * 0.78; - double descent = ptSize * 0.22; - double effTop = baselineY + ascent; - double effBottom = baselineY - descent; - return (rawLeft, rawRight, effTop, effBottom, baselineY, ptSize); - } - - return (rawLeft, rawRight, rawTop, rawBottom, baselineY, Math.Max(ptSize * 0.8, bboxH)); - } - - private class LineBucket - { - public List Words { get; } = new(); - public double BaselineY { get; set; } - public double Top { get; set; } - public double Bottom { get; set; } - } - - /// - /// Groups words into horizontal lines respecting baseline alignment, reading order, and column bounds. - /// - /// Words extracted from a PDF page. - /// Adjusts column gap sensitivity (>1 = more aggressive column splitting). - public static List ExtractLinesFromWords(List words, double columnGapMultiplier = 1.0) - { - var resultLines = new List(); - if (words == null || words.Count == 0) return resultLines; - - // Step 1: Bucket words into baseline lines - var buckets = new List(); - - // Sort roughly by vertical baseline position descending - var sortedByY = words - .OrderByDescending(w => GetWordEffectiveBounds(w).BaselineY) - .ToList(); - - foreach (var word in sortedByY) - { - var (wLeft, wRight, wTop, wBottom, wBase, wHeight) = GetWordEffectiveBounds(word); - double wordMidY = (wTop + wBottom) / 2.0; - - // For scripts with tall ascenders (Devanagari, Arabic, CJK), use a tighter midY-only match - // so characters in different rows are never bucketed together. - double threshold = Math.Max(3.0, wHeight * 0.40); - - // Find matching bucket - LineBucket? matchingBucket = null; - double bestDist = double.MaxValue; - - foreach (var b in buckets) - { - double bucketMidY = (b.Top + b.Bottom) / 2.0; - double dist = Math.Abs(wordMidY - bucketMidY); - double baseDist = Math.Abs(wBase - b.BaselineY); - - if (dist <= threshold || baseDist <= threshold) - { - if (dist < bestDist) - { - bestDist = dist; - matchingBucket = b; - } - } - } - - if (matchingBucket != null) - { - matchingBucket.Words.Add(word); - matchingBucket.Top = Math.Max(matchingBucket.Top, wTop); - matchingBucket.Bottom = Math.Min(matchingBucket.Bottom, wBottom); - matchingBucket.BaselineY = (matchingBucket.BaselineY * (matchingBucket.Words.Count - 1) + wBase) / matchingBucket.Words.Count; - } - else - { - var newBucket = new LineBucket - { - BaselineY = wBase, - Top = wTop, - Bottom = wBottom - }; - newBucket.Words.Add(word); - buckets.Add(newBucket); - } - } - - // Step 2: For each bucket, sort words left-to-right and split on column gaps - foreach (var bucket in buckets) - { - var orderedWords = bucket.Words.OrderBy(w => w.BoundingBox.Left).ToList(); - if (orderedWords.Count == 0) continue; - - var currentSegment = new List(); - - foreach (var word in orderedWords) - { - if (currentSegment.Count == 0) - { - currentSegment.Add(word); - continue; - } - - double prevRight = currentSegment.Max(w => GetWordEffectiveBounds(w).Right); - double gap = GetWordEffectiveBounds(word).Left - prevRight; - var wordBounds = GetWordEffectiveBounds(word); - double wordHeight = Math.Max(6.0, wordBounds.Height); - - // Column / element gap threshold: in typography, gaps > 25pt or > 1.1x font size represent separate columns/badges. - double baseGap = Math.Max(16.0, Math.Min(30.0, wordHeight * 1.1)); - double maxColGap = baseGap * Math.Max(0.5, columnGapMultiplier); - - if (gap > maxColGap) - { - // Split segment - var line = BuildLine(currentSegment); - if (line != null) resultLines.Add(line); - - currentSegment = new List { word }; - } - else - { - currentSegment.Add(word); - } - } - - if (currentSegment.Count > 0) - { - var line = BuildLine(currentSegment); - if (line != null) resultLines.Add(line); - } - } - - // Sort all lines in natural reading order: top-to-bottom, left-to-right - return resultLines - .OrderByDescending(l => l.Top) - .ThenBy(l => l.Left) - .ToList(); - } - - private static ExtractedPdfLine? BuildLine(List words) - { - if (words == null || words.Count == 0) return null; - - var ordered = words.OrderBy(w => GetWordEffectiveBounds(w).Left).ToList(); - var sb = new StringBuilder(); - - double minLeft = ordered.Min(w => GetWordEffectiveBounds(w).Left); - double maxRight = ordered.Max(w => GetWordEffectiveBounds(w).Right); - double maxTop = ordered.Max(w => GetWordEffectiveBounds(w).Top); - double minBottom = ordered.Min(w => GetWordEffectiveBounds(w).Bottom); - double avgBaseline = ordered.Average(w => GetWordEffectiveBounds(w).BaselineY); - - for (int i = 0; i < ordered.Count; i++) - { - if (i > 0) - { - var prev = ordered[i - 1]; - var cur = ordered[i]; - double gap = GetWordEffectiveBounds(cur).Left - GetWordEffectiveBounds(prev).Right; - double ptSize = cur.Letters.FirstOrDefault()?.PointSize ?? 10.0; - double spaceThreshold = Math.Max(1.6, ptSize * 0.18); - - bool isComplexScript = UnicodeScriptDetector.ContainsDevanagari(cur.Text) || - UnicodeScriptDetector.ContainsDevanagari(prev.Text) || - UnicodeScriptDetector.ContainsCjk(cur.Text) || - UnicodeScriptDetector.IsRtlText(cur.Text); - - if (isComplexScript) - { - // For complex scripts with split glyphs/matras, only space if gap is genuine word spacing - if (gap >= spaceThreshold) - { - sb.Append(' '); - } - } - else - { - sb.Append(' '); - } - } - sb.Append(ordered[i].Text); - } - - string text = sb.ToString().Trim(); - if (string.IsNullOrWhiteSpace(text)) return null; - - // Sample dominant font characteristics from words - var firstWord = ordered[0]; - var firstLetter = firstWord.Letters.FirstOrDefault(); - - double fontSize = 11.0; - string fontFamily = "Open Sans"; - bool isBold = false; - bool isItalic = false; - string colorHex = "#0F172A"; - - if (firstLetter != null) - { - fontSize = Math.Max(6.0, firstLetter.PointSize); - fontFamily = NormalizeFontFamily(firstLetter.FontName); - - // Unicode Script Detection: When font name normalization falls back to a generic - // Latin font (Open Sans) but the actual text contains non-Latin codepoints, use - // UnicodeScriptDetector to pick the correct world-script font from our library. - // This handles CJK, Arabic, Hebrew, Devanagari, Thai, etc. from ANY PDF source. - if (fontFamily == "Open Sans" && !string.IsNullOrEmpty(text)) - { - string detectedFont = UnicodeScriptDetector.DetectScriptFontFamily(text); - if (detectedFont != "Open Sans") - { - fontFamily = detectedFont; - } - } - - if (!string.IsNullOrEmpty(firstLetter.FontName)) - { - string fn = firstLetter.FontName.ToLowerInvariant(); - isBold = fn.Contains("bold") || fn.Contains("black") || fn.Contains("heavy") || fn.Contains("semibold") || fn.Contains("extrabold"); - isItalic = fn.Contains("italic") || fn.Contains("oblique"); - - // Type3 Identification sequence formatting (12-digit ID numbers) - if (fn.Contains("type3") && text.Length >= 12 && text.Count(char.IsDigit) >= 10) - { - isBold = true; - } - } - - colorHex = PdfColorHelper.ToHex(firstLetter.Color, null, colorHex); - } - - var rawLineSpans = new List(); - for (int i = 0; i < ordered.Count; i++) - { - var w = ordered[i]; - var let = w.Letters.FirstOrDefault(); - double wSize = let != null ? Math.Max(6.0, let.PointSize) : fontSize; - string wFam = let != null ? NormalizeFontFamily(let.FontName) : fontFamily; - bool wBold = false; - bool wItalic = false; - string wCol = colorHex; - - if (let != null && !string.IsNullOrEmpty(let.FontName)) - { - string fn = let.FontName.ToLowerInvariant(); - wBold = fn.Contains("bold") || fn.Contains("black") || fn.Contains("heavy") || fn.Contains("semibold") || fn.Contains("extrabold"); - wItalic = fn.Contains("italic") || fn.Contains("oblique"); - } - if (let?.Color != null) - { - wCol = PdfColorHelper.ToHex(let.Color, null, wCol); - } - - string wText = UnicodeScriptDetector.NormalizeExtractedText((i > 0 ? " " : "") + w.Text); - rawLineSpans.Add(new PdfTextSpan - { - Text = wText, - FontFamily = wFam, - FontSize = wSize, - IsBold = wBold, - IsItalic = wItalic, - TextColorHex = wCol - }); - } - - var normalizedLineSpans = NormalizeSpans(rawLineSpans); - - return new ExtractedPdfLine - { - Left = minLeft, - Right = maxRight, - Top = maxTop, - Bottom = minBottom, - BaselineY = avgBaseline, - Text = UnicodeScriptDetector.NormalizeExtractedText(text), - FontSize = fontSize, - FontFamily = fontFamily, - IsBold = isBold, - IsItalic = isItalic, - ColorHex = colorHex, - Spans = normalizedLineSpans - }; - } - - /// - /// Clusters lines into coherent paragraphs and standalone headings. - /// - public static List ClusterLinesIntoParagraphs(List lines, double pageHeight) - { - var paragraphs = new List(); - if (lines == null || lines.Count == 0) return paragraphs; - - // Sort lines in natural multi-column reading order (top-to-bottom of Column 1, then Column 2) - var sortedLines = SortLinesInNaturalReadingOrder(lines); - - var currentCluster = new List(); - - foreach (var line in sortedLines) - { - if (currentCluster.Count == 0) - { - currentCluster.Add(line); - continue; - } - - var prevLine = currentCluster.Last(); - - // Evaluate if this line belongs to the same paragraph - bool canCluster = ShouldClusterLines(prevLine, line); - - if (canCluster) - { - currentCluster.Add(line); - } - else - { - var para = BuildParagraphFromLines(currentCluster, pageHeight); - if (para != null) paragraphs.Add(para); - - currentCluster = new List { line }; - } - } - - if (currentCluster.Count > 0) - { - var para = BuildParagraphFromLines(currentCluster, pageHeight); - if (para != null) paragraphs.Add(para); - } - - return paragraphs; - } - - private static List SortLinesInNaturalReadingOrder(List lines) - { - if (lines.Count <= 3) return lines.OrderByDescending(l => l.Top).ThenBy(l => l.Left).ToList(); - - double minX = lines.Min(l => l.Left); - double maxX = lines.Max(l => l.Right); - double spanW = maxX - minX; - - // Detect multi-column or card layouts (2, 3, 4+ columns) on wide pages or cheat sheets - if (spanW > 240.0) - { - // Full-width headers spanning > 60% of total content width - var fullSpanHeaders = lines.Where(l => (l.Right - l.Left) >= spanW * 0.60).ToList(); - var nonSpanLines = lines.Except(fullSpanHeaders).ToList(); - - if (nonSpanLines.Count >= 4) - { - var columnGroups = new List>(); - var sortedByX = nonSpanLines.OrderBy(l => l.Left).ToList(); - - foreach (var line in sortedByX) - { - // Find a column group where line overlaps horizontally or is closely aligned with column bounds - var matchingCol = columnGroups.FirstOrDefault(col => - { - double colMinX = col.Min(cl => cl.Left); - double colMaxX = col.Max(cl => cl.Right); - return (line.Left >= colMinX - 15.0 && line.Left <= colMaxX + 15.0) || - (line.Right >= colMinX - 15.0 && line.Right <= colMaxX + 15.0); - }); - - if (matchingCol != null) - { - matchingCol.Add(line); - } - else - { - columnGroups.Add(new List { line }); - } - } - - // If multiple distinct columns were identified - if (columnGroups.Count >= 2 && columnGroups.All(c => c.Count >= 2)) - { - var ordered = new List(); - - double topContentY = nonSpanLines.Max(l => l.Top); - var topHeaders = fullSpanHeaders.Where(h => h.Bottom >= topContentY - 10.0).OrderByDescending(h => h.Top).ToList(); - var bottomFooters = fullSpanHeaders.Where(h => h.Bottom < topContentY - 10.0).OrderByDescending(h => h.Top).ToList(); - - ordered.AddRange(topHeaders); - - // Sort column groups from left to right, and lines inside each column top-to-bottom - var sortedColumns = columnGroups.OrderBy(c => c.Min(l => l.Left)).ToList(); - foreach (var col in sortedColumns) - { - ordered.AddRange(col.OrderByDescending(l => l.Top).ThenBy(l => l.Left)); - } - - ordered.AddRange(bottomFooters); - return ordered; - } - } - } - - return lines.OrderByDescending(l => l.Top).ThenBy(l => l.Left).ToList(); - } - - private static bool ShouldClusterLines(ExtractedPdfLine prev, ExtractedPdfLine next) - { - // 1. Font Size compatibility (within 1.5pt difference) - double sizeDiff = Math.Abs(prev.FontSize - next.FontSize); - if (sizeDiff > 1.5) return false; - - // 2. Bold / Weight compatibility (headings are distinct from body paragraphs) - if (prev.IsBold != next.IsBold) return false; - - // 3. Font Family compatibility - if (!string.Equals(prev.FontFamily, next.FontFamily, StringComparison.OrdinalIgnoreCase)) return false; - - // 4. Text Color compatibility - if (!string.Equals(prev.ColorHex, next.ColorHex, StringComparison.OrdinalIgnoreCase)) return false; - - // 5. Vertical line pitch calculation (distance from prev bottom to next top) - double verticalPitch = prev.Bottom - next.Top; // in PDF points, descending Y - double expectedLineGap = Math.Max(7.0, prev.FontSize * 0.95); - - // If line gap is negative (overlapping) or greater than standard paragraph spacing, break - if (verticalPitch < -2.0 || verticalPitch > expectedLineGap) return false; - - // 6. Paragraph Indentation: if next line starts indented to the right (>= 8pt), it marks a new paragraph - if (next.Left > prev.Left + 8.0) return false; - - // 7. Horizontal column & margin check - bool horizontalOverlap = (next.Left < prev.Right + 10) && (next.Right > prev.Left - 10); - if (!horizontalOverlap) return false; - - // Check left margin alignment (within 14pt for justified text variations) - double leftIndentDiff = Math.Abs(prev.Left - next.Left); - if (leftIndentDiff > 14.0) return false; - - // 7. Bullet / List Item start detection - string nextText = next.Text.Trim(); - if (nextText.StartsWith("•") || nextText.StartsWith("-") || nextText.StartsWith("—") || - (nextText.Length >= 2 && char.IsDigit(nextText[0]) && (nextText[1] == '.' || nextText[1] == ')'))) - { - // If next line starts with a list marker, break to a new paragraph/list block - return false; - } - - return true; - } - - private static ExtractedPdfParagraph? BuildParagraphFromLines(List lines, double pageHeight) - { - if (lines == null || lines.Count == 0) return null; - - double minLeft = lines.Min(l => l.Left); - double maxRight = lines.Max(l => l.Right); - double maxTop = lines.Max(l => l.Top); - double minBottom = lines.Min(l => l.Bottom); - - var firstLine = lines[0]; - double dominantFontSize = lines.GroupBy(l => Math.Round(l.FontSize, 1)).OrderByDescending(g => g.Count()).First().Key; - string dominantFontFamily = firstLine.FontFamily; - string dominantColor = firstLine.ColorHex; - bool isBold = lines.Count(l => l.IsBold) >= (lines.Count / 2.0); - bool isItalic = lines.Count(l => l.IsItalic) >= (lines.Count / 2.0); - - var sb = new StringBuilder(); - for (int i = 0; i < lines.Count; i++) - { - if (i > 0) sb.Append(Environment.NewLine); - sb.Append(lines[i].Text); - } - - string fullText = UnicodeScriptDetector.NormalizeExtractedText(sb.ToString().Trim()); - if (string.IsNullOrWhiteSpace(fullText)) return null; - - // Calculate average line pitch / height ratio - double lineHeight = 1.2; - if (lines.Count > 1) - { - double totalSpan = maxTop - minBottom; - double avgLineSpan = totalSpan / lines.Count; - if (dominantFontSize > 0) - { - lineHeight = Math.Max(1.0, Math.Min(2.0, avgLineSpan / dominantFontSize)); - } - } - - bool isHeading = (dominantFontSize >= 14.0 || (isBold && dominantFontSize >= 12.5) || (lines.Count == 1 && isBold)); - bool isBullet = fullText.StartsWith("•") || fullText.StartsWith("-") || fullText.StartsWith("—"); - bool isRtl = UnicodeScriptDetector.IsRtlText(fullText); - var alignment = isRtl ? TextAlignmentMode.Right : TextAlignmentMode.Left; - - // Convert PDF coordinates (origin bottom-left, Y goes up) to Canvas coordinates (origin top-left, Y goes down) - double canvasX = Math.Max(0, minLeft); - double canvasY = Math.Max(0, pageHeight - maxTop); - // Add a small safety padding (4-6pt) to width & height so Avalonia text controls render without unexpected line-clipping - double canvasWidth = Math.Max(30.0, (maxRight - minLeft) + 6.0); - double canvasHeight = Math.Max(16.0, (maxTop - minBottom) + 4.0); - - var paraSpans = new List(); - for (int i = 0; i < lines.Count; i++) - { - if (i > 0 && paraSpans.Count > 0) - { - paraSpans.Last().Text += "\n"; - } - if (lines[i].Spans != null && lines[i].Spans.Count > 0) - { - foreach (var s in lines[i].Spans) - { - paraSpans.Add(s.Clone()); - } - } - } - var normalizedParaSpans = NormalizeSpans(paraSpans); - - return new ExtractedPdfParagraph - { - Left = minLeft, - Right = maxRight, - Top = maxTop, - Bottom = minBottom, - Text = fullText, - FontSize = Math.Round(dominantFontSize, 1), - FontFamily = dominantFontFamily, - IsBold = isBold, - IsItalic = isItalic, - ColorHex = dominantColor, - LineHeight = Math.Round(lineHeight, 2), - IsHeading = isHeading, - IsBulletOrList = isBullet, - Alignment = alignment, - CanvasX = Math.Round(canvasX, 1), - CanvasY = Math.Round(canvasY, 1), - CanvasWidth = Math.Round(canvasWidth, 1), - CanvasHeight = Math.Round(canvasHeight, 1), - Spans = normalizedParaSpans - }; - } - - private static List NormalizeSpans(List spans) - { - if (spans.Count <= 1) return spans; - - var merged = new List(spans.Count); - PdfTextSpan? current = null; - - foreach (var s in spans) - { - if (string.IsNullOrEmpty(s.Text)) continue; - - if (current == null) - { - current = s.Clone(); - continue; - } - - if (string.Equals(current.FontFamily, s.FontFamily, StringComparison.OrdinalIgnoreCase) && - Nullable.Equals(current.FontSize, s.FontSize) && - current.IsBold == s.IsBold && - current.IsItalic == s.IsItalic && - string.Equals(current.TextColorHex, s.TextColorHex, StringComparison.OrdinalIgnoreCase)) - { - current.Text += s.Text; - } - else - { - merged.Add(current); - current = s.Clone(); - } - } - - if (current != null) - { - merged.Add(current); - } - - return merged; - } - - /// - /// Normalizes PDF font names (e.g., 'ABCDEF+Arial-BoldMT' or 'TimesNewRomanPS') to clean cross-platform font families. - /// Handles: Latin, Devanagari (Hindi/Marathi), Tamil, Telugu, Arabic, CJK (Chinese/Japanese/Korean). - /// - public static string NormalizeFontFamily(string? fontName) - { - if (string.IsNullOrWhiteSpace(fontName)) return "Open Sans"; - - string fn = fontName.ToLowerInvariant(); - - // Strip PDF subset prefixes like 'ABCDEF+' - int plusIdx = fn.IndexOf('+'); - if (plusIdx >= 0 && plusIdx < fn.Length - 1) - { - fn = fn.Substring(plusIdx + 1); - } - - // --- Latin & Common Fonts --- - if (fn.Contains("arial")) return "Arial"; - if (fn.Contains("times") || fn.Contains("libertine")) return "PT Serif"; - if (fn.Contains("cambria") || fn.Contains("crimson") || fn.Contains("garamond") || fn.Contains("palatino")) return "Crimson Text"; - if (fn.Contains("courier") || fn.Contains("firacode") || fn.Contains("robotomono")) return "Fira Code"; - if (fn.Contains("consolas") || fn.Contains("lucidasans")) return "Fira Code"; - if (fn.Contains("segoe")) return "Open Sans"; // Fallback to Open Sans for cross-platform - if (fn.Contains("roboto")) return "Roboto"; - if (fn.Contains("inter")) return "Inter"; - if (fn.Contains("calibri") || fn.Contains("cabin")) return "Cabin"; - if (fn.Contains("georgia") || fn.Contains("ptserif") || fn.Contains("pt serif")) return "PT Serif"; - if (fn.Contains("verdana")) return "Verdana"; - if (fn.Contains("helvetica")) return "Arial"; // Helvetica → Arial cross-platform - if (fn.Contains("lato")) return "Lato"; - if (fn.Contains("opensans") || fn.Contains("open sans")) return "Open Sans"; - if (fn.Contains("poppins")) return "Poppins"; - if (fn.Contains("sourcesans") || fn.Contains("source sans")) return "Source Sans 3"; - if (fn.Contains("notosans") && !fn.Contains("devanagari") && !fn.Contains("tamil") && - !fn.Contains("telugu") && !fn.Contains("arabic") && !fn.Contains("gujarati") && - !fn.Contains("kannada") && !fn.Contains("bengali") && !fn.Contains("malayalam")) - return "Noto Sans"; - if (fn.Contains("notoserif")) return "Noto Serif"; - if (fn.Contains("mono") || fn.Contains("courier")) return "Fira Code"; - if (fn.Contains("raleway")) return "Raleway"; - if (fn.Contains("nunito")) return "Nunito"; - if (fn.Contains("ubuntu")) return "Ubuntu"; - if (fn.Contains("merriweather")) return "Merriweather"; - if (fn.Contains("oswald")) return "Oswald"; - if (fn.Contains("montserrat")) return "Montserrat"; - if (fn.Contains("playfair")) return "Playfair Display"; - if (fn.Contains("josefin")) return "Josefin Sans"; - if (fn.Contains("titillium")) return "Titillium Web"; - if (fn.Contains("exo")) return "Exo 2"; - if (fn.Contains("librebaskerville") || fn.Contains("libre baskerville")) return "Libre Baskerville"; - if (fn.Contains("librefranklin") || fn.Contains("libre franklin")) return "Libre Franklin"; - if (fn.Contains("lora")) return "Lora"; - if (fn.Contains("bebas")) return "Bebas Neue"; - if (fn.Contains("cinzel")) return "Cinzel"; - if (fn.Contains("orbitron")) return "Orbitron"; - if (fn.Contains("lobster")) return "Lobster"; - if (fn.Contains("pacifico")) return "Pacifico"; - if (fn.Contains("dancingscript") || fn.Contains("dancing script")) return "Dancing Script"; - if (fn.Contains("caveat")) return "Caveat"; - if (fn.Contains("greatvibes") || fn.Contains("great vibes")) return "Great Vibes"; - if (fn.Contains("comicneue") || fn.Contains("comic")) return "Comic Neue"; - if (fn.Contains("luckiest") || fn.Contains("bungee") || fn.Contains("fredoka")) return "Poppins"; - if (fn.Contains("impact")) return "Oswald"; - if (fn.Contains("alfaslab") || fn.Contains("alfa slab")) return "Montserrat"; - - // --- Indian Scripts → mapped to Noto variants we have on disk --- - if (fn.Contains("nirmala") || fn.Contains("mangal") || fn.Contains("devanagari") || - fn.Contains("kruti") || fn.Contains("shree") || fn.Contains("aakar") || - fn.Contains("lohit") || fn.Contains("samyak") || fn.Contains("tiro") || - fn.Contains("hindi") || fn.Contains("marathi") || fn.Contains("sanskrit")) - return "Noto Sans Devanagari"; - if (fn.Contains("tamil") || fn.Contains("latha") || fn.Contains("vijaya")) - return "Noto Sans Tamil"; - if (fn.Contains("telugu") || fn.Contains("gautami") || fn.Contains("vani")) - return "Noto Sans Telugu"; - if (fn.Contains("kannada") || fn.Contains("tunga") || fn.Contains("kedage")) - return "Noto Sans Kannada"; - if (fn.Contains("malayalam") || fn.Contains("kartika") || fn.Contains("rachana")) - return "Noto Sans Malayalam"; - if (fn.Contains("gujarati") || fn.Contains("shruti") || fn.Contains("aakar")) - return "Noto Sans Gujarati"; - if (fn.Contains("punjabi") || fn.Contains("gurmukhi") || fn.Contains("raavi")) - return "Noto Sans"; - if (fn.Contains("bengali") || fn.Contains("vrinda") || fn.Contains("shonar")) - return "Noto Sans Bengali"; - if (fn.Contains("oriya") || fn.Contains("odia") || fn.Contains("kalinga")) - return "Noto Sans"; - - // --- Arabic / Urdu / Farsi --- - if (fn.Contains("arabic") || fn.Contains("urdu") || fn.Contains("farsi") || - fn.Contains("naskh") || fn.Contains("scheherazade") || fn.Contains("amiri") || - fn.Contains("traditional arabic") || fn.Contains("simplified arabic")) - return "Noto Sans Arabic"; - - // --- CJK (Chinese / Japanese / Korean) --- with proper script distinction --- - // Chinese Simplified: SimSun, SimHei, Microsoft YaHei, WenQuanYi, Noto SC - if (fn.Contains("simsun") || fn.Contains("simhei") || fn.Contains("wqy") || - fn.Contains("notosanssc") || fn.Contains("noto sans sc") || - fn.Contains("microsoftyahei") || fn.Contains("yahei") || - fn.Contains("songti") || fn.Contains("heiti") || fn.Contains("fangsong") || - fn.Contains("source han sans sc") || fn.Contains("source han sans cn")) - return "Noto Sans SC"; - // Chinese Traditional: MingLiU, PMingLiU, Microsoft JhengHei, Noto TC - if (fn.Contains("mingliu") || fn.Contains("pmingliou") || fn.Contains("kaiti") || - fn.Contains("notosanstc") || fn.Contains("noto sans tc") || - fn.Contains("microsoftjhenghei") || fn.Contains("jhenghei") || - fn.Contains("source han sans tw") || fn.Contains("source han sans hk")) - return "Noto Sans TC"; - // Japanese: MS Gothic, MS Mincho, IPAex, Meiryo, Hiragino, Noto JP - if (fn.Contains("msgothic") || fn.Contains("msmincho") || fn.Contains("ipaex") || - fn.Contains("meiryo") || fn.Contains("hiragino") || fn.Contains("yu gothic") || - fn.Contains("yugothic") || fn.Contains("yumincho") || fn.Contains("noto sans jp") || - fn.Contains("notosansjp") || fn.Contains("source han sans jp") || - fn.Contains("kozuka") || fn.Contains("morisawa")) - return "Noto Sans JP"; - // Korean: Malgun, Batang, Gulim, Dotum, Nanum, Noto KR - if (fn.Contains("malgun") || fn.Contains("batang") || fn.Contains("gulim") || - fn.Contains("dotum") || fn.Contains("nanum") || fn.Contains("nanumgothic") || - fn.Contains("notosanskr") || fn.Contains("noto sans kr") || - fn.Contains("source han sans k") || fn.Contains("apple sd gothic")) - return "Noto Sans KR"; - // General CJK fallback (unidentified CJK font) - if (fn.Contains("cjk") || fn.Contains("chinese") || fn.Contains("japanese") || fn.Contains("korean")) - return "Noto Sans SC"; - - // --- Thai --- - if (fn.Contains("thai") || fn.Contains("sarabun") || fn.Contains("thsarabun") || - fn.Contains("browallia") || fn.Contains("cordia") || fn.Contains("angsana")) - return "Noto Sans Thai"; - - // --- Hebrew --- - if (fn.Contains("hebrew") || fn.Contains("heebo") || fn.Contains("david") || - fn.Contains("miriam") || fn.Contains("frank ruehl") || fn.Contains("frankruehl")) - return "Noto Sans Hebrew"; - - // --- Greek --- - if (fn.Contains("greek") || fn.Contains("gfs") || fn.Contains("paleologos") || - fn.Contains("didot") || fn.Contains("helvetica greek")) - return "GFS Neohellenic"; - - // --- Cyrillic (Russian, Ukrainian, Bulgarian, Serbian, Macedonian) --- - if (fn.Contains("cyrillic") || fn.Contains("golos") || fn.Contains("russo") || - fn.Contains("pragmatica") || fn.Contains("paratypesans") || - fn.Contains("ptastra") || fn.Contains("ptsans")) - return "Golos Text"; - - // --- Georgian --- - if (fn.Contains("georgian") || fn.Contains("sylfaen") || fn.Contains("alk") || - fn.Contains("bpg") || fn.Contains("dejavu georgian")) - return "Noto Sans Georgian"; - - // --- Armenian --- - if (fn.Contains("armenian") || fn.Contains("mshtakan") || fn.Contains("euphemia")) - return "Noto Sans Armenian"; - - // --- Persian / Farsi --- - if (fn.Contains("persian") || fn.Contains("farsi") || fn.Contains("vazir") || - fn.Contains("iran") || fn.Contains("yekan") || fn.Contains("nazanin")) - return "Vazirmatn"; - - // --- Urdu (Nastaliq) --- - if (fn.Contains("urdu") || fn.Contains("nastaliq") || fn.Contains("noori") || fn.Contains("nafees")) - return "Noto Nastaliq Urdu"; - - // --- Myanmar --- - if (fn.Contains("myanmar") || fn.Contains("burmese") || fn.Contains("zawgyi") || fn.Contains("mon")) - return "Noto Sans Myanmar"; - - // --- Khmer --- - if (fn.Contains("khmer") || fn.Contains("cambodian") || fn.Contains("battambang")) - return "Noto Sans Khmer"; - - // --- Ethiopic --- - if (fn.Contains("ethiopic") || fn.Contains("amharic") || fn.Contains("geez") || - fn.Contains("abyssinica") || fn.Contains("nyala")) - return "Noto Sans Ethiopic"; - - return "Open Sans"; // Default: clean, widely compatible sans-serif - } -} diff --git a/src/PdfEditorApp.Core/Analysis/TableGridDetector.cs b/src/PdfEditorApp.Core/Analysis/TableGridDetector.cs deleted file mode 100644 index 9020a06..0000000 --- a/src/PdfEditorApp.Core/Analysis/TableGridDetector.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using PdfEditorApp.Core.Models.Elements; -using UglyToad.PdfPig.Content; -using UglyToad.PdfPig.Core; -using UglyToad.PdfPig.Graphics; - -namespace PdfEditorApp.Core.Analysis; - -/// -/// Result of table detection containing the reconstructed PdfTableElement -/// and the indices/bounds of swallowed lines and text blocks. -/// -public class DetectedTableResult -{ - public PdfTableElement TableElement { get; set; } = new(); - public List ConsumedPathIndices { get; } = new(); - public List ConsumedParagraphs { get; } = new(); - public PdfRectangle BoundingBox { get; set; } -} - -/// -/// Advanced Table Grid Detector and Structure Reconstructor. -/// Detects tabular grids from raw vector lines and cell alignments in 3rd-party PDFs, -/// synthesizing cohesive, editable PdfTableElement models. -/// -public static class TableGridDetector -{ - public static List DetectTables( - IReadOnlyList? paths, - List paragraphs, - double pageWidth, - double pageHeight) - { - var results = new List(); - if (paths == null || paths.Count == 0 || paragraphs == null || paragraphs.Count == 0) - return results; - - // Step 1: Extract horizontal and vertical line segments from paths - var hLines = new List<(double Y, double Left, double Right, int PathIndex)>(); - var vLines = new List<(double X, double Bottom, double Top, int PathIndex)>(); - - for (int i = 0; i < paths.Count; i++) - { - var p = paths[i]; - var bbox = p.GetBoundingRectangle(); - if (!bbox.HasValue) continue; - - var b = bbox.Value; - if (b.Width >= pageWidth * 0.98 && b.Height >= pageHeight * 0.98) continue; // Skip page border - - // Horizontal line segment: width >= 40, height <= 4 - if (b.Width >= 40.0 && b.Height <= 4.0) - { - double canvasY = pageHeight - b.Top; - hLines.Add((canvasY, b.Left, b.Right, i)); - } - // Vertical line segment: height >= 20, width <= 4 - else if (b.Height >= 20.0 && b.Width <= 4.0) - { - double canvasTop = pageHeight - b.Top; - double canvasBottom = pageHeight - b.Bottom; - vLines.Add((b.Left, canvasTop, canvasBottom, i)); - } - } - - // Need at least 2 horizontal lines to form a table - if (hLines.Count < 2) return results; - - // Group horizontal lines by proximity and overlapping X spans to find table clusters - var sortedHLines = hLines.OrderBy(h => h.Y).ToList(); - var tableHLinesGroups = new List>(); - var currentGroup = new List<(double Y, double Left, double Right, int PathIndex)>(); - - foreach (var hl in sortedHLines) - { - if (currentGroup.Count == 0) - { - currentGroup.Add(hl); - continue; - } - - var prev = currentGroup.Last(); - double yGap = hl.Y - prev.Y; - bool xOverlap = (hl.Left < prev.Right + 10) && (hl.Right > prev.Left - 10); - - // Table rows typically have pitch between 12pt and 60pt - if (yGap <= 65.0 && xOverlap) - { - currentGroup.Add(hl); - } - else - { - if (currentGroup.Count >= 3) - { - tableHLinesGroups.Add(new List<(double Y, double Left, double Right, int PathIndex)>(currentGroup)); - } - currentGroup = new List<(double Y, double Left, double Right, int PathIndex)> { hl }; - } - } - - if (currentGroup.Count >= 3) - { - tableHLinesGroups.Add(currentGroup); - } - - // Step 2: For each table line cluster, construct a structured PdfTableElement - foreach (var group in tableHLinesGroups) - { - double tableTopY = group.Min(h => h.Y); - double tableBottomY = group.Max(h => h.Y); - double tableLeftX = group.Min(h => h.Left); - double tableRightX = group.Max(h => h.Right); - double tableW = tableRightX - tableLeftX; - double tableH = tableBottomY - tableTopY; - - if (tableW < 80.0 || tableH < 30.0) continue; - - // Find all text paragraphs within table bounds - var tableParas = paragraphs.Where(p => - p.CanvasX >= tableLeftX - 10 && - p.CanvasX + p.CanvasWidth <= tableRightX + 10 && - p.CanvasY >= tableTopY - 10 && - p.CanvasY + p.CanvasHeight <= tableBottomY + 10).ToList(); - - if (tableParas.Count < 4) continue; // Need at least 4 text items to be a valid multi-cell table - - // Detect column divisions from text X positions - var distinctColXs = tableParas - .Select(p => Math.Round(p.CanvasX, -1)) // Round to nearest 10pt - .Distinct() - .OrderBy(x => x) - .ToList(); - - if (distinctColXs.Count < 2) continue; // Must have at least 2 columns - - // Group distinct rows by Y pitch - var distinctRowYs = group - .Select(h => h.Y) - .OrderBy(y => y) - .ToList(); - - int rowCount = distinctRowYs.Count - 1; - int colCount = distinctColXs.Count; - if (rowCount < 2 || colCount < 2) continue; - - var headers = new List(); - var rows = new List>(); - - // Populate header row (Row 0) - double row0Top = distinctRowYs[0]; - double row0Bottom = distinctRowYs[1]; - - for (int c = 0; c < colCount; c++) - { - double colLeft = distinctColXs[c] - 15; - double colRight = (c + 1 < colCount) ? distinctColXs[c + 1] - 5 : tableRightX + 5; - - var cellParas = tableParas.Where(p => - p.CanvasY >= row0Top - 5 && p.CanvasY <= row0Bottom + 5 && - p.CanvasX >= colLeft && p.CanvasX < colRight) - .OrderBy(p => p.CanvasX) - .ToList(); - - string headerText = string.Join(" ", cellParas.Select(p => p.Text.Trim())); - headers.Add(string.IsNullOrWhiteSpace(headerText) ? $"Col {c + 1}" : headerText); - } - - // Populate body rows (Row 1..N) - for (int r = 1; r < rowCount; r++) - { - double rTop = distinctRowYs[r]; - double rBottom = distinctRowYs[r + 1]; - var rowData = new List(); - - for (int c = 0; c < colCount; c++) - { - double colLeft = distinctColXs[c] - 15; - double colRight = (c + 1 < colCount) ? distinctColXs[c + 1] - 5 : tableRightX + 5; - - var cellParas = tableParas.Where(p => - p.CanvasY >= rTop - 5 && p.CanvasY <= rBottom + 5 && - p.CanvasX >= colLeft && p.CanvasX < colRight) - .OrderBy(p => p.CanvasX) - .ToList(); - - string cellText = string.Join(" ", cellParas.Select(p => p.Text.Trim())); - rowData.Add(cellText); - } - - rows.Add(rowData); - } - - if (headers.Count > 0 && rows.Count > 0) - { - var tableElement = new PdfTableElement - { - X = Math.Round(tableLeftX, 1), - Y = Math.Round(tableTopY, 1), - Width = Math.Round(tableW, 1), - Height = Math.Round(tableH, 1), - Headers = headers, - Rows = rows, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#CBD5E1" - }; - - var detected = new DetectedTableResult - { - TableElement = tableElement, - BoundingBox = new PdfRectangle(tableLeftX, tableBottomY, tableRightX, tableTopY) - }; - - detected.ConsumedPathIndices.AddRange(group.Select(h => h.PathIndex)); - detected.ConsumedParagraphs.AddRange(tableParas); - - results.Add(detected); - } - } - - return results; - } -} diff --git a/src/PdfEditorApp.Core/Analysis/UnicodeScriptDetector.cs b/src/PdfEditorApp.Core/Analysis/UnicodeScriptDetector.cs deleted file mode 100644 index 4d4e596..0000000 --- a/src/PdfEditorApp.Core/Analysis/UnicodeScriptDetector.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace PdfEditorApp.Core.Analysis; - -/// -/// Detects the Unicode script of text content using codepoint range analysis. -/// -/// Used as the authoritative fallback when PDF font names are ambiguous, obfuscated, -/// or use non-standard naming conventions (common in CJK, Arabic, and Indian PDFs). -/// -/// Supports 30+ world scripts including CJK (Simplified/Traditional Chinese, Japanese, Korean), -/// Indian scripts (Devanagari, Tamil, Telugu, Bengali, Gujarati, Kannada, Malayalam, Odia, Gurmukhi), -/// RTL scripts (Arabic, Hebrew, Persian/Urdu), and Southeast/Central Asian scripts. -/// -public static class UnicodeScriptDetector -{ - /// - /// Classifies the dominant Unicode script of a string. - /// Returns the best-matching FontFamily name from our embedded font library. - /// - public static string DetectScriptFontFamily(string? text) - { - if (string.IsNullOrWhiteSpace(text)) - return "Open Sans"; - - // Count codepoints per script block - var scriptCounts = new Dictionary(); - - for (int i = 0; i < text.Length;) - { - int codepoint = char.ConvertToUtf32(text, i); - i += char.IsSurrogatePair(text, i) ? 2 : 1; - - // Skip whitespace and common punctuation - if (codepoint <= 0x0040) continue; - if (IsBasicLatin(codepoint)) continue; - - var script = ClassifyCodepoint(codepoint); - if (script != UnicodeScript.Unknown) - { - scriptCounts.TryGetValue(script, out int count); - scriptCounts[script] = count + 1; - } - } - - if (scriptCounts.Count == 0) - return "Open Sans"; - - // Return the font for the dominant script - var dominant = scriptCounts.MaxBy(kvp => kvp.Value).Key; - return ScriptToFontFamily(dominant); - } - - /// - /// Normalizes extracted PDF text by decomposing typographic ligatures (ff, fi, fl, ffi, ffl, ft, st), - /// replacing non-breaking spaces with standard spaces, and stripping replacement characters. - /// Eliminates tofu boxes ("□") when rendering extracted text with standard fonts. - /// - public static string NormalizeExtractedText(string? text) - { - if (string.IsNullOrEmpty(text)) return string.Empty; - - var sb = new StringBuilder(text.Length); - for (int i = 0; i < text.Length; i++) - { - char c = text[i]; - switch (c) - { - case '\uFB00': sb.Append("ff"); break; - case '\uFB01': sb.Append("fi"); break; - case '\uFB02': sb.Append("fl"); break; - case '\uFB03': sb.Append("ffi"); break; - case '\uFB04': sb.Append("ffl"); break; - case '\uFB05': sb.Append("ft"); break; - case '\uFB06': sb.Append("st"); break; - case '\u00A0': // Non-breaking space - case '\u2007': // Figure space - case '\u202F': // Narrow no-break space - sb.Append(' '); - break; - case '\u200B': // Zero-width space - break; - case '\uFFFD': // Replacement character artifact - break; - default: - sb.Append(c); - break; - } - } - return sb.ToString(); - } - - /// - /// Quick check: does this string contain any CJK characters? - /// - public static bool ContainsCjk(string text) - { - foreach (char c in text) - { - int cp = c; - if ((cp >= 0x4E00 && cp <= 0x9FFF) || // CJK Unified Ideographs - (cp >= 0x3400 && cp <= 0x4DBF) || // CJK Extension A - (cp >= 0x20000 && cp <= 0x2A6DF) || // CJK Extension B (surrogate pair range) - (cp >= 0x3040 && cp <= 0x309F) || // Hiragana - (cp >= 0x30A0 && cp <= 0x30FF) || // Katakana - (cp >= 0xAC00 && cp <= 0xD7AF)) // Korean Hangul Syllables - return true; - } - return false; - } - - /// - /// Quick check: does this string need an RTL font? - /// - public static bool IsRtlText(string text) - { - foreach (char c in text) - { - int cp = c; - if ((cp >= 0x0600 && cp <= 0x06FF) || // Arabic - (cp >= 0x0590 && cp <= 0x05FF) || // Hebrew - (cp >= 0xFB50 && cp <= 0xFDFF) || // Arabic Presentation Forms-A - (cp >= 0xFE70 && cp <= 0xFEFF)) // Arabic Presentation Forms-B - return true; - } - return false; - } - - /// - /// Quick check: does this string contain Devanagari (Hindi/Marathi/Sanskrit)? - /// - public static bool ContainsDevanagari(string text) - { - foreach (char c in text) - { - int cp = c; - if (cp >= 0x0900 && cp <= 0x097F) return true; - } - return false; - } - - private static UnicodeScript ClassifyCodepoint(int cp) - { - // === CJK Unified Ideographs (used by Chinese, Japanese, rarely Korean) === - if (cp >= 0x4E00 && cp <= 0x9FFF) return UnicodeScript.CjkUnified; - if (cp >= 0x3400 && cp <= 0x4DBF) return UnicodeScript.CjkUnified; // Extension A - if (cp >= 0xF900 && cp <= 0xFAFF) return UnicodeScript.CjkUnified; // CJK Compatibility - - // === Japanese-Specific === - if (cp >= 0x3040 && cp <= 0x309F) return UnicodeScript.Hiragana; - if (cp >= 0x30A0 && cp <= 0x30FF) return UnicodeScript.Katakana; - if (cp >= 0x31F0 && cp <= 0x31FF) return UnicodeScript.Katakana; // Katakana Phonetic Extensions - if (cp >= 0xFF65 && cp <= 0xFF9F) return UnicodeScript.Katakana; // Halfwidth Katakana - - // === Korean === - if (cp >= 0xAC00 && cp <= 0xD7AF) return UnicodeScript.HangulSyllables; - if (cp >= 0x1100 && cp <= 0x11FF) return UnicodeScript.HangulJamo; - if (cp >= 0x3130 && cp <= 0x318F) return UnicodeScript.HangulCompatJamo; - if (cp >= 0xA960 && cp <= 0xA97F) return UnicodeScript.HangulJamoExtended; - - // === Chinese Bopomofo (Traditional Chinese phonetics) === - if (cp >= 0x02EA && cp <= 0x02EB) return UnicodeScript.TraditionalChinese; - if (cp >= 0x3100 && cp <= 0x312F) return UnicodeScript.TraditionalChinese; - if (cp >= 0x31A0 && cp <= 0x31BF) return UnicodeScript.TraditionalChinese; - - // === Chinese CJK Strokes (Traditional-specific) === - if (cp >= 0x31C0 && cp <= 0x31EF) return UnicodeScript.TraditionalChinese; - - // === Arabic === - if (cp >= 0x0600 && cp <= 0x06FF) return UnicodeScript.Arabic; - if (cp >= 0x0750 && cp <= 0x077F) return UnicodeScript.Arabic; // Arabic Supplement - if (cp >= 0x08A0 && cp <= 0x08FF) return UnicodeScript.Arabic; // Arabic Extended-A - if (cp >= 0xFB50 && cp <= 0xFDFF) return UnicodeScript.Arabic; // Arabic Presentation Forms-A - if (cp >= 0xFE70 && cp <= 0xFEFF) return UnicodeScript.Arabic; // Arabic Presentation Forms-B - if (cp >= 0x1EE00 && cp <= 0x1EEFF) return UnicodeScript.Arabic; // Arabic Mathematical - - // === Persian / Farsi (subset of Arabic block, with specific codepoints) === - // Farsi uses Arabic script with some extra chars — handled under Arabic above - - // === Hebrew === - if (cp >= 0x0590 && cp <= 0x05FF) return UnicodeScript.Hebrew; - if (cp >= 0xFB00 && cp <= 0xFB4F) return UnicodeScript.Hebrew; // Alphabetic Presentation Forms (Hebrew part) - - // === Devanagari (Hindi, Marathi, Sanskrit, Nepali) === - if (cp >= 0x0900 && cp <= 0x097F) return UnicodeScript.Devanagari; - if (cp >= 0xA8E0 && cp <= 0xA8FF) return UnicodeScript.Devanagari; // Devanagari Extended - - // === Bengali (Bangla) === - if (cp >= 0x0980 && cp <= 0x09FF) return UnicodeScript.Bengali; - - // === Gurmukhi (Punjabi) === - if (cp >= 0x0A00 && cp <= 0x0A7F) return UnicodeScript.Gurmukhi; - - // === Gujarati === - if (cp >= 0x0A80 && cp <= 0x0AFF) return UnicodeScript.Gujarati; - - // === Oriya / Odia === - if (cp >= 0x0B00 && cp <= 0x0B7F) return UnicodeScript.Odia; - - // === Tamil === - if (cp >= 0x0B80 && cp <= 0x0BFF) return UnicodeScript.Tamil; - - // === Telugu === - if (cp >= 0x0C00 && cp <= 0x0C7F) return UnicodeScript.Telugu; - - // === Kannada === - if (cp >= 0x0C80 && cp <= 0x0CFF) return UnicodeScript.Kannada; - - // === Malayalam === - if (cp >= 0x0D00 && cp <= 0x0D7F) return UnicodeScript.Malayalam; - - // === Sinhala (Sri Lanka) === - if (cp >= 0x0D80 && cp <= 0x0DFF) return UnicodeScript.Sinhala; - - // === Thai === - if (cp >= 0x0E00 && cp <= 0x0E7F) return UnicodeScript.Thai; - - // === Lao === - if (cp >= 0x0E80 && cp <= 0x0EFF) return UnicodeScript.Lao; - - // === Tibetan === - if (cp >= 0x0F00 && cp <= 0x0FFF) return UnicodeScript.Tibetan; - - // === Myanmar / Burmese === - if (cp >= 0x1000 && cp <= 0x109F) return UnicodeScript.Myanmar; - - // === Georgian === - if (cp >= 0x10A0 && cp <= 0x10FF) return UnicodeScript.Georgian; - if (cp >= 0x2D00 && cp <= 0x2D2F) return UnicodeScript.Georgian; // Georgian Supplement - - // === Ethiopic (Amharic, Tigrinya, etc.) === - if (cp >= 0x1200 && cp <= 0x137F) return UnicodeScript.Ethiopic; - if (cp >= 0x1380 && cp <= 0x139F) return UnicodeScript.Ethiopic; - - // === Armenian === - if (cp >= 0x0530 && cp <= 0x058F) return UnicodeScript.Armenian; - - // === Cyrillic (Russian, Ukrainian, Bulgarian, Serbian, etc.) === - if (cp >= 0x0400 && cp <= 0x04FF) return UnicodeScript.Cyrillic; - if (cp >= 0x0500 && cp <= 0x052F) return UnicodeScript.Cyrillic; // Cyrillic Supplement - if (cp >= 0x2DE0 && cp <= 0x2DFF) return UnicodeScript.Cyrillic; // Cyrillic Extended-A - - // === Greek === - if (cp >= 0x0370 && cp <= 0x03FF) return UnicodeScript.Greek; - if (cp >= 0x1F00 && cp <= 0x1FFF) return UnicodeScript.Greek; // Greek Extended - - // === Khmer (Cambodia) === - if (cp >= 0x1780 && cp <= 0x17FF) return UnicodeScript.Khmer; - - // === Mongolian === - if (cp >= 0x1800 && cp <= 0x18AF) return UnicodeScript.Mongolian; - - return UnicodeScript.Unknown; - } - - private static bool IsBasicLatin(int cp) - { - return cp < 0x0250; // Basic Latin + Latin-1 Supplement + Latin Extended-A+B - } - - private static string ScriptToFontFamily(UnicodeScript script) => script switch - { - // === CJK === - UnicodeScript.Hiragana => "Noto Sans JP", // Japanese hiragana → JP font - UnicodeScript.Katakana => "Noto Sans JP", // Japanese katakana → JP font - UnicodeScript.CjkUnified => "Noto Sans SC", // Default CJK → Simplified Chinese (superset) - UnicodeScript.TraditionalChinese => "Noto Sans TC", // Traditional Chinese specific - UnicodeScript.HangulSyllables => "Noto Sans KR", // Korean - UnicodeScript.HangulJamo => "Noto Sans KR", - UnicodeScript.HangulCompatJamo => "Noto Sans KR", - UnicodeScript.HangulJamoExtended => "Noto Sans KR", - - // === RTL === - UnicodeScript.Arabic => "Noto Sans Arabic", - UnicodeScript.Hebrew => "Noto Sans Hebrew", - - // === Indian Scripts === - UnicodeScript.Devanagari => "Noto Sans Devanagari", - UnicodeScript.Bengali => "Noto Sans Bengali", - UnicodeScript.Gurmukhi => "Noto Sans", // Gurmukhi → general Noto Sans - UnicodeScript.Gujarati => "Noto Sans Gujarati", - UnicodeScript.Odia => "Noto Sans", - UnicodeScript.Tamil => "Noto Sans Tamil", - UnicodeScript.Telugu => "Noto Sans Telugu", - UnicodeScript.Kannada => "Noto Sans Kannada", - UnicodeScript.Malayalam => "Noto Sans Malayalam", - UnicodeScript.Sinhala => "Noto Sans Sinhala", - UnicodeScript.Tibetan => "Noto Sans", - - // === Southeast Asian === - UnicodeScript.Thai => "Noto Sans Thai", - UnicodeScript.Lao => "Noto Sans Lao", - UnicodeScript.Myanmar => "Noto Sans Myanmar", - UnicodeScript.Khmer => "Noto Sans Khmer", - - // === Eurasian === - UnicodeScript.Georgian => "Noto Sans Georgian", - UnicodeScript.Armenian => "Noto Sans Armenian", - UnicodeScript.Ethiopic => "Noto Sans Ethiopic", - UnicodeScript.Cyrillic => "Noto Sans", // Cyrillic covered by Noto Sans Latin - UnicodeScript.Greek => "GFS Neohellenic", - UnicodeScript.Mongolian => "Noto Sans", - - _ => "Open Sans" - }; -} - -/// -/// Unicode script classification enum. -/// -public enum UnicodeScript -{ - Unknown, - - // CJK - CjkUnified, - TraditionalChinese, - Hiragana, - Katakana, - HangulSyllables, - HangulJamo, - HangulCompatJamo, - HangulJamoExtended, - - // RTL - Arabic, - Hebrew, - - // Indian - Devanagari, - Bengali, - Gurmukhi, - Gujarati, - Odia, - Tamil, - Telugu, - Kannada, - Malayalam, - Sinhala, - Tibetan, - - // Southeast Asian - Thai, - Lao, - Myanmar, - Khmer, - - // Eurasian - Georgian, - Armenian, - Ethiopic, - Cyrillic, - Greek, - Mongolian, -} diff --git a/src/PdfEditorApp.Core/Data/BatchMergeModels.cs b/src/PdfEditorApp.Core/Data/BatchMergeModels.cs deleted file mode 100644 index 71c651e..0000000 --- a/src/PdfEditorApp.Core/Data/BatchMergeModels.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Data; - -public enum BatchOutputMode -{ - SeparateFiles, - SingleMergedPdf, - ZipArchive -} - -public enum FieldTransformType -{ - None, - Currency, - Date, - Numeric, - Percentage, - Uppercase, - Lowercase, - TitleCase -} - -public class FieldMappingItem -{ - public string PlaceholderTag { get; set; } = string.Empty; - public string DataColumnName { get; set; } = string.Empty; - public FieldTransformType Transform { get; set; } = FieldTransformType.None; - public string CustomFormat { get; set; } = string.Empty; - public string DefaultValue { get; set; } = string.Empty; - public string SampleValue { get; set; } = string.Empty; - - public FieldMappingItem() { } - - public FieldMappingItem(string placeholderTag, string dataColumnName, string defaultValue = "") - { - PlaceholderTag = placeholderTag; - DataColumnName = dataColumnName; - DefaultValue = defaultValue; - } -} - -public class BatchGenerationConfig -{ - public BatchOutputMode OutputMode { get; set; } = BatchOutputMode.SeparateFiles; - public string OutputDirectory { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public string FilenamePattern { get; set; } = "Document_{{Index}}.pdf"; - public bool SkipEmptyRows { get; set; } = true; - public int? MaxRecordCount { get; set; } -} - -public class BatchProgressReport -{ - public int CurrentIndex { get; set; } - public int TotalCount { get; set; } - public string CurrentItemName { get; set; } = string.Empty; - public int SucceededCount { get; set; } - public int FailedCount { get; set; } - public string StatusMessage { get; set; } = string.Empty; - public double Percentage => TotalCount > 0 ? (double)CurrentIndex / TotalCount * 100.0 : 0; -} - -public class BatchGenerationResult -{ - public int TotalProcessed { get; set; } - public int SuccessfulCount { get; set; } - public int FailedCount { get; set; } - public List GeneratedFiles { get; set; } = new(); - public string? OutputMergedFilePath { get; set; } - public string? OutputZipFilePath { get; set; } - public List<(int RowIndex, string ErrorMessage)> Errors { get; set; } = new(); - public TimeSpan ElapsedTime { get; set; } - public bool IsSuccess => FailedCount == 0 && SuccessfulCount > 0; -} diff --git a/src/PdfEditorApp.Core/Data/DataBindingService.cs b/src/PdfEditorApp.Core/Data/DataBindingService.cs deleted file mode 100644 index 786557f..0000000 --- a/src/PdfEditorApp.Core/Data/DataBindingService.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Core.Data; - -public class DataBindingService : IDataBindingService -{ - public void ApplyToChart(DataMatrix matrix, PdfChartElement chart, int categoryColIndex = 0, List? valueColIndices = null) - { - if (matrix.RowCount == 0 || matrix.ColumnCount == 0) return; - - categoryColIndex = Math.Clamp(categoryColIndex, 0, matrix.ColumnCount - 1); - valueColIndices ??= Enumerable.Range(0, matrix.ColumnCount).Where(i => i != categoryColIndex).ToList(); - if (valueColIndices.Count == 0) - { - valueColIndices.Add(categoryColIndex == 0 ? Math.Min(1, matrix.ColumnCount - 1) : 0); - } - - var categories = matrix.GetColumnValues(categoryColIndex); - var palette = LiveChartsRenderer.GetPaletteHexColors(chart.Palette); - - chart.Categories = categories; - - if (valueColIndices.Count == 1) - { - // Single Series mapping - int valCol = valueColIndices[0]; - var rawValues = matrix.GetColumnValues(valCol); - var values = new List(); - var valueLabels = new List(); - var barColors = new List(); - - for (int r = 0; r < rawValues.Count; r++) - { - string raw = rawValues[r]; - if (DataMatrix.TryParseNumeric(raw, out double num)) - { - values.Add(num); - } - else - { - values.Add(0.0); - } - valueLabels.Add(raw); - barColors.Add(palette[r % palette.Count]); - } - - chart.Values = values; - chart.ValueLabels = valueLabels; - chart.BarColorsHex = barColors; - chart.MultiSeries = new List(); - - if (string.IsNullOrEmpty(chart.Title) || chart.Title.Contains("Chart Analysis")) - { - chart.Title = matrix.Headers.ElementAtOrDefault(valCol) ?? "Chart Analysis"; - } - } - else - { - // Multi-Series mapping (e.g. Sales, Target, Expense) - var multiSeriesList = new List(); - var firstColValues = new List(); - var firstColLabels = new List(); - var barColors = new List(); - - for (int s = 0; s < valueColIndices.Count; s++) - { - int valCol = valueColIndices[s]; - string seriesName = matrix.Headers.ElementAtOrDefault(valCol) ?? $"Series {s + 1}"; - string seriesColor = palette[s % palette.Count]; - var rawValues = matrix.GetColumnValues(valCol); - var numValues = new List(); - - for (int r = 0; r < rawValues.Count; r++) - { - if (DataMatrix.TryParseNumeric(rawValues[r], out double num)) - { - numValues.Add(num); - } - else - { - numValues.Add(0.0); - } - - if (s == 0) - { - firstColValues.Add(numValues[r]); - firstColLabels.Add(rawValues[r]); - barColors.Add(palette[r % palette.Count]); - } - } - - multiSeriesList.Add(new ChartSeriesItem - { - Name = seriesName, - Values = numValues, - ColorHex = seriesColor - }); - } - - chart.MultiSeries = multiSeriesList; - chart.Values = firstColValues; - chart.ValueLabels = firstColLabels; - chart.BarColorsHex = barColors; - } - } - - public void ApplyToTable(DataMatrix matrix, PdfTableElement table) - { - if (matrix.ColumnCount == 0) return; - - table.Headers = new List(matrix.Headers); - table.Rows = new List>(); - - foreach (var r in matrix.Rows) - { - table.Rows.Add(new List(r)); - } - - // Auto-scale height - double calculatedHeight = Math.Max(table.Height, 35 + (matrix.RowCount * 26)); - table.Height = calculatedHeight; - } - - public DataMatrix ExtractFromTable(PdfTableElement table) - { - var headers = new List(table.Headers); - var rows = new List>(); - - foreach (var r in table.Rows) - { - rows.Add(new List(r)); - } - - return new DataMatrix(headers, rows); - } - - public DataMatrix ExtractFromChart(PdfChartElement chart) - { - var headers = new List { "Category" }; - var rows = new List>(); - - if (chart.MultiSeries != null && chart.MultiSeries.Count > 0) - { - foreach (var series in chart.MultiSeries) - { - headers.Add(series.Name); - } - - for (int r = 0; r < chart.Categories.Count; r++) - { - var row = new List { chart.Categories[r] }; - foreach (var series in chart.MultiSeries) - { - row.Add(r < series.Values.Count ? series.Values[r].ToString("G") : "0"); - } - rows.Add(row); - } - } - else - { - headers.Add(string.IsNullOrWhiteSpace(chart.Title) ? "Value" : chart.Title); - for (int r = 0; r < chart.Categories.Count; r++) - { - string cat = chart.Categories[r]; - string val = r < chart.ValueLabels.Count && !string.IsNullOrWhiteSpace(chart.ValueLabels[r]) - ? chart.ValueLabels[r] - : (r < chart.Values.Count ? chart.Values[r].ToString("G") : "0"); - - rows.Add(new List { cat, val }); - } - } - - return new DataMatrix(headers, rows); - } - - public PdfChartElement ConvertTableToChart(PdfTableElement table, ChartType chartType = ChartType.BarColumn) - { - var matrix = ExtractFromTable(table); - var chart = new PdfChartElement - { - X = table.X, - Y = table.Y, - Width = Math.Max(table.Width, 380), - Height = Math.Max(table.Height, 220), - Title = matrix.Headers.Count > 1 ? $"{matrix.Headers[1]} Analysis" : "Table Data Chart", - ChartType = chartType, - Palette = ChartPalette.CorporateBlue - }; - - // Determine category column (first text column) and numeric columns - int catCol = 0; - var valCols = new List(); - - for (int c = 0; c < matrix.ColumnCount; c++) - { - var type = matrix.InferColumnType(c); - if (type == DataColumnType.Number || type == DataColumnType.Currency || type == DataColumnType.Percentage) - { - valCols.Add(c); - } - } - - if (valCols.Count == 0) - { - valCols = Enumerable.Range(1, Math.Max(0, matrix.ColumnCount - 1)).ToList(); - } - - ApplyToChart(matrix, chart, catCol, valCols); - return chart; - } - - public PdfTableElement ConvertChartToTable(PdfChartElement chart) - { - var matrix = ExtractFromChart(chart); - var table = new PdfTableElement - { - X = chart.X, - Y = chart.Y, - Width = Math.Max(chart.Width, 350), - Height = Math.Max(chart.Height, 35 + (matrix.RowCount * 26)), - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8F9FA", - BorderColorHex = "#E1DFDD" - }; - - ApplyToTable(matrix, table); - return table; - } -} diff --git a/src/PdfEditorApp.Core/Data/DataMatrix.cs b/src/PdfEditorApp.Core/Data/DataMatrix.cs deleted file mode 100644 index fae3360..0000000 --- a/src/PdfEditorApp.Core/Data/DataMatrix.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; - -namespace PdfEditorApp.Core.Data; - -public enum DataColumnType -{ - Text, - Number, - Currency, - Percentage, - Date -} - -public class DataMatrix -{ - public List Headers { get; set; } = new(); - public List> Rows { get; set; } = new(); - - public int ColumnCount => Headers.Count; - public int RowCount => Rows.Count; - - public DataMatrix() - { - } - - public DataMatrix(IEnumerable headers, IEnumerable>? rows = null) - { - Headers = new List(headers); - if (rows != null) - { - foreach (var row in rows) - { - Rows.Add(new List(row)); - } - } - } - - public string GetCellValue(int rowIndex, int colIndex) - { - if (rowIndex < 0 || rowIndex >= Rows.Count) return string.Empty; - var row = Rows[rowIndex]; - if (colIndex < 0 || colIndex >= row.Count) return string.Empty; - return row[colIndex] ?? string.Empty; - } - - public string GetCell(int rowIndex, int colIndex) => GetCellValue(rowIndex, colIndex); - public void SetCell(int rowIndex, int colIndex, string value) => SetCellValue(rowIndex, colIndex, value); - - public void SetCellValue(int rowIndex, int colIndex, string value) - { - while (rowIndex >= Rows.Count) - { - var newRow = new List(); - while (newRow.Count < ColumnCount) newRow.Add(string.Empty); - Rows.Add(newRow); - } - - var targetRow = Rows[rowIndex]; - while (colIndex >= targetRow.Count) - { - targetRow.Add(string.Empty); - } - - targetRow[colIndex] = value; - } - - public List GetColumnValues(int colIndex) - { - var result = new List(RowCount); - for (int r = 0; r < RowCount; r++) - { - result.Add(GetCellValue(r, colIndex)); - } - return result; - } - - public void AddRow(IEnumerable cells) - { - var rowList = new List(cells); - while (rowList.Count < ColumnCount) - { - rowList.Add(string.Empty); - } - Rows.Add(rowList); - } - - public void AddColumn(string header, IEnumerable? initialValues = null) - { - Headers.Add(string.IsNullOrWhiteSpace(header) ? $"Col {ColumnCount + 1}" : header); - var valuesList = initialValues?.ToList(); - - for (int r = 0; r < RowCount; r++) - { - string val = (valuesList != null && r < valuesList.Count) ? valuesList[r] : string.Empty; - Rows[r].Add(val); - } - } - - public void AddColumn(string header, string defaultValue) - { - Headers.Add(string.IsNullOrWhiteSpace(header) ? $"Col {ColumnCount + 1}" : header); - for (int r = 0; r < RowCount; r++) - { - Rows[r].Add(defaultValue ?? string.Empty); - } - } - - public void RemoveRow(int rowIndex) - { - if (rowIndex >= 0 && rowIndex < Rows.Count) - { - Rows.RemoveAt(rowIndex); - } - } - - public void RemoveColumn(int colIndex) - { - if (colIndex >= 0 && colIndex < Headers.Count) - { - Headers.RemoveAt(colIndex); - foreach (var row in Rows) - { - if (colIndex < row.Count) - { - row.RemoveAt(colIndex); - } - } - } - } - - public DataColumnType InferColumnType(int colIndex) - { - if (RowCount == 0) return DataColumnType.Text; - - int numCount = 0; - int currencyCount = 0; - int pctCount = 0; - int dateCount = 0; - int sampleCount = Math.Min(RowCount, 50); - - for (int r = 0; r < sampleCount; r++) - { - string val = GetCellValue(r, colIndex).Trim(); - if (string.IsNullOrEmpty(val)) continue; - - if (val.EndsWith("%") && TryParseNumeric(val.TrimEnd('%'), out _)) - { - pctCount++; - } - else if ((val.StartsWith("$") || val.StartsWith("€") || val.StartsWith("£") || val.StartsWith("¥") || val.StartsWith("₹")) && - TryParseNumeric(val.Substring(1), out _)) - { - currencyCount++; - } - else if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.None, out _)) - { - dateCount++; - } - else if (TryParseNumeric(val, out _)) - { - numCount++; - } - } - - if (pctCount > sampleCount * 0.6) return DataColumnType.Percentage; - if (currencyCount > sampleCount * 0.6) return DataColumnType.Currency; - if (dateCount > sampleCount * 0.6) return DataColumnType.Date; - if (numCount > sampleCount * 0.6) return DataColumnType.Number; - - return DataColumnType.Text; - } - - public static bool TryParseNumeric(string? raw, out double result) - { - result = 0; - if (string.IsNullOrWhiteSpace(raw)) return false; - - string sanitized = Regex.Replace(raw.Trim(), @"[$,€£¥₹\s]", ""); - if (sanitized.EndsWith("%")) - { - sanitized = sanitized.TrimEnd('%'); - if (double.TryParse(sanitized, NumberStyles.Any, CultureInfo.InvariantCulture, out var pct)) - { - result = pct; - return true; - } - } - - return double.TryParse(sanitized, NumberStyles.Any, CultureInfo.InvariantCulture, out result) || - double.TryParse(sanitized, NumberStyles.Any, CultureInfo.CurrentCulture, out result); - } - - public DataMatrix Clone() - { - var clone = new DataMatrix(Headers); - foreach (var r in Rows) - { - clone.Rows.Add(new List(r)); - } - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Data/DataMergeEngine.cs b/src/PdfEditorApp.Core/Data/DataMergeEngine.cs deleted file mode 100644 index 943a0e9..0000000 --- a/src/PdfEditorApp.Core/Data/DataMergeEngine.cs +++ /dev/null @@ -1,396 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Core.Data; - -public class DataMergeEngine : IDataMergeEngine -{ - private static readonly Regex PlaceholderRegex = new( - @"\{\{\s*([a-zA-Z0-9_\.\-]+)\s*(?:\:\s*([^\}\?]+?)\s*)?(?:\?\?\s*([^\}]+?)\s*)?\}\}", - RegexOptions.Compiled); - - private static readonly NumberFormatInfo InrNumberFormat = new() - { - NumberGroupSizes = [3, 2], - NumberGroupSeparator = ",", - NumberDecimalSeparator = ".", - NumberDecimalDigits = 2 - }; - - public IReadOnlyList DetectPlaceholders(PdfDocumentModel template) - { - if (template == null) return Array.Empty(); - - var foundTags = new HashSet(StringComparer.OrdinalIgnoreCase); - - void ScanString(string? text) - { - if (string.IsNullOrEmpty(text)) return; - var matches = PlaceholderRegex.Matches(text); - foreach (Match m in matches) - { - if (m.Groups[1].Success) - { - string tag = m.Groups[1].Value.Trim(); - if (!string.IsNullOrEmpty(tag)) - { - foundTags.Add(tag); - } - } - } - } - - // 1. Document Metadata - ScanString(template.Title); - ScanString(template.Author); - ScanString(template.Subject); - ScanString(template.Keywords); - - // 2. Pages - foreach (var page in template.Pages) - { - ScanString(page.HeaderLeft); - ScanString(page.HeaderCenter); - ScanString(page.HeaderRight); - ScanString(page.FooterLeft); - ScanString(page.FooterCenter); - ScanString(page.FooterRight); - - if (page.Watermark != null) - { - ScanString(page.Watermark.Text); - } - - foreach (var element in page.Elements) - { - switch (element) - { - case PdfTextElement textEl: - ScanString(textEl.Text); - if (textEl.Spans != null) - { - foreach (var span in textEl.Spans) - { - ScanString(span.Text); - } - } - ScanString(textEl.TextColorHex); - ScanString(textEl.BackgroundColorHex); - break; - - case PdfQrCodeElement qrEl: - ScanString(qrEl.Content); - ScanString(qrEl.Label); - break; - - case PdfBarcodeElement barEl: - ScanString(barEl.CodeValue); - break; - - case PdfImageElement imgEl: - ScanString(imgEl.ImagePath); - ScanString(imgEl.AltText); - break; - - case PdfTableElement tableEl: - foreach (var header in tableEl.Headers) - { - ScanString(header); - } - foreach (var row in tableEl.Rows) - { - foreach (var cell in row) - { - ScanString(cell); - } - } - break; - - case PdfChartElement chartEl: - ScanString(chartEl.Title); - foreach (var cat in chartEl.Categories) - { - ScanString(cat); - } - break; - - case PdfShapeElement shapeEl: - ScanString(shapeEl.Label); - break; - } - } - } - - return foundTags.OrderBy(t => t).ToList(); - } - - public PdfDocumentModel HydrateDocument(PdfDocumentModel template, IReadOnlyDictionary record, DataMergeOptions? options = null) - { - if (template == null) throw new ArgumentNullException(nameof(template)); - record ??= new Dictionary(); - options ??= new DataMergeOptions(); - - // 1. Deep clone template - var doc = template.Clone(); - - // 2. Hydrate Document Metadata - doc.Title = EvaluateText(doc.Title, record, options); - doc.Author = EvaluateText(doc.Author, record, options); - doc.Subject = EvaluateText(doc.Subject, record, options); - doc.Keywords = EvaluateText(doc.Keywords, record, options); - doc.ModifiedDate = DateTime.Now; - - // 3. Hydrate Pages - foreach (var page in doc.Pages) - { - page.HeaderLeft = EvaluateText(page.HeaderLeft, record, options); - page.HeaderCenter = EvaluateText(page.HeaderCenter, record, options); - page.HeaderRight = EvaluateText(page.HeaderRight, record, options); - page.FooterLeft = EvaluateText(page.FooterLeft, record, options); - page.FooterCenter = EvaluateText(page.FooterCenter, record, options); - page.FooterRight = EvaluateText(page.FooterRight, record, options); - - if (page.Watermark != null) - { - page.Watermark.Text = EvaluateText(page.Watermark.Text, record, options); - } - - foreach (var element in page.Elements) - { - HydrateElement(element, record, options); - } - } - - return doc; - } - - private void HydrateElement(PdfElementBase element, IReadOnlyDictionary record, DataMergeOptions options) - { - switch (element) - { - case PdfTextElement textEl: - textEl.Text = EvaluateText(textEl.Text, record, options); - if (textEl.Spans != null && textEl.Spans.Count > 0) - { - foreach (var span in textEl.Spans) - { - span.Text = EvaluateText(span.Text, record, options); - } - textEl.SynchronizePlainTextFromSpans(); - } - if (textEl.TextColorHex.Contains("{{")) - { - string evaluatedColor = EvaluateText(textEl.TextColorHex, record, options); - if (evaluatedColor.StartsWith("#") && (evaluatedColor.Length == 7 || evaluatedColor.Length == 9)) - { - textEl.TextColorHex = evaluatedColor; - } - } - if (textEl.BackgroundColorHex.Contains("{{")) - { - string evaluatedBg = EvaluateText(textEl.BackgroundColorHex, record, options); - if (evaluatedBg.StartsWith("#") && (evaluatedBg.Length == 7 || evaluatedBg.Length == 9)) - { - textEl.BackgroundColorHex = evaluatedBg; - } - } - break; - - case PdfQrCodeElement qrEl: - qrEl.Content = EvaluateText(qrEl.Content, record, options); - qrEl.Label = EvaluateText(qrEl.Label, record, options); - break; - - case PdfBarcodeElement barEl: - barEl.CodeValue = EvaluateText(barEl.CodeValue, record, options); - break; - - case PdfImageElement imgEl: - if (!string.IsNullOrEmpty(imgEl.ImagePath) && imgEl.ImagePath.Contains("{{")) - { - string resolvedPath = EvaluateText(imgEl.ImagePath, record, options); - imgEl.ImagePath = resolvedPath; - } - if (!string.IsNullOrEmpty(imgEl.Base64Data) && imgEl.Base64Data.Contains("{{")) - { - string resolvedBase64 = EvaluateText(imgEl.Base64Data, record, options); - imgEl.Base64Data = resolvedBase64; - } - imgEl.AltText = EvaluateText(imgEl.AltText, record, options); - break; - - case PdfTableElement tableEl: - for (int h = 0; h < tableEl.Headers.Count; h++) - { - tableEl.Headers[h] = EvaluateText(tableEl.Headers[h], record, options); - } - for (int r = 0; r < tableEl.Rows.Count; r++) - { - var row = tableEl.Rows[r]; - for (int c = 0; c < row.Count; c++) - { - row[c] = EvaluateText(row[c], record, options); - } - } - break; - - case PdfChartElement chartEl: - chartEl.Title = EvaluateText(chartEl.Title, record, options); - for (int i = 0; i < chartEl.Categories.Count; i++) - { - chartEl.Categories[i] = EvaluateText(chartEl.Categories[i], record, options); - } - break; - - case PdfShapeElement shapeEl: - if (!string.IsNullOrEmpty(shapeEl.Label)) - { - shapeEl.Label = EvaluateText(shapeEl.Label, record, options); - } - break; - } - } - - public string EvaluateText(string? templateText, IReadOnlyDictionary record, DataMergeOptions? options = null) - { - if (string.IsNullOrEmpty(templateText)) return string.Empty; - if (!templateText.Contains("{{")) return templateText; - - options ??= new DataMergeOptions(); - - return PlaceholderRegex.Replace(templateText, match => - { - string fieldName = match.Groups[1].Value.Trim(); - string format = match.Groups[2].Success ? match.Groups[2].Value.Trim() : string.Empty; - string fallback = match.Groups[3].Success ? match.Groups[3].Value.Trim() : options.DefaultFallbackValue; - - string? rawValue = FindRecordValue(record, fieldName, options.CaseInsensitiveLookup); - - if (string.IsNullOrEmpty(rawValue)) - { - return !string.IsNullOrEmpty(fallback) - ? fallback - : (options.PreserveUnmatchedPlaceholders ? match.Value : string.Empty); - } - - return FormatValue(rawValue, format); - }); - } - - private static string? FindRecordValue(IReadOnlyDictionary record, string fieldName, bool caseInsensitive) - { - if (record.TryGetValue(fieldName, out var directVal)) - { - return directVal; - } - - if (caseInsensitive) - { - foreach (var kvp in record) - { - if (string.Equals(kvp.Key, fieldName, StringComparison.OrdinalIgnoreCase)) - { - return kvp.Value; - } - - // Also check normalized name (stripping underscores, spaces, dashes) - if (NormalizeFieldName(kvp.Key) == NormalizeFieldName(fieldName)) - { - return kvp.Value; - } - } - } - - return null; - } - - private static string NormalizeFieldName(string name) - { - return Regex.Replace(name.Trim().ToLowerInvariant(), @"[\s_\-\.]", ""); - } - - private static string FormatValue(string rawValue, string format) - { - if (string.IsNullOrEmpty(format)) return rawValue; - - // 1. Case Transforms - switch (format.ToLowerInvariant()) - { - case "upper": - case "uppercase": - return rawValue.ToUpperInvariant(); - - case "lower": - case "lowercase": - return rawValue.ToLowerInvariant(); - - case "title": - case "titlecase": - return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(rawValue.ToLowerInvariant()); - - case "cap": - case "capitalize": - if (rawValue.Length == 0) return rawValue; - return char.ToUpperInvariant(rawValue[0]) + (rawValue.Length > 1 ? rawValue.Substring(1) : ""); - } - - // 2. Currency Formatting: "C", "currency", "currency:INR", "currency:EUR", "$" - if (format.StartsWith("currency", StringComparison.OrdinalIgnoreCase) || - format.Equals("C", StringComparison.OrdinalIgnoreCase) || - format.Equals("$", StringComparison.OrdinalIgnoreCase)) - { - if (DataMatrix.TryParseNumeric(rawValue, out double amount)) - { - if (format.Contains(':')) - { - string currencyCode = format.Split(':')[1].Trim().ToUpperInvariant(); - return currencyCode switch - { - "INR" or "RS" => amount < 0 ? $"-₹{Math.Abs(amount).ToString("N2", InrNumberFormat)}" : $"₹{amount.ToString("N2", InrNumberFormat)}", - "EUR" => amount < 0 ? $"-€{Math.Abs(amount).ToString("N2", CultureInfo.InvariantCulture)}" : $"€{amount.ToString("N2", CultureInfo.InvariantCulture)}", - "GBP" => amount < 0 ? $"-£{Math.Abs(amount).ToString("N2", CultureInfo.InvariantCulture)}" : $"£{amount.ToString("N2", CultureInfo.InvariantCulture)}", - "JPY" => amount < 0 ? $"-¥{Math.Abs(amount).ToString("N0", CultureInfo.InvariantCulture)}" : $"¥{amount.ToString("N0", CultureInfo.InvariantCulture)}", - _ => amount < 0 ? $"-${Math.Abs(amount).ToString("N2", CultureInfo.InvariantCulture)}" : $"${amount.ToString("N2", CultureInfo.InvariantCulture)}" - }; - } - return amount.ToString("C", CultureInfo.CurrentCulture); - } - return rawValue; - } - - // 3. Numeric Formatting: "N0", "N2", "F2", "P1", etc. - if (DataMatrix.TryParseNumeric(rawValue, out double numVal)) - { - try - { - return numVal.ToString(format, CultureInfo.InvariantCulture); - } - catch - { - // Fallback - } - } - - // 4. Date Formatting: "yyyy-MM-dd", "MMM dd, yyyy", "dd/MM/yyyy", etc. - if (DateTime.TryParse(rawValue, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) || - DateTime.TryParse(rawValue, CultureInfo.CurrentCulture, DateTimeStyles.None, out dt)) - { - try - { - return dt.ToString(format, CultureInfo.InvariantCulture); - } - catch - { - // Fallback - } - } - - return rawValue; - } -} diff --git a/src/PdfEditorApp.Core/Data/DataSourceService.cs b/src/PdfEditorApp.Core/Data/DataSourceService.cs deleted file mode 100644 index fc7dbaf..0000000 --- a/src/PdfEditorApp.Core/Data/DataSourceService.cs +++ /dev/null @@ -1,536 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Spreadsheet; - -namespace PdfEditorApp.Core.Data; - -public class DataSourceService : IDataSourceService -{ - private static readonly HttpClient SharedHttpClient = new() - { - Timeout = TimeSpan.FromSeconds(20) - }; - - public DataMatrix ParseCsv(string text, char? delimiter = null, bool firstRowIsHeader = true) - { - if (string.IsNullOrWhiteSpace(text)) return new DataMatrix(); - - char actualDelimiter = delimiter ?? DetectDelimiter(text); - var parsedRows = ParseDelimitedText(text, actualDelimiter); - - if (parsedRows.Count == 0) return new DataMatrix(); - - var headers = new List(); - var rows = new List>(); - - int startRow = 0; - int maxCols = parsedRows.Max(r => r.Count); - - if (firstRowIsHeader && parsedRows.Count > 0) - { - var headerRow = parsedRows[0]; - for (int i = 0; i < maxCols; i++) - { - string h = (i < headerRow.Count && !string.IsNullOrWhiteSpace(headerRow[i])) ? headerRow[i].Trim() : $"Column {i + 1}"; - headers.Add(h); - } - startRow = 1; - } - else - { - for (int i = 0; i < maxCols; i++) - { - headers.Add($"Column {i + 1}"); - } - } - - for (int r = startRow; r < parsedRows.Count; r++) - { - var row = parsedRows[r]; - var normalizedRow = new List(); - for (int c = 0; c < maxCols; c++) - { - normalizedRow.Add(c < row.Count ? row[c].Trim() : string.Empty); - } - // Skip entirely empty rows - if (normalizedRow.Any(v => !string.IsNullOrWhiteSpace(v))) - { - rows.Add(normalizedRow); - } - } - - return new DataMatrix(headers, rows); - } - - public DataMatrix ParseTsv(string text, bool firstRowIsHeader = true) - { - return ParseCsv(text, '\t', firstRowIsHeader); - } - - public DataMatrix ParseJson(string jsonText, string? jsonPath = null) - { - if (string.IsNullOrWhiteSpace(jsonText)) return new DataMatrix(); - - using var doc = JsonDocument.Parse(jsonText); - var root = doc.RootElement; - - // Navigate jsonPath if supplied (e.g. "data.items" or "results") - if (!string.IsNullOrWhiteSpace(jsonPath)) - { - var segments = jsonPath.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (var segment in segments) - { - if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty(segment, out var child)) - { - root = child; - } - else - { - break; - } - } - } - - // If root is an Object containing an array property (like {"data": [...]}), auto-unwrap the first array - if (root.ValueKind == JsonValueKind.Object) - { - foreach (var prop in root.EnumerateObject()) - { - if (prop.Value.ValueKind == JsonValueKind.Array) - { - root = prop.Value; - break; - } - } - } - - if (root.ValueKind != JsonValueKind.Array) - { - // If single object, treat as 1-row matrix - if (root.ValueKind == JsonValueKind.Object) - { - var headers = new List(); - var row = new List(); - foreach (var prop in root.EnumerateObject()) - { - headers.Add(prop.Name); - row.Add(prop.Value.ToString()); - } - return new DataMatrix(headers, new[] { row }); - } - return new DataMatrix(); - } - - var arrayItems = root.EnumerateArray().ToList(); - if (arrayItems.Count == 0) return new DataMatrix(); - - // Case 1: Array of Objects: [{"colA": 1, "colB": 2}, ...] - if (arrayItems[0].ValueKind == JsonValueKind.Object) - { - var headerSet = new HashSet(); - var headers = new List(); - - // Collect all unique property names across all items - foreach (var item in arrayItems) - { - if (item.ValueKind == JsonValueKind.Object) - { - foreach (var prop in item.EnumerateObject()) - { - if (headerSet.Add(prop.Name)) - { - headers.Add(prop.Name); - } - } - } - } - - var rows = new List>(); - foreach (var item in arrayItems) - { - var row = new List(); - if (item.ValueKind == JsonValueKind.Object) - { - foreach (var h in headers) - { - if (item.TryGetProperty(h, out var val)) - { - row.Add(val.ToString()); - } - else - { - row.Add(string.Empty); - } - } - } - rows.Add(row); - } - - return new DataMatrix(headers, rows); - } - - // Case 2: 2D Array: [["Header1", "Header2"], ["Val1", "Val2"]] - if (arrayItems[0].ValueKind == JsonValueKind.Array) - { - var headers = new List(); - var rows = new List>(); - - var firstRow = arrayItems[0].EnumerateArray().Select(e => e.ToString()).ToList(); - int maxCols = arrayItems.Max(item => item.ValueKind == JsonValueKind.Array ? item.GetArrayLength() : 0); - - for (int i = 0; i < maxCols; i++) - { - headers.Add(i < firstRow.Count && !string.IsNullOrWhiteSpace(firstRow[i]) ? firstRow[i] : $"Column {i + 1}"); - } - - for (int r = 1; r < arrayItems.Count; r++) - { - var item = arrayItems[r]; - var row = new List(); - if (item.ValueKind == JsonValueKind.Array) - { - var cells = item.EnumerateArray().Select(e => e.ToString()).ToList(); - for (int c = 0; c < maxCols; c++) - { - row.Add(c < cells.Count ? cells[c] : string.Empty); - } - } - rows.Add(row); - } - - return new DataMatrix(headers, rows); - } - - // Case 3: 1D Primitive Array: [10, 20, 30, 40] - { - var headers = new List { "Value" }; - var rows = arrayItems.Select(item => new List { item.ToString() }).ToList(); - return new DataMatrix(headers, rows); - } - } - - public List GetExcelSheetNames(Stream stream) - { - var names = new List(); - try - { - using var doc = SpreadsheetDocument.Open(stream, false); - var workbookPart = doc.WorkbookPart; - if (workbookPart?.Workbook?.Sheets != null) - { - foreach (Sheet sheet in workbookPart.Workbook.Sheets.Elements()) - { - if (!string.IsNullOrEmpty(sheet.Name)) - { - names.Add(sheet.Name!); - } - } - } - } - catch - { - // Ignore format errors - } - return names; - } - - public DataMatrix ParseExcel(Stream stream, string? sheetName = null, bool firstRowIsHeader = true) - { - try - { - using var doc = SpreadsheetDocument.Open(stream, false); - var workbookPart = doc.WorkbookPart; - if (workbookPart == null) return new DataMatrix(); - - var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable; - - Sheet? targetSheet = null; - if (!string.IsNullOrEmpty(sheetName)) - { - targetSheet = workbookPart.Workbook?.Sheets?.Elements() - .FirstOrDefault(s => string.Equals(s.Name, sheetName, StringComparison.OrdinalIgnoreCase)); - } - targetSheet ??= workbookPart.Workbook?.Sheets?.Elements().FirstOrDefault(); - - if (targetSheet == null || targetSheet.Id == null) return new DataMatrix(); - - var worksheetPart = (WorksheetPart)workbookPart.GetPartById(targetSheet.Id!); - var sheetData = worksheetPart.Worksheet?.Elements().FirstOrDefault(); - if (sheetData == null) return new DataMatrix(); - - var parsedRows = new List>(); - int maxColIndex = 0; - - foreach (Row row in sheetData.Elements()) - { - var rowCells = new Dictionary(); - foreach (Cell cell in row.Elements()) - { - int colIndex = GetColumnIndexFromCellReference(cell.CellReference?.Value); - string cellValue = GetCellTextValue(cell, sharedStringTable); - rowCells[colIndex] = cellValue; - if (colIndex > maxColIndex) maxColIndex = colIndex; - } - - var rowList = new List(); - for (int c = 0; c <= maxColIndex; c++) - { - rowList.Add(rowCells.TryGetValue(c, out var v) ? v : string.Empty); - } - parsedRows.Add(rowList); - } - - if (parsedRows.Count == 0) return new DataMatrix(); - - // Re-normalize all rows to have uniform column count - int totalCols = maxColIndex + 1; - var headers = new List(); - var rows = new List>(); - int startRow = 0; - - if (firstRowIsHeader && parsedRows.Count > 0) - { - var first = parsedRows[0]; - for (int c = 0; c < totalCols; c++) - { - string h = (c < first.Count && !string.IsNullOrWhiteSpace(first[c])) ? first[c].Trim() : $"Column {c + 1}"; - headers.Add(h); - } - startRow = 1; - } - else - { - for (int c = 0; c < totalCols; c++) - { - headers.Add($"Column {c + 1}"); - } - } - - for (int r = startRow; r < parsedRows.Count; r++) - { - var row = parsedRows[r]; - var normalizedRow = new List(); - for (int c = 0; c < totalCols; c++) - { - normalizedRow.Add(c < row.Count ? row[c] : string.Empty); - } - if (normalizedRow.Any(v => !string.IsNullOrWhiteSpace(v))) - { - rows.Add(normalizedRow); - } - } - - return new DataMatrix(headers, rows); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed to parse Excel workbook: {ex.Message}", ex); - } - } - - public async Task FetchFromRestApiAsync( - string url, - Dictionary? headers = null, - string? jsonPath = null, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(url)) - { - throw new ArgumentException("REST API URL cannot be empty.", nameof(url)); - } - - if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && - !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - url = "https://" + url; - } - - using var request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Add("User-Agent", "FryPDF-Studio/1.0"); - - if (headers != null) - { - foreach (var kvp in headers) - { - if (!string.IsNullOrWhiteSpace(kvp.Key) && !string.IsNullOrWhiteSpace(kvp.Value)) - { - request.Headers.TryAddWithoutValidation(kvp.Key.Trim(), kvp.Value.Trim()); - } - } - } - - var response = await SharedHttpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - - string jsonContent = await response.Content.ReadAsStringAsync(cancellationToken); - return ParseJson(jsonContent, jsonPath); - } - - public string FormatAsCsv(DataMatrix matrix, char delimiter = ',') - { - var sb = new StringBuilder(); - - // Headers - sb.AppendLine(string.Join(delimiter.ToString(), matrix.Headers.Select(h => EscapeCsvField(h, delimiter)))); - - // Rows - foreach (var row in matrix.Rows) - { - var escapedCells = new List(); - for (int c = 0; c < matrix.ColumnCount; c++) - { - string cell = c < row.Count ? row[c] : string.Empty; - escapedCells.Add(EscapeCsvField(cell, delimiter)); - } - sb.AppendLine(string.Join(delimiter.ToString(), escapedCells)); - } - - return sb.ToString(); - } - - private static string EscapeCsvField(string? field, char delimiter) - { - if (string.IsNullOrEmpty(field)) return string.Empty; - if (field.Contains(delimiter) || field.Contains('"') || field.Contains('\n') || field.Contains('\r')) - { - return $"\"{field.Replace("\"", "\"\"")}\""; - } - return field; - } - - private static char DetectDelimiter(string text) - { - var firstFewLines = text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Take(5).ToList(); - if (firstFewLines.Count == 0) return ','; - - int commas = firstFewLines.Sum(l => l.Count(c => c == ',')); - int tabs = firstFewLines.Sum(l => l.Count(c => c == '\t')); - int semicolons = firstFewLines.Sum(l => l.Count(c => c == ';')); - int pipes = firstFewLines.Sum(l => l.Count(c => c == '|')); - - if (tabs > commas && tabs > semicolons && tabs > pipes) return '\t'; - if (semicolons > commas && semicolons > tabs && semicolons > pipes) return ';'; - if (pipes > commas && pipes > tabs && pipes > semicolons) return '|'; - - return ','; - } - - private static List> ParseDelimitedText(string text, char delimiter) - { - var rows = new List>(); - var currentRow = new List(); - var currentCell = new StringBuilder(); - bool inQuotes = false; - - for (int i = 0; i < text.Length; i++) - { - char c = text[i]; - - if (inQuotes) - { - if (c == '"') - { - if (i + 1 < text.Length && text[i + 1] == '"') - { - currentCell.Append('"'); - i++; - } - else - { - inQuotes = false; - } - } - else - { - currentCell.Append(c); - } - } - else - { - if (c == '"') - { - inQuotes = true; - } - else if (c == delimiter) - { - currentRow.Add(currentCell.ToString()); - currentCell.Clear(); - } - else if (c == '\r') - { - if (i + 1 < text.Length && text[i + 1] == '\n') - { - i++; - } - currentRow.Add(currentCell.ToString()); - currentCell.Clear(); - rows.Add(currentRow); - currentRow = new List(); - } - else if (c == '\n') - { - currentRow.Add(currentCell.ToString()); - currentCell.Clear(); - rows.Add(currentRow); - currentRow = new List(); - } - else - { - currentCell.Append(c); - } - } - } - - if (currentCell.Length > 0 || currentRow.Count > 0) - { - currentRow.Add(currentCell.ToString()); - rows.Add(currentRow); - } - - return rows; - } - - private static int GetColumnIndexFromCellReference(string? cellRef) - { - if (string.IsNullOrEmpty(cellRef)) return 0; - int colIndex = 0; - foreach (char c in cellRef) - { - if (char.IsLetter(c)) - { - colIndex = (colIndex * 26) + (char.ToUpperInvariant(c) - 'A' + 1); - } - else break; - } - return Math.Max(0, colIndex - 1); - } - - private static string GetCellTextValue(Cell cell, SharedStringTable? sharedTable) - { - if (cell.CellValue == null) - { - return cell.InnerText ?? string.Empty; - } - - string value = cell.CellValue.Text; - if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString) - { - if (int.TryParse(value, out int sstIndex) && sharedTable != null) - { - var sstItem = sharedTable.Elements().ElementAtOrDefault(sstIndex); - return sstItem?.Text?.Text ?? sstItem?.InnerText ?? value; - } - } - - return value; - } -} diff --git a/src/PdfEditorApp.Core/Data/IBatchPdfGenerator.cs b/src/PdfEditorApp.Core/Data/IBatchPdfGenerator.cs deleted file mode 100644 index eda652a..0000000 --- a/src/PdfEditorApp.Core/Data/IBatchPdfGenerator.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Data; - -public interface IBatchPdfGenerator -{ - /// - /// Executes batch generation across all records in the data matrix, populating the document template and - /// outputting separate PDF files, a single merged multi-page PDF, or a compressed ZIP archive. - /// - Task GenerateBatchAsync( - PdfDocumentModel template, - DataMatrix matrix, - IReadOnlyList? mappings, - BatchGenerationConfig config, - IProgress? progress = null, - CancellationToken cancellationToken = default); -} diff --git a/src/PdfEditorApp.Core/Data/IDataBindingService.cs b/src/PdfEditorApp.Core/Data/IDataBindingService.cs deleted file mode 100644 index 9365e9a..0000000 --- a/src/PdfEditorApp.Core/Data/IDataBindingService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Core.Data; - -public interface IDataBindingService -{ - void ApplyToChart(DataMatrix matrix, PdfChartElement chart, int categoryColIndex = 0, List? valueColIndices = null); - void ApplyToTable(DataMatrix matrix, PdfTableElement table); - DataMatrix ExtractFromTable(PdfTableElement table); - DataMatrix ExtractFromChart(PdfChartElement chart); - PdfChartElement ConvertTableToChart(PdfTableElement table, ChartType chartType = ChartType.BarColumn); - PdfTableElement ConvertChartToTable(PdfChartElement chart); -} diff --git a/src/PdfEditorApp.Core/Data/IDataMergeEngine.cs b/src/PdfEditorApp.Core/Data/IDataMergeEngine.cs deleted file mode 100644 index db8d5b7..0000000 --- a/src/PdfEditorApp.Core/Data/IDataMergeEngine.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Data; - -public class DataMergeOptions -{ - public bool CaseInsensitiveLookup { get; set; } = true; - public bool PreserveUnmatchedPlaceholders { get; set; } = false; - public string DefaultFallbackValue { get; set; } = string.Empty; -} - -public interface IDataMergeEngine -{ - /// - /// Scans a PDF document model and returns all unique placeholder tags found (e.g. "EmployeeName", "BasicSalary"). - /// - IReadOnlyList DetectPlaceholders(PdfDocumentModel template); - - /// - /// Clones the template PDF document and replaces all placeholder expressions with data from the record dictionary. - /// Supports Text, QR Codes, Barcodes, Image URLs/Base64, Table cells, Page Headers/Footers, and Document Metadata. - /// - PdfDocumentModel HydrateDocument(PdfDocumentModel template, IReadOnlyDictionary record, DataMergeOptions? options = null); - - /// - /// Evaluates placeholders and format expressions inside a single string. - /// Syntax: {{FieldName}}, {{FieldName:C}}, {{FieldName:yyyy-MM-dd}}, {{FieldName:upper}}, {{FieldName ?? Default}} - /// - string EvaluateText(string templateText, IReadOnlyDictionary record, DataMergeOptions? options = null); -} diff --git a/src/PdfEditorApp.Core/Data/IDataSourceService.cs b/src/PdfEditorApp.Core/Data/IDataSourceService.cs deleted file mode 100644 index b1614d8..0000000 --- a/src/PdfEditorApp.Core/Data/IDataSourceService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace PdfEditorApp.Core.Data; - -public interface IDataSourceService -{ - DataMatrix ParseCsv(string text, char? delimiter = null, bool firstRowIsHeader = true); - DataMatrix ParseTsv(string text, bool firstRowIsHeader = true); - DataMatrix ParseJson(string jsonText, string? jsonPath = null); - DataMatrix ParseExcel(Stream stream, string? sheetName = null, bool firstRowIsHeader = true); - List GetExcelSheetNames(Stream stream); - Task FetchFromRestApiAsync(string url, Dictionary? headers = null, string? jsonPath = null, CancellationToken cancellationToken = default); - string FormatAsCsv(DataMatrix matrix, char delimiter = ','); -} diff --git a/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfFormExtractor.cs b/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfFormExtractor.cs deleted file mode 100644 index 383c726..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfFormExtractor.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using UglyToad.PdfPig; -using UglyToad.PdfPig.AcroForms; -using UglyToad.PdfPig.AcroForms.Fields; - -namespace PdfEditorApp.Core.Deconstruction.Extractors; - -/// -/// Strongly-typed, reflection-free AcroForms interactive form field extractor. -/// Maps native PdfPig AcroField hierarchy into editable models. -/// -public static class PdfFormExtractor -{ - /// - /// Extracts all AcroForms on the given document/page using strongly-typed patterns without reflection. - /// - public static List ExtractFormFields( - PdfDocument? doc, - int pageNumber, - double pageHeight, - ref int formZIndex, - PdfDeconstructionOptions options, - ILogger? logger = null) - { - var formElements = new List(); - if (doc == null) return formElements; - - try - { - if (doc.TryGetForm(out var form) && form != null && form.Fields != null) - { - foreach (var field in form.Fields) - { - if (!field.Bounds.HasValue || field.Bounds.Value.Width <= 0 || field.Bounds.Value.Height <= 0) - continue; - - var b = field.Bounds.Value; - double fX = Math.Max(0, b.Left); - double fY = Math.Max(0, pageHeight - b.Top); - double fW = Math.Max(20, b.Width); - double fH = Math.Max(14, b.Height); - - string fieldName = field.Information?.PartialName - ?? field.Information?.AlternateName - ?? field.Information?.MappingName - ?? "FormField"; - - string fieldValue = ExtractFieldValue(field); - FormFieldType fieldType = MapFieldType(field); - - // PDF spec 32000-1: bit 1 of FieldFlags is ReadOnly (0x1) - bool isReadOnly = (field.FieldFlags & 1) != 0; - - var formElement = new PdfFormFieldElement - { - X = Math.Round(fX, 1), - Y = Math.Round(fY, 1), - Width = Math.Round(fW, 1), - Height = Math.Round(fH, 1), - FieldName = fieldName, - Value = fieldValue, - DefaultValue = fieldValue, - FieldType = fieldType, - IsReadOnly = isReadOnly, - ZIndex = formZIndex++ - }; - formElements.Add(formElement); - } - } - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed to extract AcroForms on page {PageNumber}", pageNumber); - } - - return formElements; - } - - private static string ExtractFieldValue(AcroFieldBase field) - { - return field switch - { - AcroTextField tf => tf.Value ?? string.Empty, - AcroCheckboxField cb => cb.IsChecked ? "true" : "false", - AcroRadioButtonField rb => rb.IsSelected ? "true" : "false", - AcroComboBoxField combo => combo.SelectedOptions.FirstOrDefault() ?? combo.Options.FirstOrDefault()?.ToString() ?? string.Empty, - AcroListBoxField list => list.SelectedOptions.FirstOrDefault() ?? list.Options.FirstOrDefault()?.ToString() ?? string.Empty, - _ => string.Empty - }; - } - - private static FormFieldType MapFieldType(AcroFieldBase field) - { - return field.FieldType switch - { - AcroFieldType.Checkbox => FormFieldType.Checkbox, - AcroFieldType.RadioButton => FormFieldType.Radio, - AcroFieldType.ComboBox or AcroFieldType.ListBox => FormFieldType.Dropdown, - AcroFieldType.Signature => FormFieldType.Signature, - _ => FormFieldType.Text - }; - } -} diff --git a/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfImageExtractor.cs b/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfImageExtractor.cs deleted file mode 100644 index b1c341f..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfImageExtractor.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using SkiaSharp; -using UglyToad.PdfPig.Content; -using PdfEditorApp.Core.Deconstruction.Utils; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Core.Deconstruction.Extractors; - -/// -/// High-performance SkiaSharp & PdfPig Image Extraction Subsystem. -/// Decodes DCT/JPEG, JPEG2000, uncompressed CMYK, Grayscale, RGB, and 1-bit monochrome bitmaps. -/// Stores raw binary image bytes directly to avoid Large Object Heap (LOH) string fragmentation. -/// -public static class PdfImageExtractor -{ - /// - /// Extracts images from a pure scanned PDF page (where an image covers most of the page canvas). - /// - public static List ExtractScannedCanvasImages( - IReadOnlyList images, - int pageNumber, - double pageHeight, - ref int bgZIndex, - PdfDeconstructionOptions options, - ILogger? logger = null) - { - var elements = new List(); - - foreach (var img in images) - { - try - { - byte[]? imgBytes = ExtractImageBytes(img, logger); - if (imgBytes != null && imgBytes.Length > 0) - { - double imgX = Math.Max(0, img.BoundingBox.Left); - double imgY = Math.Max(0, pageHeight - img.BoundingBox.Top); - double imgW = Math.Max(10, img.BoundingBox.Width); - double imgH = Math.Max(10, img.BoundingBox.Height); - - var scannedImgElement = new PdfImageElement - { - X = Math.Round(imgX, 1), - Y = Math.Round(imgY, 1), - Width = Math.Round(imgW, 1), - Height = Math.Round(imgH, 1), - ImageData = imgBytes, - ZIndex = bgZIndex++, - IsLocked = false, - CornerRadius = 0, - BorderThickness = 0, - BorderColorHex = "Transparent", - AltText = $"Scanned Page {pageNumber} Canvas" - }; - elements.Add(scannedImgElement); - } - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed to extract scanned canvas image on page {PageNumber}", pageNumber); - } - } - - return elements; - } - - /// - /// Extracts embedded images (photos, logos, figures, icons, watermarks) from a born-digital or mixed PDF page. - /// - public static List ExtractEmbeddedImages( - IReadOnlyList images, - int pageNumber, - double pageWidth, - double pageHeight, - bool hasSufficientText, - ref int bgZIndex, - ref int imgZIndex, - PdfDeconstructionOptions options, - ILogger? logger = null) - { - var elements = new List(); - - foreach (var img in images) - { - try - { - double imgW = Math.Max(10, img.BoundingBox.Width); - double imgH = Math.Max(10, img.BoundingBox.Height); - - byte[]? imgBytes = ExtractImageBytes(img, logger); - if (imgBytes != null && imgBytes.Length > 0) - { - double imgX = Math.Max(0, img.BoundingBox.Left); - double imgY = Math.Max(0, pageHeight - img.BoundingBox.Top); - - // Classify full-page background or centered document watermark - bool isFullPageBg = imgW >= pageWidth * options.FullPageBgRatio && - imgH >= pageHeight * options.FullPageBgRatio && - hasSufficientText; - - bool isWatermark = (imgW >= pageWidth * options.WatermarkWidthRatio && - imgH >= pageHeight * options.WatermarkHeightRatio) && - hasSufficientText; - - var imgElement = new PdfImageElement - { - X = Math.Round(imgX, 1), - Y = Math.Round(imgY, 1), - Width = Math.Round(imgW, 1), - Height = Math.Round(imgH, 1), - ImageData = imgBytes, - ZIndex = (isFullPageBg || isWatermark) ? bgZIndex++ : imgZIndex++, - IsLocked = isFullPageBg || isWatermark, - Opacity = isWatermark ? options.WatermarkOpacity : 1.0, - CornerRadius = 0, - BorderThickness = 0, - BorderColorHex = "Transparent", - AltText = (isFullPageBg || isWatermark) - ? $"Watermark Background ({Math.Round(imgW):F0}x{Math.Round(imgH):F0})" - : $"Embedded Image ({Math.Round(imgW):F0}x{Math.Round(imgH):F0})" - }; - elements.Add(imgElement); - } - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed to extract embedded image on page {PageNumber}", pageNumber); - } - } - - return elements; - } - - /// - /// Decodes raw PDF image samples into a clean, standardized PNG byte array using SkiaSharp with unsafe pointer optimizations. - /// - public static byte[]? ExtractImageBytes(IPdfImage img, ILogger? logger = null) - { - // 1. Try native PNG extraction from PdfPig - try - { - if (img.TryGetPng(out var pngBytes) && pngBytes != null && pngBytes.Length >= 8) - { - // Verify standard PNG magic header (0x89, 'P', 'N', 'G') - if (pngBytes[0] == 0x89 && pngBytes[1] == 0x50 && pngBytes[2] == 0x4E && pngBytes[3] == 0x47) - { - return pngBytes; - } - } - } - catch (Exception ex) - { - logger?.LogDebug(ex, "PdfPig native TryGetPng failed; proceeding to SkiaSharp multi-format fallback"); - } - - // 2. Try decoding raw image bytes (JPEG / JPEG2000 / WEBP / BMP) via SkiaSharp - try - { - if (!img.RawBytes.IsEmpty && img.RawBytes.Length > 0) - { - var rawArray = img.RawBytes.ToArray(); - using var skData = SKData.CreateCopy(rawArray); - using var skImg = SKImage.FromEncodedData(skData); - if (skImg != null) - { - using var encoded = skImg.Encode(SKEncodedImageFormat.Png, 100); - if (encoded != null && encoded.Size > 0) - { - return encoded.ToArray(); - } - } - - // Check for JPEG direct magic header - if (rawArray.Length > 3 && rawArray[0] == 0xFF && rawArray[1] == 0xD8) - { - using var skBmp = SKBitmap.Decode(rawArray); - if (skBmp != null) - { - using var imgFromBmp = SKImage.FromBitmap(skBmp); - using var encoded = imgFromBmp.Encode(SKEncodedImageFormat.Png, 100); - if (encoded != null && encoded.Size > 0) - { - return encoded.ToArray(); - } - } - } - } - } - catch (Exception ex) - { - logger?.LogDebug(ex, "SkiaSharp encoded stream decode failed; proceeding to raw pixel sample extraction"); - } - - // 3. Raw pixel samples conversion via SkiaSharp using unsafe pointer loops (zero bounds-checking overhead) - try - { - int w = img.WidthInSamples; - int h = img.HeightInSamples; - - if (w > 0 && h > 0 && img.TryGetBytesAsMemory(out var pixelMem) && pixelMem.Length > 0) - { - var rawPixels = pixelMem.ToArray(); - - // Case A: 24-bit RGB (3 bytes per pixel) - if (rawPixels.Length >= w * h * 3) - { - using var bitmap = new SKBitmap(w, h, SKColorType.Rgba8888, SKAlphaType.Opaque); - unsafe - { - byte* dstPtr = (byte*)bitmap.GetPixels().ToPointer(); - fixed (byte* srcPtr = rawPixels) - { - int pixelCount = w * h; - byte* src = srcPtr; - byte* dst = dstPtr; - for (int i = 0; i < pixelCount; i++) - { - dst[0] = src[0]; // R - dst[1] = src[1]; // G - dst[2] = src[2]; // B - dst[3] = 255; // A - src += 3; - dst += 4; - } - } - } - - using var image = SKImage.FromBitmap(bitmap); - using var encoded = image.Encode(SKEncodedImageFormat.Png, 100); - if (encoded != null && encoded.Size > 0) - { - return encoded.ToArray(); - } - } - // Case B: 8-bit Grayscale (1 byte per pixel) - else if (rawPixels.Length >= w * h) - { - using var bitmap = new SKBitmap(w, h, SKColorType.Rgba8888, SKAlphaType.Opaque); - unsafe - { - byte* dstPtr = (byte*)bitmap.GetPixels().ToPointer(); - fixed (byte* srcPtr = rawPixels) - { - int pixelCount = w * h; - byte* src = srcPtr; - byte* dst = dstPtr; - for (int i = 0; i < pixelCount; i++) - { - byte g = *src++; - dst[0] = g; - dst[1] = g; - dst[2] = g; - dst[3] = 255; - dst += 4; - } - } - } - - using var image = SKImage.FromBitmap(bitmap); - using var encoded = image.Encode(SKEncodedImageFormat.Png, 100); - if (encoded != null && encoded.Size > 0) - { - return encoded.ToArray(); - } - } - // Case C: 32-bit CMYK (4 bytes per pixel) - else if (rawPixels.Length >= w * h * 4) - { - using var bitmap = new SKBitmap(w, h, SKColorType.Rgba8888, SKAlphaType.Opaque); - unsafe - { - byte* dstPtr = (byte*)bitmap.GetPixels().ToPointer(); - fixed (byte* srcPtr = rawPixels) - { - int pixelCount = w * h; - byte* src = srcPtr; - byte* dst = dstPtr; - for (int i = 0; i < pixelCount; i++) - { - CmykColorConverter.ConvertCmykToRgb(src[0], src[1], src[2], src[3], out byte r, out byte g, out byte b); - dst[0] = r; - dst[1] = g; - dst[2] = b; - dst[3] = 255; - src += 4; - dst += 4; - } - } - } - - using var image = SKImage.FromBitmap(bitmap); - using var encoded = image.Encode(SKEncodedImageFormat.Png, 100); - if (encoded != null && encoded.Size > 0) - { - return encoded.ToArray(); - } - } - // Case D: 1-bit Monochrome (e.g. stamps, fax/signatures) - else if (img.BitsPerComponent == 1) - { - using var bitmap = new SKBitmap(w, h, SKColorType.Rgba8888, SKAlphaType.Opaque); - unsafe - { - byte* dstPtr = (byte*)bitmap.GetPixels().ToPointer(); - fixed (byte* srcPtr = rawPixels) - { - int rowStride = (w + 7) / 8; - for (int y = 0; y < h; y++) - { - byte* rowSrc = srcPtr + (y * rowStride); - byte* rowDst = dstPtr + (y * w * 4); - for (int x = 0; x < w; x++) - { - int byteIdx = x >> 3; - int bitIdx = 7 - (x & 7); - bool isWhite = ((rowSrc[byteIdx] >> bitIdx) & 1) != 0; - byte val = isWhite ? (byte)255 : (byte)0; - rowDst[0] = val; - rowDst[1] = val; - rowDst[2] = val; - rowDst[3] = 255; - rowDst += 4; - } - } - } - } - - using var image = SKImage.FromBitmap(bitmap); - using var encoded = image.Encode(SKEncodedImageFormat.Png, 100); - if (encoded != null && encoded.Size > 0) - { - return encoded.ToArray(); - } - } - } - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed to decode raw pixel sample memory"); - } - - // 4. Raw bytes fallback - try - { - if (!img.RawBytes.IsEmpty && img.RawBytes.Length > 0) - { - return img.RawBytes.ToArray(); - } - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed raw bytes fallback for image"); - } - - return null; - } -} diff --git a/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfTextExtractor.cs b/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfTextExtractor.cs deleted file mode 100644 index 7f91abc..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfTextExtractor.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using UglyToad.PdfPig.Content; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Deconstruction.Utils; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Core.Deconstruction.Extractors; - -/// -/// Subsystem for extracting, clustering, and formatting text elements from PDF pages. -/// Implements WCAG 2.1 relative luminance dynamic contrast protection and card/container grouping. -/// -public static class PdfTextExtractor -{ - /// - /// Clusters raw PDF words into structured paragraphs and maps them to models. - /// - public static List ExtractTextElements( - Page page, - List paragraphs, - HashSet consumedParagraphs, - IReadOnlyList currentElements, - int pageNumber, - double pageWidth, - double pageHeight, - ref int textZIndex, - PdfDeconstructionOptions options, - ILogger? logger = null) - { - var textElements = new List(); - - try - { - var shapeElements = currentElements.OfType().ToList(); - - foreach (var para in paragraphs) - { - if (consumedParagraphs.Contains(para)) continue; // Swallowed by table - if (string.IsNullOrWhiteSpace(para.Text)) continue; - - // Dynamic WCAG Contrast Protection against underlying shape fills - var underlyingShape = shapeElements - .Where(s => s.X <= para.CanvasX + 10 && - s.Y <= para.CanvasY + 10 && - s.X + s.Width >= para.CanvasX + para.CanvasWidth - 10 && - s.Y + s.Height >= para.CanvasY + para.CanvasHeight - 10 && - !string.Equals(s.FillColorHex, "Transparent", StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(s => s.ZIndex) - .FirstOrDefault(); - - string underlyingBgHex = underlyingShape?.FillColorHex ?? "#FFFFFF"; - string finalColor = ColorContrastHelper.EnsureLegibleContrast( - para.ColorHex, - underlyingBgHex, - options.MinContrastRatio, - options.HighContrastDarkTextColor, - options.HighContrastLightTextColor); - - var textElement = new PdfTextElement - { - X = para.CanvasX, - Y = para.CanvasY, - Width = para.CanvasWidth, - Height = para.CanvasHeight, - Text = para.Text, - FontSize = para.FontSize, - FontFamily = para.FontFamily, - IsBold = para.IsBold, - IsItalic = para.IsItalic, - TextColorHex = finalColor, - LineHeight = para.LineHeight, - Rotation = para.Rotation, - Alignment = para.Alignment, - TextWrap = false, - ZIndex = textZIndex++ - }; - - if (para.Spans != null && para.Spans.Count > 1) - { - textElement.Spans = new List(para.Spans.Count); - foreach (var span in para.Spans) - { - var spanClone = span.Clone(); - if (string.Equals(spanClone.TextColorHex, para.ColorHex, StringComparison.OrdinalIgnoreCase)) - { - spanClone.TextColorHex = finalColor; - } - textElement.Spans.Add(spanClone); - } - } - - textElements.Add(textElement); - } - - // Card & Container Box Grouping: Associate background shapes with their contained text - GroupContainersAndText(shapeElements, textElements, options); - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed to extract structured text on page {PageNumber}", pageNumber); - - // Fallback: single page text element if unusual encodings prevent grouping - if (textElements.Count == 0 && !string.IsNullOrWhiteSpace(page.Text)) - { - var fallbackText = new PdfTextElement - { - X = 36, - Y = 36, - Width = pageWidth - 72, - Height = pageHeight - 72, - Text = page.Text, - FontSize = 11, - FontFamily = "Arial", - TextColorHex = options.HighContrastDarkTextColor, - ZIndex = textZIndex++ - }; - textElements.Add(fallbackText); - } - } - - return textElements; - } - - /// - /// Groups card/container shapes with their contained text elements by assigning a shared GroupId. - /// - public static void GroupContainersAndText( - List shapeElements, - List textElements, - PdfDeconstructionOptions options) - { - var backgroundShapes = shapeElements - .Where(s => s.Width >= options.MinContainerCardWidth && - s.Height >= options.MinContainerCardHeight && - !string.Equals(s.FillColorHex, "Transparent", StringComparison.OrdinalIgnoreCase)) - .ToList(); - - foreach (var shape in backgroundShapes) - { - var innerTexts = textElements - .Where(t => t.X >= shape.X - 5 && t.Y >= shape.Y - 5 && - t.X + t.Width <= shape.X + shape.Width + 5 && - t.Y + t.Height <= shape.Y + shape.Height + 5) - .ToList(); - - if (innerTexts.Count > 0) - { - string containerGroupId = Guid.NewGuid().ToString("N"); - shape.GroupId = containerGroupId; - foreach (var t in innerTexts) - { - t.GroupId = containerGroupId; - } - } - } - } -} diff --git a/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfVectorExtractor.cs b/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfVectorExtractor.cs deleted file mode 100644 index b6751b4..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/Extractors/PdfVectorExtractor.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using UglyToad.PdfPig.Core; -using UglyToad.PdfPig.Graphics; -using PdfEditorApp.Core.Deconstruction.Utils; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Core.Deconstruction.Extractors; - -/// -/// Subsystem for extracting vector graphics, dividers, and background containers from PDF paths. -/// Automatically groups excess micro-paths into a single resolution-independent -/// so no charts, diagrams, or illustrations are dropped. -/// -public static class PdfVectorExtractor -{ - /// - /// Extracts vector shapes, divider lines, and grouped SVG clusters from the page paths. - /// - public static List ExtractVectors( - IReadOnlyList? paths, - HashSet consumedPathIndices, - int pageNumber, - double pageWidth, - double pageHeight, - ref int bgZIndex, - ref int shapeZIndex, - PdfDeconstructionOptions options, - ILogger? logger = null, - UglyToad.PdfPig.PdfDocument? doc = null) - { - var elements = new List(); - if (paths == null || paths.Count == 0) return elements; - - int extractedShapeCount = 0; - var excessPaths = new List(); - - for (int pathIdx = 0; pathIdx < paths.Count; pathIdx++) - { - if (consumedPathIndices.Contains(pathIdx)) continue; // Swallowed by table - - try - { - var path = paths[pathIdx]; - var bbox = path.GetBoundingRectangle(); - if (!bbox.HasValue) continue; - - var b = bbox.Value; - if (b.Width <= 0.5 && b.Height <= 0.5) continue; - - // Ignore full-page border clipping frames - if (b.Width >= pageWidth * 0.98 && b.Height >= pageHeight * 0.98) continue; - - // If we have exceeded the maximum individual shape threshold, accumulate into excess group - if (extractedShapeCount >= options.MaxVectorShapesPerPage) - { - excessPaths.Add(path); - continue; - } - - double canvasX = Math.Max(0, b.Left); - double canvasY = Math.Max(0, pageHeight - b.Top); - double canvasW = Math.Max(1.0, b.Width); - double canvasH = Math.Max(1.0, b.Height); - - string strokeHex = PdfColorHelper.ToHex(path.StrokeColor, doc, "#0F172A"); - string fillHex = path.IsStroked && path.StrokeColor != null ? strokeHex : "#000000"; - if (path.FillColor != null) - { - fillHex = PdfColorHelper.ToHex(path.FillColor, doc, fillHex); - } - - // 1. Check if it's a thin horizontal divider line - if (canvasH <= options.DividerMaxHeight && canvasW >= options.DividerMinWidth) - { - var divider = new PdfDividerElement - { - X = Math.Round(canvasX, 1), - Y = Math.Round(canvasY, 1), - Width = Math.Round(canvasW, 1), - Height = Math.Round(Math.Max(1.0, canvasH), 1), - Thickness = Math.Round(Math.Max(1.0, canvasH), 1), - IsVertical = false, - ColorHex = path.IsStroked ? strokeHex : (path.IsFilled ? fillHex : "#CBD5E1"), - ZIndex = shapeZIndex++ - }; - elements.Add(divider); - extractedShapeCount++; - } - // 2. Check if it's a thin vertical divider line - else if (canvasW <= options.DividerMaxHeight && canvasH >= options.DividerMinWidth) - { - var divider = new PdfDividerElement - { - X = Math.Round(canvasX, 1), - Y = Math.Round(canvasY, 1), - Width = Math.Round(Math.Max(1.0, canvasW), 1), - Height = Math.Round(canvasH, 1), - Thickness = Math.Round(Math.Max(1.0, canvasW), 1), - IsVertical = true, - ColorHex = path.IsStroked ? strokeHex : (path.IsFilled ? fillHex : "#CBD5E1"), - ZIndex = shapeZIndex++ - }; - elements.Add(divider); - extractedShapeCount++; - } - // 3. 2D shape / container / badge - else if (canvasW >= options.MinShapeDimension && canvasH >= options.MinShapeDimension && (path.IsFilled || path.IsStroked)) - { - // Distinguish large background container cards vs foreground shapes - bool isLargeContainer = (canvasW >= options.LargeContainerMinWidth && canvasH >= options.LargeContainerMinHeight) && - (path.IsFilled || (path.IsStroked && !path.IsFilled)); - int targetZIndex = isLargeContainer ? bgZIndex++ : shapeZIndex++; - - bool isSimpleRect = IsSimpleAxisAlignedRectangle(path); - if (isSimpleRect) - { - var shape = new PdfShapeElement - { - X = Math.Round(canvasX, 1), - Y = Math.Round(canvasY, 1), - Width = Math.Round(canvasW, 1), - Height = Math.Round(canvasH, 1), - FillColorHex = path.IsFilled ? fillHex : "Transparent", - StrokeColorHex = path.IsStroked ? strokeHex : "Transparent", - StrokeThickness = path.IsStroked ? Math.Max(1.0, path.LineWidth) : 0, - CornerRadius = 0, - ShapeType = ShapeType.Rectangle, - ZIndex = targetZIndex - }; - elements.Add(shape); - extractedShapeCount++; - } - else - { - // Complex polygon, L-bracket, curve, or path with cutouts: - // Preserve exact geometry and transparent cutouts as a native vector CustomSvgPath shape - string pathData = SvgPathBuilder.BuildPathData(path, canvasX, canvasY, pageHeight); - if (!string.IsNullOrWhiteSpace(pathData)) - { - var shape = new PdfShapeElement - { - X = Math.Round(canvasX, 1), - Y = Math.Round(canvasY, 1), - Width = Math.Round(canvasW, 1), - Height = Math.Round(canvasH, 1), - FillColorHex = path.IsFilled ? fillHex : "Transparent", - StrokeColorHex = path.IsStroked ? strokeHex : "Transparent", - StrokeThickness = path.IsStroked ? Math.Max(1.0, path.LineWidth) : 0, - CornerRadius = 0, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = pathData.Trim(), - ZIndex = targetZIndex - }; - elements.Add(shape); - extractedShapeCount++; - } - } - } - } - catch (Exception ex) - { - logger?.LogDebug(ex, "Failed to process vector path {PathIndex} on page {PageNumber}", pathIdx, pageNumber); - } - } - - // Group excess vector paths into a single SVG element to guarantee zero data loss - if (options.GroupExcessVectorsAsSvg && excessPaths.Count > 0) - { - try - { - double minX = double.MaxValue; - double minY = double.MaxValue; - double maxX = double.MinValue; - double maxY = double.MinValue; - - foreach (var p in excessPaths) - { - var bbox = p.GetBoundingRectangle(); - if (bbox.HasValue) - { - var b = bbox.Value; - double cX = Math.Max(0, b.Left); - double cY = Math.Max(0, pageHeight - b.Top); - minX = Math.Min(minX, cX); - minY = Math.Min(minY, cY); - maxX = Math.Max(maxX, cX + b.Width); - maxY = Math.Max(maxY, cY + b.Height); - } - } - - if (minX < maxX && minY < maxY) - { - double groupW = Math.Max(10, maxX - minX); - double groupH = Math.Max(10, maxY - minY); - string svgMarkup = SvgPathBuilder.BuildSvgFromPaths(excessPaths, minX, minY, groupW, groupH, pageHeight, doc); - - var svgElement = new PdfSvgElement - { - X = Math.Round(minX, 1), - Y = Math.Round(minY, 1), - Width = Math.Round(groupW, 1), - Height = Math.Round(groupH, 1), - SvgSource = svgMarkup, - PresetName = "VectorIllustrationCluster", - ZIndex = shapeZIndex++ - }; - elements.Add(svgElement); - } - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed to group excess vector paths on page {PageNumber}", pageNumber); - } - } - - return elements; - } - - /// - /// Evaluates whether a raw PDF vector path is a simple 4-corner axis-aligned rectangle. - /// If not (e.g. L-shaped borders, triangles, polygons with cutouts, curves), it must be preserved - /// as a resolution-independent SVG rather than flattened into an occluding solid box. - /// - public static bool IsSimpleAxisAlignedRectangle(PdfPath path) - { - if (path.Count != 1) return false; - var subpath = path[0]; - if (subpath.Commands == null) return false; - if (subpath.Commands.Any(c => c is PdfSubpath.BezierCurve)) return false; - - var lines = subpath.Commands.OfType().ToList(); - if (lines.Count != 3 && lines.Count != 4) return false; - - var moves = subpath.Commands.OfType().ToList(); - if (moves.Count != 1) return false; - - var pts = new List { moves[0].Location }; - foreach (var l in lines) pts.Add(l.To); - - for (int i = 0; i < pts.Count - 1; i++) - { - double dx = Math.Abs(pts[i].X - pts[i + 1].X); - double dy = Math.Abs(pts[i].Y - pts[i + 1].Y); - if (dx > 0.1 && dy > 0.1) return false; - } - - var bbox = path.GetBoundingRectangle(); - if (!bbox.HasValue) return false; - var b = bbox.Value; - - foreach (var pt in pts) - { - bool nearCorner = (Math.Abs(pt.X - b.Left) < 0.5 || Math.Abs(pt.X - b.Right) < 0.5) && - (Math.Abs(pt.Y - b.Bottom) < 0.5 || Math.Abs(pt.Y - b.Top) < 0.5); - if (!nearCorner) return false; - } - - return true; - } -} diff --git a/src/PdfEditorApp.Core/Deconstruction/PdfDeconstructionEngine.cs b/src/PdfEditorApp.Core/Deconstruction/PdfDeconstructionEngine.cs deleted file mode 100644 index cdd5c60..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/PdfDeconstructionEngine.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using UglyToad.PdfPig; -using UglyToad.PdfPig.Content; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Deconstruction.Extractors; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Utils; - -namespace PdfEditorApp.Core.Deconstruction; - -/// -/// Professional PDF Deconstruction Engine (Adobe Acrobat & Wondershare PDFelement Architecture). -/// Orchestrates modular extractors to parse any standard or complex PDF into clean, editable, multi-layered document models. -/// -public static class PdfDeconstructionEngine -{ - /// - /// Deconstructs a binary PDF stream into a fully editable . - /// If the PDF contains an embedded FryPDF native model, restores it losslessly. - /// Otherwise, performs intelligent deconstruction using default options. - /// - public static PdfDocumentModel Deconstruct( - byte[] pdfBytes, - string title = "Imported_Document.pdf", - string? password = null) - { - return Deconstruct(pdfBytes, PdfDeconstructionOptions.Default, null, title, password); - } - - /// - /// Deconstructs a binary PDF stream with custom options and diagnostic logging. - /// - public static PdfDocumentModel Deconstruct( - byte[] pdfBytes, - PdfDeconstructionOptions options, - ILogger? logger = null, - string title = "Imported_Document.pdf", - string? password = null) - { - // 1. Check if the PDF contains an embedded native FryPDF project model for 100% lossless roundtrip - if (FryPdfEmbeddingHelper.TryExtractEmbeddedModel(pdfBytes, out var embeddedModel) && embeddedModel != null) - { - if (!string.IsNullOrWhiteSpace(title) && string.Equals(embeddedModel.Title, "Untitled.pdf", StringComparison.OrdinalIgnoreCase)) - { - embeddedModel.Title = title; - } - return embeddedModel; - } - - byte[] sanitized = PdfDocumentSanitizer.SanitizePdfBytes(pdfBytes); - - var parsingOptions = new ParsingOptions(); - if (!string.IsNullOrEmpty(password)) - { - parsingOptions.Password = password; - } - - PdfDocument? doc = null; - try - { - doc = PdfDocument.Open(sanitized, parsingOptions); - } - catch (Exception ex) - { - logger?.LogDebug(ex, "Primary PDF open failed; attempting salvage and repair"); - try - { - byte[] repaired = PdfDocumentSanitizer.SalvageAndRepairPdfBytes(sanitized); - doc = PdfDocument.Open(repaired, parsingOptions); - } - catch - { - doc = PdfDocument.Open(pdfBytes, parsingOptions); - } - } - - using (doc) - { - return DeconstructDocument(doc, title, options, logger); - } - } - - /// - /// Deconstructs an opened PdfPig document into a structured . - /// - public static PdfDocumentModel DeconstructDocument( - PdfDocument doc, - string defaultTitle, - PdfDeconstructionOptions? options = null, - ILogger? logger = null) - { - options ??= PdfDeconstructionOptions.Default; - - var model = new PdfDocumentModel - { - Title = string.IsNullOrWhiteSpace(doc.Information.Title) ? defaultTitle : doc.Information.Title, - Author = doc.Information.Author ?? "Unknown Author", - Subject = doc.Information.Subject ?? "Imported PDF Document", - Keywords = doc.Information.Keywords ?? "", - Creator = string.IsNullOrWhiteSpace(doc.Information.Creator) ? "FryPDF" : doc.Information.Creator, - Producer = string.IsNullOrWhiteSpace(doc.Information.Producer) ? "codefrydev.in" : doc.Information.Producer, - CreatedDate = DateTime.Now, - ModifiedDate = DateTime.Now - }; - - int totalPages = doc.NumberOfPages; - for (int pageNum = 1; pageNum <= totalPages; pageNum++) - { - var page = doc.GetPage(pageNum); - var pageModel = DeconstructPage(page, pageNum, doc, options, logger); - model.Pages.Add(pageModel); - } - - return model; - } - - /// - /// Deconstructs an individual PDF page into vector shapes, images, grouped text paragraphs, and form fields. - /// - public static PdfPageModel DeconstructPage( - Page page, - int pageNumber, - PdfDocument? doc = null, - PdfDeconstructionOptions? options = null, - ILogger? logger = null) - { - options ??= PdfDeconstructionOptions.Default; - - double pageWidth = Math.Max(100.0, page.Width); - double pageHeight = Math.Max(100.0, page.Height); - - var pageModel = new PdfPageModel - { - PageNumber = pageNumber, - Width = pageWidth, - Height = pageHeight, - Format = DeterminePageFormat(pageWidth, pageHeight), - Orientation = pageWidth > pageHeight ? PageOrientation.Landscape : PageOrientation.Portrait, - RotationAngle = (int)page.Rotation.Value, - BackgroundColorHex = "#FFFFFF", - ShowHeaderFooter = false - }; - - int bgZIndex = options.InitialBgZIndex; - int imgZIndex = options.InitialImgZIndex; - int tableZIndex = options.InitialTableZIndex; - int shapeZIndex = options.InitialShapeZIndex; - int textZIndex = options.InitialTextZIndex; - int formZIndex = options.InitialFormZIndex; - - var words = page.GetWords() - .Where(w => !string.IsNullOrWhiteSpace(w.Text)) - .ToList(); - - var images = page.GetImages().ToList(); - - // 1. Detect page type: - // - Pure scanned: image covers >= threshold of page AND <= max word count - // - Born-digital / Mixed: has both digital text and images/vectors - bool hasMassiveImageCover = images.Any(img => - img.BoundingBox.Width >= pageWidth * options.PureScannedImageCoverageThreshold && - img.BoundingBox.Height >= pageHeight * options.PureScannedImageCoverageThreshold); - - bool isPureScanned = words.Count <= options.PureScannedWordCountMax && images.Count > 0 && hasMassiveImageCover; - bool hasSufficientText = words.Count > options.PureScannedWordCountMax; - - if (isPureScanned) - { - // Pure Scanned Page Mode: Extract high-res image as base canvas layer - var scannedImages = PdfImageExtractor.ExtractScannedCanvasImages(images, pageNumber, pageHeight, ref bgZIndex, options, logger); - pageModel.Elements.AddRange(scannedImages); - } - else - { - // Born-Digital / Mixed Document Mode: - pageModel.BackgroundColorHex = "#FFFFFF"; - - // A. Extract Embedded Images (Photos, Logos, Figures, Icons, Watermarks) - var embeddedImages = PdfImageExtractor.ExtractEmbeddedImages( - images, pageNumber, pageWidth, pageHeight, hasSufficientText, ref bgZIndex, ref imgZIndex, options, logger); - pageModel.Elements.AddRange(embeddedImages); - - // B. Extract and Cluster Text Paragraphs - var paragraphs = new List(); - try - { - bool isLandscape = pageWidth > pageHeight; - double colGapMultiplier = isLandscape ? options.ColumnGapMultiplierLandscape : options.ColumnGapMultiplierPortrait; - paragraphs = PdfLayoutAnalyzer.AnalyzeAndGroupPageText(page, pageHeight, colGapMultiplier); - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed to analyze page text layout on page {PageNumber}", pageNumber); - } - - // C. Detect Structured Tables from intersecting vector lines and cell alignments - var consumedPathIndices = new HashSet(); - var consumedParagraphs = new HashSet(); - - try - { - var detectedTables = TableGridDetector.DetectTables(page.Paths, paragraphs, pageWidth, pageHeight); - foreach (var tableResult in detectedTables) - { - tableResult.TableElement.ZIndex = tableZIndex++; - pageModel.Elements.Add(tableResult.TableElement); - - foreach (var idx in tableResult.ConsumedPathIndices) - consumedPathIndices.Add(idx); - - foreach (var para in tableResult.ConsumedParagraphs) - consumedParagraphs.Add(para); - } - } - catch (Exception ex) - { - logger?.LogWarning(ex, "Failed to detect tables on page {PageNumber}", pageNumber); - } - - // D. Extract Vector Paths & Geometric Shapes (Dividers, Rectangles, Borders, SVG Clusters) - var vectorElements = PdfVectorExtractor.ExtractVectors( - page.Paths, consumedPathIndices, pageNumber, pageWidth, pageHeight, ref bgZIndex, ref shapeZIndex, options, logger, doc); - pageModel.Elements.AddRange(vectorElements); - - // E. Add Unconsumed Text Elements with Dynamic Contrast and Container Grouping - var textElements = PdfTextExtractor.ExtractTextElements( - page, paragraphs, consumedParagraphs, pageModel.Elements, pageNumber, pageWidth, pageHeight, ref textZIndex, options, logger); - pageModel.Elements.AddRange(textElements); - } - - // 3. Extract AcroForm Form Fields (Text Boxes, Checkboxes, Signatures, Dropdowns) - var formElements = PdfFormExtractor.ExtractFormFields(doc, pageNumber, pageHeight, ref formZIndex, options, logger); - pageModel.Elements.AddRange(formElements); - - return pageModel; - } - - /// - /// Determines standard page format (A4, Letter, Legal, A3, A5) based on physical point dimensions. - /// - public static PageFormat DeterminePageFormat(double width, double height) - { - double maxDim = Math.Max(width, height); - double minDim = Math.Min(width, height); - - if (Math.Abs(minDim - 595.28) < 20 && Math.Abs(maxDim - 841.89) < 20) return PageFormat.A4; - if (Math.Abs(minDim - 612.0) < 20 && Math.Abs(maxDim - 792.0) < 20) return PageFormat.Letter; - if (Math.Abs(minDim - 612.0) < 20 && Math.Abs(maxDim - 1008.0) < 20) return PageFormat.Legal; - if (Math.Abs(minDim - 841.89) < 30 && Math.Abs(maxDim - 1190.55) < 30) return PageFormat.A3; - if (Math.Abs(minDim - 419.53) < 20 && Math.Abs(maxDim - 595.28) < 20) return PageFormat.A5; - if (Math.Abs(minDim - 2592.0) < 50 && Math.Abs(maxDim - 3456.0) < 50) return PageFormat.Poster; - - return PageFormat.A4; - } -} diff --git a/src/PdfEditorApp.Core/Deconstruction/PdfDeconstructionOptions.cs b/src/PdfEditorApp.Core/Deconstruction/PdfDeconstructionOptions.cs deleted file mode 100644 index 149b49e..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/PdfDeconstructionOptions.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace PdfEditorApp.Core.Deconstruction; - -/// -/// Configurable options and geometric/typographic heuristics for the PDF Deconstruction Engine. -/// Replaces hardcoded magic numbers and enables tuning for diverse document archetypes (e.g. scanned IDs, textbooks, reports, forms). -/// -public class PdfDeconstructionOptions -{ - /// Default singleton instance with production-tuned heuristics. - public static PdfDeconstructionOptions Default => new(); - - /// Minimum percentage of page area an image must occupy to be classified as a scanned page canvas (0.0..1.0). - public double PureScannedImageCoverageThreshold { get; set; } = 0.85; - - /// Maximum digital word count on a page to still treat it as a pure scanned document. - public int PureScannedWordCountMax { get; set; } = 5; - - /// Minimum width percentage for an image to be classified as a document watermark (0.0..1.0). - public double WatermarkWidthRatio { get; set; } = 0.65; - - /// Minimum height percentage for an image to be classified as a document watermark (0.0..1.0). - public double WatermarkHeightRatio { get; set; } = 0.55; - - /// Opacity applied to detected watermark images (0.0..1.0). - public double WatermarkOpacity { get; set; } = 0.10; - - /// Minimum width and height ratio for an image to be considered a full-page background underlay. - public double FullPageBgRatio { get; set; } = 0.88; - - /// Minimum width in points for a filled vector rectangle to be considered a background container card. - public double LargeContainerMinWidth { get; set; } = 120.0; - - /// Minimum height in points for a filled vector rectangle to be considered a background container card. - public double LargeContainerMinHeight { get; set; } = 80.0; - - /// Maximum height in points for a vector path to be categorized as a horizontal divider line. - public double DividerMaxHeight { get; set; } = 3.5; - - /// Minimum width in points for a vector path to be categorized as a horizontal divider line. - public double DividerMinWidth { get; set; } = 6.0; - - /// Maximum number of individual vector shapes extracted before grouping remaining excess micro-paths into a combined SVG. - public int MaxVectorShapesPerPage { get; set; } = 1000; - - /// Whether to group vector paths exceeding into a single instead of dropping them. - public bool GroupExcessVectorsAsSvg { get; set; } = true; - - /// Minimum dimension (width and height in points) for a vector shape to be extracted as a standalone element. - public double MinShapeDimension { get; set; } = 2.0; - - /// Minimum width in points for card/container grouping with inner text elements. - public double MinContainerCardWidth { get; set; } = 60.0; - - /// Minimum height in points for card/container grouping with inner text elements. - public double MinContainerCardHeight { get; set; } = 30.0; - - /// Default high-contrast text color hex when contrast against background shape is insufficient. - public string HighContrastDarkTextColor { get; set; } = "#0F172A"; - - /// Default high-contrast light text color hex when contrast against dark background shape is insufficient. - public string HighContrastLightTextColor { get; set; } = "#FFFFFF"; - - /// Minimum WCAG relative luminance contrast ratio before dynamic text contrast adjustment is applied. - public double MinContrastRatio { get; set; } = 1.25; - - /// Column gap multiplier for landscape layout analysis. - public double ColumnGapMultiplierLandscape { get; set; } = 1.5; - - /// Column gap multiplier for portrait layout analysis. - public double ColumnGapMultiplierPortrait { get; set; } = 1.0; - - /// Initial Z-index for background container cards and full-page watermarks. - public int InitialBgZIndex { get; set; } = 0; - - /// Initial Z-index for content images (photos, QR codes, emblems, logos). - public int InitialImgZIndex { get; set; } = 100; - - /// Initial Z-index for structured tables and data grids. - public int InitialTableZIndex { get; set; } = 500; - - /// Initial Z-index for foreground vector shapes, badges, and divider lines. - public int InitialShapeZIndex { get; set; } = 600; - - /// Initial Z-index for text paragraphs and headings. - public int InitialTextZIndex { get; set; } = 1000; - - /// Initial Z-index for interactive AcroForm form fields. - public int InitialFormZIndex { get; set; } = 2000; -} diff --git a/src/PdfEditorApp.Core/Deconstruction/Utils/CmykColorConverter.cs b/src/PdfEditorApp.Core/Deconstruction/Utils/CmykColorConverter.cs deleted file mode 100644 index 74197b9..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/Utils/CmykColorConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace PdfEditorApp.Core.Deconstruction.Utils; - -/// -/// High-precision CMYK-to-sRGB colorimetric conversion engine. -/// Provides ink density adjustments and black point compensation to eliminate washed-out colors. -/// -public static class CmykColorConverter -{ - /// - /// Converts CMYK byte channels (0..255) to calibrated sRGB byte channels (0..255). - /// - public static void ConvertCmykToRgb(byte cByte, byte mByte, byte yByte, byte kByte, out byte rByte, out byte gByte, out byte bByte) - { - float c = cByte / 255f; - float m = mByte / 255f; - float y = yByte / 255f; - float k = kByte / 255f; - - // Subtractive CMYK color model with black point compensation - float r = (1f - c) * (1f - k); - float g = (1f - m) * (1f - k); - float b = (1f - y) * (1f - k); - - rByte = (byte)Math.Clamp((int)Math.Round(r * 255f), 0, 255); - gByte = (byte)Math.Clamp((int)Math.Round(g * 255f), 0, 255); - bByte = (byte)Math.Clamp((int)Math.Round(b * 255f), 0, 255); - } -} diff --git a/src/PdfEditorApp.Core/Deconstruction/Utils/ColorContrastHelper.cs b/src/PdfEditorApp.Core/Deconstruction/Utils/ColorContrastHelper.cs deleted file mode 100644 index c44f343..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/Utils/ColorContrastHelper.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Globalization; - -namespace PdfEditorApp.Core.Deconstruction.Utils; - -/// -/// WCAG 2.1 compliant color luminance and contrast calculation helper. -/// Used to dynamically protect text readability against arbitrary background shapes and cards. -/// -public static class ColorContrastHelper -{ - /// - /// Calculates the relative luminance of a given hex color according to WCAG 2.1 guidelines. - /// Returns a value between 0.0 (darkest black) and 1.0 (lightest white). - /// - public static double GetRelativeLuminance(string hexColor) - { - if (!TryParseRgb(hexColor, out byte r, out byte g, out byte b)) - { - // Default to white canvas luminance - return 1.0; - } - - double rLin = Linearize(r / 255.0); - double gLin = Linearize(g / 255.0); - double bLin = Linearize(b / 255.0); - - return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin; - } - - /// - /// Calculates the WCAG contrast ratio between two colors. - /// Result ranges from 1:1 (no contrast) to 21:1 (maximum contrast, e.g. pure black on pure white). - /// - public static double GetContrastRatio(string color1Hex, string color2Hex) - { - double l1 = GetRelativeLuminance(color1Hex); - double l2 = GetRelativeLuminance(color2Hex); - - double lighter = Math.Max(l1, l2); - double darker = Math.Min(l1, l2); - - return (lighter + 0.05) / (darker + 0.05); - } - - /// - /// Evaluates the contrast of text color against an underlying background color. - /// If the contrast ratio is below , dynamically adjusts the text color - /// to either or depending on background luminance. - /// - public static string EnsureLegibleContrast( - string textColorHex, - string backgroundColorHex, - double minContrastRatio = 3.0, - string darkFallback = "#0F172A", - string lightFallback = "#FFFFFF") - { - if (string.IsNullOrWhiteSpace(backgroundColorHex) || - string.Equals(backgroundColorHex, "Transparent", StringComparison.OrdinalIgnoreCase)) - { - backgroundColorHex = "#FFFFFF"; - } - - double contrast = GetContrastRatio(textColorHex, backgroundColorHex); - if (contrast >= minContrastRatio) - { - return textColorHex; - } - - double bgLuminance = GetRelativeLuminance(backgroundColorHex); - return bgLuminance > 0.5 ? darkFallback : lightFallback; - } - - public static bool TryParseRgb(string? hex, out byte r, out byte g, out byte b) - { - r = g = b = 0; - if (string.IsNullOrWhiteSpace(hex)) return false; - - string clean = hex.Trim().TrimStart('#'); - if (string.Equals(clean, "Transparent", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (clean.Length == 3) - { - if (byte.TryParse(new string(clean[0], 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r) && - byte.TryParse(new string(clean[1], 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g) && - byte.TryParse(new string(clean[2], 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b)) - { - return true; - } - } - else if (clean.Length == 6) - { - if (byte.TryParse(clean.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r) && - byte.TryParse(clean.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g) && - byte.TryParse(clean.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b)) - { - return true; - } - } - else if (clean.Length == 8) - { - // AARRGGBB or RRGGBBAA -> treat as AARRGGBB standard - if (byte.TryParse(clean.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r) && - byte.TryParse(clean.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g) && - byte.TryParse(clean.Substring(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b)) - { - return true; - } - } - - return false; - } - - private static double Linearize(double val) - { - return val <= 0.04045 ? val / 12.92 : Math.Pow((val + 0.055) / 1.055, 2.4); - } -} diff --git a/src/PdfEditorApp.Core/Deconstruction/Utils/PdfColorHelper.cs b/src/PdfEditorApp.Core/Deconstruction/Utils/PdfColorHelper.cs deleted file mode 100644 index bcc2e1d..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/Utils/PdfColorHelper.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using UglyToad.PdfPig; -using UglyToad.PdfPig.Core; -using UglyToad.PdfPig.Graphics.Colors; -using UglyToad.PdfPig.Tokens; - -namespace PdfEditorApp.Core.Deconstruction.Utils; - -/// -/// Robust color conversion utility for converting arbitrary PdfPig IColor instances -/// (RGB, CMYK, Gray, Pattern, Separation, DeviceN) into standard #RRGGBB hex strings. -/// Prevents unhandled exceptions from crashing the deconstruction engine on complex color spaces. -/// -public static class PdfColorHelper -{ - /// - /// Converts any PdfPig IColor to a standard #RRGGBB hex string. - /// Never throws; returns if the color cannot be evaluated. - /// - public static string ToHex(IColor? color, PdfDocument? doc = null, string fallbackHex = "#0F172A") - { - if (color == null) return fallbackHex; - - try - { - // 1. Handle Pattern colors (Tiling & Shading) - if (color is PatternColor pat) - { - string? patHex = TryExtractPatternHex(pat, doc); - if (!string.IsNullOrEmpty(patHex)) - { - return patHex; - } - return fallbackHex; - } - - // 2. Standard direct conversion (RGB, CMYK, Gray, DeviceN) - var (r, g, b) = color.ToRGBValues(); - int rI = Math.Clamp((int)Math.Round(r * 255.0), 0, 255); - int gI = Math.Clamp((int)Math.Round(g * 255.0), 0, 255); - int bI = Math.Clamp((int)Math.Round(b * 255.0), 0, 255); - - return $"#{rI:X2}{gI:X2}{bI:X2}"; - } - catch - { - return fallbackHex; - } - } - - /// - /// Recursively extracts dominant/representative RGB color from pattern dictionaries and shading functions. - /// - private static string? TryExtractPatternHex(PatternColor pat, PdfDocument? doc) - { - if (pat is TilingPatternColor tp && tp.Resources != null && doc != null) - { - var hex = TryExtractHexFromResources(tp.Resources, doc); - if (hex != null) return hex; - } - - // Return a vibrant blue hero banner fallback for tiling pattern backgrounds - return "#2563EB"; - } - - private static string? TryExtractHexFromResources(DictionaryToken resDict, PdfDocument doc) - { - try - { - // 1. Check direct Shading in resources - if (resDict.TryGet(NameToken.Shading, out var shDict)) - { - foreach (var kvp in shDict.Data) - { - var hex = ExtractHexFromShadingToken(kvp.Value, doc); - if (hex != null) return hex; - } - } - - // 2. Check XObject (Forms) in resources - if (resDict.TryGet(NameToken.Xobject, out var xobjDict)) - { - foreach (var kvp in xobjDict.Data) - { - DictionaryToken? formRes = null; - if (kvp.Value is IndirectReferenceToken irt) - { - var formObj = doc.Structure.GetObject(irt.Data); - if (formObj?.Data is StreamToken st && st.StreamDictionary.TryGet(NameToken.Resources, out var r)) - { - formRes = r; - } - else if (formObj?.Data is StreamToken st2 && - st2.StreamDictionary.Data.TryGetValue(NameToken.Resources, out var rToken) && - rToken is IndirectReferenceToken rIrt) - { - formRes = doc.Structure.GetObject(rIrt.Data)?.Data as DictionaryToken; - } - } - - if (formRes != null) - { - var hex = TryExtractHexFromResources(formRes, doc); - if (hex != null) return hex; - } - } - } - } - catch - { - // Suppress and return null to trigger fallback - } - - return null; - } - - private static string? ExtractHexFromShadingToken(IToken token, PdfDocument doc) - { - DictionaryToken? shData = null; - if (token is IndirectReferenceToken irt) - { - shData = doc.Structure.GetObject(irt.Data)?.Data as DictionaryToken; - } - else if (token is DictionaryToken dt) - { - shData = dt; - } - - if (shData != null && shData.TryGet(NameToken.Function, out var fnDict)) - { - return ExtractColorFromFunctionDict(fnDict, doc); - } - - return null; - } - - private static string? ExtractColorFromFunctionDict(DictionaryToken fnDict, PdfDocument doc) - { - // Check C0 (initial color) - if (fnDict.TryGet(NameToken.C0, out var c0) && c0.Data.Count >= 3) - { - double r = (c0.Data[0] as NumericToken)?.Double ?? 0; - double g = (c0.Data[1] as NumericToken)?.Double ?? 0; - double b = (c0.Data[2] as NumericToken)?.Double ?? 0; - - int rI = Math.Clamp((int)Math.Round(r * 255.0), 0, 255); - int gI = Math.Clamp((int)Math.Round(g * 255.0), 0, 255); - int bI = Math.Clamp((int)Math.Round(b * 255.0), 0, 255); - return $"#{rI:X2}{gI:X2}{bI:X2}"; - } - - // Check C1 (ending color) if C0 not present - if (fnDict.TryGet(NameToken.C1, out var c1) && c1.Data.Count >= 3) - { - double r = (c1.Data[0] as NumericToken)?.Double ?? 0; - double g = (c1.Data[1] as NumericToken)?.Double ?? 0; - double b = (c1.Data[2] as NumericToken)?.Double ?? 0; - - int rI = Math.Clamp((int)Math.Round(r * 255.0), 0, 255); - int gI = Math.Clamp((int)Math.Round(g * 255.0), 0, 255); - int bI = Math.Clamp((int)Math.Round(b * 255.0), 0, 255); - return $"#{rI:X2}{gI:X2}{bI:X2}"; - } - - // Recursively inspect Functions array if stitched function (FunctionType 3) - if (fnDict.TryGet(NameToken.Functions, out var fns)) - { - foreach (var fToken in fns.Data) - { - DictionaryToken? subFn = null; - if (fToken is IndirectReferenceToken irt) - { - subFn = doc.Structure.GetObject(irt.Data)?.Data as DictionaryToken; - } - else if (fToken is DictionaryToken dt) - { - subFn = dt; - } - - if (subFn != null) - { - var hex = ExtractColorFromFunctionDict(subFn, doc); - if (hex != null) return hex; - } - } - } - - return null; - } -} diff --git a/src/PdfEditorApp.Core/Deconstruction/Utils/SvgPathBuilder.cs b/src/PdfEditorApp.Core/Deconstruction/Utils/SvgPathBuilder.cs deleted file mode 100644 index 675e3fc..0000000 --- a/src/PdfEditorApp.Core/Deconstruction/Utils/SvgPathBuilder.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; -using UglyToad.PdfPig.Core; -using UglyToad.PdfPig.Graphics; - -namespace PdfEditorApp.Core.Deconstruction.Utils; - -/// -/// Converts collections of raw PdfPig vector paths into optimized standard SVG markup. -/// Used to group excess vector paths into a single resolution-independent . -/// -public static class SvgPathBuilder -{ - /// - /// Builds an SVG string containing all the provided paths with their respective fill, stroke, and geometry commands. - /// Coordinates are transformed from PDF bottom-left to standard SVG top-left coordinates. - /// - public static string BuildSvgFromPaths( - IReadOnlyList paths, - double minX, - double minY, - double width, - double height, - double pageHeight, - UglyToad.PdfPig.PdfDocument? doc = null) - { - var sb = new StringBuilder(); - var inv = CultureInfo.InvariantCulture; - - sb.Append($"\n"); - - foreach (var path in paths) - { - string pathData = BuildPathData(path, minX, minY, pageHeight); - if (string.IsNullOrWhiteSpace(pathData)) continue; - - string strokeHex = "none"; - double strokeWidth = 0; - if (path.IsStroked) - { - strokeWidth = Math.Max(0.5, path.LineWidth); - strokeHex = PdfColorHelper.ToHex(path.StrokeColor, doc, "#0F172A"); - } - - string fillHex = "none"; - if (path.IsFilled) - { - fillHex = PdfColorHelper.ToHex(path.FillColor, doc, strokeHex != "none" ? strokeHex : "#000000"); - } - - sb.Append($" \n"); - } - - sb.Append(""); - return sb.ToString(); - } - - public static string BuildPathData(PdfPath path, double offsetX, double offsetY, double pageHeight) - { - var sb = new StringBuilder(); - var inv = CultureInfo.InvariantCulture; - - for (int sIdx = 0; sIdx < path.Count; sIdx++) - { - var subpath = path[sIdx]; - if (subpath.Commands == null) continue; - - foreach (var cmd in subpath.Commands) - { - switch (cmd) - { - case PdfSubpath.Move move: - double mx = move.Location.X - offsetX; - double my = (pageHeight - move.Location.Y) - offsetY; - sb.Append($"M {mx.ToString("F1", inv)} {my.ToString("F1", inv)} "); - break; - - case PdfSubpath.Line line: - double lx = line.To.X - offsetX; - double ly = (pageHeight - line.To.Y) - offsetY; - sb.Append($"L {lx.ToString("F1", inv)} {ly.ToString("F1", inv)} "); - break; - - case PdfSubpath.BezierCurve bezier: - var lines = bezier.ToLines(8); - foreach (var l in lines) - { - double ex = l.To.X - offsetX; - double ey = (pageHeight - l.To.Y) - offsetY; - sb.Append($"L {ex.ToString("F1", inv)} {ey.ToString("F1", inv)} "); - } - break; - - case PdfSubpath.Close: - sb.Append("Z "); - break; - } - } - } - - string result = sb.ToString().TrimEnd(); - if (path.IsFilled && !result.EndsWith("Z", StringComparison.OrdinalIgnoreCase)) - { - result += " Z"; - } - return result; - } -} diff --git a/src/PdfEditorApp.Core/Models/AiModels.cs b/src/PdfEditorApp.Core/Models/AiModels.cs deleted file mode 100644 index aa2b4ca..0000000 --- a/src/PdfEditorApp.Core/Models/AiModels.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace PdfEditorApp.Core.Models; - -/// -/// Supported AI provider categories in FryPDF. -/// -public enum AiProviderType -{ - /// - /// Local self-hosted Ollama runtime (100% private, free, offline). - /// - OllamaLocal = 0, - - /// - /// Cloud OpenAI API (requires user API key, paid usage). - /// - OpenAiCloud = 1, - - /// - /// Custom OpenAI-compatible endpoint (Groq, OpenRouter, LM Studio, LocalAI, vLLM). - /// - CustomOpenAiCompatible = 2 -} - -/// -/// Economic and locality classification tier of an AI model. -/// -public enum AiModelTier -{ - /// - /// Free, zero cloud data transfer, run locally on device. - /// - FreeLocal = 0, - - /// - /// Free cloud tier (e.g. Ollama Cloud free tier, OpenRouter free models, Groq free tier). - /// - FreeCloud = 1, - - /// - /// Cloud-hosted API model requiring tokens / paid subscription. - /// - PaidCloud = 2 -} - -/// -/// Metadata representing an available AI language model in FryPDF. -/// -public class AiModelInfo -{ - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; - - [JsonPropertyName("provider")] - public AiProviderType Provider { get; set; } = AiProviderType.OllamaLocal; - - [JsonPropertyName("tier")] - public AiModelTier Tier { get; set; } = AiModelTier.FreeLocal; - - [JsonPropertyName("sizeBytes")] - public long? SizeBytes { get; set; } - - [JsonPropertyName("formattedSize")] - public string FormattedSize { get; set; } = string.Empty; - - [JsonPropertyName("parameterSize")] - public string ParameterSize { get; set; } = string.Empty; - - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - [JsonPropertyName("isInstalledLocally")] - public bool IsInstalledLocally { get; set; } - - [JsonPropertyName("category")] - public string Category { get; set; } = "Ollama Library"; - - [JsonPropertyName("capabilities")] - public string Capabilities { get; set; } = string.Empty; - - [JsonIgnore] - public bool IsFree => Tier == AiModelTier.FreeLocal || Tier == AiModelTier.FreeCloud; - - [JsonIgnore] - public bool IsCloud => Tier == AiModelTier.FreeCloud || Tier == AiModelTier.PaidCloud; - - [JsonIgnore] - public string TierBadgeText => Tier switch - { - AiModelTier.FreeLocal => "Free / Local", - AiModelTier.FreeCloud => "Free / Cloud", - AiModelTier.PaidCloud => "Paid / Cloud", - _ => "Free" - }; - - [JsonIgnore] - public string BadgeBackgroundColor => Tier switch - { - AiModelTier.FreeLocal => "#DCFCE7", - AiModelTier.FreeCloud => "#E0F2FE", - AiModelTier.PaidCloud => "#EDE9FE", - _ => "#F3F4F6" - }; - - [JsonIgnore] - public string BadgeBorderColor => Tier switch - { - AiModelTier.FreeLocal => "#86EFAC", - AiModelTier.FreeCloud => "#7DD3FC", - AiModelTier.PaidCloud => "#C4B5FD", - _ => "#D1D5DB" - }; - - [JsonIgnore] - public string BadgeForegroundColor => Tier switch - { - AiModelTier.FreeLocal => "#15803D", - AiModelTier.FreeCloud => "#0369A1", - AiModelTier.PaidCloud => "#6D28D9", - _ => "#374151" - }; - - [JsonIgnore] - public string FullTitleWithBadge => $"[{TierBadgeText}] {DisplayName}"; - - public override bool Equals(object? obj) => - obj is AiModelInfo other && string.Equals(Id, other.Id, StringComparison.OrdinalIgnoreCase); - - public override int GetHashCode() => - string.IsNullOrEmpty(Id) ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(Id); - - public override string ToString() => string.IsNullOrWhiteSpace(DisplayName) ? Id : FullTitleWithBadge; - - public static AiModelInfo CreateForCustomId(string id, AiProviderType provider, bool isEndpointRemote = false, string? endpoint = null) - { - if (string.IsNullOrWhiteSpace(id)) - { - id = provider == AiProviderType.CustomOpenAiCompatible ? "openai/gpt-oss-120b" : "llama3.2"; - } - - id = id.Trim(); - bool isCloud = id.Contains("cloud", StringComparison.OrdinalIgnoreCase) || - id.Contains(":free", StringComparison.OrdinalIgnoreCase) || - isEndpointRemote; - - AiModelTier tier; - if (provider == AiProviderType.OllamaLocal) - { - tier = isCloud ? AiModelTier.FreeCloud : AiModelTier.FreeLocal; - } - else if (provider == AiProviderType.CustomOpenAiCompatible) - { - bool isGroq = (endpoint?.Contains("groq", StringComparison.OrdinalIgnoreCase) == true) || id.Contains("groq", StringComparison.OrdinalIgnoreCase); - bool isFree = id.Contains("free", StringComparison.OrdinalIgnoreCase) || isGroq; - if (!isEndpointRemote) - { - tier = AiModelTier.FreeLocal; - } - else - { - tier = isFree ? AiModelTier.FreeCloud : AiModelTier.PaidCloud; - } - } - else - { - tier = AiModelTier.PaidCloud; - } - - string formattedSize = isCloud - ? (provider == AiProviderType.CustomOpenAiCompatible && (endpoint?.Contains("groq", StringComparison.OrdinalIgnoreCase) == true) ? "Groq LPU Cloud" : "Cloud API") - : "Local / Custom"; - - return new AiModelInfo - { - Id = id, - DisplayName = id, - Provider = provider, - Tier = tier, - Category = provider == AiProviderType.OllamaLocal ? "Ollama Custom" : (provider == AiProviderType.CustomOpenAiCompatible ? "Custom Compatible" : "OpenAI Cloud"), - Description = $"Model identifier: {id}", - FormattedSize = formattedSize, - IsInstalledLocally = !isCloud && provider == AiProviderType.OllamaLocal - }; - } - - public static string FormatBytes(long bytes) - { - if (bytes <= 0) return string.Empty; - if (bytes >= 1024L * 1024 * 1024) - return $"{bytes / (1024.0 * 1024 * 1024):0.0} GB"; - if (bytes >= 1024L * 1024) - return $"{bytes / (1024.0 * 1024):0.0} MB"; - return $"{bytes / 1024.0:0.0} KB"; - } -} - -/// -/// Result of an AI Studio Agent prompt execution. -/// -public class AiAgentResult -{ - public bool Success { get; set; } - public string Message { get; set; } = string.Empty; - public int ElementsCreatedCount { get; set; } - public List ActionDescriptions { get; set; } = new(); - public string? RawOutput { get; set; } - public TimeSpan Duration { get; set; } -} diff --git a/src/PdfEditorApp.Core/Models/CommandPaletteItem.cs b/src/PdfEditorApp.Core/Models/CommandPaletteItem.cs deleted file mode 100644 index 31a4fbf..0000000 --- a/src/PdfEditorApp.Core/Models/CommandPaletteItem.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace PdfEditorApp.Core.Models; - -public class CommandPaletteItem -{ - public string Title { get; set; } = ""; - public string Subtitle { get; set; } = ""; - public string Category { get; set; } = "General"; - public string IconKind { get; set; } = "LightningBolt"; - public string Shortcut { get; set; } = ""; - public Action Action { get; set; } = () => { }; -} diff --git a/src/PdfEditorApp.Core/Models/CommentItem.cs b/src/PdfEditorApp.Core/Models/CommentItem.cs deleted file mode 100644 index 1f48acf..0000000 --- a/src/PdfEditorApp.Core/Models/CommentItem.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace PdfEditorApp.Core.Models; - -public class CommentItem -{ - public string Author { get; set; } = ""; - public string Timestamp { get; set; } = ""; - public string Text { get; set; } = ""; - public string Status { get; set; } = ""; - public int PageIndex { get; set; } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/DynamicElementJsonResolver.cs b/src/PdfEditorApp.Core/Models/Elements/DynamicElementJsonResolver.cs deleted file mode 100644 index a66f1bc..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/DynamicElementJsonResolver.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Core.Models.Elements; - -/// -/// Configures System.Text.Json polymorphic serialization for -/// by dynamically reflecting all registered canvas element types from . -/// Enables 3rd-party plugins to contribute novel canvas element models without modifying core enums. -/// -public static class DynamicElementJsonResolver -{ - public static JsonSerializerOptions CreateOptions(Func>? descriptorsProvider = null, bool writeIndented = false) - { - var resolver = new DefaultJsonTypeInfoResolver(); - - if (descriptorsProvider != null) - { - resolver.Modifiers.Add(typeInfo => - { - if (typeInfo.Type == typeof(PdfElementBase)) - { - var descriptors = descriptorsProvider(); - if (descriptors != null && typeInfo.PolymorphismOptions != null) - { - foreach (var desc in descriptors) - { - if (!typeInfo.PolymorphismOptions.DerivedTypes.Any(d => d.DerivedType == desc.ModelType)) - { - typeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(desc.ModelType, desc.ElementTypeId)); - } - } - } - } - }); - } - - var options = new JsonSerializerOptions - { - WriteIndented = writeIndented, - TypeInfoResolver = resolver, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - options.Converters.Add(new JsonStringEnumConverter()); - return options; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfBarcodeElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfBarcodeElement.cs deleted file mode 100644 index e4aecc4..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfBarcodeElement.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfBarcodeElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Barcode; - - public string CodeValue { get; set; } = "DOC-2026-984210"; - public string BarcodeFormat { get; set; } = "Code128"; - public string BarColorHex { get; set; } = "#0F172A"; - public string BackgroundColorHex { get; set; } = "#FFFFFF"; - public bool ShowText { get; set; } = true; - - public override PdfElementBase Clone() - { - return (PdfBarcodeElement)base.Clone(); - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfChartElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfChartElement.cs deleted file mode 100644 index 3418370..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfChartElement.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Models.Elements; - -public class ChartSeriesItem -{ - public string Name { get; set; } = "Series 1"; - public List Values { get; set; } = new(); - public string? ColorHex { get; set; } - - public ChartSeriesItem Clone() => new() - { - Name = Name, - Values = new List(Values), - ColorHex = ColorHex - }; -} - -public class PdfChartElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Chart; - - public string Title { get; set; } = "Revenue Growth (Q1-Q4)"; - public ChartType ChartType { get; set; } = ChartType.BarColumn; - public ChartPalette Palette { get; set; } = ChartPalette.CorporateBlue; - public ChartLegendPosition LegendPosition { get; set; } = ChartLegendPosition.Top; - - public List Categories { get; set; } = new() { "Q1", "Q2", "Q3", "Q4" }; - public List Values { get; set; } = new() { 1.2, 1.8, 2.5, 3.1 }; - public List ValueLabels { get; set; } = new() { "$1.2B", "$1.8B", "$2.5B", "$3.1B" }; - public List BarColorsHex { get; set; } = new() { "#C7E0F4", "#82BDF0", "#3D95E6", "#0F6CBD" }; - public List MultiSeries { get; set; } = new(); - - public bool ShowDataLabels { get; set; } = true; - public bool ShowGridlines { get; set; } = true; - public double DonutHoleRatio { get; set; } = 0.6; - public double CurveSmoothness { get; set; } = 0.65; - public double StrokeThickness { get; set; } = 3.0; - public string? XAxisTitle { get; set; } - public string? YAxisTitle { get; set; } - - public string BackgroundColorHex { get; set; } = "#F8F9FA"; - public string BorderColorHex { get; set; } = "#E1DFDD"; - - public override PdfElementBase Clone() - { - var clone = (PdfChartElement)base.Clone(); - clone.Categories = new List(Categories); - clone.Values = new List(Values); - clone.ValueLabels = new List(ValueLabels); - clone.BarColorsHex = new List(BarColorsHex); - clone.MultiSeries = new List(); - foreach (var s in MultiSeries) - { - clone.MultiSeries.Add(new ChartSeriesItem - { - Name = s.Name, - Values = new List(s.Values), - ColorHex = s.ColorHex - }); - } - return clone; - } -} - diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfDividerElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfDividerElement.cs deleted file mode 100644 index 030a8f1..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfDividerElement.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfDividerElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Divider; - - public string ColorHex { get; set; } = "#0F6CBD"; - public double Thickness { get; set; } = 2; - public bool IsVertical { get; set; } = false; - public DividerStyle Style { get; set; } = DividerStyle.Straight; - public double WaveAmplitude { get; set; } = 6.0; - public double WaveFrequency { get; set; } = 4.0; - public LineDashStyle DashStyle { get; set; } = LineDashStyle.Solid; - - public override PdfElementBase Clone() - { - var clone = (PdfDividerElement)base.Clone(); - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfElementBase.cs b/src/PdfEditorApp.Core/Models/Elements/PdfElementBase.cs deleted file mode 100644 index 7357038..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfElementBase.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace PdfEditorApp.Core.Models.Elements; - -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -[JsonDerivedType(typeof(PdfTextElement), typeDiscriminator: "text")] -[JsonDerivedType(typeof(PdfImageElement), typeDiscriminator: "image")] -[JsonDerivedType(typeof(PdfShapeElement), typeDiscriminator: "shape")] -[JsonDerivedType(typeof(PdfDividerElement), typeDiscriminator: "divider")] -[JsonDerivedType(typeof(PdfChartElement), typeDiscriminator: "chart")] -[JsonDerivedType(typeof(PdfTableElement), typeDiscriminator: "table")] -[JsonDerivedType(typeof(PdfWatermarkElement), typeDiscriminator: "watermark")] -[JsonDerivedType(typeof(PdfFormFieldElement), typeDiscriminator: "formfield")] -[JsonDerivedType(typeof(PdfQrCodeElement), typeDiscriminator: "qrcode")] -[JsonDerivedType(typeof(PdfBarcodeElement), typeDiscriminator: "barcode")] -[JsonDerivedType(typeof(PdfRedactionElement), typeDiscriminator: "redaction")] -[JsonDerivedType(typeof(PdfInkElement), typeDiscriminator: "ink")] -[JsonDerivedType(typeof(PdfStickyNoteElement), typeDiscriminator: "stickynote")] -[JsonDerivedType(typeof(PdfMeasurementElement), typeDiscriminator: "measurement")] -[JsonDerivedType(typeof(PdfSvgElement), typeDiscriminator: "svg")] -[JsonDerivedType(typeof(PdfMathElement), typeDiscriminator: "math")] -public abstract class PdfElementBase -{ - public string Id { get; set; } = Guid.NewGuid().ToString("N"); - public abstract ElementKind Kind { get; } - - public double X { get; set; } - public double Y { get; set; } - public double Width { get; set; } = 200; - public double Height { get; set; } = 100; - public int ZIndex { get; set; } = 0; - public double Rotation { get; set; } = 0; - public double Opacity { get; set; } = 1.0; - public bool IsLocked { get; set; } = false; - public string? GroupId { get; set; } - - public virtual PdfElementBase Clone() - { - var clone = (PdfElementBase)MemberwiseClone(); - clone.Id = Guid.NewGuid().ToString("N"); - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfFormFieldElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfFormFieldElement.cs deleted file mode 100644 index c69f8b2..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfFormFieldElement.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfFormFieldElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.FormField; - - public FormFieldType FieldType { get; set; } = FormFieldType.Text; - public string FieldName { get; set; } = "form_field_1"; - public string Label { get; set; } = "Full Name:"; - public string Placeholder { get; set; } = "Enter your full legal name..."; - public string Value { get; set; } = ""; - public string DefaultValue { get; set; } = ""; - public string Tooltip { get; set; } = ""; - public bool IsRequired { get; set; } = true; - public bool IsReadOnly { get; set; } = false; - public bool IsChecked { get; set; } = false; - public FormValidationType ValidationType { get; set; } = FormValidationType.None; - public string CustomValidationRegex { get; set; } = ""; - public string BorderColorHex { get; set; } = "#0F6CBD"; - public string BackgroundColorHex { get; set; } = "#F8FAFC"; - public double FontSize { get; set; } = 12; - public List Options { get; set; } = new() { "Option 1", "Option 2", "Option 3" }; - - // Acrobat Form Calculations & Action Buttons - public CalculationFormula CalculationFormula { get; set; } = CalculationFormula.None; - public string CalculationSourceFields { get; set; } = ""; - public FormButtonAction ButtonAction { get; set; } = FormButtonAction.SubmitForm; - public string ActionTarget { get; set; } = ""; - - public override PdfElementBase Clone() - { - var clone = (PdfFormFieldElement)base.Clone(); - clone.Options = new List(Options); - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfImageElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfImageElement.cs deleted file mode 100644 index 13fd78d..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfImageElement.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfImageElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Image; - - public string? ImagePath { get; set; } - - /// - /// Raw binary image bytes (PNG, JPEG, etc.). - /// Storing bytes directly eliminates Large Object Heap (LOH) string allocation bloat during PDF deconstruction. - /// - [JsonIgnore] - public byte[]? ImageData - { - get => _imageData; - set - { - _imageData = value; - _cachedBase64 = null; - } - } - private byte[]? _imageData; - - /// - /// Base64 encoded representation for JSON persistence and legacy interop. - /// Evaluated lazily on demand from to avoid memory overhead. - /// - public string? Base64Data - { - get - { - if (_cachedBase64 != null) return _cachedBase64; - if (_imageData != null && _imageData.Length > 0) - { - _cachedBase64 = Convert.ToBase64String(_imageData); - return _cachedBase64; - } - return null; - } - set - { - _cachedBase64 = value; - if (!string.IsNullOrEmpty(value)) - { - try - { - _imageData = Convert.FromBase64String(value); - } - catch - { - // Invalid base64 string - } - } - } - } - private string? _cachedBase64; - - public bool KeepAspectRatio { get; set; } = true; - public double CornerRadius { get; set; } = 4; - public string BorderColorHex { get; set; } = "#E1DFDD"; - public double BorderThickness { get; set; } = 1; - public string AltText { get; set; } = "Image"; - - public override PdfElementBase Clone() - { - var clone = (PdfImageElement)base.Clone(); - if (_imageData != null) - { - clone.ImageData = (byte[])_imageData.Clone(); - } - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfInkElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfInkElement.cs deleted file mode 100644 index 34ea9e1..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfInkElement.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfInkElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Ink; - - public string PointsData { get; set; } = "10,10 50,40 100,20 180,60 220,30"; - public string StrokeColorHex { get; set; } = "#0F6CBD"; - public double StrokeThickness { get; set; } = 3.0; - public bool IsHighlighter { get; set; } = false; - public bool IsSmoothSpline { get; set; } = true; - - public override PdfElementBase Clone() - { - return (PdfInkElement)base.Clone(); - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfMathElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfMathElement.cs deleted file mode 100644 index 887aa79..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfMathElement.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -/// -/// Native mathematical equation & formula element supporting standard LaTeX / Math syntax, -/// vector SVG rendering, equation numbering tags, and full typography customization. -/// -public class PdfMathElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Math; - - /// The mathematical expression in standard LaTeX syntax. - public string Formula { get; set; } = @"\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}"; - - /// Base typography font size for rendered math symbols. - public double FontSize { get; set; } = 16; - - /// Hex color for mathematical symbols, text, and operators. - public string TextColorHex { get; set; } = "#0F172A"; - - /// Optional background color fill for equation card/callout. - public string BackgroundColorHex { get; set; } = "#00000000"; - - /// Border stroke color. - public string BorderColorHex { get; set; } = "#00000000"; - - /// Border stroke thickness. - public double BorderThickness { get; set; } = 0; - - /// Corner radius for rounded equation card borders. - public double CornerRadius { get; set; } = 4; - - /// Internal padding around the rendered equation. - public double Padding { get; set; } = 8; - - /// Whether to render a visible background box. - public bool ShowBackground { get; set; } = false; - - /// Whether to render a visible border around the equation. - public bool ShowBorder { get; set; } = false; - - /// Whether to render an equation number tag (e.g. (1), (2.3)) aligned to the right. - public bool ShowEquationNumber { get; set; } = false; - - /// Equation numbering label (e.g. "(1)", "(2.4)", "(IV.1)"). - public string EquationNumber { get; set; } = "(1)"; - - /// Alignment of the equation within its bounding box (Left, Center, Right). - public TextAlignmentMode Alignment { get; set; } = TextAlignmentMode.Center; - - /// Display mode (DisplayBlock for standalone centered equations, Inline for compact formulas). - public MathDisplayStyle DisplayStyle { get; set; } = MathDisplayStyle.DisplayBlock; - - /// Subject category classification (Algebra, Calculus, Physics, Quantum, Finance, etc.). - public MathCategory Category { get; set; } = MathCategory.Calculus; - - /// Optional preset name if created from template library. - public string? PresetName { get; set; } = "Gaussian Integral"; - - /// Optional user explanation or theorem title. - public string? Description { get; set; } = "Euler-Poisson Gaussian Integral"; - - public override PdfElementBase Clone() - { - return (PdfMathElement)base.Clone(); - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfMeasurementElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfMeasurementElement.cs deleted file mode 100644 index 8c265af..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfMeasurementElement.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfMeasurementElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Measurement; - - public double StartX { get; set; } = 0; - public double StartY { get; set; } = 0; - public double EndX { get; set; } = 200; - public double EndY { get; set; } = 0; - - public RulerUnit Unit { get; set; } = RulerUnit.Points; - public double ScaleFactor { get; set; } = 1.0; // 1 pt = ScaleFactor * Unit - public string CustomLabel { get; set; } = ""; - public string StrokeColorHex { get; set; } = "#DC2626"; - public double StrokeThickness { get; set; } = 1.5; - public double ArrowSize { get; set; } = 6.0; - public double ExtensionLineLength { get; set; } = 10.0; - public double FontSize { get; set; } = 10.0; - - public double CalculateDistance() - { - double dx = EndX - StartX; - double dy = EndY - StartY; - return Math.Sqrt(dx * dx + dy * dy); - } - - public string GetFormattedDistance() - { - if (!string.IsNullOrWhiteSpace(CustomLabel)) - return CustomLabel; - - double distancePts = CalculateDistance(); - return Unit switch - { - RulerUnit.Inches => $"{distancePts / 72.0:F2} in", - RulerUnit.Millimeters => $"{distancePts * 25.4 / 72.0:F1} mm", - _ => $"{distancePts:F1} pt" - }; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfQrCodeElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfQrCodeElement.cs deleted file mode 100644 index be6d4e2..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfQrCodeElement.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfQrCodeElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.QrCode; - - public string Content { get; set; } = "https://github.com/PrashantUnity/PDFCreator"; - public string DarkColorHex { get; set; } = "#0F172A"; - public string LightColorHex { get; set; } = "#FFFFFF"; - public string Label { get; set; } = "SCAN TO VERIFY CREDENTIAL"; - public QrCodeEccLevel EccLevel { get; set; } = QrCodeEccLevel.M; - public bool DrawQuietZones { get; set; } = true; - - public override PdfElementBase Clone() - { - return (PdfQrCodeElement)base.Clone(); - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfRedactionElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfRedactionElement.cs deleted file mode 100644 index 8456bf6..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfRedactionElement.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfRedactionElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Redaction; - - public RedactionMode Mode { get; set; } = RedactionMode.Blackout; - public string ExemptionCode { get; set; } = "[REDACTED - (b)(4) PRIVILEGED]"; - public string FillColorHex { get; set; } = "#0F172A"; - public string TextColorHex { get; set; } = "#FFFFFF"; - public string BorderColorHex { get; set; } = "#DC2626"; - public double BorderThickness { get; set; } = 1.5; - public bool ShowOverlayText { get; set; } = true; - - public override PdfElementBase Clone() - { - return (PdfRedactionElement)base.Clone(); - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfShapeElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfShapeElement.cs deleted file mode 100644 index 82d0594..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfShapeElement.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfShapeElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Shape; - - public ShapeType ShapeType { get; set; } = ShapeType.Rectangle; - public string FillColorHex { get; set; } = "#F8F9FA"; - public string StrokeColorHex { get; set; } = "#0F6CBD"; - public double StrokeThickness { get; set; } = 1.5; - public double CornerRadius { get; set; } = 4; - public string? Label { get; set; } - public string? LabelColorHex { get; set; } = "#201F1E"; - public double LabelFontSize { get; set; } = 12; - - public string? CustomPathData { get; set; } - public string? SecondaryFillColorHex { get; set; } - public string? SecondaryStrokeColorHex { get; set; } - - // Bézier Curves, Connectors & Line Caps - public double BezierP0X { get; set; } = 0.0; - public double BezierP0Y { get; set; } = 0.5; - public double BezierP1X { get; set; } = 0.33; - public double BezierP1Y { get; set; } = 0.10; - public double BezierP2X { get; set; } = 0.67; - public double BezierP2Y { get; set; } = 0.90; - public double BezierP3X { get; set; } = 1.0; - public double BezierP3Y { get; set; } = 0.5; - - public LineEndCap StartCap { get; set; } = LineEndCap.None; - public LineEndCap EndCap { get; set; } = LineEndCap.None; - public LineDashStyle DashStyle { get; set; } = LineDashStyle.Solid; - public double WaveFrequency { get; set; } = 2.0; - public double CurvatureDepth { get; set; } = 40.0; - - public override PdfElementBase Clone() - { - var clone = (PdfShapeElement)base.Clone(); - clone.CustomPathData = CustomPathData; - clone.SecondaryFillColorHex = SecondaryFillColorHex; - clone.SecondaryStrokeColorHex = SecondaryStrokeColorHex; - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfStickyNoteElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfStickyNoteElement.cs deleted file mode 100644 index c631ec6..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfStickyNoteElement.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfStickyNoteElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.StickyNote; - - public string Author { get; set; } = "Senior Reviewer"; - public string Timestamp { get; set; } = DateTime.Now.ToString("MMM dd, yyyy HH:mm"); - public string NoteText { get; set; } = "Please verify the audit figures with the legal compliance team prior to final PDF release."; - public string Status { get; set; } = "Pending Review"; - public string ColorHex { get; set; } = "#FEF3C7"; - public string BorderColorHex { get; set; } = "#F59E0B"; - public bool IsExpanded { get; set; } = true; - - public override PdfElementBase Clone() - { - return (PdfStickyNoteElement)base.Clone(); - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfSvgElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfSvgElement.cs deleted file mode 100644 index ff956ad..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfSvgElement.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -/// -/// Native vector SVG element for logos, icons, ceremonial crests, garlands, and complex illustrations. -/// -public class PdfSvgElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Svg; - - /// Raw SVG XML string markup. - public string SvgSource { get; set; } = ""; - - /// Optional file path if loaded from external disk file. - public string? FilePath { get; set; } - - /// Optional color override / tint (if specified, replaces default fills with this color). - public string? TintColorHex { get; set; } - - /// Preset ornament identifier if selected from built-in library. - public string? PresetName { get; set; } - - public bool KeepAspectRatio { get; set; } = true; - public double CornerRadius { get; set; } = 0; - public string? BorderColorHex { get; set; } - public double BorderThickness { get; set; } = 0; - - public override PdfElementBase Clone() - { - return (PdfSvgElement)base.Clone(); - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfTableElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfTableElement.cs deleted file mode 100644 index 22e0802..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfTableElement.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfTableElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Table; - - public List Headers { get; set; } = new() { "Item Description", "Qty", "Rate", "Amount" }; - public List> Rows { get; set; } = new() - { - new() { "Cloud Architecture Consulting", "40 hrs", "$150.00", "$6,000.00" }, - new() { "Avalonia Desktop UI Engineering", "60 hrs", "$140.00", "$8,400.00" }, - new() { "QuestPDF Engine Integration", "25 hrs", "$160.00", "$4,000.00" }, - new() { "QA, Testing & Performance Tuning", "15 hrs", "$120.00", "$1,800.00" } - }; - - public string HeaderBackgroundHex { get; set; } = "#0F6CBD"; - public string HeaderTextHex { get; set; } = "#FFFFFF"; - public string AlternateRowBackgroundHex { get; set; } = "#F8F9FA"; - public string BorderColorHex { get; set; } = "#E1DFDD"; - - public override PdfElementBase Clone() - { - var clone = (PdfTableElement)base.Clone(); - clone.Headers = new List(Headers); - clone.Rows = new List>(); - foreach (var row in Rows) - { - clone.Rows.Add(new List(row)); - } - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfTextElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfTextElement.cs deleted file mode 100644 index ca01a35..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfTextElement.cs +++ /dev/null @@ -1,122 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfTextElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Text; - - // Content & Typography - public string Text { get; set; } = "Enter text here"; - public string FontFamily { get; set; } = "Segoe UI"; - public double FontSize { get; set; } = 14; - public bool IsBold { get; set; } = false; - public bool IsItalic { get; set; } = false; - public bool IsUnderline { get; set; } = false; - public bool IsDoubleUnderline { get; set; } = false; - public bool IsStrikethrough { get; set; } = false; - public string TextColorHex { get; set; } = "#201F1E"; - public double TextOpacity { get; set; } = 1.0; - - // Paragraph & Spacing - public TextAlignmentMode Alignment { get; set; } = TextAlignmentMode.Left; - public TextVerticalAlignment VerticalAlignment { get; set; } = TextVerticalAlignment.Top; - public double LineHeight { get; set; } = 1.4; - public double CharacterSpacing { get; set; } = 0; - public double WordSpacing { get; set; } = 0; - public double ParagraphSpacing { get; set; } = 0; - public bool TextWrap { get; set; } = true; - - // Stroke / Outline - public bool HasStroke { get; set; } = false; - public string StrokeColorHex { get; set; } = "#000000"; - public double StrokeWidth { get; set; } = 1.0; - - // Shadow & Glow - public bool HasShadow { get; set; } = false; - public string ShadowColorHex { get; set; } = "#80000000"; - public double ShadowOffsetX { get; set; } = 2.0; - public double ShadowOffsetY { get; set; } = 2.0; - public double ShadowBlurRadius { get; set; } = 4.0; - public double ShadowOpacity { get; set; } = 0.5; - - // Box Background, Border & Padding - public double Padding { get; set; } = 0; - public string BackgroundColorHex { get; set; } = "#00000000"; // Transparent by default - public string BorderColorHex { get; set; } = "#00000000"; - public double BorderThickness { get; set; } = 0; - public double CornerRadius { get; set; } = 0; - - // Curved & Circular Typography - public TextShapeMode ShapeMode { get; set; } = TextShapeMode.Normal; - public double CurveRadius { get; set; } = 120; - public double CurveArcAngle { get; set; } = 180; // Sweep angle in degrees (e.g. 30 to 360) - public double CurveStartAngle { get; set; } = 0; // Center offset angle in degrees - public bool CurveClockwise { get; set; } = true; - public bool CurveInvert { get; set; } = false; // Invert text along curve (inside vs outside) - public CircularTextPlacement CircularPlacement { get; set; } = CircularTextPlacement.TopArc; - - // Per-Character & Transform Offsets - public double BaselineShift { get; set; } = 0; - public double CharacterRotation { get; set; } = 0; - public double ScaleX { get; set; } = 1.0; - public double ScaleY { get; set; } = 1.0; - public bool FlipX { get; set; } = false; - public bool FlipY { get; set; } = false; - - // Bézier Curve Typography (Normalized 0.0 to 1.0 relative coordinates) - public BezierCurvePreset BezierPreset { get; set; } = BezierCurvePreset.Wave; - public double BezierP0X { get; set; } = 0.0; - public double BezierP0Y { get; set; } = 0.5; - public double BezierP1X { get; set; } = 0.33; - public double BezierP1Y { get; set; } = 0.10; - public double BezierP2X { get; set; } = 0.67; - public double BezierP2Y { get; set; } = 0.90; - public double BezierP3X { get; set; } = 1.0; - public double BezierP3Y { get; set; } = 0.5; - - // Multi-Span Rich Text Runs (Granular inline formatting) - public System.Collections.Generic.List? Spans { get; set; } - - /// - /// Returns the effective plain text for search, display titles, and fallback rendering. - /// Concatenates inline spans if present, otherwise returns . - /// - public string GetEffectivePlainText() - { - if (Spans == null || Spans.Count == 0) - { - return Text ?? string.Empty; - } - - var sb = new System.Text.StringBuilder(); - foreach (var span in Spans) - { - sb.Append(span.Text); - } - return sb.ToString(); - } - - /// - /// Synchronizes with the concatenated content of . - /// - public void SynchronizePlainTextFromSpans() - { - if (Spans != null && Spans.Count > 0) - { - Text = GetEffectivePlainText(); - } - } - - public override PdfElementBase Clone() - { - var clone = (PdfTextElement)base.Clone(); - if (Spans != null) - { - clone.Spans = new System.Collections.Generic.List(Spans.Count); - foreach (var span in Spans) - { - clone.Spans.Add(span.Clone()); - } - } - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfTextSpan.cs b/src/PdfEditorApp.Core/Models/Elements/PdfTextSpan.cs deleted file mode 100644 index 9730897..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfTextSpan.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -/// -/// Specifies the vertical script alignment for inline rich text (e.g. chemical formulas H₂O, exponents x²). -/// -public enum TextScriptMode -{ - Normal = 0, - Superscript = 1, - Subscript = 2 -} - -/// -/// Represents an inline text span (run) within a , -/// allowing granular control over fonts, weights, colors, styles, scripts, and links. -/// -public class PdfTextSpan -{ - /// - /// The plain text content of this inline span. - /// - public string Text { get; set; } = string.Empty; - - /// - /// Optional font family override. If null, inherits from parent . - /// - public string? FontFamily { get; set; } - - /// - /// Optional font size override in points. If null, inherits from parent . - /// - public double? FontSize { get; set; } - - /// - /// Optional bold override. If null, inherits from parent . - /// - public bool? IsBold { get; set; } - - /// - /// Optional italic override. If null, inherits from parent . - /// - public bool? IsItalic { get; set; } - - /// - /// Optional underline override. If null, inherits from parent . - /// - public bool? IsUnderline { get; set; } - - /// - /// Optional strikethrough override. If null, inherits from parent . - /// - public bool? IsStrikethrough { get; set; } - - /// - /// Optional text color hex override (e.g. #0F6CBD). If null, inherits from parent . - /// - public string? TextColorHex { get; set; } - - /// - /// Optional background highlight color hex (e.g. #FFEB3B). If null or empty, transparent. - /// - public string? HighlightColorHex { get; set; } - - /// - /// Vertical script alignment (Normal, Superscript, Subscript). - /// - public TextScriptMode Script { get; set; } = TextScriptMode.Normal; - - /// - /// Optional clickable hyperlink URI (e.g. https://example.com). - /// - public string? LinkUrl { get; set; } - - /// - /// Creates a deep clone of this text span. - /// - public PdfTextSpan Clone() - { - return new PdfTextSpan - { - Text = Text, - FontFamily = FontFamily, - FontSize = FontSize, - IsBold = IsBold, - IsItalic = IsItalic, - IsUnderline = IsUnderline, - IsStrikethrough = IsStrikethrough, - TextColorHex = TextColorHex, - HighlightColorHex = HighlightColorHex, - Script = Script, - LinkUrl = LinkUrl - }; - } -} diff --git a/src/PdfEditorApp.Core/Models/Elements/PdfWatermarkElement.cs b/src/PdfEditorApp.Core/Models/Elements/PdfWatermarkElement.cs deleted file mode 100644 index 0606f4a..0000000 --- a/src/PdfEditorApp.Core/Models/Elements/PdfWatermarkElement.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace PdfEditorApp.Core.Models.Elements; - -public class PdfWatermarkElement : PdfElementBase -{ - public override ElementKind Kind => ElementKind.Watermark; - - public string Text { get; set; } = "CONFIDENTIAL"; - public double FontSize { get; set; } = 48; - public string ColorHex { get; set; } = "#CC0000"; - public new double Opacity { get; set; } = 0.15; - public double Angle { get; set; } = -35; - - public override PdfElementBase Clone() - { - var clone = (PdfWatermarkElement)base.Clone(); - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/Enums.cs b/src/PdfEditorApp.Core/Models/Enums.cs deleted file mode 100644 index b02184e..0000000 --- a/src/PdfEditorApp.Core/Models/Enums.cs +++ /dev/null @@ -1,519 +0,0 @@ -namespace PdfEditorApp.Core.Models; - -public enum ElementKind -{ - Text, - Heading, - Image, - Shape, - Divider, - Table, - Chart, - Watermark, - Stamp, - StickyNote, - FormField, - QrCode, - Barcode, - Redaction, - Ink, - Measurement, - Svg, - Math -} - -public enum MathDisplayStyle -{ - DisplayBlock, - Inline -} - -public enum MathCategory -{ - SchoolArithmetic, - Algebra, - Calculus, - Physics, - QuantumMechanics, - Finance, - Statistics, - DiscreteMath, - Geometry, - Logic, - Chemistry, - Custom -} - -public enum FormFieldType -{ - Text, - MultilineText, - Checkbox, - Radio, - Dropdown, - Signature, - Button, - DatePicker, - Number, - SignatureLine -} - -public enum CalculationFormula -{ - None, - Sum, - Average, - Product, - Min, - Max -} - -public enum FormButtonAction -{ - SubmitForm, - ResetForm, - PrintDocument, - GotoPage -} - -public enum BatesPosition -{ - TopLeft, - TopCenter, - TopRight, - BottomLeft, - BottomCenter, - BottomRight -} - -public enum SplitExtractMode -{ - SplitEveryNPages, - SplitByPageRanges, - ExtractSelectedPages -} - -public enum CompareDiffType -{ - ElementAdded, - ElementRemoved, - ElementModified, - TextModified, - FormattingModified, - MetadataModified, - SecurityModified, - PageCountChanged -} - -public enum RulerUnit -{ - Points, - Inches, - Millimeters -} - -public enum FormValidationType -{ - None, - Email, - Numeric, - Phone, - Date, - CustomRegex -} - -public enum SignatureStyle -{ - DrawnInk, - CursiveElegance, - SignatureCasual, - ClassicScript, - ModernHandwriting, - UploadedImage -} - -public enum GridSnapSize -{ - None = 0, - Points10 = 10, - Points20 = 20, - Points50 = 50 -} - -public enum RedactionMode -{ - Blackout, - Whiteout, - Grayout -} - -public enum PageFormat -{ - A4, - Letter, - Legal, - Executive, - A3, - A5, - Tabloid, - Poster, - Custom -} - -public enum PageOrientation -{ - Portrait, - Landscape -} - -public enum TextAlignmentMode -{ - Left, - Center, - Right, - Justify -} - -public enum TextVerticalAlignment -{ - Top, - Center, - Bottom -} - -public enum TextShapeMode -{ - Normal, - Curved, - Circular, - BezierCurve -} - -public enum BezierCurvePreset -{ - Custom, - Wave, - SCurve, - Bridge, - Valley, - Rise -} - -public enum TextWrappingMode -{ - Wrap, - NoWrap -} - -public enum CircularTextPlacement -{ - TopArc, - BottomArc, - FullCircle, - CustomArc -} - -public enum CurveDirectionMode -{ - Clockwise, - CounterClockwise -} - -public enum ShapeType -{ - Rectangle, - RoundedRectangle, - Circle, - Triangle, - RightTriangle, - Diamond, - Pentagon, - Hexagon, - Octagon, - Star5, - Star4Badge, - Star8Badge, - Star12Seal, - RosetteSeal, - LaurelWreathSeal, - MedalRibbonBadge, - RibbonBanner, - CornerPolygonalAccentTopLeft, - CornerPolygonalAccentBottomRight, - CornerDiagonalWedge, - Chevron, - Trapezoid, - Parallelogram, - ShieldBadge, - AwardBadge, - ArrowRight, - ArrowLeft, - Callout, - Heart, - Cloud, - Line, - Arrow, - BezierCurve, - CurvedArrow, - SCurveConnector, - WaveLine, - ArcLine, - CurlyBrace, - CurvedCallout, - Teardrop, - WaveRibbon, - OrganicBlob, - Star, - Card, - StickyNote, - RevisionCloud, - DimensionLine, - CustomSvgPath -} - -public enum LineEndCap -{ - None, - Arrow, - StealthArrow, - OpenArrow, - Circle, - Diamond, - Square -} - -public enum LineDashStyle -{ - Solid, - Dashed, - Dotted, - DashDot -} - -public enum DividerStyle -{ - Straight, - Wave, - SCurve, - Arch, - DoubleWave, - CalligraphicFlourish -} - -public enum ChartType -{ - BarColumn, - HorizontalBar, - Line, - SmoothLine, - Area, - DonutPie, - StackedBar, - StackedHorizontalBar, - ScatterPlot, - Radar, - PolarArea, - Funnel, - Waterfall, - GaugeProgress, - StepLine, - Pyramid, - Candlestick -} - -public enum ChartPalette -{ - CorporateBlue, - EmeraldGreen, - SunsetOrange, - CyberNeon, - ExecutiveSlate, - PastelHarmony, - VibrantRainbow -} - -public enum ChartLegendPosition -{ - Hidden, - Top, - Bottom, - Left, - Right -} - - -public enum TablePresetStyle -{ - ModernMinimal, - EnterpriseBlue, - DarkModeSlate, - ZebraStriped, - FinancialBordered, - EmeraldGreen, - AmberAccent, - CompactClean -} - -public enum BarcodeType -{ - Code128, - Code39, - Ean13, - UpcA, - Itf14, - Codabar, - Pdf417 -} - -public enum QrCodeEccLevel -{ - L, // Low (~7% recovery) - M, // Medium (~15% recovery) - Q, // Quartile (~25% recovery) - H // High (~30% recovery) -} - -public enum QrCodePresetKind -{ - Url, - Wifi, - VCard, - PlainText, - Email, - PhoneCall, - Sms, - GeoLocation, - CryptoAddress, - EventCalendar -} - -public enum StampType -{ - Approved, - Confidential, - Draft, - Urgent, - SignHere, - Void, - Completed, - Final, - Expired, - Received, - Custom -} - -public enum RibbonTabKind -{ - File, - Home, - Edit, - Comment, - Insert, - Sign, - Forms, - Organize, - Protect, - Audit, - Export, - Plugins -} - -public enum ToolMode -{ - Select, - Pan, - Zoom, - Text, - Shape, - Highlight, - Draw, - Form, - Redact, - Signature, - Measure -} - -public enum SidebarTabKind -{ - Thumbnails, - Outline, - Comments, - AiAssistant -} - -public enum HomeNavSection -{ - Home, - NewDocument, - PdfReader, - AllTools, - OrganizeAndPage, - OptimizeAndSecurity, - ConvertFromPdf, - ConvertToPdf, - EditAndForms, - AiAndAutomation, - Starred, - Trash, - Licensing, - FontPackages, - TesseractData, - Help, - Settings, - Plugins -} - -public enum ToastPosition -{ - BottomCenter, - BottomRight, - BottomLeft, - TopCenter, - TopRight, - TopLeft -} - -public enum ToastStyleVariant -{ - Solid, - Subtle, - Auto -} - -public enum ToastNotificationType -{ - Primary, - Success, - Danger, - Warning, - General -} - -public enum PdfReaderTheme -{ - Default, - Sepia, - Dark, - HighContrast -} - -public enum PdfViewLayoutMode -{ - ContinuousScroll, - SinglePage, - TwoPageSpread -} - -public enum PdfViewerZoomMode -{ - Custom, - FitWidth, - FitPage -} - -public enum AppThemeMode -{ - System, - Light, - Dark -} diff --git a/src/PdfEditorApp.Core/Models/FontPackageModels.cs b/src/PdfEditorApp.Core/Models/FontPackageModels.cs deleted file mode 100644 index cc3acd4..0000000 --- a/src/PdfEditorApp.Core/Models/FontPackageModels.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Models; - -/// -/// Categories for grouping language and font packages. -/// -public enum FontPackageCategory -{ - All, - EastAsia, - SouthAsia, - MiddleEast, - SoutheastAsia, - EuropeAndEurasia, - DesignAndTypography -} - -/// -/// Status of an on-demand font package. -/// -public enum FontPackageStatus -{ - NotInstalled, - Downloading, - Installed, - UpdateAvailable, - Error -} - -/// -/// Metadata for an individual font file inside a package. -/// -public class FontFileItem -{ - public string FileName { get; set; } = string.Empty; - public string DownloadUrl { get; set; } = string.Empty; - public long FileSizeBytes { get; set; } - public string FontFamilyName { get; set; } = string.Empty; - public bool IsBold { get; set; } - public bool IsItalic { get; set; } -} - -/// -/// Representation of a language or font pack available for on-demand installation. -/// -public class FontPackageInfo -{ - public string Id { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public string NativeName { get; set; } = string.Empty; - public string FlagEmoji { get; set; } = "🌐"; - public string Region { get; set; } = string.Empty; - public FontPackageCategory Category { get; set; } = FontPackageCategory.EastAsia; - public string Description { get; set; } = string.Empty; - public string SampleText { get; set; } = string.Empty; - public List SupportedLanguages { get; set; } = new(); - public List IncludedFontFamilies { get; set; } = new(); - public List Files { get; set; } = new(); - public long TotalEstimatedSizeBytes { get; set; } - public string FormattedSize => FormatBytes(TotalEstimatedSizeBytes); - public bool IsBundledByDefault { get; set; } - - public static string FormatBytes(long bytes) - { - if (bytes <= 0) return "0 KB"; - if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; - return $"{bytes / (1024.0 * 1024.0):F1} MB"; - } -} diff --git a/src/PdfEditorApp.Core/Models/OcrModels.cs b/src/PdfEditorApp.Core/Models/OcrModels.cs deleted file mode 100644 index 29e0241..0000000 --- a/src/PdfEditorApp.Core/Models/OcrModels.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace PdfEditorApp.Core.Models; - -/// -/// Type of OCR engine used for text recognition. -/// -public enum OcrEngineType -{ - Auto, - OsNative, - Tesseract, - None -} - -/// -/// Lightweight normalized or point bounding box independent of UI framework. -/// -public readonly record struct OcrBoundingBox(double X, double Y, double Width, double Height) -{ - public double Right => X + Width; - public double Bottom => Y + Height; - - public static OcrBoundingBox Empty => new(0, 0, 0, 0); - - public bool Contains(double px, double py) => - px >= X && px <= Right && py >= Y && py <= Bottom; -} - -/// -/// A single recognized word with bounds normalized between 0.0 and 1.0. -/// -public class OcrWordItem -{ - public string Text { get; set; } = string.Empty; - public OcrBoundingBox NormalizedBounds { get; set; } - public float Confidence { get; set; } = 1.0f; -} - -/// -/// A line of recognized words. -/// -public class OcrLineItem -{ - public string Text { get; set; } = string.Empty; - public OcrBoundingBox NormalizedBounds { get; set; } - public List Words { get; set; } = new(); -} - -/// -/// Output of an OCR recognition operation on an image or page. -/// -public class OcrResult -{ - public bool Success { get; set; } - public string ErrorMessage { get; set; } = string.Empty; - public string FullText { get; set; } = string.Empty; - public List Lines { get; set; } = new(); - public List Words { get; set; } = new(); - public string EngineUsed { get; set; } = string.Empty; - public long DurationMs { get; set; } -} - -/// -/// Metadata for an on-demand downloadable Tesseract language model. -/// -public class TesseractLanguagePackageInfo -{ - public string Code { get; set; } = string.Empty; - public string DisplayName { get; set; } = string.Empty; - public string NativeName { get; set; } = string.Empty; - public string FlagEmoji { get; set; } = "🌐"; - public string Category { get; set; } = "Latin & European"; - public string Description { get; set; } = string.Empty; - public string SampleText { get; set; } = string.Empty; - public long EstimatedSizeBytes { get; set; } - public string DownloadUrl { get; set; } = string.Empty; - public string FileName => $"{Code}.traineddata"; - public bool IsInstalled { get; set; } - - public string FormattedSize => FormatBytes(EstimatedSizeBytes); - - public static string FormatBytes(long bytes) - { - if (bytes <= 0) return "0 KB"; - if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; - return $"{bytes / (1024.0 * 1024.0):F1} MB"; - } -} - -/// -/// Unified contract for optical character recognition providers. -/// -public interface IOcrEngine -{ - string EngineName { get; } - OcrEngineType EngineType { get; } - bool IsAvailable { get; } - Task RecognizeTextAsync(byte[] imageBytes, string language = "eng", CancellationToken ct = default); -} diff --git a/src/PdfEditorApp.Core/Models/OutlineItem.cs b/src/PdfEditorApp.Core/Models/OutlineItem.cs deleted file mode 100644 index c4b3296..0000000 --- a/src/PdfEditorApp.Core/Models/OutlineItem.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PdfEditorApp.Core.Models; - -public class OutlineItem -{ - public string Title { get; set; } = ""; - public int PageIndex { get; set; } - public string Kind { get; set; } = "Heading"; -} diff --git a/src/PdfEditorApp.Core/Models/PdfDocumentModel.cs b/src/PdfEditorApp.Core/Models/PdfDocumentModel.cs deleted file mode 100644 index 454cc48..0000000 --- a/src/PdfEditorApp.Core/Models/PdfDocumentModel.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Models; - -public class PdfDocumentModel -{ - public string Id { get; set; } = Guid.NewGuid().ToString("N"); - public string Title { get; set; } = "Document_2026.pdf"; - public string Author { get; set; } = "CodeFryDev"; - public string Subject { get; set; } = "Annual Financial & Operations Report"; - public string Keywords { get; set; } = ""; - public string Creator { get; set; } = "FryPDF"; - public string Producer { get; set; } = "codefrydev.in"; - public DateTime CreatedDate { get; set; } = DateTime.Now; - public DateTime ModifiedDate { get; set; } = DateTime.Now; - - public List Pages { get; set; } = new(); - public PdfSecuritySettings SecuritySettings { get; set; } = new(); - - public PdfDocumentModel Clone() - { - var clone = new PdfDocumentModel - { - Id = Guid.NewGuid().ToString("N"), - Title = Title, - Author = Author, - Subject = Subject, - Keywords = Keywords, - Creator = Creator, - Producer = Producer, - CreatedDate = CreatedDate, - ModifiedDate = DateTime.Now, - SecuritySettings = SecuritySettings.Clone() - }; - - foreach (var page in Pages) - { - clone.Pages.Add(page.Clone()); - } - - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/PdfPageModel.cs b/src/PdfEditorApp.Core/Models/PdfPageModel.cs deleted file mode 100644 index e361e34..0000000 --- a/src/PdfEditorApp.Core/Models/PdfPageModel.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Core.Models; - -public class PdfPageModel -{ - public string Id { get; set; } = Guid.NewGuid().ToString("N"); - public int PageNumber { get; set; } = 1; - - public PageFormat Format { get; set; } = PageFormat.A4; - public PageOrientation Orientation { get; set; } = PageOrientation.Portrait; - public int RotationAngle { get; set; } = 0; // 0, 90, 180, 270 - - public double Width { get; set; } = 800; // Screen display canvas points (A4 ratio = 800 x 1131) - public double Height { get; set; } = 1131; - - public string BackgroundColorHex { get; set; } = "#FFFFFF"; - - public bool ShowHeaderFooter { get; set; } = true; - public string? HeaderLeft { get; set; } - public string? HeaderCenter { get; set; } - public string? HeaderRight { get; set; } - public string? FooterLeft { get; set; } = "CONFIDENTIAL & PROPRIETARY"; - public string? FooterCenter { get; set; } - public string? FooterRight { get; set; } = "Page {P} of {N}"; - - public PdfWatermarkElement? Watermark { get; set; } - - public List Elements { get; set; } = new(); - - public PdfPageModel Clone() - { - var clone = new PdfPageModel - { - Id = Guid.NewGuid().ToString("N"), - PageNumber = PageNumber, - Format = Format, - Orientation = Orientation, - RotationAngle = RotationAngle, - Width = Width, - Height = Height, - BackgroundColorHex = BackgroundColorHex, - ShowHeaderFooter = ShowHeaderFooter, - HeaderLeft = HeaderLeft, - HeaderCenter = HeaderCenter, - HeaderRight = HeaderRight, - FooterLeft = FooterLeft, - FooterCenter = FooterCenter, - FooterRight = FooterRight, - Watermark = (PdfWatermarkElement?)Watermark?.Clone() - }; - - foreach (var element in Elements) - { - clone.Elements.Add(element.Clone()); - } - - return clone; - } -} diff --git a/src/PdfEditorApp.Core/Models/PdfSecuritySettings.cs b/src/PdfEditorApp.Core/Models/PdfSecuritySettings.cs deleted file mode 100644 index 4a1f36a..0000000 --- a/src/PdfEditorApp.Core/Models/PdfSecuritySettings.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace PdfEditorApp.Core.Models; - -public class PdfSecuritySettings -{ - public bool IsPasswordProtected { get; set; } = false; - public string OpenPassword { get; set; } = ""; - public string PermissionsPassword { get; set; } = ""; - - public bool AllowPrinting { get; set; } = true; - public bool AllowContentCopying { get; set; } = true; - public bool AllowModifications { get; set; } = true; - public bool AllowAnnotations { get; set; } = true; - - public bool ScrubMetadataOnExport { get; set; } = false; - public bool RemoveCommentsOnExport { get; set; } = false; - - public PdfSecuritySettings Clone() - { - return new PdfSecuritySettings - { - IsPasswordProtected = IsPasswordProtected, - OpenPassword = OpenPassword, - PermissionsPassword = PermissionsPassword, - AllowPrinting = AllowPrinting, - AllowContentCopying = AllowContentCopying, - AllowModifications = AllowModifications, - AllowAnnotations = AllowAnnotations, - ScrubMetadataOnExport = ScrubMetadataOnExport, - RemoveCommentsOnExport = RemoveCommentsOnExport - }; - } -} diff --git a/src/PdfEditorApp.Core/PdfEditorApp.Core.csproj b/src/PdfEditorApp.Core/PdfEditorApp.Core.csproj deleted file mode 100644 index 5edc5c7..0000000 --- a/src/PdfEditorApp.Core/PdfEditorApp.Core.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - net10.0 - enable - true - $(NoWarn);NU1901;NU1902;NU1903;NU1904 - PdfEditorApp.Core - PdfEditorApp.Core - 1.0.0 - Code Fry Dev - Copyright © 2026 Code Fry Dev - true - High-performance PDF deconstruction, layout analysis, and document engineering core library - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/CanvasElementDescriptor.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/CanvasElementDescriptor.cs deleted file mode 100644 index 8b7d275..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/CanvasElementDescriptor.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Descriptor defining a canvas element type contributed by a plugin or built-in system. -/// -public record CanvasElementDescriptor -{ - /// Unique identifier for this element type, e.g. "frypdf.element.text" or "vendor.element.barcode". - public required string ElementTypeId { get; init; } - - /// Human-readable display name, e.g. "Barcode / QR Code". - public required string DisplayName { get; init; } - - /// The domain model type (must inherit from ). - public required Type ModelType { get; init; } - - /// The presentation ViewModel type for the canvas element. - public required Type ViewModelType { get; init; } - - /// Icon representing this element type in the inserter menu or toolbar. - public string IconKind { get; init; } = "ShapeOutline"; - - /// Default initial width in points when placed on canvas. - public double DefaultWidth { get; init; } = 150; - - /// Default initial height in points when placed on canvas. - public double DefaultHeight { get; init; } = 100; - - /// Search tags for element discovery. - public IReadOnlyList Tags { get; init; } = Array.Empty(); - - /// Optional factory delegate to instantiate the ViewModel from an optional model instance. - public Func? Factory { get; init; } - - /// Whether this element can be inserted directly from the Ribbon Insert tab / toolbar. - public bool CanInsertFromToolbar { get; init; } = true; - - /// Category grouping in the inserter menu (e.g. "Basic", "Media", "Data", "Forms", "Math"). - public string InsertionCategory { get; init; } = "Basic"; - - /// Optional single-key shortcut for rapid insertion (e.g. "T", "R", "I"). - public string? ShortcutKey { get; init; } - - /// Ordering weight in the inserter menu (lower numbers appear first). - public int SortOrder { get; init; } = 100; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/ICanvasElementRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/ICanvasElementRegistry.cs deleted file mode 100644 index 5822ab7..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/ICanvasElementRegistry.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Registry for canvas element types contributed by plugins or core modules. -/// -public interface ICanvasElementRegistry -{ - /// - /// Registers a canvas element descriptor. - /// - void RegisterElement(CanvasElementDescriptor descriptor); - - /// - /// Unregisters a canvas element descriptor by its unique ID. - /// - bool UnregisterElement(string elementTypeId); - - /// - /// Looks up an element descriptor by its unique element type ID. - /// - CanvasElementDescriptor? GetDescriptor(string elementTypeId); - - /// - /// Looks up an element descriptor matching a domain model type. - /// - CanvasElementDescriptor? GetDescriptorByModelType(Type modelType); - - /// - /// Returns all registered canvas element descriptors. - /// - IReadOnlyList GetAllDescriptors(); - - /// - /// Returns all canvas element descriptors that can be inserted from the toolbar. - /// - IReadOnlyList GetInsertableElements(); -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/ICommandPaletteRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/ICommandPaletteRegistry.cs deleted file mode 100644 index bf1aa38..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/ICommandPaletteRegistry.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Descriptor for a command or action searchable in the Command Palette (⌘K / Ctrl+K), -/// contributed dynamically by plugins. -/// -public class CommandPaletteDescriptor -{ - public string Id { get; init; } = ""; - public string Title { get; init; } = ""; - public string Subtitle { get; init; } = ""; - public string Category { get; init; } = "General"; - public string IconKind { get; init; } = "LightningBolt"; - public string? Shortcut { get; init; } - public int Order { get; init; } = 100; - public Action? Action { get; init; } -} - -/// -/// Registry for discovering and dispatching Command Palette commands across plugins. -/// -public interface ICommandPaletteRegistry -{ - /// - /// Registers a command into the palette. - /// - IDisposable RegisterCommand(CommandPaletteDescriptor descriptor); - - /// - /// Unregisters a command by its unique ID. - /// - bool UnregisterCommand(string commandId); - - /// - /// Gets all registered command palette descriptors. - /// - IReadOnlyList GetAllCommands(); - - /// - /// Triggered when commands are registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IDataConnectorRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IDataConnectorRegistry.cs deleted file mode 100644 index 728a4bd..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IDataConnectorRegistry.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Data; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Contract for a data connector plugin that loads tabular datasets into a . -/// -public interface IDataConnector -{ - string ConnectorId { get; } - string DisplayName { get; } - string Description { get; } - string IconKind { get; } - IReadOnlyList SupportedExtensions { get; } - - Task LoadDataAsync(Dictionary parameters, CancellationToken ct = default); -} - -/// -/// Registry for discovering and dispatching data connectors contributed by plugins. -/// -public interface IDataConnectorRegistry -{ - IDisposable RegisterConnector(IDataConnector connector); - IDataConnector? GetConnector(string connectorId); - IReadOnlyList GetAllConnectors(); - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IDialogRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IDialogRegistry.cs deleted file mode 100644 index 028bf9a..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IDialogRegistry.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Descriptor representing a modal dialog or floating studio contributed by a plugin. -/// -public sealed class DialogDescriptor -{ - public required string Id { get; init; } - public required string Title { get; init; } - public Type? ViewModelType { get; init; } - public Type? ViewType { get; init; } - public Func? ViewFactory { get; init; } - public Func? ViewModelFactory { get; init; } - public bool IsModal { get; init; } = true; - public double? PreferredWidth { get; init; } - public double? PreferredHeight { get; init; } -} - -/// -/// Registry for pluggable modal dialogs and studio overlays. -/// -public interface IDialogRegistry -{ - /// - /// Registers a dialog descriptor. - /// - IDisposable RegisterDialog(DialogDescriptor descriptor); - - /// - /// Unregisters a dialog by its unique ID. - /// - bool UnregisterDialog(string dialogId); - - /// - /// Gets a registered dialog descriptor by its ID. - /// - DialogDescriptor? GetDialog(string dialogId); - - /// - /// Gets all registered dialog descriptors. - /// - IReadOnlyList GetAllDialogs(); - - /// - /// Fired when dialogs are registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IDocumentExporterRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IDocumentExporterRegistry.cs deleted file mode 100644 index f785790..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IDocumentExporterRegistry.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Options provided to document exporters during export execution. -/// -public record DocumentExportOptions -{ - public string OutputPath { get; init; } = ""; - public int Dpi { get; init; } = 300; - public bool IncludeAnnotations { get; init; } = true; - public bool OptimizeForWeb { get; init; } = false; - public string? Password { get; init; } - public Dictionary CustomParameters { get; init; } = new(); -} - -/// -/// Contract for a document exporter plugin that serializes a -/// to an output format (PDF, Markdown, HTML, Plain Text, PNG/JPEG, SVG). -/// -public interface IDocumentExporter -{ - /// - /// Unique identifier for this exporter, e.g. "frypdf.exporter.pdf" or "frypdf.exporter.markdown". - /// - string ExporterId { get; } - - /// - /// User-facing display name, e.g. "PDF Document (.pdf)" or "GitHub-Flavored Markdown (.md)". - /// - string DisplayName { get; } - - /// - /// Default file extension with leading dot, e.g. ".pdf", ".md", ".html". - /// - string DefaultExtension { get; } - - /// - /// File filter description for OS save dialogs, e.g. "PDF Documents (*.pdf)|*.pdf". - /// - string FileFilterDescription { get; } - - /// - /// Material icon name for UI representation. - /// - string IconKind { get; } - - /// - /// Exports the document model and returns the generated bytes. - /// - Task ExportAsync(PdfDocumentModel document, DocumentExportOptions options, CancellationToken ct = default); -} - -/// -/// Registry for discovering and managing multi-format document exporters contributed by plugins. -/// -public interface IDocumentExporterRegistry -{ - /// - /// Registers an exporter into the system. - /// - IDisposable RegisterExporter(IDocumentExporter exporter); - - /// - /// Retrieves an exporter by its unique ID. - /// - IDocumentExporter? GetExporter(string exporterId); - - /// - /// Finds an exporter by output file extension (e.g. ".md" or ".html"). - /// - IDocumentExporter? FindExporterByExtension(string extension); - - /// - /// Gets all registered document exporters. - /// - IReadOnlyList GetAllExporters(); - - /// - /// Raised whenever an exporter is registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IDocumentImporterRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IDocumentImporterRegistry.cs deleted file mode 100644 index 578a9dd..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IDocumentImporterRegistry.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Contract for a document importer plugin that converts raw file streams or bytes -/// into editable, multi-layered instances. -/// -public interface IDocumentImporter -{ - /// - /// Unique identifier for this importer, e.g. "frypdf.importer.pdfpig" or "frypdf.importer.image". - /// - string ImporterId { get; } - - /// - /// User-facing display name, e.g. "Adobe PDF Deconstruction Engine" or "Raster Image Converter". - /// - string DisplayName { get; } - - /// - /// Supported file extensions including leading dot, e.g. [".pdf"] or [".png", ".jpg", ".jpeg", ".webp"]. - /// - IReadOnlyList SupportedExtensions { get; } - - /// - /// Priority order for resolution when multiple importers claim the same extension (higher = higher priority). - /// - int Priority => 0; - - /// - /// Imports a document stream and returns an editable . - /// - Task ImportAsync(Stream stream, string fileName, string? password = null, CancellationToken ct = default); -} - -/// -/// Registry for discovering and dispatching document importers contributed by plugins. -/// -public interface IDocumentImporterRegistry -{ - /// - /// Registers an importer into the system. - /// - IDisposable RegisterImporter(IDocumentImporter importer); - - /// - /// Finds the highest-priority importer capable of handling the specified file path or extension. - /// - IDocumentImporter? FindImporter(string filePathOrExtension); - - /// - /// Retrieves an importer by its unique ID. - /// - IDocumentImporter? GetImporter(string importerId); - - /// - /// Gets all registered document importers. - /// - IReadOnlyList GetAllImporters(); - - /// - /// Raised whenever an importer is registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IInspectorRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IInspectorRegistry.cs deleted file mode 100644 index 755c180..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IInspectorRegistry.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Descriptor for an inspector property panel section contributed by a plugin. -/// -public class InspectorSectionDescriptor -{ - /// - /// Unique identifier for this section, e.g. "frypdf.inspector.geometry" or "frypdf.inspector.barcode". - /// - public string SectionId { get; init; } = ""; - - /// - /// Section header title displayed in the inspector sidebar. - /// - public string Title { get; init; } = ""; - - /// - /// Material icon name for the section header. - /// - public string IconKind { get; init; } = "Tune"; - - /// - /// Display sort order within the inspector (lower numbers appear higher). - /// - public int Order { get; init; } = 100; - - /// - /// Evaluates whether this section applies to the given element instance (or null for page/document level). - /// - public Func AppliesTo { get; init; } = _ => true; - - /// - /// Factory delegate to create the section ViewModel or control. - /// - public Func Factory { get; init; } = (sp, el) => new object(); -} - -/// -/// Registry for discovering and managing dynamic inspector property panels. -/// -public interface IInspectorRegistry -{ - /// - /// Registers a new inspector section into the system. - /// - IDisposable RegisterSection(InspectorSectionDescriptor descriptor); - - /// - /// Gets all inspector sections applicable to the selected element or context. - /// - IReadOnlyList GetSectionsForTarget(object? target); - - /// - /// Gets all registered inspector sections. - /// - IReadOnlyList GetAllSections(); - - /// - /// Raised whenever a section is registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/INavigationRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/INavigationRegistry.cs deleted file mode 100644 index 9170b3a..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/INavigationRegistry.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Descriptor representing a workspace page or navigation section contributed by a plugin. -/// -public sealed class NavigationItemDescriptor -{ - public required string Id { get; init; } - public required string Title { get; init; } - public string Group { get; init; } = "General"; // Overview, Categories, Library, Preferences, Extensions - public string IconKind { get; init; } = "ApplicationOutline"; - public string? BadgeText { get; init; } - public string? BadgeColorHex { get; init; } - public int Order { get; init; } = 100; - public Type? ViewModelType { get; init; } - public Func? ViewFactory { get; init; } - public Func? ViewModelFactory { get; init; } -} - -/// -/// Registry for pluggable navigation sections and full-page workspace views. -/// -public interface INavigationRegistry -{ - /// - /// Registers a new navigation item descriptor. - /// - IDisposable RegisterNavigationItem(NavigationItemDescriptor descriptor); - - /// - /// Unregisters a navigation item by its unique ID. - /// - bool UnregisterNavigationItem(string itemId); - - /// - /// Gets all registered navigation items in ascending display order. - /// - IReadOnlyList GetAllItems(); - - /// - /// Gets registered navigation items belonging to a specific group. - /// - IReadOnlyList GetItemsByGroup(string group); - - /// - /// Gets a single navigation item by its unique ID. - /// - NavigationItemDescriptor? GetItem(string itemId); - - /// - /// Fired when navigation items are registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IOcrEngineRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IOcrEngineRegistry.cs deleted file mode 100644 index 8cb7169..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IOcrEngineRegistry.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Registry for discovering and dispatching OCR engines contributed by plugins. -/// -public interface IOcrEngineRegistry -{ - /// - /// Registers an OCR engine into the system. - /// - IDisposable RegisterEngine(IOcrEngine engine); - - /// - /// Finds an OCR engine by its display name. - /// - IOcrEngine? GetEngine(string engineName); - - /// - /// Gets all registered OCR engines. - /// - IReadOnlyList GetAllEngines(); - - /// - /// Gets all available OCR engines that are supported on the current host OS. - /// - IReadOnlyList GetAvailableEngines(); - - /// - /// Raised whenever an OCR engine is registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IOverlayRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IOverlayRegistry.cs deleted file mode 100644 index 4605d5d..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IOverlayRegistry.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Thread-safe registry and manager for non-modal floating overlays targeting the 'shell.overlay' slot. -/// -public interface IOverlayRegistry -{ - /// - /// Registers an overlay descriptor contributed by a plugin. - /// - IDisposable RegisterOverlay(OverlayDescriptor descriptor); - - /// - /// Unregisters an overlay by its unique ID. - /// - bool UnregisterOverlay(string overlayId); - - /// - /// Retrieves a registered overlay descriptor by its ID. - /// - OverlayDescriptor? GetOverlay(string overlayId); - - /// - /// Gets all currently registered overlay descriptors. - /// - IReadOnlyList GetAllOverlays(); - - /// - /// Displays the specified overlay in the active shell. - /// - void ShowOverlay(string overlayId); - - /// - /// Hides the specified overlay. - /// - void HideOverlay(string overlayId); - - /// - /// Toggles the visibility of the specified overlay. - /// - void ToggleOverlay(string overlayId); - - /// - /// Checks whether the specified overlay is currently visible and active. - /// - bool IsOverlayVisible(string overlayId); - - /// - /// Fired when an overlay descriptor is registered or unregistered. - /// - event Action? RegistryChanged; - - /// - /// Fired when active overlay instances are shown, hidden, or modified. - /// - event Action? ActiveOverlaysChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IRibbonRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IRibbonRegistry.cs deleted file mode 100644 index 7c4fd5d..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IRibbonRegistry.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Descriptor representing a Ribbon Tab contributed dynamically by a plugin. -/// -public sealed class RibbonTabDescriptor -{ - public required string Id { get; init; } - public required string Title { get; init; } - public int Order { get; init; } = 100; - public bool IsDynamic { get; init; } = true; -} - -/// -/// Descriptor representing a logical group of tools within a Ribbon Tab. -/// -public sealed class RibbonGroupDescriptor -{ - public required string Id { get; init; } - public required string TabId { get; init; } - public required string Title { get; init; } - public int Order { get; init; } = 100; -} - -/// -/// Registry for dynamic Ribbon tabs, groups, and actions contributed by plugins. -/// -public interface IRibbonRegistry -{ - /// - /// Registers a dynamic ribbon tab. - /// - IDisposable RegisterTab(RibbonTabDescriptor tab); - - /// - /// Registers a ribbon tool group inside a tab. - /// - IDisposable RegisterGroup(RibbonGroupDescriptor group); - - /// - /// Registers a ribbon action descriptor. - /// - void RegisterAction(RibbonActionDescriptor action); - - /// - /// Unregisters a ribbon action by its unique ID. - /// - bool UnregisterAction(string actionId); - - /// - /// Gets all registered ribbon tabs. - /// - IReadOnlyList GetAllTabs(); - - /// - /// Gets all groups registered under a specific ribbon tab. - /// - IReadOnlyList GetGroupsForTab(string tabId); - - /// - /// Gets all actions registered for a specific Ribbon tab (e.g. "Home", "Tools", "Plugins"). - /// - IReadOnlyList GetActionsForTab(string tabId); - - /// - /// Gets all actions registered for a specific group within a tab. - /// - IReadOnlyList GetActionsForGroup(string tabId, string groupId); - - /// - /// Gets all registered ribbon actions across all tabs. - /// - IReadOnlyList GetAllActions(); - - /// - /// Triggered when tabs, groups, or actions are added or removed. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/ISidebarRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/ISidebarRegistry.cs deleted file mode 100644 index 246e69a..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/ISidebarRegistry.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Descriptor representing an editor sidebar panel contributed by a plugin. -/// -public sealed class SidebarTabDescriptor -{ - public required string Id { get; init; } - public required string Title { get; init; } - public string IconKind { get; init; } = "DockLeft"; - public string Tooltip { get; init; } = ""; - public int Order { get; init; } = 100; - public Type? ViewModelType { get; init; } - public Func? ViewFactory { get; init; } - public Func? ViewModelFactory { get; init; } -} - -/// -/// Registry for pluggable document editor left/right sidebar tabs. -/// -public interface ISidebarRegistry -{ - /// - /// Registers a sidebar tab descriptor. - /// - IDisposable RegisterTab(SidebarTabDescriptor descriptor); - - /// - /// Unregisters a sidebar tab by its ID. - /// - bool UnregisterTab(string tabId); - - /// - /// Gets all registered sidebar tabs ordered by priority. - /// - IReadOnlyList GetAllTabs(); - - /// - /// Gets a single tab descriptor by its ID. - /// - SidebarTabDescriptor? GetTab(string tabId); - - /// - /// Fired when sidebar tabs are registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/IStatusBarRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/IStatusBarRegistry.cs deleted file mode 100644 index 8c04587..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/IStatusBarRegistry.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -public enum StatusBarAlignment -{ - Left, - Right -} - -/// -/// Descriptor for a status bar widget contributed by a plugin. -/// -public class StatusBarWidgetDescriptor -{ - public string WidgetId { get; init; } = ""; - public string ToolTip { get; init; } = ""; - public StatusBarAlignment Alignment { get; init; } = StatusBarAlignment.Left; - public int Order { get; init; } = 100; - public Func Factory { get; init; } = _ => new object(); -} - -/// -/// Registry for discovering and dispatching status bar widgets. -/// -public interface IStatusBarRegistry -{ - IDisposable RegisterWidget(StatusBarWidgetDescriptor widget); - IReadOnlyList GetWidgets(StatusBarAlignment alignment); - IReadOnlyList GetAllWidgets(); - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/ITemplateRegistry.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/ITemplateRegistry.cs deleted file mode 100644 index 2b555e9..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/ITemplateRegistry.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Contract for a document template contributed by a plugin. -/// -public interface ITemplateDescriptor -{ - string Id { get; } - string Name { get; } - string Description { get; } - string Category { get; } - string IconKind { get; } - string AccentColorHex { get; } - - PdfDocumentModel Create(); -} - -/// -/// Registry for discovering and dispatching document templates. -/// -public interface ITemplateRegistry -{ - IDisposable RegisterTemplate(ITemplateDescriptor template); - ITemplateDescriptor? GetTemplate(string id); - IReadOnlyList GetAllTemplates(); - IReadOnlyList GetCategories(); - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/OverlayDescriptor.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/OverlayDescriptor.cs deleted file mode 100644 index a49978f..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/OverlayDescriptor.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Mode determining how the shell overlay container renders framing, titlebar, and chrome for the overlay view. -/// -public enum OverlayChromeMode -{ - /// - /// Automatic Google Material Design 3 Expressive window card frame with draggable header, title, icon, minimize pill, and close button. - /// Ideal for standard user controls, tools, forms, and utilities without requiring manual header implementation. - /// - StandardCard = 0, - - /// - /// Borderless / custom chrome where the view provides its own entire visual frame, header, and dragging physics. - /// Used for games, custom HUDs, or full-bleed graphics overlays (e.g. Retro Snake). - /// - CustomChrome = 1, - - /// - /// Compact floating pill / HUD bar anchored or free-floating. - /// - FloatingPill = 2 -} - -/// -/// Descriptor representing a non-modal floating overlay panel contributed by a plugin (targeting 'shell.overlay'). -/// Inspired by the Cordis dynamic overlay slot architecture. -/// -public sealed class OverlayDescriptor -{ - public required string Id { get; init; } - public required string Title { get; init; } - public string Slot { get; init; } = "shell.overlay"; - public double DefaultWidth { get; init; } = 320; - public double DefaultHeight { get; init; } = 420; - public double? InitialX { get; init; } - public double? InitialY { get; init; } - public bool IsDraggable { get; init; } = true; - public bool IsMinimizable { get; init; } = true; - public bool IsClosable { get; init; } = true; - public string IconKind { get; init; } = "WindowRestore"; - public OverlayChromeMode ChromeMode { get; init; } = OverlayChromeMode.StandardCard; - public bool HasCustomChrome => ChromeMode == OverlayChromeMode.CustomChrome; - public bool IsResizable { get; init; } = true; - public double MinWidth { get; init; } = 220; - public double MinHeight { get; init; } = 100; - public double MaxWidth { get; init; } = 1600; - public double MaxHeight { get; init; } = 1400; - public Type? ViewType { get; init; } - public Type? ViewModelType { get; init; } - public Func? ViewFactory { get; init; } - public Func? ViewModelFactory { get; init; } -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/PdfToolDescriptor.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/PdfToolDescriptor.cs deleted file mode 100644 index 705c285..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/PdfToolDescriptor.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Metadata descriptor defining a PDF tool contributed by a plugin. -/// -public record PdfToolDescriptor -{ - /// Unique string identifier, e.g. "frypdf.tool.merge" or "vendor.tool.stamp". - public required string Id { get; init; } - - /// Optional integer or legacy enum tag for backwards compatibility with legacy UI. - public int? LegacyId { get; init; } - - /// Human-readable display title, e.g. "Merge PDF". - public required string Name { get; init; } - - /// Descriptive summary explaining the tool's capabilities. - public required string Description { get; init; } - - /// Display category grouping, e.g. "Organize & Page", "Security", "Conversion", "Intelligence". - public required string Category { get; init; } - - /// Material Design / vector icon identifier. - public required string IconKind { get; init; } - - /// Brand or accent hex color for the icon, e.g. "#EA580C". - public string IconColorHex { get; init; } = "#2563EB"; - - /// Subtle tonal background container hex color, e.g. "#FFF7ED". - public string BackgroundAccentHex { get; init; } = "#EFF6FF"; - - /// Whether this tool supports bulk multi-file processing. - public bool SupportsMultiFile { get; init; } - - /// Comma-separated file extensions accepted by this tool, e.g. ".pdf". - public string AcceptedFileExtensions { get; init; } = ".pdf"; - - /// Search tags and keywords for quick discovery. - public IReadOnlyList Tags { get; init; } = Array.Empty(); - - /// - /// Factory delegate to instantiate the tool's execution ViewModel given a service provider. - /// - public Func? CreateViewModel { get; init; } - - /// Arbitrary metadata properties. - public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); -} diff --git a/src/PdfEditorApp.Core/Plugins/Descriptors/RibbonActionDescriptor.cs b/src/PdfEditorApp.Core/Plugins/Descriptors/RibbonActionDescriptor.cs deleted file mode 100644 index 7a0b7f3..0000000 --- a/src/PdfEditorApp.Core/Plugins/Descriptors/RibbonActionDescriptor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace PdfEditorApp.Core.Plugins.Descriptors; - -/// -/// Descriptor defining a Ribbon action button or menu item contributed dynamically by a plugin. -/// -public record RibbonActionDescriptor -{ - /// Unique identifier, e.g. "ribbon.action.ocr" or "ribbon.action.stamp". - public required string Id { get; init; } - - /// Target Ribbon Tab, e.g. "Home", "Tools", "Insert", "Security", "View". - public required string TabId { get; init; } - - /// Target Group inside the tab, e.g. "Organize", "AI Studio", "Annotations". - public required string GroupId { get; init; } - - /// Display label on the button or menu item. - public required string Label { get; init; } - - /// Optional descriptive tooltip. - public string? Tooltip { get; init; } - - /// Material Design / vector icon identifier. - public required string IconKind { get; init; } - - /// Display sort order within the group. - public int Order { get; init; } = 100; - - /// Optional action delegate to execute when clicked. - public Action? Action { get; init; } -} diff --git a/src/PdfEditorApp.Core/Plugins/FryPluginContext.cs b/src/PdfEditorApp.Core/Plugins/FryPluginContext.cs deleted file mode 100644 index d31ce7f..0000000 --- a/src/PdfEditorApp.Core/Plugins/FryPluginContext.cs +++ /dev/null @@ -1,855 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Pipelines; - -namespace PdfEditorApp.Core.Plugins; - -/// -/// Root implementation of . -/// Provides dynamic service discovery, pipeline orchestration, tool registration, and scoped lifecycle wrapping. -/// -public class FryPluginContext : IFryPluginContext -{ - private readonly ConcurrentDictionary _services = new(); - private readonly ConcurrentDictionary _tools = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _canvasElements = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _ribbonActions = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _importers = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _exporters = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _inspectorSections = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _templates = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _ocrEngines = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _dataConnectors = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _statusBarWidgets = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _commands = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _navigationItems = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _dialogs = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _sidebarTabs = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _ribbonTabs = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _ribbonGroups = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _overlays = new(StringComparer.OrdinalIgnoreCase); - private readonly IPipelineManager _pipelines; - - private readonly IServiceProvider? _fallbackServiceProvider; - private readonly PluginScope? _scope; - - public IPipelineManager Pipelines => _pipelines; - - public FryPluginContext( - IServiceProvider? fallbackServiceProvider = null, - IPipelineManager? pipelines = null, - PluginScope? scope = null) - { - _fallbackServiceProvider = fallbackServiceProvider; - _pipelines = pipelines ?? new PipelineManager(); - _scope = scope; - } - - /// - /// Creates a child context scoped to a specific plugin. - /// Registrations in the scoped context automatically attach their disposal to the plugin's . - /// - public FryPluginContext CreateScopedContext(PluginScope pluginScope) - { - ArgumentNullException.ThrowIfNull(pluginScope); - return new ScopedPluginContext(this, pluginScope); - } - - // --- Service Registration & Discovery --- - - public virtual void RegisterService(TService implementation) where TService : class - { - ArgumentNullException.ThrowIfNull(implementation); - _services[typeof(TService)] = implementation; - } - - public virtual void RegisterService(Type serviceType, object implementation) - { - ArgumentNullException.ThrowIfNull(serviceType); - ArgumentNullException.ThrowIfNull(implementation); - _services[serviceType] = implementation; - } - - public virtual TService GetService() where TService : class - { - if (TryGetService(out var service)) - { - return service; - } - - throw new KeyNotFoundException($"Service of type '{typeof(TService).FullName}' is not registered."); - } - - public virtual bool TryGetService([NotNullWhen(true)] out TService? service) where TService : class - { - if (_services.TryGetValue(typeof(TService), out var obj) && obj is TService typedObj) - { - service = typedObj; - return true; - } - - if (_fallbackServiceProvider?.GetService(typeof(TService)) is TService fallbackObj) - { - service = fallbackObj; - return true; - } - - service = null; - return false; - } - - public virtual bool HasService() where TService : class => HasService(typeof(TService)); - - public virtual bool HasService(Type serviceType) - { - ArgumentNullException.ThrowIfNull(serviceType); - return _services.ContainsKey(serviceType) || (_fallbackServiceProvider?.GetService(serviceType) != null); - } - - public virtual object? GetService(Type serviceType) - { - ArgumentNullException.ThrowIfNull(serviceType); - if (_services.TryGetValue(serviceType, out var obj)) - { - return obj; - } - - return _fallbackServiceProvider?.GetService(serviceType); - } - - // --- Effects --- - - public virtual IDisposable RegisterEffect(Action onDispose) - { - if (_scope != null) - { - return _scope.RegisterEffect(onDispose); - } - - return new DisposableAction(onDispose); - } - - public virtual IDisposable RegisterDisposable(IDisposable disposable) - { - ArgumentNullException.ThrowIfNull(disposable); - return RegisterEffect(disposable.Dispose); - } - - // --- Pipelines --- - - public virtual IDisposable RegisterWaterfall(string pipelineName, Func, Task> middleware) - { - var registration = _pipelines.RegisterWaterfall(pipelineName, middleware); - return RegisterDisposable(registration); - } - - public virtual Task ExecuteWaterfallAsync(string pipelineName, TContext context, Func? terminal = null) - { - return _pipelines.ExecuteWaterfallAsync(pipelineName, context, terminal); - } - - public virtual IDisposable RegisterBail(string pipelineName, Func> handler) where TResult : class - { - var registration = _pipelines.RegisterBail(pipelineName, handler); - return RegisterDisposable(registration); - } - - public virtual Task ExecuteBailAsync(string pipelineName, TContext context) where TResult : class - { - return _pipelines.ExecuteBailAsync(pipelineName, context); - } - - public virtual IDisposable RegisterParallel(string pipelineName, Func handler) - { - var registration = _pipelines.RegisterParallel(pipelineName, handler); - return RegisterDisposable(registration); - } - - public virtual Task ExecuteParallelAsync(string pipelineName, TContext context) - { - return _pipelines.ExecuteParallelAsync(pipelineName, context); - } - - public virtual IDisposable RegisterSerial(string pipelineName, Func handler) - { - var registration = _pipelines.RegisterSerial(pipelineName, handler); - return RegisterDisposable(registration); - } - - public virtual Task ExecuteSerialAsync(string pipelineName, TContext context) - { - return _pipelines.ExecuteSerialAsync(pipelineName, context); - } - - // --- Tools --- - - public event Action? ToolsChanged; - - public virtual IDisposable RegisterTool(PdfToolDescriptor tool) - { - ArgumentNullException.ThrowIfNull(tool); - ArgumentException.ThrowIfNullOrWhiteSpace(tool.Id); - - _tools[tool.Id] = tool; - ToolsChanged?.Invoke(); - - return RegisterEffect(() => - { - _tools.TryRemove(tool.Id, out _); - ToolsChanged?.Invoke(); - }); - } - - public virtual IReadOnlyList GetRegisteredTools() - { - return _tools.Values.ToList(); - } - - public virtual PdfToolDescriptor? GetTool(string id) - { - if (string.IsNullOrWhiteSpace(id)) return null; - return _tools.GetValueOrDefault(id); - } - - // --- Canvas Elements --- - - public virtual IDisposable RegisterCanvasElement(CanvasElementDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - ArgumentException.ThrowIfNullOrWhiteSpace(descriptor.ElementTypeId); - - _canvasElements[descriptor.ElementTypeId] = descriptor; - - if (TryGetService(out var elementRegistry)) - { - elementRegistry.RegisterElement(descriptor); - } - - return RegisterEffect(() => - { - _canvasElements.TryRemove(descriptor.ElementTypeId, out _); - if (TryGetService(out var registry)) - { - registry.UnregisterElement(descriptor.ElementTypeId); - } - }); - } - - public virtual IReadOnlyList GetRegisteredCanvasElements() - { - return _canvasElements.Values.ToList(); - } - - public virtual CanvasElementDescriptor? GetCanvasElement(string elementTypeId) - { - if (string.IsNullOrWhiteSpace(elementTypeId)) return null; - return _canvasElements.GetValueOrDefault(elementTypeId); - } - - // --- Ribbon Actions --- - - public virtual IDisposable RegisterRibbonAction(RibbonActionDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - ArgumentException.ThrowIfNullOrWhiteSpace(descriptor.Id); - - _ribbonActions[descriptor.Id] = descriptor; - - if (TryGetService(out var ribbonRegistry)) - { - ribbonRegistry.RegisterAction(descriptor); - } - - return RegisterEffect(() => - { - _ribbonActions.TryRemove(descriptor.Id, out _); - if (TryGetService(out var reg)) - { - reg.UnregisterAction(descriptor.Id); - } - }); - } - - public virtual IReadOnlyList GetRegisteredRibbonActions() - { - return _ribbonActions.Values.OrderBy(r => r.Order).ToList(); - } - - // --- Document Importers --- - - public virtual IDisposable RegisterImporter(IDocumentImporter importer) - { - ArgumentNullException.ThrowIfNull(importer); - _importers[importer.ImporterId] = importer; - - if (TryGetService(out var reg)) - { - reg.RegisterImporter(importer); - } - - return RegisterEffect(() => - { - _importers.TryRemove(importer.ImporterId, out _); - }); - } - - public virtual IReadOnlyList GetRegisteredImporters() - { - return _importers.Values.OrderByDescending(i => i.Priority).ToList(); - } - - // --- Document Exporters --- - - public virtual IDisposable RegisterExporter(IDocumentExporter exporter) - { - ArgumentNullException.ThrowIfNull(exporter); - _exporters[exporter.ExporterId] = exporter; - - if (TryGetService(out var reg)) - { - reg.RegisterExporter(exporter); - } - - return RegisterEffect(() => - { - _exporters.TryRemove(exporter.ExporterId, out _); - }); - } - - public virtual IReadOnlyList GetRegisteredExporters() - { - return _exporters.Values.ToList(); - } - - // --- Inspector Panels --- - - public virtual IDisposable RegisterInspectorSection(InspectorSectionDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _inspectorSections[descriptor.SectionId] = descriptor; - - if (TryGetService(out var reg)) - { - reg.RegisterSection(descriptor); - } - - return RegisterEffect(() => - { - _inspectorSections.TryRemove(descriptor.SectionId, out _); - }); - } - - public virtual IReadOnlyList GetRegisteredInspectorSections() - { - return _inspectorSections.Values.OrderBy(s => s.Order).ToList(); - } - - // --- Document Templates --- - - public virtual IDisposable RegisterTemplate(ITemplateDescriptor template) - { - ArgumentNullException.ThrowIfNull(template); - _templates[template.Id] = template; - - if (TryGetService(out var reg)) - { - reg.RegisterTemplate(template); - } - - return RegisterEffect(() => - { - _templates.TryRemove(template.Id, out _); - }); - } - - public virtual IReadOnlyList GetRegisteredTemplates() - { - return _templates.Values.ToList(); - } - - // --- OCR Engines --- - - public virtual IDisposable RegisterOcrEngine(IOcrEngine engine) - { - ArgumentNullException.ThrowIfNull(engine); - _ocrEngines[engine.EngineName] = engine; - - if (TryGetService(out var reg)) - { - reg.RegisterEngine(engine); - } - - return RegisterEffect(() => - { - _ocrEngines.TryRemove(engine.EngineName, out _); - }); - } - - public virtual IReadOnlyList GetRegisteredOcrEngines() - { - return _ocrEngines.Values.ToList(); - } - - // --- Data Connectors --- - - public virtual IDisposable RegisterDataConnector(IDataConnector connector) - { - ArgumentNullException.ThrowIfNull(connector); - _dataConnectors[connector.ConnectorId] = connector; - - if (TryGetService(out var reg)) - { - reg.RegisterConnector(connector); - } - - return RegisterEffect(() => - { - _dataConnectors.TryRemove(connector.ConnectorId, out _); - }); - } - - public virtual IReadOnlyList GetRegisteredDataConnectors() - { - return _dataConnectors.Values.ToList(); - } - - // --- Status Bar Widgets --- - - public virtual IDisposable RegisterStatusBarWidget(StatusBarWidgetDescriptor widget) - { - ArgumentNullException.ThrowIfNull(widget); - _statusBarWidgets[widget.WidgetId] = widget; - - IDisposable? sub = null; - if (TryGetService(out var reg)) - { - sub = reg.RegisterWidget(widget); - } - - return RegisterEffect(() => - { - _statusBarWidgets.TryRemove(widget.WidgetId, out _); - sub?.Dispose(); - }); - } - - public virtual IReadOnlyList GetRegisteredStatusBarWidgets() - { - return _statusBarWidgets.Values.OrderBy(w => w.Order).ToList(); - } - - // --- Command Palette --- - - public virtual IDisposable RegisterCommand(CommandPaletteDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _commands[descriptor.Id] = descriptor; - - if (TryGetService(out var reg)) - { - reg.RegisterCommand(descriptor); - } - - return RegisterEffect(() => - { - _commands.TryRemove(descriptor.Id, out _); - if (TryGetService(out var r)) - { - r.UnregisterCommand(descriptor.Id); - } - }); - } - - public virtual IReadOnlyList GetRegisteredCommands() - { - return _commands.Values.OrderBy(c => c.Order).ToList(); - } - - // --- Navigation Registry --- - - public virtual IDisposable RegisterNavigationItem(NavigationItemDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _navigationItems[descriptor.Id] = descriptor; - - if (TryGetService(out var reg)) - { - reg.RegisterNavigationItem(descriptor); - } - - return RegisterEffect(() => - { - _navigationItems.TryRemove(descriptor.Id, out _); - if (TryGetService(out var r)) - { - r.UnregisterNavigationItem(descriptor.Id); - } - }); - } - - public virtual IReadOnlyList GetRegisteredNavigationItems() - { - return _navigationItems.Values.OrderBy(n => n.Order).ToList(); - } - - // --- Dialog Registry --- - - public virtual IDisposable RegisterDialog(DialogDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _dialogs[descriptor.Id] = descriptor; - - if (TryGetService(out var reg)) - { - reg.RegisterDialog(descriptor); - } - - return RegisterEffect(() => - { - _dialogs.TryRemove(descriptor.Id, out _); - if (TryGetService(out var r)) - { - r.UnregisterDialog(descriptor.Id); - } - }); - } - - public virtual IReadOnlyList GetRegisteredDialogs() - { - return _dialogs.Values.ToList(); - } - - // --- Sidebar Registry --- - - public virtual IDisposable RegisterSidebarTab(SidebarTabDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _sidebarTabs[descriptor.Id] = descriptor; - - if (TryGetService(out var reg)) - { - reg.RegisterTab(descriptor); - } - - return RegisterEffect(() => - { - _sidebarTabs.TryRemove(descriptor.Id, out _); - if (TryGetService(out var r)) - { - r.UnregisterTab(descriptor.Id); - } - }); - } - - public virtual IReadOnlyList GetRegisteredSidebarTabs() - { - return _sidebarTabs.Values.OrderBy(s => s.Order).ToList(); - } - - // --- Dynamic Ribbon Tabs & Groups --- - - public virtual IDisposable RegisterRibbonTab(RibbonTabDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _ribbonTabs[descriptor.Id] = descriptor; - - if (TryGetService(out var reg)) - { - reg.RegisterTab(descriptor); - } - - return RegisterEffect(() => - { - _ribbonTabs.TryRemove(descriptor.Id, out _); - }); - } - - public virtual IDisposable RegisterRibbonGroup(RibbonGroupDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _ribbonGroups[descriptor.Id] = descriptor; - - if (TryGetService(out var reg)) - { - reg.RegisterGroup(descriptor); - } - - return RegisterEffect(() => - { - _ribbonGroups.TryRemove(descriptor.Id, out _); - }); - } - - // --- Shell Overlay Registry (Cordis Architecture Inspired) --- - - public virtual IDisposable RegisterOverlay(OverlayDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _overlays[descriptor.Id] = descriptor; - - if (TryGetService(out var reg)) - { - reg.RegisterOverlay(descriptor); - } - - return RegisterEffect(() => - { - _overlays.TryRemove(descriptor.Id, out _); - if (TryGetService(out var r)) - { - r.UnregisterOverlay(descriptor.Id); - } - }); - } - - public virtual IReadOnlyList GetRegisteredOverlays() - { - return _overlays.Values.ToList(); - } - - - /// - /// Scoped context subclass wrapping a parent context with a plugin-specific . - /// - private sealed class ScopedPluginContext : FryPluginContext - { - private readonly FryPluginContext _parent; - private readonly PluginScope _pluginScope; - - public ScopedPluginContext(FryPluginContext parent, PluginScope pluginScope) - : base(parent._fallbackServiceProvider, parent._pipelines, pluginScope) - { - _parent = parent; - _pluginScope = pluginScope; - } - - public override void RegisterService(TService implementation) - { - _parent.RegisterService(implementation); - _pluginScope.RegisterEffect(() => - { - // Remove from parent services dictionary - _parent._services.TryRemove(typeof(TService), out _); - }); - } - - public override void RegisterService(Type serviceType, object implementation) - { - _parent.RegisterService(serviceType, implementation); - _pluginScope.RegisterEffect(() => - { - _parent._services.TryRemove(serviceType, out _); - }); - } - - public override TService GetService() => _parent.GetService(); - public override bool TryGetService([NotNullWhen(true)] out TService? service) where TService : class => _parent.TryGetService(out service); - public override bool HasService() where TService : class => _parent.HasService(); - public override bool HasService(Type serviceType) => _parent.HasService(serviceType); - public override object? GetService(Type serviceType) => _parent.GetService(serviceType); - - public override IDisposable RegisterEffect(Action onDispose) => _pluginScope.RegisterEffect(onDispose); - public override IDisposable RegisterDisposable(IDisposable disposable) => _pluginScope.RegisterDisposable(disposable); - - public override IDisposable RegisterWaterfall(string pipelineName, Func, Task> middleware) - { - var reg = _parent.Pipelines.RegisterWaterfall(pipelineName, middleware); - return _pluginScope.RegisterDisposable(reg); - } - - public override Task ExecuteWaterfallAsync(string pipelineName, TContext context, Func? terminal = null) - { - return _parent.ExecuteWaterfallAsync(pipelineName, context, terminal); - } - - public override IDisposable RegisterBail(string pipelineName, Func> handler) where TResult : class - { - var reg = _parent.Pipelines.RegisterBail(pipelineName, handler); - return _pluginScope.RegisterDisposable(reg); - } - - public override Task ExecuteBailAsync(string pipelineName, TContext context) where TResult : class - { - return _parent.ExecuteBailAsync(pipelineName, context); - } - - public override IDisposable RegisterParallel(string pipelineName, Func handler) - { - var reg = _parent.Pipelines.RegisterParallel(pipelineName, handler); - return _pluginScope.RegisterDisposable(reg); - } - - public override Task ExecuteParallelAsync(string pipelineName, TContext context) - { - return _parent.ExecuteParallelAsync(pipelineName, context); - } - - public override IDisposable RegisterSerial(string pipelineName, Func handler) - { - var reg = _parent.Pipelines.RegisterSerial(pipelineName, handler); - return _pluginScope.RegisterDisposable(reg); - } - - public override Task ExecuteSerialAsync(string pipelineName, TContext context) - { - return _parent.ExecuteSerialAsync(pipelineName, context); - } - - public override IDisposable RegisterTool(PdfToolDescriptor tool) - { - var reg = _parent.RegisterTool(tool); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredTools() => _parent.GetRegisteredTools(); - public override PdfToolDescriptor? GetTool(string id) => _parent.GetTool(id); - - public override IDisposable RegisterCanvasElement(CanvasElementDescriptor descriptor) - { - var reg = _parent.RegisterCanvasElement(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredCanvasElements() => _parent.GetRegisteredCanvasElements(); - public override CanvasElementDescriptor? GetCanvasElement(string elementTypeId) => _parent.GetCanvasElement(elementTypeId); - - public override IDisposable RegisterRibbonAction(RibbonActionDescriptor descriptor) - { - var reg = _parent.RegisterRibbonAction(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredRibbonActions() => _parent.GetRegisteredRibbonActions(); - - public override IDisposable RegisterImporter(IDocumentImporter importer) - { - var reg = _parent.RegisterImporter(importer); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredImporters() => _parent.GetRegisteredImporters(); - - public override IDisposable RegisterExporter(IDocumentExporter exporter) - { - var reg = _parent.RegisterExporter(exporter); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredExporters() => _parent.GetRegisteredExporters(); - - public override IDisposable RegisterInspectorSection(InspectorSectionDescriptor descriptor) - { - var reg = _parent.RegisterInspectorSection(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredInspectorSections() => _parent.GetRegisteredInspectorSections(); - - public override IDisposable RegisterTemplate(ITemplateDescriptor template) - { - var reg = _parent.RegisterTemplate(template); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredTemplates() => _parent.GetRegisteredTemplates(); - - public override IDisposable RegisterOcrEngine(IOcrEngine engine) - { - var reg = _parent.RegisterOcrEngine(engine); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredOcrEngines() => _parent.GetRegisteredOcrEngines(); - - public override IDisposable RegisterDataConnector(IDataConnector connector) - { - var reg = _parent.RegisterDataConnector(connector); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredDataConnectors() => _parent.GetRegisteredDataConnectors(); - - public override IDisposable RegisterStatusBarWidget(StatusBarWidgetDescriptor widget) - { - var reg = _parent.RegisterStatusBarWidget(widget); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredStatusBarWidgets() => _parent.GetRegisteredStatusBarWidgets(); - - public override IDisposable RegisterCommand(CommandPaletteDescriptor descriptor) - { - var reg = _parent.RegisterCommand(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredCommands() => _parent.GetRegisteredCommands(); - - public override IDisposable RegisterNavigationItem(NavigationItemDescriptor descriptor) - { - var reg = _parent.RegisterNavigationItem(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredNavigationItems() => _parent.GetRegisteredNavigationItems(); - - public override IDisposable RegisterDialog(DialogDescriptor descriptor) - { - var reg = _parent.RegisterDialog(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredDialogs() => _parent.GetRegisteredDialogs(); - - public override IDisposable RegisterSidebarTab(SidebarTabDescriptor descriptor) - { - var reg = _parent.RegisterSidebarTab(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredSidebarTabs() => _parent.GetRegisteredSidebarTabs(); - - public override IDisposable RegisterRibbonTab(RibbonTabDescriptor descriptor) - { - var reg = _parent.RegisterRibbonTab(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IDisposable RegisterRibbonGroup(RibbonGroupDescriptor descriptor) - { - var reg = _parent.RegisterRibbonGroup(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IDisposable RegisterOverlay(OverlayDescriptor descriptor) - { - var reg = _parent.RegisterOverlay(descriptor); - return _pluginScope.RegisterDisposable(reg); - } - - public override IReadOnlyList GetRegisteredOverlays() => _parent.GetRegisteredOverlays(); - } - - - private sealed class DisposableAction : IDisposable - { - private Action? _action; - - public DisposableAction(Action action) - { - _action = action; - } - - public void Dispose() - { - var act = System.Threading.Interlocked.Exchange(ref _action, null); - act?.Invoke(); - } - } -} diff --git a/src/PdfEditorApp.Core/Plugins/IFryPlugin.cs b/src/PdfEditorApp.Core/Plugins/IFryPlugin.cs deleted file mode 100644 index b4c5f7f..0000000 --- a/src/PdfEditorApp.Core/Plugins/IFryPlugin.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace PdfEditorApp.Core.Plugins; - -/// -/// Contract implemented by all FryPDF plugins. -/// Inspired by Cordis architecture: every capability mounts into a shared context. -/// -public interface IFryPlugin -{ - /// - /// Unique identifier for this plugin, e.g. "frypdf.tool.merge" or "frypdf.engine.questpdf". - /// - string Id { get; } - - /// - /// User-friendly name of the plugin. - /// - string Name { get; } - - /// - /// Plugin version. - /// - Version Version { get; } - - /// - /// Service contracts required by this plugin before it can mount (declarative injection graph). - /// - IReadOnlyList RequiredServices { get; } - - /// - /// Service contracts provided by this plugin (used for dependency graph cycle detection and validation). - /// - IReadOnlyList ProvidedServices => Array.Empty(); - - /// - /// Optional declarative configuration schema describing configurable user settings for this plugin. - /// - IReadOnlyDictionary? SettingsSchema => null; - - /// - /// Mounts the plugin into the active context, registering services, pipelines, tools, and effects. - /// - /// The active plugin context. - /// Cancellation token. - Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default); -} diff --git a/src/PdfEditorApp.Core/Plugins/IFryPluginContext.cs b/src/PdfEditorApp.Core/Plugins/IFryPluginContext.cs deleted file mode 100644 index f9601c3..0000000 --- a/src/PdfEditorApp.Core/Plugins/IFryPluginContext.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Pipelines; - -namespace PdfEditorApp.Core.Plugins; - -/// -/// The central capability context provided to plugins during their lifecycle. -/// Acts as a dynamic service bus, pipeline orchestrator, and effect tracker. -/// -public interface IFryPluginContext : IServiceProvider -{ - // --- 1. Service Bus --- - - /// - /// Registers a service contract and implementation into the context. - /// - void RegisterService(TService implementation) where TService : class; - - /// - /// Registers a service contract by type and implementation into the context. - /// - void RegisterService(Type serviceType, object implementation); - - /// - /// Retrieves a required registered service. Throws if the service is not found. - /// - TService GetService() where TService : class; - - /// - /// Attempts to retrieve a registered service. - /// - bool TryGetService([NotNullWhen(true)] out TService? service) where TService : class; - - /// - /// Checks whether a service contract has been registered. - /// - bool HasService() where TService : class; - - /// - /// Checks whether a service contract of the given type has been registered. - /// - bool HasService(Type serviceType); - - // --- 2. Reversible Effects --- - - /// - /// Registers a cleanup action that will be executed when the current plugin unloads. - /// - IDisposable RegisterEffect(Action onDispose); - - /// - /// Registers an to be disposed when the current plugin unloads. - /// - IDisposable RegisterDisposable(IDisposable disposable); - - // --- 3. Pipeline Operations --- - - /// - /// Gets the pipeline manager for registering and executing typed pipelines. - /// - IPipelineManager Pipelines { get; } - - /// - /// Registers a middleware into a named Waterfall pipeline. - /// - IDisposable RegisterWaterfall(string pipelineName, Func, Task> middleware); - - /// - /// Executes a named Waterfall pipeline. - /// - Task ExecuteWaterfallAsync(string pipelineName, TContext context, Func? terminal = null); - - /// - /// Registers a handler into a named Bail pipeline. - /// - IDisposable RegisterBail(string pipelineName, Func> handler) where TResult : class; - - /// - /// Executes a named Bail pipeline. - /// - Task ExecuteBailAsync(string pipelineName, TContext context) where TResult : class; - - /// - /// Registers a handler into a named Parallel pipeline. - /// - IDisposable RegisterParallel(string pipelineName, Func handler); - - /// - /// Executes a named Parallel pipeline. - /// - Task ExecuteParallelAsync(string pipelineName, TContext context); - - /// - /// Registers a handler into a named Serial pipeline. - /// - IDisposable RegisterSerial(string pipelineName, Func handler); - - /// - /// Executes a named Serial pipeline. - /// - Task ExecuteSerialAsync(string pipelineName, TContext context); - - // --- 4. Tool Registry --- - - /// - /// Event fired when a tool is dynamically registered or unregistered. - /// - event Action? ToolsChanged; - - /// - /// Registers a PDF tool contributed by a plugin. - /// - IDisposable RegisterTool(PdfToolDescriptor tool); - - /// - /// Gets all currently registered PDF tools. - /// - IReadOnlyList GetRegisteredTools(); - - /// - /// Finds a tool by its unique string identifier. - /// - PdfToolDescriptor? GetTool(string id); - - // --- 5. Canvas Element Registry --- - - /// - /// Registers a custom canvas element type contributed by a plugin. - /// - IDisposable RegisterCanvasElement(CanvasElementDescriptor descriptor); - - /// - /// Gets all registered canvas element types. - /// - IReadOnlyList GetRegisteredCanvasElements(); - - /// - /// Finds a canvas element descriptor by its unique element type identifier. - /// - CanvasElementDescriptor? GetCanvasElement(string elementTypeId); - - // --- 6. Ribbon Action Registry --- - - /// - /// Registers a dynamic Ribbon action button or menu item contributed by a plugin. - /// - IDisposable RegisterRibbonAction(RibbonActionDescriptor descriptor); - - /// - /// Gets all registered Ribbon actions. - /// - IReadOnlyList GetRegisteredRibbonActions(); - - // --- 7. Document Importer Registry --- - - /// - /// Registers a document importer contributed by a plugin. - /// - IDisposable RegisterImporter(IDocumentImporter importer); - - /// - /// Gets all registered document importers. - /// - IReadOnlyList GetRegisteredImporters(); - - // --- 8. Document Exporter Registry --- - - /// - /// Registers a document exporter contributed by a plugin. - /// - IDisposable RegisterExporter(IDocumentExporter exporter); - - /// - /// Gets all registered document exporters. - /// - IReadOnlyList GetRegisteredExporters(); - - // --- 9. Inspector Panel Registry --- - - /// - /// Registers an inspector section property panel contributed by a plugin. - /// - IDisposable RegisterInspectorSection(InspectorSectionDescriptor descriptor); - - /// - /// Gets all registered inspector sections. - /// - IReadOnlyList GetRegisteredInspectorSections(); - - // --- 10. Document Template Registry --- - - /// - /// Registers a document template contributed by a plugin. - /// - IDisposable RegisterTemplate(ITemplateDescriptor template); - - /// - /// Gets all registered document templates. - /// - IReadOnlyList GetRegisteredTemplates(); - - // --- 12. OCR Engine Registry --- - - /// - /// Registers an OCR engine contributed by a plugin. - /// - IDisposable RegisterOcrEngine(IOcrEngine engine); - - /// - /// Gets all registered OCR engines. - /// - IReadOnlyList GetRegisteredOcrEngines(); - - // --- 13. Data Connector Registry --- - - /// - /// Registers a data connector contributed by a plugin. - /// - IDisposable RegisterDataConnector(IDataConnector connector); - - /// - /// Gets all registered data connectors. - /// - IReadOnlyList GetRegisteredDataConnectors(); - - // --- 14. Status Bar Registry --- - - /// - /// Registers a status bar widget contributed by a plugin. - /// - IDisposable RegisterStatusBarWidget(StatusBarWidgetDescriptor widget); - - /// - /// Gets all registered status bar widgets. - /// - IReadOnlyList GetRegisteredStatusBarWidgets(); - - // --- 15. Command Palette Registry --- - - /// - /// Registers a command searchable in the Command Palette (⌘K / Ctrl+K). - /// - IDisposable RegisterCommand(CommandPaletteDescriptor descriptor); - - /// - /// Gets all registered command palette descriptors. - /// - IReadOnlyList GetRegisteredCommands(); - - // --- 16. Navigation Registry --- - - /// - /// Registers a workspace navigation section or full-page view contributed by a plugin. - /// - IDisposable RegisterNavigationItem(NavigationItemDescriptor descriptor); - - /// - /// Gets all registered navigation item descriptors. - /// - IReadOnlyList GetRegisteredNavigationItems(); - - // --- 17. Dialog Registry --- - - /// - /// Registers a modal dialog or floating studio contributed by a plugin. - /// - IDisposable RegisterDialog(DialogDescriptor descriptor); - - /// - /// Gets all registered modal dialog descriptors. - /// - IReadOnlyList GetRegisteredDialogs(); - - // --- 18. Sidebar Registry --- - - /// - /// Registers an editor sidebar panel contributed by a plugin. - /// - IDisposable RegisterSidebarTab(SidebarTabDescriptor descriptor); - - /// - /// Gets all registered editor sidebar tab descriptors. - /// - IReadOnlyList GetRegisteredSidebarTabs(); - - // --- 19. Dynamic Ribbon Tabs & Groups --- - - /// - /// Registers a dynamic Ribbon Tab contributed by a plugin. - /// - IDisposable RegisterRibbonTab(RibbonTabDescriptor descriptor); - - /// - /// Registers a dynamic Ribbon tool group inside a Ribbon tab. - /// - IDisposable RegisterRibbonGroup(RibbonGroupDescriptor descriptor); - - // --- 20. Shell Overlay Registry (Cordis Architecture Inspired) --- - - /// - /// Registers a floating shell overlay contributed by a plugin (targeting 'shell.overlay'). - /// - IDisposable RegisterOverlay(OverlayDescriptor descriptor); - - /// - /// Gets all registered floating overlay descriptors. - /// - IReadOnlyList GetRegisteredOverlays(); -} - diff --git a/src/PdfEditorApp.Core/Plugins/Manifests/PluginManifest.cs b/src/PdfEditorApp.Core/Plugins/Manifests/PluginManifest.cs deleted file mode 100644 index 03009fd..0000000 --- a/src/PdfEditorApp.Core/Plugins/Manifests/PluginManifest.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace PdfEditorApp.Core.Plugins.Manifests; - -/// -/// Declarative manifest representation parsed from a plugin package's "plugin.json". -/// -public class PluginManifest -{ - [JsonPropertyName("id")] - public string Id { get; set; } = ""; - - [JsonPropertyName("name")] - public string Name { get; set; } = ""; - - [JsonPropertyName("version")] - public string Version { get; set; } = "1.0.0"; - - [JsonPropertyName("author")] - public string Author { get; set; } = ""; - - [JsonPropertyName("description")] - public string Description { get; set; } = ""; - - [JsonPropertyName("entryPoint")] - public string EntryPoint { get; set; } = ""; - - [JsonPropertyName("icon")] - public string Icon { get; set; } = "PuzzleOutline"; - - [JsonPropertyName("dependencies")] - public List Dependencies { get; set; } = new(); - - [JsonPropertyName("settingsSchema")] - public Dictionary SettingsSchema { get; set; } = new(); -} - -public class PluginSettingDefinition -{ - [JsonPropertyName("type")] - public string Type { get; set; } = "string"; // string, boolean, number, select - - [JsonPropertyName("label")] - public string Label { get; set; } = ""; - - [JsonPropertyName("description")] - public string Description { get; set; } = ""; - - [JsonPropertyName("default")] - public object? DefaultValue { get; set; } - - [JsonPropertyName("options")] - public List? Options { get; set; } -} diff --git a/src/PdfEditorApp.Core/Plugins/Marketplace/FileInstalledPluginStore.cs b/src/PdfEditorApp.Core/Plugins/Marketplace/FileInstalledPluginStore.cs deleted file mode 100644 index f61ef7c..0000000 --- a/src/PdfEditorApp.Core/Plugins/Marketplace/FileInstalledPluginStore.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; - -namespace PdfEditorApp.Core.Plugins.Marketplace; - -/// -/// File-backed persistent store for installed plugin history saving to "installed_plugins.json". -/// -public class FileInstalledPluginStore : IInstalledPluginStore -{ - private readonly string _filePath; - private readonly Dictionary _records = new(StringComparer.OrdinalIgnoreCase); - private readonly object _lock = new(); - - private static readonly JsonSerializerOptions JsonOptions = new() - { - WriteIndented = true, - PropertyNameCaseInsensitive = true - }; - - public FileInstalledPluginStore(string? filePath = null) - { - _filePath = filePath ?? Path.Combine(AppContext.BaseDirectory, "installed_plugins.json"); - LoadFromFile(); - } - - private void LoadFromFile() - { - lock (_lock) - { - _records.Clear(); - try - { - if (File.Exists(_filePath)) - { - var json = File.ReadAllText(_filePath); - var list = JsonSerializer.Deserialize>(json, JsonOptions); - if (list != null) - { - foreach (var item in list) - { - if (!string.IsNullOrWhiteSpace(item.PluginId)) - { - _records[item.PluginId] = item; - } - } - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[FileInstalledPluginStore] Load error: {ex.Message}"); - } - } - } - - public IReadOnlyList GetAll() - { - lock (_lock) - { - return _records.Values.OrderByDescending(r => r.InstalledAt).ToList(); - } - } - - public InstalledPluginRecord? Get(string pluginId) - { - if (string.IsNullOrWhiteSpace(pluginId)) return null; - - lock (_lock) - { - return _records.TryGetValue(pluginId, out var record) ? record : null; - } - } - - public bool IsInstalled(string pluginId) - { - if (string.IsNullOrWhiteSpace(pluginId)) return false; - - lock (_lock) - { - return _records.ContainsKey(pluginId); - } - } - - public void AddOrUpdate(InstalledPluginRecord record) - { - ArgumentNullException.ThrowIfNull(record); - if (string.IsNullOrWhiteSpace(record.PluginId)) return; - - lock (_lock) - { - _records[record.PluginId] = record; - SaveInternal(); - } - } - - public bool Remove(string pluginId) - { - if (string.IsNullOrWhiteSpace(pluginId)) return false; - - lock (_lock) - { - if (_records.Remove(pluginId)) - { - SaveInternal(); - return true; - } - return false; - } - } - - public void Save() - { - lock (_lock) - { - SaveInternal(); - } - } - - private void SaveInternal() - { - try - { - var dir = Path.GetDirectoryName(_filePath); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - var list = _records.Values.OrderByDescending(r => r.InstalledAt).ToList(); - var json = JsonSerializer.Serialize(list, JsonOptions); - File.WriteAllText(_filePath, json); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[FileInstalledPluginStore] Save error: {ex.Message}"); - } - } -} diff --git a/src/PdfEditorApp.Core/Plugins/Marketplace/IInstalledPluginStore.cs b/src/PdfEditorApp.Core/Plugins/Marketplace/IInstalledPluginStore.cs deleted file mode 100644 index 81a6f26..0000000 --- a/src/PdfEditorApp.Core/Plugins/Marketplace/IInstalledPluginStore.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Marketplace; - -/// -/// Contract for persisting and retrieving installed plugin records and history across application sessions. -/// -public interface IInstalledPluginStore -{ - IReadOnlyList GetAll(); - InstalledPluginRecord? Get(string pluginId); - bool IsInstalled(string pluginId); - void AddOrUpdate(InstalledPluginRecord record); - bool Remove(string pluginId); - void Save(); -} diff --git a/src/PdfEditorApp.Core/Plugins/Marketplace/IPluginMarketplaceService.cs b/src/PdfEditorApp.Core/Plugins/Marketplace/IPluginMarketplaceService.cs deleted file mode 100644 index 5eec948..0000000 --- a/src/PdfEditorApp.Core/Plugins/Marketplace/IPluginMarketplaceService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace PdfEditorApp.Core.Plugins.Marketplace; - -/// -/// Service providing access to the curated FryPDF Plugin Store and Extensions Marketplace. -/// Enables discovery, searching, downloading, and package installation of official and community extensions. -/// -public interface IPluginMarketplaceService -{ - /// - /// Retrieves all available extensions from the marketplace catalog. - /// - Task> GetCatalogAsync(CancellationToken ct = default); - - /// - /// Searches the marketplace catalog by query keywords, category, or tags. - /// - Task> SearchAsync(string query, string? category = null, CancellationToken ct = default); - - /// - /// Simulates or executes downloading and installing a marketplace plugin package by ID. - /// - Task InstallPluginAsync(string pluginId, IProgress? progress = null, Action? statusCallback = null, CancellationToken ct = default); - - /// - /// Uninstalls an installed marketplace plugin. - /// - Task UninstallPluginAsync(string pluginId, CancellationToken ct = default); - - /// - /// Checks whether a specific plugin is installed locally. - /// - bool IsPluginInstalled(string pluginId); - - /// - /// Checks for available updates across all installed marketplace plugins. - /// - Task> CheckForUpdatesAsync(CancellationToken ct = default); - - /// - /// Fetches and synchronizes extensions from the remote registry (e.g. PDFCreator-resources). - /// - Task> FetchRemoteCatalogAsync(bool forceRefresh = false, CancellationToken ct = default); -} diff --git a/src/PdfEditorApp.Core/Plugins/Marketplace/InstalledPluginRecord.cs b/src/PdfEditorApp.Core/Plugins/Marketplace/InstalledPluginRecord.cs deleted file mode 100644 index 52e987c..0000000 --- a/src/PdfEditorApp.Core/Plugins/Marketplace/InstalledPluginRecord.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace PdfEditorApp.Core.Plugins.Marketplace; - -/// -/// Record representing an installed plugin persisted to disk. -/// Tracks installation timestamp, enabled state, and last known overlay position. -/// -public class InstalledPluginRecord -{ - public string PluginId { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public string Version { get; set; } = "1.0.0"; - public DateTime InstalledAt { get; set; } = DateTime.UtcNow; - public bool IsEnabled { get; set; } = true; - public bool WasOverlayOpen { get; set; } = false; - public double? LastX { get; set; } - public double? LastY { get; set; } -} diff --git a/src/PdfEditorApp.Core/Plugins/Marketplace/MarketplacePluginItem.cs b/src/PdfEditorApp.Core/Plugins/Marketplace/MarketplacePluginItem.cs deleted file mode 100644 index 63ce3dd..0000000 --- a/src/PdfEditorApp.Core/Plugins/Marketplace/MarketplacePluginItem.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Marketplace; - -/// -/// Status of a plugin in the FryPDF Plugin Store / Marketplace. -/// -public enum MarketplacePluginStatus -{ - Available, - Installing, - Installed, - UpdateAvailable -} - -/// -/// Domain model for an extension or plugin available in the FryPDF Marketplace / Store. -/// Inspired by the VS Code Extensions Gallery item schema. -/// -public sealed class MarketplacePluginItem -{ - public required string Id { get; init; } - public required string Name { get; init; } - public required string Publisher { get; init; } - public required string Version { get; init; } - public required string Description { get; init; } - public string LongDescription { get; init; } = string.Empty; - public string Category { get; init; } = "General"; - public IReadOnlyList Tags { get; init; } = Array.Empty(); - public double Rating { get; init; } = 4.8; - public int RatingCount { get; init; } = 120; - public int InstallCount { get; init; } = 1500; - public string FormattedSize { get; init; } = "1.2 MB"; - public string IconKind { get; init; } = "PuzzleOutline"; - public string IconColorHex { get; init; } = "#7C3AED"; - public string License { get; init; } = "MIT"; - public bool IsVerified { get; init; } = true; - public bool IsOfficial { get; init; } = false; - public string DownloadUrl { get; init; } = string.Empty; - public IReadOnlyList Highlights { get; init; } = Array.Empty(); - public IReadOnlyList ContributedFeatures { get; init; } = Array.Empty(); - public IReadOnlyList Dependencies { get; init; } = Array.Empty(); - public MarketplacePluginStatus Status { get; set; } = MarketplacePluginStatus.Available; -} diff --git a/src/PdfEditorApp.Core/Plugins/Pipelines/DocumentPipelineContexts.cs b/src/PdfEditorApp.Core/Plugins/Pipelines/DocumentPipelineContexts.cs deleted file mode 100644 index 1dadf20..0000000 --- a/src/PdfEditorApp.Core/Plugins/Pipelines/DocumentPipelineContexts.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Plugins.Pipelines; - -/// -/// Execution context for document export waterfall pipelines ("document:export"). -/// Allows plugins to intercept document models before generation, and inspect/transform -/// the generated binary PDF bytes afterwards. -/// -public class PdfExportPipelineContext -{ - /// The document model to be exported. - public PdfDocumentModel Document { get; set; } - - /// The compiled output PDF bytes. Can be transformed or substituted by pipeline filters. - public byte[]? ResultPdfBytes { get; set; } - - /// Optional progress reporting channel. - public IProgress? Progress { get; init; } - - /// Cancellation token for aborting generation. - public CancellationToken CancellationToken { get; init; } - - /// Dynamic properties and options passed to pipeline filters. - public IDictionary Properties { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public PdfExportPipelineContext(PdfDocumentModel document, IProgress? progress = null, CancellationToken cancellationToken = default) - { - Document = document ?? throw new ArgumentNullException(nameof(document)); - Progress = progress; - CancellationToken = cancellationToken; - } -} - -/// -/// Execution context for document import waterfall pipelines ("document:import"). -/// Allows plugins to preprocess input streams, parse proprietary formats, or enrich deconstructed models. -/// -public class PdfImportPipelineContext -{ - /// The raw stream of the document being imported. - public Stream InputStream { get; init; } - - /// The resulting deconstructed document model. - public PdfDocumentModel? Document { get; set; } - - /// Cancellation token for aborting import. - public CancellationToken CancellationToken { get; init; } - - /// Dynamic properties and options passed to pipeline handlers. - public IDictionary Properties { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// The file name or title of the document being imported. - public string FileName { get; init; } = "Document.pdf"; - - /// Optional password provided for decryption. - public string? Password { get; init; } - - public PdfImportPipelineContext(Stream inputStream, string fileName = "Document.pdf", string? password = null, CancellationToken cancellationToken = default) - { - InputStream = inputStream ?? throw new ArgumentNullException(nameof(inputStream)); - FileName = fileName; - Password = password; - CancellationToken = cancellationToken; - } -} - -/// -/// Execution context for PDF tool execution waterfall pipelines ("tool:execute"). -/// Allows plugins to wrap tool invocations with auditing, telemetry, progress reporting, and output post-processing. -/// -public class PdfToolExecutionPipelineContext -{ - /// The identifier of the tool being executed. - public string ToolId { get; init; } - - /// Tool options object. - public object Options { get; set; } - - /// Optional progress reporting channel. - public IProgress? Progress { get; init; } - - /// Cancellation token for aborting the tool. - public CancellationToken CancellationToken { get; init; } - - /// The result produced by the tool execution. - public object? Result { get; set; } - - /// Dynamic properties and options passed to pipeline filters. - public IDictionary Properties { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public PdfToolExecutionPipelineContext(string toolId, object options, IProgress? progress = null, CancellationToken cancellationToken = default) - { - ToolId = toolId ?? throw new ArgumentNullException(nameof(toolId)); - Options = options ?? throw new ArgumentNullException(nameof(options)); - Progress = progress; - CancellationToken = cancellationToken; - } -} diff --git a/src/PdfEditorApp.Core/Plugins/Pipelines/IPipelineManager.cs b/src/PdfEditorApp.Core/Plugins/Pipelines/IPipelineManager.cs deleted file mode 100644 index 95a96e2..0000000 --- a/src/PdfEditorApp.Core/Plugins/Pipelines/IPipelineManager.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace PdfEditorApp.Core.Plugins.Pipelines; - -/// -/// Orchestrates typed execution pipelines inspired by Cordis architecture. -/// Supports 4 primary dispatch modes: Waterfall (around-middleware), Bail (first handled result), -/// Parallel (concurrent execution), and Serial (ordered execution). -/// -public interface IPipelineManager -{ - /// - /// Registers a middleware component into a named Waterfall pipeline. - /// Middleware components wrap subsequent handlers using the provided next delegate. - /// - IDisposable RegisterWaterfall(string pipelineName, Func, Task> middleware); - - /// - /// Executes a named Waterfall pipeline with the given context and optional terminal delegate. - /// - Task ExecuteWaterfallAsync(string pipelineName, TContext context, Func? terminal = null); - - /// - /// Registers a handler into a named Bail pipeline. - /// The pipeline will evaluate handlers until the first handler returns a non-null result. - /// - IDisposable RegisterBail(string pipelineName, Func> handler) where TResult : class; - - /// - /// Executes a named Bail pipeline, returning the result of the first handler that produces a non-null value. - /// - Task ExecuteBailAsync(string pipelineName, TContext context) where TResult : class; - - /// - /// Registers a handler into a named Parallel pipeline. - /// All handlers will be executed concurrently using . - /// - IDisposable RegisterParallel(string pipelineName, Func handler); - - /// - /// Executes all handlers registered in a Parallel pipeline concurrently. - /// - Task ExecuteParallelAsync(string pipelineName, TContext context); - - /// - /// Registers a handler into a named Serial pipeline. - /// Handlers will be awaited sequentially in order of registration. - /// - IDisposable RegisterSerial(string pipelineName, Func handler); - - /// - /// Executes all handlers registered in a Serial pipeline in sequential order. - /// - Task ExecuteSerialAsync(string pipelineName, TContext context); -} diff --git a/src/PdfEditorApp.Core/Plugins/Pipelines/PipelineManager.cs b/src/PdfEditorApp.Core/Plugins/Pipelines/PipelineManager.cs deleted file mode 100644 index 5589d36..0000000 --- a/src/PdfEditorApp.Core/Plugins/Pipelines/PipelineManager.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace PdfEditorApp.Core.Plugins.Pipelines; - -/// -/// Thread-safe implementation of . -/// Supports Waterfall (around-middleware), Bail (first non-null return), Parallel, and Serial dispatch modes. -/// -public class PipelineManager : IPipelineManager -{ - private readonly ConcurrentDictionary> _pipelines = new(); - private readonly object _syncLock = new(); - - public IDisposable RegisterWaterfall(string pipelineName, Func, Task> middleware) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pipelineName); - ArgumentNullException.ThrowIfNull(middleware); - - var key = $"waterfall:{pipelineName}:{typeof(TContext).FullName}"; - return RegisterHandler(key, middleware); - } - - public async Task ExecuteWaterfallAsync(string pipelineName, TContext context, Func? terminal = null) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pipelineName); - - var key = $"waterfall:{pipelineName}:{typeof(TContext).FullName}"; - List, Task>>? middlewares = null; - - lock (_syncLock) - { - if (!_pipelines.TryGetValue(key, out var list) || list.Count == 0) - { - middlewares = null; - } - else - { - middlewares = list.Cast, Task>>().ToList(); - } - } - - if (middlewares == null || middlewares.Count == 0) - { - if (terminal != null) - { - await terminal(); - } - return; - } - - // Build the pipeline chain: last middleware calls terminal, preceding middlewares call next - Func pipeline = terminal ?? (() => Task.CompletedTask); - - for (int i = middlewares.Count - 1; i >= 0; i--) - { - var currentMiddleware = middlewares[i]; - var nextDelegate = pipeline; - pipeline = () => currentMiddleware(context, nextDelegate); - } - - await pipeline(); - } - - public IDisposable RegisterBail(string pipelineName, Func> handler) where TResult : class - { - ArgumentException.ThrowIfNullOrWhiteSpace(pipelineName); - ArgumentNullException.ThrowIfNull(handler); - - var key = $"bail:{pipelineName}:{typeof(TContext).FullName}:{typeof(TResult).FullName}"; - return RegisterHandler(key, handler); - } - - public async Task ExecuteBailAsync(string pipelineName, TContext context) where TResult : class - { - ArgumentException.ThrowIfNullOrWhiteSpace(pipelineName); - - var key = $"bail:{pipelineName}:{typeof(TContext).FullName}:{typeof(TResult).FullName}"; - List>> handlers; - - lock (_syncLock) - { - if (!_pipelines.TryGetValue(key, out var list) || list.Count == 0) - { - return default; - } - - handlers = list.Cast>>().ToList(); - } - - foreach (var handler in handlers) - { - var result = await handler(context); - if (result is not null) - { - return result; - } - } - - return default; - } - - public IDisposable RegisterParallel(string pipelineName, Func handler) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pipelineName); - ArgumentNullException.ThrowIfNull(handler); - - var key = $"parallel:{pipelineName}:{typeof(TContext).FullName}"; - return RegisterHandler(key, handler); - } - - public async Task ExecuteParallelAsync(string pipelineName, TContext context) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pipelineName); - - var key = $"parallel:{pipelineName}:{typeof(TContext).FullName}"; - List> handlers; - - lock (_syncLock) - { - if (!_pipelines.TryGetValue(key, out var list) || list.Count == 0) - { - return; - } - - handlers = list.Cast>().ToList(); - } - - var tasks = handlers.Select(h => h(context)).ToArray(); - await Task.WhenAll(tasks); - } - - public IDisposable RegisterSerial(string pipelineName, Func handler) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pipelineName); - ArgumentNullException.ThrowIfNull(handler); - - var key = $"serial:{pipelineName}:{typeof(TContext).FullName}"; - return RegisterHandler(key, handler); - } - - public async Task ExecuteSerialAsync(string pipelineName, TContext context) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pipelineName); - - var key = $"serial:{pipelineName}:{typeof(TContext).FullName}"; - List> handlers; - - lock (_syncLock) - { - if (!_pipelines.TryGetValue(key, out var list) || list.Count == 0) - { - return; - } - - handlers = list.Cast>().ToList(); - } - - foreach (var handler in handlers) - { - await handler(context); - } - } - - private IDisposable RegisterHandler(string key, object handler) - { - lock (_syncLock) - { - var list = _pipelines.GetOrAdd(key, _ => new List()); - list.Add(handler); - } - - return new DisposableAction(() => - { - lock (_syncLock) - { - if (_pipelines.TryGetValue(key, out var list)) - { - list.Remove(handler); - if (list.Count == 0) - { - _pipelines.TryRemove(key, out _); - } - } - } - }); - } - - private sealed class DisposableAction : IDisposable - { - private Action? _action; - - public DisposableAction(Action action) - { - _action = action; - } - - public void Dispose() - { - var act = System.Threading.Interlocked.Exchange(ref _action, null); - act?.Invoke(); - } - } -} diff --git a/src/PdfEditorApp.Core/Plugins/PluginExceptions.cs b/src/PdfEditorApp.Core/Plugins/PluginExceptions.cs deleted file mode 100644 index c42f551..0000000 --- a/src/PdfEditorApp.Core/Plugins/PluginExceptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace PdfEditorApp.Core.Plugins; - -/// -/// Base exception for all plugin-related runtime errors. -/// -public class PluginException : Exception -{ - public PluginException(string message) : base(message) { } - public PluginException(string message, Exception innerException) : base(message, innerException) { } -} - -/// -/// Thrown when a plugin requires a service contract that was not registered by any loaded plugin or the host. -/// -public class PluginMissingDependencyException : PluginException -{ - public string PluginId { get; } - public Type MissingServiceType { get; } - - public PluginMissingDependencyException(string pluginId, Type missingServiceType) - : base($"Plugin '{pluginId}' requires service '{missingServiceType.FullName}', but it was not provided by any active plugin or host service.") - { - PluginId = pluginId; - MissingServiceType = missingServiceType; - } -} - -/// -/// Thrown when a cycle is detected in plugin dependency declarations. -/// -public class PluginCircularDependencyException : PluginException -{ - public PluginCircularDependencyException(string message) : base(message) { } -} diff --git a/src/PdfEditorApp.Core/Plugins/PluginHost.cs b/src/PdfEditorApp.Core/Plugins/PluginHost.cs deleted file mode 100644 index ca83386..0000000 --- a/src/PdfEditorApp.Core/Plugins/PluginHost.cs +++ /dev/null @@ -1,443 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace PdfEditorApp.Core.Plugins; - -/// -/// Lifecycle state of a registered FryPDF plugin. -/// -public enum PluginState -{ - Unloaded, - Registered, - Resolving, - Active, - Suspended, - Faulted -} - -/// -/// Internal tracking descriptor for a plugin and its activation scope. -/// -public class PluginEntry -{ - public IFryPlugin Plugin { get; } - public PluginState State { get; set; } = PluginState.Registered; - public PluginScope? Scope { get; set; } - public Exception? LastError { get; set; } - - public PluginEntry(IFryPlugin plugin) - { - Plugin = plugin; - } -} - -/// -/// Orchestrates plugin registration, dependency-driven topological mounting, reactive lifecycle states, -/// cascading dependency activation/deactivation, and scoped LIFO teardown. -/// -public class PluginHost : IAsyncDisposable, IDisposable -{ - private readonly object _lock = new(); - private readonly List _registeredPlugins = new(); - private readonly Dictionary _entries = new(StringComparer.OrdinalIgnoreCase); - private readonly List<(IFryPlugin Plugin, PluginScope Scope)> _activePlugins = new(); - private readonly FryPluginContext _context; - private bool _isRunning; - - /// - /// Event fired when an individual plugin transitions lifecycle states (e.g. Active, Suspended, Faulted). - /// - public event Action? PluginStateChanged; - - /// - /// The root plugin context. - /// - public IFryPluginContext Context => _context; - - /// - /// Gets all currently active plugins in order of activation. - /// - public IReadOnlyList LoadedPlugins - { - get - { - lock (_lock) - { - return _activePlugins.Select(p => p.Plugin).ToList(); - } - } - } - - /// - /// Gets all registered plugins, regardless of state. - /// - public IReadOnlyList RegisteredPlugins - { - get - { - lock (_lock) - { - return _registeredPlugins.ToList(); - } - } - } - - public PluginHost(FryPluginContext? context = null) - { - _context = context ?? new FryPluginContext(); - } - - /// - /// Gets the current lifecycle state of a plugin. - /// - public PluginState GetPluginState(string pluginId) - { - lock (_lock) - { - return _entries.TryGetValue(pluginId, out var entry) ? entry.State : PluginState.Unloaded; - } - } - - /// - /// Checks whether a plugin is currently in the Active state. - /// - public bool IsPluginActive(string pluginId) => GetPluginState(pluginId) == PluginState.Active; - - /// - /// Registers a plugin for mounting. - /// - public void RegisterPlugin(IFryPlugin plugin) - { - ArgumentNullException.ThrowIfNull(plugin); - lock (_lock) - { - if (_entries.TryGetValue(plugin.Id, out var existing)) - { - if (ReferenceEquals(existing.Plugin, plugin)) return; - - // Replace previous registration with the new plugin instance - _registeredPlugins.Remove(existing.Plugin); - _registeredPlugins.Add(plugin); - _entries[plugin.Id] = new PluginEntry(plugin); - return; - } - _registeredPlugins.Add(plugin); - _entries[plugin.Id] = new PluginEntry(plugin); - } - } - - /// - /// Registers multiple plugins for mounting. - /// - public void RegisterPlugins(IEnumerable plugins) - { - ArgumentNullException.ThrowIfNull(plugins); - foreach (var plugin in plugins) - { - RegisterPlugin(plugin); - } - } - - /// - /// Mounts all registered plugins in dependency-resolved topological order. - /// - public async Task StartAsync(CancellationToken ct = default) - { - List pending; - lock (_lock) - { - if (_isRunning) return; - pending = new List(_registeredPlugins.Where(p => _entries[p.Id].State != PluginState.Active)); - } - - while (pending.Count > 0) - { - ct.ThrowIfCancellationRequested(); - - var activatable = pending - .Where(p => p.RequiredServices.All(reqType => _context.HasService(reqType))) - .ToList(); - - if (activatable.Count == 0) - { - var allProvided = new HashSet(pending.SelectMany(p => p.ProvidedServices)); - - foreach (var p in pending) - { - var missing = p.RequiredServices.FirstOrDefault(r => !_context.HasService(r) && !allProvided.Contains(r)); - if (missing != null) - { - throw new PluginMissingDependencyException(p.Id, missing); - } - } - - throw new PluginCircularDependencyException( - $"Circular dependency detected among plugins: {string.Join(", ", pending.Select(p => p.Id))}"); - } - - foreach (var plugin in activatable) - { - ct.ThrowIfCancellationRequested(); - await MountPluginCoreAsync(plugin, ct); - pending.Remove(plugin); - } - } - - lock (_lock) - { - _isRunning = true; - } - } - - /// - /// Dynamically enables an individual plugin at runtime, mounting its capabilities and - /// reactively cascading activation to any downstream plugins waiting on its provided services. - /// - public async Task EnablePluginAsync(string pluginId, CancellationToken ct = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pluginId); - - PluginEntry entry; - lock (_lock) - { - if (!_entries.TryGetValue(pluginId, out var e)) - { - throw new KeyNotFoundException($"Plugin with ID '{pluginId}' is not registered."); - } - if (e.State == PluginState.Active) return; - entry = e; - } - - // Validate dependencies - var missing = entry.Plugin.RequiredServices.FirstOrDefault(req => !_context.HasService(req)); - if (missing != null) - { - throw new PluginMissingDependencyException(entry.Plugin.Id, missing); - } - - await MountPluginCoreAsync(entry.Plugin, ct); - - // Cascading auto-mount for any suspended or registered plugins that can now activate - await AutoMountSatisfiedPluginsAsync(ct); - } - - /// - /// Dynamically disables an individual plugin at runtime, cleanly tearing down its effects - /// and cascading suspension to any dependent plugins that require services provided by this plugin. - /// - public async Task DisablePluginAsync(string pluginId, CancellationToken ct = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pluginId); - - PluginEntry entry; - lock (_lock) - { - if (!_entries.TryGetValue(pluginId, out var e) || e.State != PluginState.Active) - { - return; - } - entry = e; - } - - // Unmount the target plugin first - await UnmountPluginCoreAsync(entry, PluginState.Suspended); - - // Cascading deactivation: iteratively suspend any active plugins whose RequiredServices are no longer satisfied in Context! - bool progress = true; - while (progress) - { - progress = false; - PluginEntry? nextToSuspend = null; - - lock (_lock) - { - nextToSuspend = _activePlugins - .Select(p => _entries[p.Plugin.Id]) - .FirstOrDefault(p => p.Plugin.RequiredServices.Any(req => !_context.HasService(req))); - } - - if (nextToSuspend != null) - { - await UnmountPluginCoreAsync(nextToSuspend, PluginState.Suspended); - progress = true; - } - } - } - - /// - /// Reloads a plugin by disabling it and re-enabling it. - /// - public async Task ReloadPluginAsync(string pluginId, CancellationToken ct = default) - { - await DisablePluginAsync(pluginId, ct); - await EnablePluginAsync(pluginId, ct); - } - - private async Task MountPluginCoreAsync(IFryPlugin plugin, CancellationToken ct) - { - PluginScope scope = new(); - var scopedContext = _context.CreateScopedContext(scope); - - lock (_lock) - { - if (_entries.TryGetValue(plugin.Id, out var entry)) - { - entry.State = PluginState.Resolving; - entry.Scope = scope; - entry.LastError = null; - } - } - - try - { - await plugin.ApplyAsync(scopedContext, ct); - - lock (_lock) - { - _activePlugins.Add((plugin, scope)); - if (_entries.TryGetValue(plugin.Id, out var entry)) - { - entry.State = PluginState.Active; - } - } - - PluginStateChanged?.Invoke(plugin.Id, PluginState.Active); - } - catch (Exception ex) - { - scope.Dispose(); - - lock (_lock) - { - if (_entries.TryGetValue(plugin.Id, out var entry)) - { - entry.State = PluginState.Faulted; - entry.Scope = null; - entry.LastError = ex; - } - } - - PluginStateChanged?.Invoke(plugin.Id, PluginState.Faulted); - throw; - } - } - - private Task UnmountPluginCoreAsync(PluginEntry entry, PluginState nextState) - { - PluginScope? scopeToDispose = null; - - lock (_lock) - { - var idx = _activePlugins.FindIndex(p => string.Equals(p.Plugin.Id, entry.Plugin.Id, StringComparison.OrdinalIgnoreCase)); - if (idx >= 0) - { - scopeToDispose = _activePlugins[idx].Scope; - _activePlugins.RemoveAt(idx); - } - - entry.State = nextState; - entry.Scope = null; - } - - if (scopeToDispose != null) - { - try - { - scopeToDispose.Dispose(); - } - catch (Exception ex) - { - entry.LastError = ex; - } - } - - PluginStateChanged?.Invoke(entry.Plugin.Id, nextState); - return Task.CompletedTask; - } - - private async Task AutoMountSatisfiedPluginsAsync(CancellationToken ct) - { - bool progress = true; - while (progress) - { - progress = false; - List candidates; - - lock (_lock) - { - candidates = _entries.Values - .Where(e => e.State is PluginState.Suspended or PluginState.Registered) - .Where(e => e.Plugin.RequiredServices.All(req => _context.HasService(req))) - .ToList(); - } - - foreach (var candidate in candidates) - { - ct.ThrowIfCancellationRequested(); - await MountPluginCoreAsync(candidate.Plugin, ct); - progress = true; - } - } - } - - /// - /// Unmounts all active plugins in reverse (LIFO) order and disposes their scopes. - /// - public async Task StopAsync(CancellationToken ct = default) - { - List<(IFryPlugin Plugin, PluginScope Scope)> toUnload; - lock (_lock) - { - if (!_isRunning && _activePlugins.Count == 0) return; - _isRunning = false; - - toUnload = new List<(IFryPlugin Plugin, PluginScope Scope)>(_activePlugins); - _activePlugins.Clear(); - - foreach (var entry in _entries.Values) - { - entry.State = PluginState.Unloaded; - entry.Scope = null; - } - } - - toUnload.Reverse(); - - List? exceptions = null; - foreach (var (plugin, scope) in toUnload) - { - try - { - scope.Dispose(); - } - catch (Exception ex) - { - exceptions ??= new List(); - exceptions.Add(new PluginException($"Error while unloading plugin '{plugin.Id}': {ex.Message}", ex)); - } - } - - if (exceptions is { Count: > 0 }) - { - throw new AggregateException("One or more errors occurred while stopping plugins.", exceptions); - } - - await Task.CompletedTask; - } - - public async ValueTask DisposeAsync() - { - await StopAsync(); - GC.SuppressFinalize(this); - } - - public void Dispose() - { - StopAsync().GetAwaiter().GetResult(); - GC.SuppressFinalize(this); - } -} diff --git a/src/PdfEditorApp.Core/Plugins/PluginScope.cs b/src/PdfEditorApp.Core/Plugins/PluginScope.cs deleted file mode 100644 index 8016935..0000000 --- a/src/PdfEditorApp.Core/Plugins/PluginScope.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins; - -/// -/// Manages the reversible effects and resources registered by a plugin during its lifetime. -/// When the scope is disposed, registered effects and unmanaged handles are unwound in reverse (LIFO) order. -/// -public sealed class PluginScope : IDisposable -{ - private readonly object _lock = new(); - private readonly Stack _disposers = new(); - private bool _isDisposed; - - /// - /// Gets whether this scope has been disposed. - /// - public bool IsDisposed - { - get - { - lock (_lock) return _isDisposed; - } - } - - /// - /// Registers a reversible effect or cleanup action to execute when this scope is disposed. - /// - /// The cleanup action to execute on disposal. - /// An token that can prematurely unregister or execute this effect. - public IDisposable RegisterEffect(Action onDispose) - { - ArgumentNullException.ThrowIfNull(onDispose); - - lock (_lock) - { - ObjectDisposedException.ThrowIf(_isDisposed, this); - _disposers.Push(onDispose); - } - - return new ScopeEffectToken(onDispose); - } - - /// - /// Registers an instance to be disposed when this scope is torn down. - /// - public IDisposable RegisterDisposable(IDisposable disposable) - { - ArgumentNullException.ThrowIfNull(disposable); - return RegisterEffect(disposable.Dispose); - } - - /// - /// Unwinds all registered effects in reverse (LIFO) order. - /// - public void Dispose() - { - List actionsToRun; - - lock (_lock) - { - if (_isDisposed) return; - _isDisposed = true; - - actionsToRun = new List(_disposers); - _disposers.Clear(); - } - - // Execute in LIFO order (Stack enumeration is top-to-bottom, which is LIFO) - List? exceptions = null; - foreach (var action in actionsToRun) - { - try - { - action(); - } - catch (Exception ex) - { - exceptions ??= new List(); - exceptions.Add(ex); - } - } - - if (exceptions is { Count: > 0 }) - { - throw new AggregateException("One or more errors occurred while unwinding plugin effects.", exceptions); - } - } - - private sealed class ScopeEffectToken : IDisposable - { - private Action? _action; - - public ScopeEffectToken(Action action) - { - _action = action; - } - - public void Dispose() - { - // Note: The main scope disposal will safely no-op or run whatever is registered. - _action = null; - } - } -} diff --git a/src/PdfEditorApp.Core/Plugins/Profiles/IFryPluginBundle.cs b/src/PdfEditorApp.Core/Plugins/Profiles/IFryPluginBundle.cs deleted file mode 100644 index 2a9db22..0000000 --- a/src/PdfEditorApp.Core/Plugins/Profiles/IFryPluginBundle.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace PdfEditorApp.Core.Plugins.Profiles; - -/// -/// Represents a cohesive bundle of plugins, inspired by the Cordis bundle concept. -/// -public interface IFryPluginBundle -{ - /// Unique identifier for this bundle, e.g. "FryPdf.Bundle.Organize". - string Id { get; } - - /// Display name of the bundle. - string Name { get; } - - /// Description of the bundle's capabilities. - string Description { get; } - - /// Collection of plugins contained within this bundle. - IReadOnlyList Plugins { get; } -} diff --git a/src/PdfEditorApp.Core/Plugins/Profiles/PluginProfile.cs b/src/PdfEditorApp.Core/Plugins/Profiles/PluginProfile.cs deleted file mode 100644 index 28a1f34..0000000 --- a/src/PdfEditorApp.Core/Plugins/Profiles/PluginProfile.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace PdfEditorApp.Core.Plugins.Profiles; - -/// -/// Defines an application runtime profile configuration. -/// Defines composable application profiles (e.g. 'desktop', 'headless', 'sdk'). -/// -public class PluginProfile -{ - /// Name of the profile, e.g. "desktop" or "headless". - public string ProfileName { get; set; } = "desktop"; - - /// Alias for ProfileName to support standard metadata naming. - public string Name - { - get => ProfileName; - set => ProfileName = value; - } - - /// Profile version description. - public string Version { get; set; } = "1.0.0"; - - /// Profile summary description. - public string Description { get; set; } = string.Empty; - - /// List of bundle IDs to mount in this profile. - public List Bundles { get; set; } = new(); - - /// Optional list of individually enabled plugin IDs. - public List EnabledPlugins { get; set; } = new(); - - /// Optional list of individual plugin IDs to disable even if their bundle is included. - public List DisabledPlugins { get; set; } = new(); - - /// Profile-specific configuration key-value pairs. - public Dictionary Settings { get; set; } = new(StringComparer.OrdinalIgnoreCase); - - /// Checks whether a plugin is enabled under this profile. - public bool IsPluginEnabled(string pluginId) - { - ArgumentException.ThrowIfNullOrWhiteSpace(pluginId); - if (DisabledPlugins.Any(d => string.Equals(d, pluginId, StringComparison.OrdinalIgnoreCase))) - return false; - if (EnabledPlugins.Count > 0) - return EnabledPlugins.Any(e => string.Equals(e, pluginId, StringComparison.OrdinalIgnoreCase)); - return true; - } - - /// Checks whether a bundle is configured in this profile. - public bool IsBundleEnabled(string bundleId) - { - ArgumentException.ThrowIfNullOrWhiteSpace(bundleId); - return Bundles.Any(b => string.Equals(b, bundleId, StringComparison.OrdinalIgnoreCase)); - } -} diff --git a/src/PdfEditorApp.Core/Plugins/Profiles/ProfileLoader.cs b/src/PdfEditorApp.Core/Plugins/Profiles/ProfileLoader.cs deleted file mode 100644 index ad2a89c..0000000 --- a/src/PdfEditorApp.Core/Plugins/Profiles/ProfileLoader.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; - -namespace PdfEditorApp.Core.Plugins.Profiles; - -/// -/// Deserializes profile configuration files and mounts active bundles into the . -/// -public static class ProfileLoader -{ - private static readonly JsonSerializerOptions JsonOptions = new() - { - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true - }; - - /// - /// Parses a from raw JSON text. - /// - public static PluginProfile LoadFromJson(string json) - { - ArgumentException.ThrowIfNullOrWhiteSpace(json); - return JsonSerializer.Deserialize(json, JsonOptions) - ?? throw new JsonException("Failed to deserialize profile JSON."); - } - - /// - /// Alias for . - /// - public static PluginProfile LoadProfileFromJson(string json) => LoadFromJson(json); - - /// - /// Reads and parses a from a file on disk. - /// - public static PluginProfile LoadFromFile(string filePath) - { - ArgumentException.ThrowIfNullOrWhiteSpace(filePath); - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"Profile configuration file '{filePath}' was not found."); - } - - var json = File.ReadAllText(filePath); - return LoadFromJson(json); - } - - /// - /// Alias for . - /// - public static PluginProfile LoadProfileFromFile(string filePath) => LoadFromFile(filePath); - - /// - /// Mounts bundles defined in the profile into the target , excluding any disabled plugins. - /// - public static void ApplyProfile( - PluginProfile profile, - PluginHost host, - IEnumerable availableBundles) - { - ArgumentNullException.ThrowIfNull(profile); - ArgumentNullException.ThrowIfNull(host); - ArgumentNullException.ThrowIfNull(availableBundles); - - var bundleMap = availableBundles.ToDictionary(b => b.Id, StringComparer.OrdinalIgnoreCase); - var disabledSet = new HashSet(profile.DisabledPlugins, StringComparer.OrdinalIgnoreCase); - - foreach (var bundleId in profile.Bundles) - { - if (bundleMap.TryGetValue(bundleId, out var bundle)) - { - foreach (var plugin in bundle.Plugins) - { - if (!disabledSet.Contains(plugin.Id)) - { - host.RegisterPlugin(plugin); - } - } - } - } - } - - /// - /// Filters available plugins according to the profile and registers & starts them on the host. - /// - public static async Task ApplyToHostAsync( - PluginHost host, - PluginProfile profile, - IEnumerable availablePlugins) - { - ArgumentNullException.ThrowIfNull(host); - ArgumentNullException.ThrowIfNull(profile); - ArgumentNullException.ThrowIfNull(availablePlugins); - - foreach (var plugin in availablePlugins) - { - if (profile.IsPluginEnabled(plugin.Id)) - { - host.RegisterPlugin(plugin); - } - } - - await host.StartAsync(); - } -} diff --git a/src/PdfEditorApp.Core/Plugins/Settings/IPluginSettingsStore.cs b/src/PdfEditorApp.Core/Plugins/Settings/IPluginSettingsStore.cs deleted file mode 100644 index a9dc042..0000000 --- a/src/PdfEditorApp.Core/Plugins/Settings/IPluginSettingsStore.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; - -namespace PdfEditorApp.Core.Plugins.Settings; - -/// -/// Contract for persisting and retrieving user configurations for plugins. -/// -public interface IPluginSettingsStore -{ - T GetSetting(string pluginId, string key, T defaultValue); - void SetSetting(string pluginId, string key, T value); - Dictionary GetPluginSettings(string pluginId); - void Save(); -} - -/// -/// File-backed persistent settings store saving to "plugins.settings.json". -/// -public class FilePluginSettingsStore : IPluginSettingsStore -{ - private readonly string _filePath; - private readonly Dictionary> _data; - private readonly object _lock = new(); - - public FilePluginSettingsStore(string? filePath = null) - { - _filePath = filePath ?? Path.Combine(AppContext.BaseDirectory, "plugins.settings.json"); - _data = LoadFromFile(); - } - - private Dictionary> LoadFromFile() - { - try - { - if (File.Exists(_filePath)) - { - var json = File.ReadAllText(_filePath); - return JsonSerializer.Deserialize>>(json) ?? new(); - } - } - catch - { - // Fall back to clean state on corruption - } - return new(); - } - - public T GetSetting(string pluginId, string key, T defaultValue) - { - lock (_lock) - { - if (_data.TryGetValue(pluginId, out var dict) && dict.TryGetValue(key, out var val)) - { - if (val is JsonElement elem) - { - return JsonSerializer.Deserialize(elem.GetRawText()) ?? defaultValue; - } - if (val is T typedVal) - { - return typedVal; - } - } - return defaultValue; - } - } - - public void SetSetting(string pluginId, string key, T value) - { - lock (_lock) - { - if (!_data.TryGetValue(pluginId, out var dict)) - { - dict = new(); - _data[pluginId] = dict; - } - dict[key] = value!; - Save(); - } - } - - public Dictionary GetPluginSettings(string pluginId) - { - lock (_lock) - { - if (_data.TryGetValue(pluginId, out var dict)) - { - return new(dict); - } - return new(); - } - } - - public void Save() - { - lock (_lock) - { - try - { - var dir = Path.GetDirectoryName(_filePath); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - var json = JsonSerializer.Serialize(_data, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(_filePath, json); - } - catch - { - // Silently ignore disk write failures - } - } - } -} diff --git a/src/PdfEditorApp.Core/QuestPdfInitializer.cs b/src/PdfEditorApp.Core/QuestPdfInitializer.cs deleted file mode 100644 index ae655da..0000000 --- a/src/PdfEditorApp.Core/QuestPdfInitializer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Runtime.CompilerServices; -using QuestPDF.Infrastructure; - -namespace PdfEditorApp.Core; - -/// -/// Global static module initializer that configures QuestPDF license tier automatically -/// on module load before any document generation code or tests run. -/// -internal static class QuestPdfInitializer -{ -#pragma warning disable CA2255 // The 'ModuleInitializer' attribute is only intended to be used in application code - [ModuleInitializer] - internal static void Initialize() - { - QuestPDF.Settings.License = LicenseType.Community; - } -#pragma warning restore CA2255 -} diff --git a/src/PdfEditorApp.Core/Services/IFontPackageService.cs b/src/PdfEditorApp.Core/Services/IFontPackageService.cs deleted file mode 100644 index b304de4..0000000 --- a/src/PdfEditorApp.Core/Services/IFontPackageService.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Services; - -/// -/// Service managing on-demand language and font packs for international and creative PDF rendering. -/// Keeps the application lightweight while enabling global script support through local caching. -/// -public interface IFontPackageService -{ - /// - /// Event fired whenever fonts are downloaded, imported, or deleted. - /// - event Action? FontLibraryChanged; - - /// - /// Returns the catalog of all available font packages. - /// - IReadOnlyList GetAllPackages(); - - /// - /// Returns the local user directory where downloaded/custom fonts are stored. - /// - string GetUserFontDirectory(); - - /// - /// Checks if a package's font files are present on disk. - /// - bool IsPackageInstalled(FontPackageInfo package); - - /// - /// Downloads all font files in a package asynchronously with progress reporting. - /// - Task DownloadPackageAsync( - FontPackageInfo package, - IProgress? progress = null, - Action? statusCallback = null, - CancellationToken ct = default); - - /// - /// Deletes a downloaded font package from local disk to reclaim space. - /// - Task DeletePackageAsync(FontPackageInfo package); - - /// - /// Calculates the total disk space used by downloaded and custom fonts. - /// - Task GetTotalCacheSizeBytesAsync(); - - /// - /// Deletes all downloaded font cache files. - /// - Task ClearAllCacheAsync(); - - /// - /// Imports a user-supplied custom .ttf / .otf font file into the local font directory. - /// - Task ImportCustomFontAsync(string sourceFilePath); - - /// - /// Scans text for non-Latin script characters and returns the corresponding font package if not yet installed. - /// - FontPackageInfo? DetectMissingPackageForText(string text); - - /// - /// Returns all font file paths available across embedded and downloaded directories. - /// - IEnumerable GetAllAvailableFontFilePaths(); -} diff --git a/src/PdfEditorApp.Core/Utils/FryPdfEmbeddingHelper.cs b/src/PdfEditorApp.Core/Utils/FryPdfEmbeddingHelper.cs deleted file mode 100644 index 4b8d509..0000000 --- a/src/PdfEditorApp.Core/Utils/FryPdfEmbeddingHelper.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Core.Utils; - -/// -/// Embeds and extracts lossless native FryPDF document models (including tables, charts, -/// math formulas, barcodes, QR codes, layers, and vector shapes) directly inside standard PDF files. -/// -public static class FryPdfEmbeddingHelper -{ - public const string StartMarker = "%FRYPDF_MODEL_V1_START%"; - public const string EndMarker = "%FRYPDF_MODEL_V1_END%"; - - private static readonly JsonSerializerOptions JsonOptions = new() - { - WriteIndented = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter() } - }; - - /// - /// Embeds the serialized, compressed PdfDocumentModel into the PDF bytes as a valid PDF comment stream. - /// The resulting PDF remains 100% compliant with standard PDF viewers (Acrobat, Chrome, Preview). - /// - public static byte[] EmbedModelInPdfBytes(byte[] pdfBytes, PdfDocumentModel model) - { - if (pdfBytes == null || pdfBytes.Length == 0 || model == null) - return pdfBytes ?? Array.Empty(); - - try - { - string json = JsonSerializer.Serialize(model, JsonOptions); - byte[] jsonBytes = Encoding.UTF8.GetBytes(json); - - byte[] compressedBytes; - using (var ms = new MemoryStream()) - { - using (var deflate = new DeflateStream(ms, CompressionLevel.Optimal, leaveOpen: true)) - { - deflate.Write(jsonBytes, 0, jsonBytes.Length); - } - compressedBytes = ms.ToArray(); - } - - string base64 = Convert.ToBase64String(compressedBytes); - - var sb = new StringBuilder(); - sb.AppendLine(); - sb.AppendLine(StartMarker); - - // Chunk base64 into 76-char lines formatted as valid PDF comments - for (int i = 0; i < base64.Length; i += 76) - { - int len = Math.Min(76, base64.Length - i); - sb.Append('%'); - sb.AppendLine(base64.Substring(i, len)); - } - - sb.AppendLine(EndMarker); - sb.AppendLine("%%EOF"); - - byte[] payloadBytes = Encoding.ASCII.GetBytes(sb.ToString()); - - byte[] result = new byte[pdfBytes.Length + payloadBytes.Length]; - Buffer.BlockCopy(pdfBytes, 0, result, 0, pdfBytes.Length); - Buffer.BlockCopy(payloadBytes, 0, result, pdfBytes.Length, payloadBytes.Length); - - return result; - } - catch - { - // If serialization fails, return unmodified PDF bytes - return pdfBytes; - } - } - - /// - /// Attempts to extract and deserialize an embedded FryPdfModel from the PDF bytes. - /// Returns true if a valid, lossless model was successfully retrieved. - /// - public static bool TryExtractEmbeddedModel(byte[] pdfBytes, out PdfDocumentModel? model) - { - model = null; - if (pdfBytes == null || pdfBytes.Length < 64) return false; - - try - { - // Look for StartMarker and EndMarker in the byte stream - string content = Encoding.ASCII.GetString(pdfBytes); - int startIdx = content.IndexOf(StartMarker, StringComparison.Ordinal); - if (startIdx < 0) return false; - - int dataStart = startIdx + StartMarker.Length; - int endIdx = content.IndexOf(EndMarker, dataStart, StringComparison.Ordinal); - if (endIdx < 0) return false; - - string rawPayload = content.Substring(dataStart, endIdx - dataStart); - - // Clean lines and strip leading '%' comment characters - var base64Sb = new StringBuilder(rawPayload.Length); - using (var reader = new StringReader(rawPayload)) - { - string? line; - while ((line = reader.ReadLine()) != null) - { - string trimmed = line.Trim(); - if (trimmed.StartsWith("%")) - { - trimmed = trimmed.Substring(1).Trim(); - } - if (!string.IsNullOrEmpty(trimmed)) - { - base64Sb.Append(trimmed); - } - } - } - - string base64 = base64Sb.ToString(); - if (string.IsNullOrWhiteSpace(base64)) return false; - - byte[] compressedBytes = Convert.FromBase64String(base64); - - byte[] jsonBytes; - using (var inputMs = new MemoryStream(compressedBytes)) - using (var deflate = new DeflateStream(inputMs, CompressionMode.Decompress)) - using (var outputMs = new MemoryStream()) - { - deflate.CopyTo(outputMs); - jsonBytes = outputMs.ToArray(); - } - - string json = Encoding.UTF8.GetString(jsonBytes); - var deserialized = JsonSerializer.Deserialize(json, JsonOptions); - - if (deserialized != null && deserialized.Pages.Count > 0) - { - model = deserialized; - return true; - } - } - catch - { - // Ignore extraction errors and fallback to deconstruction - } - - return false; - } -} diff --git a/src/PdfEditorApp.Core/Utils/PdfDocumentSanitizer.cs b/src/PdfEditorApp.Core/Utils/PdfDocumentSanitizer.cs deleted file mode 100644 index e4a6b82..0000000 --- a/src/PdfEditorApp.Core/Utils/PdfDocumentSanitizer.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.IO; -using System.Text; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.IO; -using UglyToad.PdfPig; -using UglyToad.PdfPig.Writer; - -namespace PdfEditorApp.Core.Utils; - -/// -/// Low-level PDF byte sanitizer, header repair, cross-reference rebuild, and producer metadata engine. -/// -public static class PdfDocumentSanitizer -{ - public static byte[] SanitizePdfBytes(byte[] rawBytes) - { - if (rawBytes == null || rawBytes.Length < 10) return rawBytes ?? Array.Empty(); - - // Look for the last %%EOF token in the file - string rawText = Encoding.ASCII.GetString(rawBytes, Math.Max(0, rawBytes.Length - 8192), Math.Min(rawBytes.Length, 8192)); - int eofIdx = rawText.LastIndexOf("%%EOF", StringComparison.OrdinalIgnoreCase); - - if (eofIdx >= 0) - { - int startOffset = Math.Max(0, rawBytes.Length - 8192); - int eofAbsoluteEnd = startOffset + eofIdx + 5; - - if (eofAbsoluteEnd < rawBytes.Length) - { - var trimmed = new byte[eofAbsoluteEnd + 2]; - Array.Copy(rawBytes, 0, trimmed, 0, eofAbsoluteEnd); - trimmed[eofAbsoluteEnd] = (byte)'\r'; - trimmed[eofAbsoluteEnd + 1] = (byte)'\n'; - return trimmed; - } - } - else - { - // Missing %%EOF, append standard trailer terminator - using var ms = new MemoryStream(); - ms.Write(rawBytes, 0, rawBytes.Length); - byte[] terminator = Encoding.ASCII.GetBytes("\r\n%%EOF\r\n"); - ms.Write(terminator, 0, terminator.Length); - return ms.ToArray(); - } - - return rawBytes; - } - - public static byte[] SalvageAndRepairPdfBytes(byte[] rawBytes) - { - if (rawBytes == null || rawBytes.Length < 10) return rawBytes ?? Array.Empty(); - - string fullText = Encoding.ASCII.GetString(rawBytes); - int rootIdx = fullText.IndexOf("/Root", StringComparison.OrdinalIgnoreCase); - if (rootIdx >= 0 && !fullText.Contains("trailer")) - { - int spaceIdx = fullText.IndexOf(" ", rootIdx + 5); - string rootRef = "1 0 R"; - if (spaceIdx > 0 && spaceIdx + 10 < fullText.Length) - { - string snippet = fullText.Substring(rootIdx + 5, Math.Min(20, fullText.Length - (rootIdx + 5))).Trim(); - var parts = snippet.Split(new[] { ' ', '\r', '\n', '/' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 3 && parts[2] == "R") - { - rootRef = $"{parts[0]} {parts[1]} R"; - } - } - - using var ms = new MemoryStream(); - ms.Write(rawBytes, 0, rawBytes.Length); - string synthTrailer = $"\r\ntrailer\r\n<<\r\n/Root {rootRef}\r\n>>\r\nstartxref\r\n0\r\n%%EOF\r\n"; - byte[] tBytes = Encoding.ASCII.GetBytes(synthTrailer); - ms.Write(tBytes, 0, tBytes.Length); - return ms.ToArray(); - } - return rawBytes; - } - - public static byte[] ReconstructCleanPdfWithPdfPig(string filePath) - { - var builder = new PdfDocumentBuilder(); - using (var pigDoc = UglyToad.PdfPig.PdfDocument.Open(filePath)) - { - for (int i = 1; i <= pigDoc.NumberOfPages; i++) - { - builder.AddPage(pigDoc, i); - } - } - return builder.Build(); - } - - public static byte[] PatchProducerInBytes(byte[] bytes, string producer = "codefrydev.in") - { - if (bytes == null || bytes.Length < 20) return bytes ?? Array.Empty(); - - try - { - string text = Encoding.ASCII.GetString(bytes); - if (text.Contains("/Encrypt")) - { - // Never binary patch encrypted PDF streams - return bytes; - } - - int searchIdx = 0; - while ((searchIdx = text.IndexOf("/Producer", searchIdx, StringComparison.Ordinal)) >= 0) - { - int openParen = text.IndexOf('(', searchIdx); - if (openParen > searchIdx && openParen < searchIdx + 25) - { - int closeParen = text.IndexOf(')', openParen); - if (closeParen > openParen) - { - int origSpanLen = closeParen - openParen + 1; - string replacement = $"({producer})"; - if (replacement.Length <= origSpanLen) - { - string filler = "%".PadRight(origSpanLen - replacement.Length, ' '); - string fullPatch = replacement + filler; - byte[] patchBytes = Encoding.ASCII.GetBytes(fullPatch); - Array.Copy(patchBytes, 0, bytes, openParen, patchBytes.Length); - } - } - } - searchIdx += 9; - } - } - catch { } - - return bytes; - } -} diff --git a/src/PdfEditorApp/App.axaml b/src/PdfEditorApp/App.axaml deleted file mode 100644 index 1573427..0000000 --- a/src/PdfEditorApp/App.axaml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 8 - 0 - 0 - 24 - 24 - 20 - 20 - 20 - 20 - 10 - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/PdfEditorApp/App.axaml.cs b/src/PdfEditorApp/App.axaml.cs deleted file mode 100644 index 943ff88..0000000 --- a/src/PdfEditorApp/App.axaml.cs +++ /dev/null @@ -1,293 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using System; -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Canvas; -using PdfEditorApp.Services.Ribbon; -using PdfEditorApp.Services.AI; -using PdfEditorApp.Services.Data; -using PdfEditorApp.Services.Export; -using PdfEditorApp.Services.Import; -using PdfEditorApp.Services.Inspector; -using PdfEditorApp.Services.Ocr; -using PdfEditorApp.Services.StatusBar; -using PdfEditorApp.ViewModels; -using PdfEditorApp.Views; - -namespace PdfEditorApp; - -public partial class App : Application -{ - public static IServiceProvider Services { get; private set; } = null!; - - public override void Initialize() - { - Name = "FryPDF"; - LiveChartsCore.LiveCharts.Configure(config => LiveChartsCore.SkiaSharpView.LiveChartsSkiaSharp.UseDefaults(config)); - AvaloniaXamlLoader.Load(this); - } - - private void AboutMenuItem_OnClick(object? sender, EventArgs e) - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.DataContext is MainViewModel mainVm) - { - mainVm.OpenAboutDialogCommand.Execute(null); - } - } - - public override void OnFrameworkInitializationCompleted() - { - var serviceCollection = new ServiceCollection(); - ConfigureServices(serviceCollection); - Services = serviceCollection.BuildServiceProvider(); - - var themeService = Services.GetRequiredService(); - themeService.Initialize(); - - InitializePluginSystem(); - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - var mainVm = Services.GetRequiredService(); - desktop.MainWindow = new MainWindow - { - DataContext = mainVm, - }; - } - - base.OnFrameworkInitializationCompleted(); - } - - private static void InitializePluginSystem() - { - try - { - PageViewModel.DefaultElementService = Services.GetRequiredService(); - var host = Services.GetRequiredService(); - - var profilePath = System.IO.Path.Combine(AppContext.BaseDirectory, "profiles", "desktop.profile.json"); - if (!System.IO.File.Exists(profilePath)) - { - profilePath = "profiles/desktop.profile.json"; - } - - PdfEditorApp.Core.Plugins.Profiles.PluginProfile profile; - if (System.IO.File.Exists(profilePath)) - { - profile = PdfEditorApp.Core.Plugins.Profiles.ProfileLoader.LoadFromFile(profilePath); - } - else - { - profile = new PdfEditorApp.Core.Plugins.Profiles.PluginProfile { ProfileName = "desktop" }; - } - - var availableBundles = new PdfEditorApp.Core.Plugins.Profiles.IFryPluginBundle[] - { - new PdfEditorApp.Plugins.Bundles.ToolsOrganizeBundle(), - new PdfEditorApp.Plugins.Bundles.ToolsSecurityBundle(), - new PdfEditorApp.Plugins.Bundles.ToolsConversionBundle(), - new PdfEditorApp.Plugins.Bundles.ToolsIntelligenceBundle(), - new PdfEditorApp.Plugins.Bundles.DataStudioBundle(), - new PdfEditorApp.Plugins.Bundles.CanvasElementsBundle(), - new PdfEditorApp.Plugins.Bundles.DocumentIoBundle(), - new PdfEditorApp.Plugins.Bundles.AiProvidersBundle(), - new PdfEditorApp.Plugins.Bundles.OcrEnginesBundle(), - new PdfEditorApp.Plugins.Bundles.StandardTemplatesBundle(), - new PdfEditorApp.Plugins.Bundles.StatusBarBundle(), - new PdfEditorApp.Plugins.Bundles.InspectorBundle(), - new PdfEditorApp.Plugins.Bundles.CommandPaletteBundle(), - new PdfEditorApp.Plugins.Bundles.WorkspacePagesBundle(), - new PdfEditorApp.Plugins.Bundles.DialogsBundle(), - new PdfEditorApp.Plugins.Bundles.EditorSidebarsBundle(), - new PdfEditorApp.Plugins.Bundles.ShellOverlaysBundle() - }; - - PdfEditorApp.Core.Plugins.Profiles.ProfileLoader.ApplyProfile(profile, host, availableBundles); - - string[] directoriesToScan = - [ - System.IO.Path.Combine(AppContext.BaseDirectory, "plugins"), - System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FryPdf", "plugins") - ]; - - foreach (var dir in directoriesToScan) - { - if (System.IO.Directory.Exists(dir)) - { - var externalPackages = PdfEditorApp.Plugins.Loader.PluginAssemblyLoader.DiscoverAndLoadDirectory(dir); - foreach (var pkg in externalPackages) - { - foreach (var plugin in pkg.Plugins) - { - host.RegisterPlugin(plugin); - } - } - } - } - - host.StartAsync().GetAwaiter().GetResult(); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[App] Plugin system initialization warning: {ex.Message}"); - } - } - - public static void ConfigureServices(IServiceCollection services) - { - // Core Services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => new PdfExportService(sp.GetService())); - services.AddSingleton(sp => new PdfImportService(sp.GetRequiredService(), sp.GetService())); - services.AddSingleton(sp => new ProjectPersistenceService(sp.GetRequiredService(), sp.GetService())); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - - // Plugin Infrastructure (Everything is a Plugin) - services.AddSingleton(sp => new CanvasElementRegistry(sp, seedBuiltIns: false)); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => new InspectorRegistry(seedDefaults: false)); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => (ITemplateRegistry)sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => new StatusBarRegistry(seedDefaults: false)); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(); - - - services.AddSingleton(sp => new FryPluginContext(sp)); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => new PluginHost(sp.GetRequiredService())); - services.AddSingleton(); - - // PDF Tools Platform Services - services.AddSingleton(); - services.AddSingleton(sp => new PdfToolRegistry(sp.GetRequiredService(), seedDefaults: false)); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => - new PdfDocumentOperationsService( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetService())); - services.AddSingleton(sp => - new PdfToolViewModelFactory( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp)); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // ViewModels - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - // Individual Tool ViewModels (resolvable directly or via IPdfToolViewModelFactory) - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - } -} \ No newline at end of file diff --git a/src/PdfEditorApp/Assets/Fonts/BebasNeue.ttf b/src/PdfEditorApp/Assets/Fonts/BebasNeue.ttf deleted file mode 100644 index c328c6e..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/BebasNeue.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Caveat.ttf b/src/PdfEditorApp/Assets/Fonts/Caveat.ttf deleted file mode 100644 index f84acf2..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Caveat.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Cinzel.ttf b/src/PdfEditorApp/Assets/Fonts/Cinzel.ttf deleted file mode 100644 index d218a0b..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Cinzel.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/ComicNeue.ttf b/src/PdfEditorApp/Assets/Fonts/ComicNeue.ttf deleted file mode 100644 index 88e9417..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/ComicNeue.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/DancingScript.ttf b/src/PdfEditorApp/Assets/Fonts/DancingScript.ttf deleted file mode 100644 index 9f521e7..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/DancingScript.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/FiraCode.ttf b/src/PdfEditorApp/Assets/Fonts/FiraCode.ttf deleted file mode 100644 index 83f2b38..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/FiraCode.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/GreatVibes.ttf b/src/PdfEditorApp/Assets/Fonts/GreatVibes.ttf deleted file mode 100644 index a1327ff..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/GreatVibes.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Inter.ttf b/src/PdfEditorApp/Assets/Fonts/Inter.ttf deleted file mode 100644 index 047c92f..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Inter.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Lobster.ttf b/src/PdfEditorApp/Assets/Fonts/Lobster.ttf deleted file mode 100644 index f30b6f6..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Lobster.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Lora.ttf b/src/PdfEditorApp/Assets/Fonts/Lora.ttf deleted file mode 100644 index ee1914c..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Lora.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Merriweather.ttf b/src/PdfEditorApp/Assets/Fonts/Merriweather.ttf deleted file mode 100644 index cb775b8..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Merriweather.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Montserrat.ttf b/src/PdfEditorApp/Assets/Fonts/Montserrat.ttf deleted file mode 100644 index c97aca1..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Montserrat.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/NotoSans.ttf b/src/PdfEditorApp/Assets/Fonts/NotoSans.ttf deleted file mode 100644 index 366513b..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/NotoSans.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/OpenSans.ttf b/src/PdfEditorApp/Assets/Fonts/OpenSans.ttf deleted file mode 100644 index 9db8569..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/OpenSans.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Orbitron.ttf b/src/PdfEditorApp/Assets/Fonts/Orbitron.ttf deleted file mode 100644 index 1fc54ce..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Orbitron.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Oswald.ttf b/src/PdfEditorApp/Assets/Fonts/Oswald.ttf deleted file mode 100644 index d1a3b9c..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Oswald.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Pacifico.ttf b/src/PdfEditorApp/Assets/Fonts/Pacifico.ttf deleted file mode 100644 index 27765d0..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Pacifico.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/PlayfairDisplay.ttf b/src/PdfEditorApp/Assets/Fonts/PlayfairDisplay.ttf deleted file mode 100644 index 4ee80fd..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/PlayfairDisplay.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/Roboto.ttf b/src/PdfEditorApp/Assets/Fonts/Roboto.ttf deleted file mode 100644 index 5522a36..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/Roboto.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/RobotoMono.ttf b/src/PdfEditorApp/Assets/Fonts/RobotoMono.ttf deleted file mode 100644 index f21d1d7..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/RobotoMono.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/Fonts/SourceSans3.ttf b/src/PdfEditorApp/Assets/Fonts/SourceSans3.ttf deleted file mode 100644 index d259aa4..0000000 Binary files a/src/PdfEditorApp/Assets/Fonts/SourceSans3.ttf and /dev/null differ diff --git a/src/PdfEditorApp/Assets/IconCodefrydev.svg b/src/PdfEditorApp/Assets/IconCodefrydev.svg deleted file mode 100644 index 07c4e45..0000000 --- a/src/PdfEditorApp/Assets/IconCodefrydev.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/PdfEditorApp/Assets/app-logo.ico b/src/PdfEditorApp/Assets/app-logo.ico deleted file mode 100644 index 9c72c35..0000000 Binary files a/src/PdfEditorApp/Assets/app-logo.ico and /dev/null differ diff --git a/src/PdfEditorApp/Assets/app-logo.png b/src/PdfEditorApp/Assets/app-logo.png deleted file mode 100644 index 7e12c6e..0000000 Binary files a/src/PdfEditorApp/Assets/app-logo.png and /dev/null differ diff --git a/src/PdfEditorApp/Assets/app-logo.svg b/src/PdfEditorApp/Assets/app-logo.svg deleted file mode 100644 index ad1f2f5..0000000 --- a/src/PdfEditorApp/Assets/app-logo.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FryPDF - diff --git a/src/PdfEditorApp/Converters/CommonConverters.cs b/src/PdfEditorApp/Converters/CommonConverters.cs deleted file mode 100644 index 947e255..0000000 --- a/src/PdfEditorApp/Converters/CommonConverters.cs +++ /dev/null @@ -1,395 +0,0 @@ -using System; -using System.Globalization; -using Avalonia; -using Avalonia.Data.Converters; -using Avalonia.Media; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Converters; - -public class EqualityToBooleanConverter : IValueConverter -{ - public static readonly EqualityToBooleanConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null && parameter == null) return true; - if (value == null || parameter == null) return false; - return string.Equals(value.ToString(), parameter.ToString(), StringComparison.OrdinalIgnoreCase); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is true && parameter != null) - { - if (targetType == typeof(int) || targetType == typeof(int?)) - { - if (int.TryParse(parameter.ToString(), out int intVal)) - return intVal; - } - if (targetType.IsEnum && parameter is string enumStr) - { - if (Enum.TryParse(targetType, enumStr, true, out var enumVal)) - return enumVal; - } - if (targetType == typeof(string)) - { - return parameter.ToString(); - } - return parameter; - } - return Avalonia.Data.BindingOperations.DoNothing; - } -} - -public class BooleanToStretchConverter : IValueConverter -{ - public static readonly BooleanToStretchConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isTrue = value is true; - return isTrue ? Stretch.Uniform : Stretch.Fill; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} - -public class ZoomPercentageConverter : IValueConverter -{ - public static readonly ZoomPercentageConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is double zoom) - { - return $"{(int)Math.Round(zoom * 100)}%"; - } - return "100%"; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} - -public class NullToBooleanConverter : IValueConverter -{ - public static readonly NullToBooleanConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isNull = value == null; - if (parameter is string paramStr && paramStr.Equals("invert", StringComparison.OrdinalIgnoreCase)) - { - return isNull; - } - return !isNull; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} - -public class BooleanToBrushConverter : IValueConverter -{ - public static readonly BooleanToBrushConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isTrue = value is true; - if (parameter is string paramStr) - { - var parts = paramStr.Split('|'); - string trueHex = parts.Length > 0 ? parts[0] : "#0F6CBD"; - string falseHex = parts.Length > 1 ? parts[1] : "#E2E8F0"; - - string selectedHex = isTrue ? trueHex : falseHex; - if (Color.TryParse(selectedHex, out var col)) - { - return new SolidColorBrush(col); - } - } - return isTrue ? new SolidColorBrush(Color.Parse("#0F6CBD")) : new SolidColorBrush(Color.Parse("#E2E8F0")); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} - -public class BooleanToThicknessConverter : IValueConverter -{ - public static readonly BooleanToThicknessConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isTrue = value is true; - if (parameter is string paramStr) - { - var parts = paramStr.Split('|'); - double trueVal = parts.Length > 0 && double.TryParse(parts[0], out var t) ? t : 2.0; - double falseVal = parts.Length > 1 && double.TryParse(parts[1], out var f) ? f : 1.0; - return new Thickness(isTrue ? trueVal : falseVal); - } - return new Thickness(isTrue ? 2.0 : 1.0); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} - -public class DoubleToThicknessConverter : IValueConverter -{ - public static readonly DoubleToThicknessConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is double d) - { - return new Thickness(d); - } - if (value is int i) - { - return new Thickness(i); - } - if (value is float f) - { - return new Thickness(f); - } - return new Thickness(0); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is Thickness t) - { - return t.Left; - } - return 0.0; - } -} - -public class DoubleToCornerRadiusConverter : IValueConverter -{ - public static readonly DoubleToCornerRadiusConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is double d) - { - return new CornerRadius(d); - } - if (value is int i) - { - return new CornerRadius(i); - } - if (value is float f) - { - return new CornerRadius(f); - } - return new CornerRadius(0); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is CornerRadius c) - { - return c.TopLeft; - } - return 0.0; - } -} - -public class TextAlignmentConverter : IValueConverter -{ - public static readonly TextAlignmentConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is TextAlignmentMode mode) - { - return mode switch - { - TextAlignmentMode.Left => Avalonia.Media.TextAlignment.Left, - TextAlignmentMode.Center => Avalonia.Media.TextAlignment.Center, - TextAlignmentMode.Right => Avalonia.Media.TextAlignment.Right, - TextAlignmentMode.Justify => Avalonia.Media.TextAlignment.Justify, - _ => Avalonia.Media.TextAlignment.Left - }; - } - return Avalonia.Media.TextAlignment.Left; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} - -public class BooleanToFontWeightConverter : IValueConverter -{ - public static readonly BooleanToFontWeightConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isBold = value is true; - return isBold ? FontWeight.Bold : FontWeight.Normal; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is FontWeight weight && weight >= FontWeight.Bold; - } -} - -public class BooleanToFontStyleConverter : IValueConverter -{ - public static readonly BooleanToFontStyleConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isItalic = value is true; - return isItalic ? FontStyle.Italic : FontStyle.Normal; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is FontStyle style && style == FontStyle.Italic; - } -} - -public class BooleanToTextDecorationsConverter : IValueConverter -{ - public static readonly BooleanToTextDecorationsConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isUnderline = value is true; - return isUnderline ? TextDecorations.Underline : null; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is TextDecorationCollection decs && decs.Count > 0; - } -} - -public class StringToFontFamilyConverter : IValueConverter -{ - public static readonly StringToFontFamilyConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string fontName && !string.IsNullOrWhiteSpace(fontName)) - { - return PdfEditorApp.Services.FontHelper.CreateFontFamily(fontName); - } - if (value is FontFamily ff) - { - return ff; - } - return FontFamily.Default; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is FontFamily ff) - { - return ff.Name; - } - return value?.ToString(); - } -} - -public class BooleanToStringConverter : IValueConverter -{ - public static readonly BooleanToStringConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isTrue = value is true; - if (parameter is string paramStr) - { - var parts = paramStr.Split('|'); - string trueVal = parts.Length > 0 ? parts[0] : "True"; - string falseVal = parts.Length > 1 ? parts[1] : "False"; - return isTrue ? trueVal : falseVal; - } - return isTrue ? "True" : "False"; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} - -public class MultiplyConverter : IMultiValueConverter -{ - public static readonly MultiplyConverter Instance = new(); - - public object? Convert(System.Collections.Generic.IList values, Type targetType, object? parameter, CultureInfo culture) - { - if (values != null && values.Count >= 2 && values[0] != null && values[1] != null) - { - try - { - double v1 = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture); - double v2 = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture); - return v1 * v2; - } - catch - { - return 0.0; - } - } - return 0.0; - } -} - -public class RotationToTransformConverter : IValueConverter -{ - public static readonly RotationToTransformConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is double angle && Math.Abs(angle) > 0.001) - { - return new RotateTransform(angle); - } - if (value is int angleInt && angleInt != 0) - { - return new RotateTransform(angleInt); - } - if (value is float angleFloat && Math.Abs(angleFloat) > 0.001f) - { - return new RotateTransform(angleFloat); - } - if (value is string str && double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed) && Math.Abs(parsed) > 0.001) - { - return new RotateTransform(parsed); - } - return null; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is RotateTransform rt) - { - return rt.Angle; - } - return 0.0; - } -} - diff --git a/src/PdfEditorApp/Converters/ExtendedConverters.cs b/src/PdfEditorApp/Converters/ExtendedConverters.cs deleted file mode 100644 index f603cc5..0000000 --- a/src/PdfEditorApp/Converters/ExtendedConverters.cs +++ /dev/null @@ -1,1158 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using Avalonia; -using Avalonia.Data.Converters; -using Avalonia.Media; - -namespace PdfEditorApp.Converters; - -#region 1. File Size & Formatting Converters - -/// -/// Formats byte counts (long/int/double) into human-readable strings like "1.45 MB", "340 KB", "12 B", "2.1 GB". -/// Parameter can specify decimal places (e.g. "0", "1", "2"). Defaults to 1 decimal place. -/// -public class ByteSizeToStringConverter : IValueConverter -{ - public static readonly ByteSizeToStringConverter Instance = new(); - - private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB" }; - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return "0 B"; - - double bytes; - try - { - bytes = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - } - catch - { - return "0 B"; - } - - if (bytes < 0) return "-" + Convert(-bytes, targetType, parameter, culture); - if (bytes == 0) return "0 B"; - - int decimalPlaces = 1; - if (parameter is string paramStr && int.TryParse(paramStr, out int dp)) - { - decimalPlaces = Math.Clamp(dp, 0, 4); - } - - int mag = (int)Math.Log(bytes, 1024); - mag = Math.Clamp(mag, 0, SizeSuffixes.Length - 1); - - double adjustedSize = bytes / Math.Pow(1024, mag); - - // For Bytes ("B"), don't show fractional parts - if (mag == 0) - { - return $"{(long)bytes} B"; - } - - string format = $"F{decimalPlaces}"; - return $"{adjustedSize.ToString(format, culture ?? CultureInfo.InvariantCulture)} {SizeSuffixes[mag]}"; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string str && !string.IsNullOrWhiteSpace(str)) - { - str = str.Trim(); - for (int i = SizeSuffixes.Length - 1; i >= 0; i--) - { - string suffix = SizeSuffixes[i]; - if (str.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) - { - string numPart = str[..^suffix.Length].Trim(); - if (double.TryParse(numPart, NumberStyles.Float, culture ?? CultureInfo.InvariantCulture, out double num)) - { - long result = (long)(num * Math.Pow(1024, i)); - if (targetType == typeof(int)) return (int)result; - if (targetType == typeof(double)) return (double)result; - return result; - } - } - } - } - return 0L; - } -} - -/// -/// Formats DateTime / DateTimeOffset / DateOnly into customized date/time format strings. -/// Supports parameter strings like "yyyy-MM-dd", "g", "MMM dd, yyyy", "t", "Relative". -/// -public class DateTimeFormatConverter : IValueConverter -{ - public static readonly DateTimeFormatConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return string.Empty; - - DateTime dt; - if (value is DateTime dateTime) - { - dt = dateTime; - } - else if (value is DateTimeOffset dto) - { - dt = dto.LocalDateTime; - } - else if (value is DateOnly dateOnly) - { - dt = dateOnly.ToDateTime(TimeOnly.MinValue); - } - else - { - return value.ToString(); - } - - string format = parameter as string ?? "g"; - - if (format.Equals("Relative", StringComparison.OrdinalIgnoreCase)) - { - var span = DateTime.Now - dt; - if (span.TotalSeconds < 60) return "Just now"; - if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes}m ago"; - if (span.TotalHours < 24) return $"{(int)span.TotalHours}h ago"; - if (span.TotalDays < 7) return $"{(int)span.TotalDays}d ago"; - return dt.ToString("MMM dd, yyyy", culture ?? CultureInfo.InvariantCulture); - } - - return dt.ToString(format, culture ?? CultureInfo.InvariantCulture); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string str && DateTime.TryParse(str, culture ?? CultureInfo.InvariantCulture, out var dt)) - { - if (targetType == typeof(DateTimeOffset)) return new DateTimeOffset(dt); - if (targetType == typeof(DateOnly)) return DateOnly.FromDateTime(dt); - return dt; - } - return null; - } -} - -/// -/// Formats TimeSpan or seconds into human-readable duration strings like "01:23", "1h 30m", or "45s". -/// -public class TimeSpanFormatConverter : IValueConverter -{ - public static readonly TimeSpanFormatConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return "0:00"; - - TimeSpan ts; - if (value is TimeSpan t) - { - ts = t; - } - else - { - try - { - double seconds = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - ts = TimeSpan.FromSeconds(seconds); - } - catch - { - return "0:00"; - } - } - - string format = parameter as string ?? "timer"; - - if (format.Equals("words", StringComparison.OrdinalIgnoreCase)) - { - if (ts.TotalHours >= 1) return $"{(int)ts.TotalHours}h {ts.Minutes}m"; - if (ts.TotalMinutes >= 1) return $"{(int)ts.TotalMinutes}m {ts.Seconds}s"; - return $"{ts.Seconds}s"; - } - - if (ts.TotalHours >= 1) - { - return $"{(int)ts.TotalHours}:{ts.Minutes:D2}:{ts.Seconds:D2}"; - } - return $"{ts.Minutes}:{ts.Seconds:D2}"; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -#endregion - -#region 2. Math & Arithmetic Converters - -/// -/// Adds a parameter numeric offset to the value: value + parameter. -/// -public class MathAddConverter : IValueConverter -{ - public static readonly MathAddConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double p = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 0.0; - double res = v + p; - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - catch - { - return value; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double p = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 0.0; - double res = v - p; - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - catch - { - return value; - } - } -} - -/// -/// Subtracts a parameter numeric offset from the value: value - parameter. -/// -public class MathSubtractConverter : IValueConverter -{ - public static readonly MathSubtractConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double p = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 0.0; - double res = v - p; - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - catch - { - return value; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double p = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 0.0; - double res = v + p; - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - catch - { - return value; - } - } -} - -/// -/// Multiplies value by a parameter numeric factor: value * parameter. -/// -public class MathMultiplyConverter : IValueConverter -{ - public static readonly MathMultiplyConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double p = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 1.0; - double res = v * p; - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - catch - { - return value; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double p = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 1.0; - if (Math.Abs(p) < 0.000001) return 0.0; - double res = v / p; - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - catch - { - return value; - } - } -} - -/// -/// Divides value by a parameter numeric divisor: value / parameter. -/// -public class MathDivideConverter : IValueConverter -{ - public static readonly MathDivideConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double p = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 1.0; - if (Math.Abs(p) < 0.000001) return 0.0; - double res = v / p; - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - catch - { - return value; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double p = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 1.0; - double res = v * p; - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - catch - { - return value; - } - } -} - -/// -/// Converts double (0..1 or 0..100) to formatted percentage string ("75%") and roundtrips. -/// Parameter "0-1" interprets input as 0..1 scale (default). Parameter "0-100" interprets as 0..100 scale. -/// -public class PercentageConverter : IValueConverter -{ - public static readonly PercentageConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return "0%"; - try - { - double d = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - bool isHundredScale = parameter is string p && p.Contains("100"); - double pct = isHundredScale ? d : (d * 100.0); - return $"{(int)Math.Round(pct)}%"; - } - catch - { - return "0%"; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string str) - { - string clean = str.Replace("%", "").Trim(); - if (double.TryParse(clean, NumberStyles.Float, culture ?? CultureInfo.InvariantCulture, out double parsed)) - { - bool isHundredScale = parameter is string p && p.Contains("100"); - double res = isHundredScale ? parsed : (parsed / 100.0); - if (targetType == typeof(int)) return (int)Math.Round(res); - return res; - } - } - return 0.0; - } -} - -#endregion - -#region 3. Units & Dimension Converters (PDF Studio) - -/// -/// Converts PDF points (72 pt/inch) to millimeters (25.4 mm/inch): 1 pt = 0.352778 mm. -/// Two-way binding supported: mm converted back to points accurately. -/// -public class PointsToMillimetersConverter : IValueConverter -{ - public static readonly PointsToMillimetersConverter Instance = new(); - private const double PtToMmFactor = 25.4 / 72.0; // ~0.3527777777777778 - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double pt = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double mm = pt * PtToMmFactor; - - if (parameter is string paramStr && paramStr.Equals("format", StringComparison.OrdinalIgnoreCase)) - { - return $"{mm:F1} mm"; - } - return Math.Round(mm, 2); - } - catch - { - return 0.0; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - string str = value.ToString()?.Replace("mm", "", StringComparison.OrdinalIgnoreCase).Trim() ?? "0"; - double mm = double.Parse(str, CultureInfo.InvariantCulture); - double pt = mm / PtToMmFactor; - return Math.Round(pt, 2); - } - catch - { - return 0.0; - } - } -} - -/// -/// Converts PDF points (72 pt/inch) to inches: 1 pt = 1/72 inch. -/// Two-way binding supported. -/// -public class PointsToInchesConverter : IValueConverter -{ - public static readonly PointsToInchesConverter Instance = new(); - private const double PtToInchFactor = 1.0 / 72.0; - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double pt = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double inches = pt * PtToInchFactor; - - if (parameter is string paramStr && paramStr.Equals("format", StringComparison.OrdinalIgnoreCase)) - { - return $"{inches:F2} in"; - } - return Math.Round(inches, 3); - } - catch - { - return 0.0; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - string str = value.ToString()?.Replace("in", "", StringComparison.OrdinalIgnoreCase).Replace("\"", "").Trim() ?? "0"; - double inches = double.Parse(str, CultureInfo.InvariantCulture); - double pt = inches * 72.0; - return Math.Round(pt, 2); - } - catch - { - return 0.0; - } - } -} - -/// -/// Converts PDF points (72 pt/inch) to screen/display pixels at a specified DPI (default 96 DPI). -/// -public class PointsToPixelsConverter : IValueConverter -{ - public static readonly PointsToPixelsConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double pt = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double dpi = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 96.0; - return pt * (dpi / 72.0); - } - catch - { - return 0.0; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return 0.0; - try - { - double px = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - double dpi = parameter != null ? System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture) : 96.0; - return px * (72.0 / dpi); - } - catch - { - return 0.0; - } - } -} - -#endregion - -#region 4. Logic, Boolean & Collection Converters - -/// -/// Inverts boolean values: true -> false, false -> true. Two-way supported. -/// -public class InverseBooleanConverter : IValueConverter -{ - public static readonly InverseBooleanConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is not true; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is not true; - } -} - -/// -/// Returns true if a count or numeric value is greater than 0 (or greater than a parameter threshold). -/// Parameter "invert" or "-1" inverts the condition. -/// -public class CountToBooleanConverter : IValueConverter -{ - public static readonly CountToBooleanConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return false; - - double count = 0; - if (value is ICollection coll) - { - count = coll.Count; - } - else if (value is IEnumerable enumr && value is not string) - { - count = enumr.Cast().Count(); - } - else - { - try - { - count = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - } - catch - { - count = 0; - } - } - - double threshold = 0.0; - bool invert = false; - - if (parameter is string paramStr) - { - if (paramStr.Equals("invert", StringComparison.OrdinalIgnoreCase)) - { - invert = true; - } - else if (double.TryParse(paramStr, NumberStyles.Float, CultureInfo.InvariantCulture, out double t)) - { - threshold = t; - } - } - - bool result = count > threshold; - return invert ? !result : result; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -/// -/// Checks if a string or collection is null or empty. Returns true if empty, or false if populated. -/// Parameter "invert" reverses logic (true when has content). -/// -public class NullOrEmptyToBooleanConverter : IValueConverter -{ - public static readonly NullOrEmptyToBooleanConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - bool isEmpty = false; - - if (value == null) - { - isEmpty = true; - } - else if (value is string s) - { - isEmpty = string.IsNullOrWhiteSpace(s); - } - else if (value is ICollection c) - { - isEmpty = c.Count == 0; - } - else if (value is IEnumerable e) - { - isEmpty = !e.Cast().Any(); - } - - bool invert = parameter is string p && p.Equals("invert", StringComparison.OrdinalIgnoreCase); - return invert ? !isEmpty : isEmpty; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -/// -/// Evaluates comparison expressions against a value: ">0", ">=1", "<10", "<=5", "==10", "!=0". -/// -public class ComparisonToBooleanConverter : IValueConverter -{ - public static readonly ComparisonToBooleanConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null || parameter == null) return false; - - try - { - double v = System.Convert.ToDouble(value, CultureInfo.InvariantCulture); - string expr = parameter.ToString()?.Trim() ?? string.Empty; - - if (expr.StartsWith(">=")) - { - double target = double.Parse(expr[2..].Trim(), CultureInfo.InvariantCulture); - return v >= target; - } - if (expr.StartsWith("<=")) - { - double target = double.Parse(expr[2..].Trim(), CultureInfo.InvariantCulture); - return v <= target; - } - if (expr.StartsWith(">")) - { - double target = double.Parse(expr[1..].Trim(), CultureInfo.InvariantCulture); - return v > target; - } - if (expr.StartsWith("<")) - { - double target = double.Parse(expr[1..].Trim(), CultureInfo.InvariantCulture); - return v < target; - } - if (expr.StartsWith("==")) - { - double target = double.Parse(expr[2..].Trim(), CultureInfo.InvariantCulture); - return Math.Abs(v - target) < 0.0001; - } - if (expr.StartsWith("!=")) - { - double target = double.Parse(expr[2..].Trim(), CultureInfo.InvariantCulture); - return Math.Abs(v - target) >= 0.0001; - } - - if (double.TryParse(expr, NumberStyles.Float, CultureInfo.InvariantCulture, out double eqTarget)) - { - return Math.Abs(v - eqTarget) < 0.0001; - } - } - catch - { - // fallback string equality - return string.Equals(value?.ToString(), parameter?.ToString(), StringComparison.OrdinalIgnoreCase); - } - - return false; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -/// -/// Formats an integer count with pluralization: e.g. Parameter="page|pages" -> "0 pages", "1 page", "5 pages". -/// -public class CountToStringConverter : IValueConverter -{ - public static readonly CountToStringConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - int count = 0; - if (value is int i) count = i; - else if (value is long l) count = (int)l; - else if (value is ICollection coll) count = coll.Count; - else if (value != null && int.TryParse(value.ToString(), out int parsed)) count = parsed; - - if (parameter is string paramStr) - { - var parts = paramStr.Split('|'); - string singular = parts.Length > 0 ? parts[0] : "item"; - string plural = parts.Length > 1 ? parts[1] : (singular + "s"); - - return count == 1 ? $"{count} {singular}" : $"{count} {plural}"; - } - - return count.ToString(); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -#endregion - -#region 5. Graphics, Color & Transform Converters - -/// -/// Converts Avalonia Color to SolidColorBrush and back. -/// -public class ColorToBrushConverter : IValueConverter -{ - public static readonly ColorToBrushConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is Color color) - { - return new SolidColorBrush(color); - } - return Brushes.Transparent; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is ISolidColorBrush brush) - { - return brush.Color; - } - return Colors.Transparent; - } -} - -/// -/// Dynamically scales an element via ScaleTransform(scaleX, scaleY). -/// Parameter can specify uniform scale or "scaleX,scaleY". -/// -public class ScaleToTransformConverter : IValueConverter -{ - public static readonly ScaleToTransformConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return null; - - try - { - double scaleX = 1.0; - double scaleY = 1.0; - - if (value is double d) - { - scaleX = scaleY = d; - } - else if (value is string s) - { - var parts = s.Split(','); - if (parts.Length == 1 && double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double uniform)) - { - scaleX = scaleY = uniform; - } - else if (parts.Length >= 2 && - double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double sx) && - double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double sy)) - { - scaleX = sx; - scaleY = sy; - } - } - - if (Math.Abs(scaleX - 1.0) < 0.001 && Math.Abs(scaleY - 1.0) < 0.001) - { - return null; // Identity -> avoid transform overhead - } - - return new ScaleTransform(scaleX, scaleY); - } - catch - { - return null; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is ScaleTransform st) - { - return st.ScaleX; - } - return 1.0; - } -} - -/// -/// Converts numeric X,Y offset values or comma-separated "X,Y" string to TranslateTransform. -/// -public class TranslateToTransformConverter : IValueConverter -{ - public static readonly TranslateToTransformConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return null; - - try - { - double x = 0.0; - double y = 0.0; - - if (value is string s) - { - var parts = s.Split(','); - if (parts.Length >= 2 && - double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double px) && - double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double py)) - { - x = px; - y = py; - } - } - else if (value is double d) - { - x = d; - } - - if (Math.Abs(x) < 0.001 && Math.Abs(y) < 0.001) return null; - return new TranslateTransform(x, y); - } - catch - { - return null; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is TranslateTransform tt) return tt.X; - return 0.0; - } -} - -/// -/// Parses SVG path data string (e.g. "M 0,0 L 100,100 Z") into an Avalonia Geometry. -/// -public class StringToGeometryConverter : IValueConverter -{ - public static readonly StringToGeometryConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string pathData && !string.IsNullOrWhiteSpace(pathData)) - { - try - { - return Geometry.Parse(pathData); - } - catch - { - try - { - return StreamGeometry.Parse(pathData); - } - catch - { - return null; - } - } - } - if (value is Geometry g) return g; - return null; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is Geometry g) return g.ToString(); - return null; - } -} - -#endregion - -#region 6. String & Text Converters - -/// -/// Modifies string casing: "upper", "lower", "title", "pascal", "camel". -/// -public class StringCaseConverter : IValueConverter -{ - public static readonly StringCaseConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return string.Empty; - string str = value.ToString() ?? string.Empty; - string mode = parameter as string ?? "upper"; - - return mode.ToLowerInvariant() switch - { - "upper" => str.ToUpper(culture ?? CultureInfo.CurrentCulture), - "lower" => str.ToLower(culture ?? CultureInfo.CurrentCulture), - "title" => (culture ?? CultureInfo.CurrentCulture).TextInfo.ToTitleCase(str.ToLower(culture ?? CultureInfo.CurrentCulture)), - "capitalize" => str.Length > 0 ? char.ToUpper(str[0], culture ?? CultureInfo.CurrentCulture) + str[1..] : str, - _ => str - }; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -/// -/// Truncates long strings to a specified maximum length and appends an ellipsis. -/// Parameter specifies max characters (default 30). -/// -public class StringTruncateConverter : IValueConverter -{ - public static readonly StringTruncateConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return string.Empty; - string str = value.ToString() ?? string.Empty; - - int maxLength = 30; - if (parameter is string paramStr && int.TryParse(paramStr, out int max)) - { - maxLength = Math.Max(3, max); - } - - if (str.Length <= maxLength) return str; - return str[..(maxLength - 3)] + "..."; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -/// -/// Formats a value with prefix and/or suffix: Parameter="{0} pt" or "Page {0}". -/// -public class StringPrefixSuffixConverter : IValueConverter -{ - public static readonly StringPrefixSuffixConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return string.Empty; - string format = parameter as string ?? "{0}"; - if (!format.Contains("{0}")) format = $"{format}{{0}}"; - return string.Format(culture ?? CultureInfo.InvariantCulture, format, value); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); -} - -/// -/// Converts Enum values to human-friendly description strings with space separated words. -/// e.g. TextAlignmentMode.Center -> "Center", PdfPageSize.A4Portrait -> "A4 Portrait". -/// -public class EnumToDescriptionConverter : IValueConverter -{ - public static readonly EnumToDescriptionConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value == null) return string.Empty; - - Type type = value.GetType(); - if (!type.IsEnum) return value.ToString(); - - string name = value.ToString()!; - var field = type.GetField(name); - if (field != null) - { - var attr = field.GetCustomAttribute(); - if (attr != null) return attr.Description; - } - - // Split CamelCase words into spaced words: "A4Portrait" -> "A4 Portrait" - return Regex.Replace(name, "(\\B[A-Z])", " $1"); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string str && targetType.IsEnum) - { - string clean = str.Replace(" ", ""); - if (Enum.TryParse(targetType, clean, true, out var result)) - { - return result; - } - } - return null; - } -} - -#endregion - -#region 7. Multi-Value Converters - -/// -/// Multi-value converter that returns true only if ALL bound values are true. -/// -public class AllTrueMultiConverter : IMultiValueConverter -{ - public static readonly AllTrueMultiConverter Instance = new(); - - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) - { - if (values == null || values.Count == 0) return false; - return values.All(v => v is true); - } -} - -/// -/// Multi-value converter that returns true if ANY bound value is true. -/// -public class AnyTrueMultiConverter : IMultiValueConverter -{ - public static readonly AnyTrueMultiConverter Instance = new(); - - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) - { - if (values == null || values.Count == 0) return false; - return values.Any(v => v is true); - } -} - -/// -/// Multi-value converter that returns true if the first two bound values are equal (case-insensitive string comparison). -/// -public class ValuesEqualMultiConverter : IMultiValueConverter -{ - public static readonly ValuesEqualMultiConverter Instance = new(); - - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) - { - if (values == null || values.Count < 2) return false; - if (values[0] == null && values[1] == null) return true; - if (values[0] == null || values[1] == null) return false; - return string.Equals(values[0]?.ToString(), values[1]?.ToString(), StringComparison.OrdinalIgnoreCase); - } -} - -/// -/// Concatenates multiple string values together with an optional delimiter parameter. -/// -public class StringConcatMultiConverter : IMultiValueConverter -{ - public static readonly StringConcatMultiConverter Instance = new(); - - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) - { - if (values == null || values.Count == 0) return string.Empty; - string delimiter = parameter as string ?? string.Empty; - return string.Join(delimiter, values.Where(v => v != null).Select(v => v!.ToString())); - } -} - -/// -/// Returns the maximum value among numeric multi-bindings. -/// -public class MathMaxMultiConverter : IMultiValueConverter -{ - public static readonly MathMaxMultiConverter Instance = new(); - - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) - { - if (values == null || values.Count == 0) return 0.0; - double max = double.MinValue; - bool found = false; - - foreach (var v in values) - { - if (v != null && double.TryParse(v.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double d)) - { - if (d > max) max = d; - found = true; - } - } - - return found ? max : 0.0; - } -} - -/// -/// Returns the minimum value among numeric multi-bindings. -/// -public class MathMinMultiConverter : IMultiValueConverter -{ - public static readonly MathMinMultiConverter Instance = new(); - - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) - { - if (values == null || values.Count == 0) return 0.0; - double min = double.MaxValue; - bool found = false; - - foreach (var v in values) - { - if (v != null && double.TryParse(v.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double d)) - { - if (d < min) min = d; - found = true; - } - } - - return found ? min : 0.0; - } -} - -#endregion diff --git a/src/PdfEditorApp/Converters/HexToBrushConverter.cs b/src/PdfEditorApp/Converters/HexToBrushConverter.cs deleted file mode 100644 index 07a98a3..0000000 --- a/src/PdfEditorApp/Converters/HexToBrushConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; -using Avalonia.Media; - -namespace PdfEditorApp.Converters; - -public class HexToBrushConverter : IValueConverter -{ - public static readonly HexToBrushConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string hex && !string.IsNullOrWhiteSpace(hex)) - { - if (Color.TryParse(hex, out var color)) - { - return new SolidColorBrush(color); - } - } - return Brushes.Transparent; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is ISolidColorBrush brush) - { - return brush.Color.ToString(); - } - return "#00000000"; - } -} - -public class HexToColorConverter : IValueConverter -{ - public static readonly HexToColorConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is string hex && !string.IsNullOrWhiteSpace(hex)) - { - if (Color.TryParse(hex, out var color)) - { - return color; - } - } - return Colors.Black; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is Color color) - { - if (color.A == 255) - { - return $"#{color.R:X2}{color.G:X2}{color.B:X2}"; - } - return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; - } - return "#201F1E"; - } -} diff --git a/src/PdfEditorApp/Messages/ShellMessages.cs b/src/PdfEditorApp/Messages/ShellMessages.cs deleted file mode 100644 index 1570dee..0000000 --- a/src/PdfEditorApp/Messages/ShellMessages.cs +++ /dev/null @@ -1,89 +0,0 @@ -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Messages; - -/// -/// Broadcast message to display a toast notification anywhere in the application. -/// -public record ShowToastMessage(string Message, ToastNotificationType Type = ToastNotificationType.Primary, string? ActionLabel = null); - -/// -/// Request to return to the Home dashboard view. -/// -public record NavigateToHomeMessage(); - -/// -/// Request to open a PDF project or file in the Studio Editor. -/// -public record OpenInEditorMessage(string FilePath); - -/// -/// Request to open a PDF document in the standalone Reader / Viewer. -/// -public record OpenInViewerMessage(string FilePath); - -/// -/// Request to open a .frypdf document in the dedicated Interactive Document Viewer. -/// -public record OpenInFryPdfViewerMessage(string FilePath); - -/// -/// Request to launch the showcase interactive landscape presentation deck. -/// -public record LaunchInteractiveDeckMessage(); - -/// -/// Request to launch a specific PDF tool on a given input file. -/// -public record RunToolMessage(PdfToolId ToolId, string FilePath); - -/// -/// Notification that the user-preferred reading theme has changed. -/// -public record ReadingThemeChangedMessage(PdfReaderTheme Theme); - -/// -/// Notification that a project file was renamed on disk. -/// -public record ProjectRenamedMessage(string OldPath, string NewPath); - -/// -/// Notification that a project file was deleted from disk. -/// -public record ProjectDeletedMessage(string DeletedPath); - -/// -/// Request to prompt the user to rename a document. -/// -public record PromptRenameMessage(string FilePath); - -/// -/// Request to prompt the user to confirm document deletion. -/// -public record PromptDeleteMessage(string FilePath); - -/// -/// Request to open a template in the Studio Editor. -/// -public record OpenTemplateMessage(string? TemplateName); - -/// -/// Request to prompt the user to pick and open a project file in the Studio Editor. -/// -public record OpenProjectFileMessage(); - -/// -/// Request to open the Workflow Studio. -/// -public record OpenWorkflowStudioMessage(); - -/// -/// Request to open the Batch Generation Studio. -/// -public record OpenBatchGenerationMessage(); - -/// -/// Request to prompt the user to pick and open a PDF document in the Viewer. -/// -public record OpenPdfPickerMessage(); diff --git a/src/PdfEditorApp/Models/AiChatMessage.cs b/src/PdfEditorApp/Models/AiChatMessage.cs deleted file mode 100644 index cc9734a..0000000 --- a/src/PdfEditorApp/Models/AiChatMessage.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; - -namespace PdfEditorApp.Models; - -public enum AiChatRole -{ - User, - Assistant, - System -} - -/// -/// Represents a single message within an AI Studio Assistant conversation thread. -/// -public partial class AiChatMessage : ObservableObject -{ - public string Id { get; init; } = Guid.NewGuid().ToString("N"); - - public AiChatRole Role { get; init; } = AiChatRole.User; - - public bool IsUser => Role == AiChatRole.User; - public bool IsAssistant => Role == AiChatRole.Assistant; - public bool IsSystem => Role == AiChatRole.System; - - [ObservableProperty] - private string _content = string.Empty; - - public DateTime Timestamp { get; init; } = DateTime.Now; - - public string FormattedTime => Timestamp.ToString("HH:mm"); - - [ObservableProperty] - private string? _targetElementTitle; - - [ObservableProperty] - private string? _targetElementKind; - - public bool HasTargetElement => !string.IsNullOrEmpty(TargetElementTitle); - - [ObservableProperty] - private bool _isGenerating; - - [ObservableProperty] - private bool _isSuccess = true; - - [ObservableProperty] - private TimeSpan _duration = TimeSpan.Zero; - - public string FormattedDuration => Duration.TotalSeconds > 0 ? $"{Duration.TotalSeconds:0.1}s" : ""; - - [ObservableProperty] - private bool _canUndo; - - public Action? UndoAction { get; set; } - - public ObservableCollection ToolCalls { get; } = new(); - - public bool HasToolCalls => ToolCalls.Count > 0; - - [RelayCommand] - public void Undo() - { - if (CanUndo) - { - UndoAction?.Invoke(); - CanUndo = false; - } - } -} diff --git a/src/PdfEditorApp/Models/AiChatSession.cs b/src/PdfEditorApp/Models/AiChatSession.cs deleted file mode 100644 index 083d5a2..0000000 --- a/src/PdfEditorApp/Models/AiChatSession.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace PdfEditorApp.Models; - -/// -/// Represents a conversation session containing a sequence of AI chat messages. -/// -public partial class AiChatSession : ObservableObject -{ - public string Id { get; init; } = Guid.NewGuid().ToString("N"); - - [ObservableProperty] - private string _title = "New Chat"; - - public DateTime CreatedAt { get; init; } = DateTime.Now; - - [ObservableProperty] - private DateTime _lastModified = DateTime.Now; - - public ObservableCollection Messages { get; } = new(); - - public int MessageCount => Messages.Count; - - public string PreviewSnippet - { - get - { - var last = Messages.LastOrDefault(); - if (last == null) return "Empty conversation"; - string text = last.Content?.Trim() ?? ""; - return text.Length <= 40 ? text : text[..40] + "..."; - } - } - - public string FormattedLastModified => LastModified.ToString("MMM d, HH:mm"); - - public AiChatSession() - { - Messages.CollectionChanged += OnMessagesChanged; - } - - private void OnMessagesChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(MessageCount)); - OnPropertyChanged(nameof(PreviewSnippet)); - } -} diff --git a/src/PdfEditorApp/Models/AiSettingsModel.cs b/src/PdfEditorApp/Models/AiSettingsModel.cs deleted file mode 100644 index 38e8167..0000000 --- a/src/PdfEditorApp/Models/AiSettingsModel.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Models; - -/// -/// Persisted configuration settings for AI providers, models, and local LLM runtime. -/// -public class AiSettingsModel -{ - [JsonPropertyName("selectedProvider")] - public AiProviderType SelectedProvider { get; set; } = AiProviderType.OllamaLocal; - - [JsonPropertyName("ollamaEndpoint")] - public string OllamaEndpoint { get; set; } = "http://localhost:11434"; - - [JsonPropertyName("ollamaApiKey")] - public string OllamaApiKey { get; set; } = string.Empty; - - [JsonPropertyName("selectedModelId")] - public string SelectedModelId { get; set; } = "llama3.2"; - - [JsonPropertyName("openAiApiKey")] - public string OpenAiApiKey { get; set; } = string.Empty; - - [JsonPropertyName("customBaseUrl")] - public string CustomBaseUrl { get; set; } = string.Empty; - - [JsonPropertyName("customModelName")] - public string CustomModelName { get; set; } = string.Empty; - - [JsonPropertyName("customModelHistory")] - public List CustomModelHistory { get; set; } = new() - { - "openai/gpt-oss-120b", - "llama-3.3-70b-versatile", - "qwen/qwen-2.5-coder-32b", - "deepseek-r1-distill-llama-70b", - "llama-3.1-8b-instant" - }; - - [JsonPropertyName("temperature")] - public float Temperature { get; set; } = 0.7f; - - [JsonPropertyName("systemInstructions")] - public string SystemInstructions { get; set; } = string.Empty; - - [JsonPropertyName("discoveredOllamaModels")] - public List DiscoveredOllamaModels { get; set; } = new(); - - [JsonPropertyName("ollamaLibraryCache")] - public List OllamaLibraryCache { get; set; } = new(); - - [JsonPropertyName("lastConnectedAt")] - public DateTime? LastConnectedAt { get; set; } - - [JsonIgnore] - public bool IsOllamaRemote => !string.IsNullOrWhiteSpace(OllamaEndpoint) && - !OllamaEndpoint.Contains("localhost", StringComparison.OrdinalIgnoreCase) && - !OllamaEndpoint.Contains("127.0.0.1"); - - public AiSettingsModel Clone() - { - var copy = new AiSettingsModel - { - SelectedProvider = this.SelectedProvider, - OllamaEndpoint = this.OllamaEndpoint, - OllamaApiKey = this.OllamaApiKey, - SelectedModelId = this.SelectedModelId, - OpenAiApiKey = this.OpenAiApiKey, - CustomBaseUrl = this.CustomBaseUrl, - CustomModelName = this.CustomModelName, - Temperature = this.Temperature, - SystemInstructions = this.SystemInstructions, - LastConnectedAt = this.LastConnectedAt - }; - - foreach (var m in DiscoveredOllamaModels) - { - copy.DiscoveredOllamaModels.Add(new AiModelInfo - { - Id = m.Id, - DisplayName = m.DisplayName, - Provider = m.Provider, - Tier = m.Tier, - Category = m.Category, - Capabilities = m.Capabilities, - SizeBytes = m.SizeBytes, - FormattedSize = m.FormattedSize, - ParameterSize = m.ParameterSize, - Description = m.Description, - IsInstalledLocally = m.IsInstalledLocally - }); - } - - foreach (var m in OllamaLibraryCache) - { - copy.OllamaLibraryCache.Add(new AiModelInfo - { - Id = m.Id, - DisplayName = m.DisplayName, - Provider = m.Provider, - Tier = m.Tier, - Category = m.Category, - Capabilities = m.Capabilities, - SizeBytes = m.SizeBytes, - FormattedSize = m.FormattedSize, - ParameterSize = m.ParameterSize, - Description = m.Description, - IsInstalledLocally = m.IsInstalledLocally - }); - } - - copy.CustomModelHistory.Clear(); - copy.CustomModelHistory.AddRange(this.CustomModelHistory); - - return copy; - } -} diff --git a/src/PdfEditorApp/Models/DocumentComparisonModels.cs b/src/PdfEditorApp/Models/DocumentComparisonModels.cs deleted file mode 100644 index c3d942f..0000000 --- a/src/PdfEditorApp/Models/DocumentComparisonModels.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Models; - -public class DocumentComparisonReport -{ - public string BaseDocumentTitle { get; set; } = ""; - public string ComparedDocumentTitle { get; set; } = ""; - public DateTime ComparisonTimestamp { get; set; } = DateTime.UtcNow; - - public int BasePageCount { get; set; } - public int ComparedPageCount { get; set; } - - public int TotalDifferencesCount => Differences.Count; - public int AdditionsCount { get; set; } - public int DeletionsCount { get; set; } - public int ModificationsCount { get; set; } - - public List Differences { get; set; } = new(); -} - -public class DocumentDifferenceItem -{ - public int PageNumber { get; set; } - public CompareDiffType DiffType { get; set; } - public string Description { get; set; } = ""; - public string OldValue { get; set; } = ""; - public string NewValue { get; set; } = ""; - public string ElementKindDisplay { get; set; } = ""; - public string BadgeColorHex => DiffType switch - { - CompareDiffType.ElementAdded => "#16A34A", - CompareDiffType.ElementRemoved => "#DC2626", - CompareDiffType.ElementModified => "#0F6CBD", - CompareDiffType.TextModified => "#D97706", - CompareDiffType.FormattingModified => "#7C3AED", - CompareDiffType.SecurityModified => "#E11D48", - CompareDiffType.MetadataModified => "#2563EB", - _ => "#475569" - }; - public string BadgeIcon => DiffType switch - { - CompareDiffType.ElementAdded => "PlusCircleOutline", - CompareDiffType.ElementRemoved => "MinusCircleOutline", - CompareDiffType.ElementModified => "PencilOutline", - CompareDiffType.TextModified => "FormatColorText", - CompareDiffType.FormattingModified => "FormatFont", - CompareDiffType.SecurityModified => "ShieldAlertOutline", - CompareDiffType.MetadataModified => "TagOutline", - _ => "InformationOutline" - }; -} diff --git a/src/PdfEditorApp/Models/HelpGuideItem.cs b/src/PdfEditorApp/Models/HelpGuideItem.cs deleted file mode 100644 index 22c999a..0000000 --- a/src/PdfEditorApp/Models/HelpGuideItem.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Models; - -/// -/// Model representing a comprehensive help guide topic, tool walkthrough, or troubleshooting article. -/// -public class HelpGuideItem -{ - public string Id { get; set; } = ""; - public string Title { get; set; } = ""; - public string Category { get; set; } = ""; - public string Summary { get; set; } = ""; - public string Description { get; set; } = ""; - public List Steps { get; set; } = new(); - public List KeyFeatures { get; set; } = new(); - public List ProTips { get; set; } = new(); - public string SupportedFormats { get; set; } = ""; - public PdfToolId? RelatedToolId { get; set; } - public string? KeyboardShortcut { get; set; } - public string IconKind { get; set; } = "HelpCircleOutline"; - public string IconColorHex { get; set; } = "#0284C7"; - public string BackgroundAccentHex { get; set; } = "#E0F2FE"; - public string Badge { get; set; } = "Guide"; - public string Keywords { get; set; } = ""; - public bool IsFeatured { get; set; } - - public bool HasRelatedTool => RelatedToolId.HasValue; - public bool HasShortcut => !string.IsNullOrWhiteSpace(KeyboardShortcut); - public bool HasFormats => !string.IsNullOrWhiteSpace(SupportedFormats); - public bool HasSteps => Steps != null && Steps.Count > 0; - public bool HasKeyFeatures => KeyFeatures != null && KeyFeatures.Count > 0; - public bool HasProTips => ProTips != null && ProTips.Count > 0; -} diff --git a/src/PdfEditorApp/Models/PdfFilePreviewItem.cs b/src/PdfEditorApp/Models/PdfFilePreviewItem.cs deleted file mode 100644 index ed4539f..0000000 --- a/src/PdfEditorApp/Models/PdfFilePreviewItem.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.IO; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Models; - -/// -/// Rich metadata and preview representation for a PDF document selected in a tool workspace. -/// -public partial class PdfFilePreviewItem : ObservableObject -{ - [ObservableProperty] - private string _filePath = string.Empty; - - [ObservableProperty] - private string _fileName = string.Empty; - - [ObservableProperty] - private string _directoryPath = string.Empty; - - [ObservableProperty] - private long _fileSizeBytes; - - [ObservableProperty] - private string _fileSizeFormatted = "0 KB"; - - [ObservableProperty] - private int _pageCount = 1; - - [ObservableProperty] - private string _pageCountText = "1 Page"; - - [ObservableProperty] - private int _orderIndex = 1; - - [ObservableProperty] - private string _orderIndexText = "#1"; - - [ObservableProperty] - private string _pageDimensionsText = "A4 · Standard"; - - [ObservableProperty] - private bool _isCorrupted; - - [ObservableProperty] - private PageViewModel? _pagePreview; - - public bool HasLivePreview => PagePreview != null; - - public static string FormatBytes(long bytes) - { - if (bytes < 1024) return $"{bytes} B"; - if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; - return $"{bytes / (1024.0 * 1024.0):F1} MB"; - } - - public static PdfFilePreviewItem CreateFromFile(string filePath, int orderIndex = 1, int pageCount = 1) - { - var fi = new FileInfo(filePath); - long size = fi.Exists ? fi.Length : 0; - string name = Path.GetFileName(filePath); - string dir = Path.GetDirectoryName(filePath) ?? string.Empty; - - return new PdfFilePreviewItem - { - FilePath = filePath, - FileName = name, - DirectoryPath = dir, - FileSizeBytes = size, - FileSizeFormatted = FormatBytes(size), - PageCount = Math.Max(1, pageCount), - PageCountText = pageCount == 1 ? "1 Page" : $"{pageCount} Pages", - OrderIndex = orderIndex, - OrderIndexText = $"#{orderIndex}", - PageDimensionsText = "Standard PDF", - IsCorrupted = false - }; - } -} diff --git a/src/PdfEditorApp/Models/PdfPagePreviewThumbnail.cs b/src/PdfEditorApp/Models/PdfPagePreviewThumbnail.cs deleted file mode 100644 index 87a23be..0000000 --- a/src/PdfEditorApp/Models/PdfPagePreviewThumbnail.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace PdfEditorApp.Models; - -/// -/// Representation of an individual page in a merged or output PDF for in-app page strip browsing. -/// -public partial class PdfPagePreviewThumbnail : ObservableObject -{ - [ObservableProperty] - private int _pageNumber = 1; - - [ObservableProperty] - private string _pageLabel = "Page 1"; - - [ObservableProperty] - private double _widthPoints = 595; - - [ObservableProperty] - private double _heightPoints = 842; - - [ObservableProperty] - private bool _isLandscape; - - [ObservableProperty] - private string _dimensionsText = "595 × 842 pt"; - - [ObservableProperty] - private string _pageSummary = string.Empty; - - [ObservableProperty] - private bool _isSelected; -} diff --git a/src/PdfEditorApp/Models/PdfToolModels.cs b/src/PdfEditorApp/Models/PdfToolModels.cs deleted file mode 100644 index bbb9aba..0000000 --- a/src/PdfEditorApp/Models/PdfToolModels.cs +++ /dev/null @@ -1,522 +0,0 @@ -using System; -using System.Collections.Generic; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.Models; - -public enum PdfToolId -{ - MergePdf, - SplitPdf, - CompressPdf, - PdfToWord, - PdfToPowerPoint, - PdfToExcel, - WordToPdf, - PowerPointToPdf, - ExcelToPdf, - EditPdf, - PdfToJpg, - JpgToPdf, - SignPdf, - Watermark, - RotatePdf, - HtmlToPdf, - UnlockPdf, - ProtectPdf, - OrganizePdf, - PdfToPdfA, - RepairPdf, - PageNumbers, - ScanToPdf, - OcrPdf, - ComparePdf, - RedactPdf, - CropPdf, - PdfForms, - AiSummarizer, - TranslatePdf, - PdfToMarkdown, - WorkflowBuilder, - BatchMailMerge -} - -public enum PdfToolCategory -{ - All, - OrganizeAndPage, - OptimizeAndSecurity, - ConvertFromPdf, - ConvertToPdf, - EditAndForms, - AiAndAutomation -} - -public enum PdfCompressionLevel -{ - MaximumQuality, - HighQuality, - Balanced, - SmallSize, - MaximumCompression -} - -public enum PdfAStandard -{ - PdfA1b, - PdfA2b, - PdfA3b -} - -public enum WatermarkType -{ - Text, - Image -} - -public enum WatermarkPosition -{ - Center, - TopLeft, - TopCenter, - TopRight, - BottomLeft, - BottomCenter, - BottomRight, - Tiled -} - -public enum PageNumberPosition -{ - TopLeft, - TopCenter, - TopRight, - BottomLeft, - BottomCenter, - BottomRight -} - -public enum PageTargetSelection -{ - AllPages, - OddPagesOnly, - EvenPagesOnly, - CustomRange -} - -public class PdfToolDefinition -{ - public PdfToolId Id { get; set; } - public string Name { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public PdfToolCategory Category { get; set; } - public string CategoryDisplayName => Category switch - { - PdfToolCategory.OrganizeAndPage => "Organize & Page", - PdfToolCategory.OptimizeAndSecurity => "Optimize & Security", - PdfToolCategory.ConvertFromPdf => "Convert from PDF", - PdfToolCategory.ConvertToPdf => "Convert to PDF", - PdfToolCategory.EditAndForms => "Edit & Forms", - PdfToolCategory.AiAndAutomation => "AI & Automation", - _ => "All Tools" - }; - - public string IconKind { get; set; } = "FileDocumentOutline"; - public string IconColorHex { get; set; } = "#0F6CBD"; - public string BackgroundAccentHex { get; set; } = "#EFF6FF"; - public bool IsNew { get; set; } - public bool IsWorkflowBanner { get; set; } - public string AcceptedFileExtensions { get; set; } = ".pdf"; - public bool SupportsMultiFile { get; set; } - - private string? _stringId; - public string StringId - { - get => string.IsNullOrWhiteSpace(_stringId) ? Id.ToString() : _stringId; - set => _stringId = value; - } - - public Func? ViewModelFactory { get; set; } -} - -public record ToolExecutionResult -{ - public bool Success { get; init; } - public string? OutputFilePath { get; init; } - public List OutputFiles { get; init; } = new(); - public string? Message { get; init; } - public string? ErrorMessage { get; init; } - public long OriginalSizeBytes { get; init; } - public long OutputSizeBytes { get; init; } - public double SavingsPercentage => OriginalSizeBytes > 0 && OutputSizeBytes > 0 - ? Math.Max(0, (OriginalSizeBytes - OutputSizeBytes) / (double)OriginalSizeBytes * 100.0) - : 0; - public Dictionary ExtraData { get; init; } = new(); -} - -public enum PdfProcessingEngine -{ - QuestPdfNative = 0, - StandardPdfSharp = 1 -} - -public enum PdfEncryptionLevel -{ - Aes256Bit = 0, - Aes128Bit = 1, - Rc440Bit = 2 -} - -public class MergeToolOptions -{ - public List InputFiles { get; set; } = new(); - public string OutputFilePath { get; set; } = string.Empty; - public bool PreserveBookmarks { get; set; } = true; - public bool NormalizePageSizes { get; set; } - public PdfProcessingEngine Engine { get; set; } = PdfProcessingEngine.QuestPdfNative; - public Dictionary FilePageRanges { get; set; } = new(StringComparer.OrdinalIgnoreCase); -} - -public class SplitToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputDirectory { get; set; } = string.Empty; - public SplitExtractMode Mode { get; set; } = SplitExtractMode.SplitEveryNPages; - public int PagesPerSplit { get; set; } = 1; - public string RangeExpression { get; set; } = "1-3, 5, 7-10"; - public bool SplitOddEven { get; set; } - public bool ExtractOddPages { get; set; } - public PdfProcessingEngine Engine { get; set; } = PdfProcessingEngine.QuestPdfNative; -} - -public class CompressToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public PdfCompressionLevel Level { get; set; } = PdfCompressionLevel.Balanced; - public int ImageQualityDpi { get; set; } = 150; - public int JpegQuality { get; set; } = 0; // 0 = automatic based on level, or 20-100 - public int MaxImageDimension { get; set; } = 0; // 0 = automatic based on level - public bool ConvertToGrayscale { get; set; } - public bool RemoveMetadata { get; set; } - public bool RemoveDuplicateObjects { get; set; } = true; - public bool CompressStreams { get; set; } = true; - public bool LinearizeForWeb { get; set; } -} - -public class LinearizeToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; -} - -public class WordConversionOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public bool ExtractTables { get; set; } = true; - public bool ExtractImages { get; set; } = true; - public bool OcrFallback { get; set; } -} - -public class ExcelConversionOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public bool DetectAllTables { get; set; } = true; - public bool SeparateSheetsPerPage { get; set; } -} - -public class PptxConversionOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public bool EditableText { get; set; } = true; -} - -public class OfficeToPdfOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public PageOrientation Orientation { get; set; } = PageOrientation.Portrait; -} - -public class ImageConversionOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputDirectory { get; set; } = string.Empty; - public string OutputFormat { get; set; } = "jpg"; // jpg, png - public int Dpi { get; set; } = 300; - public int JpgQuality { get; set; } = 90; - public bool Grayscale { get; set; } - public PageTargetSelection TargetPages { get; set; } = PageTargetSelection.AllPages; - public string PageRange { get; set; } = "1"; -} - -public class ImagesToPdfOptions -{ - public List ImageFiles { get; set; } = new(); - public string OutputFilePath { get; set; } = string.Empty; - public PageFormat PageFormat { get; set; } = PageFormat.A4; - public PageOrientation Orientation { get; set; } = PageOrientation.Portrait; - public bool AutoOrientation { get; set; } = true; - public bool FitToPage { get; set; } = true; - public double MarginPoints { get; set; } = 20; - public int ImagesPerPage { get; set; } = 1; // 1, 2, 4 -} - -public class SignToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public SignatureStyle Style { get; set; } = SignatureStyle.CursiveElegance; - public string SignerName { get; set; } = "Jane Doe"; - public string? Reason { get; set; } = "Document Approved and Verified"; - public string? Location { get; set; } - public string? CertificatePath { get; set; } - public string? CertificatePassword { get; set; } - public int TargetPageNumber { get; set; } = 1; - public double X { get; set; } = 100; - public double Y { get; set; } = 600; - public double Width { get; set; } = 200; - public double Height { get; set; } = 70; - public string? SignatureImageDataUri { get; set; } -} - -public class WatermarkToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public WatermarkType Type { get; set; } = WatermarkType.Text; - public string Text { get; set; } = "CONFIDENTIAL"; - public string? ImagePath { get; set; } - public string FontFamily { get; set; } = "Helvetica"; - public double FontSize { get; set; } = 48; - public string ColorHex { get; set; } = "#EF4444"; - public double Opacity { get; set; } = 0.35; - public double RotationAngle { get; set; } = -45; - public WatermarkPosition Position { get; set; } = WatermarkPosition.Center; - public PageTargetSelection TargetPages { get; set; } = PageTargetSelection.AllPages; - public string CustomRange { get; set; } = ""; - public bool LayerOnTop { get; set; } = true; -} - -public class RotateToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public int RotationDegrees { get; set; } = 90; // 90, 180, 270 - public PageFilterTarget TargetFilter { get; set; } = PageFilterTarget.All; - public string CustomRange { get; set; } = ""; -} - -public class HtmlToPdfOptions -{ - public string HtmlContentOrUrl { get; set; } = string.Empty; - public bool IsUrl { get; set; } - public string OutputFilePath { get; set; } = string.Empty; - public PageFormat Format { get; set; } = PageFormat.A4; - public PageOrientation Orientation { get; set; } = PageOrientation.Portrait; - public double MarginPoints { get; set; } = 36; - public bool IncludePageNumbers { get; set; } = true; -} - -public class SecurityToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public string UserPassword { get; set; } = string.Empty; - public string OwnerPassword { get; set; } = string.Empty; - public bool AllowPrinting { get; set; } = true; - public bool AllowModifying { get; set; } = false; - public bool AllowCopying { get; set; } = false; - public bool AllowAnnotating { get; set; } = true; - public bool AllowFormFilling { get; set; } = true; - public bool AllowAssembly { get; set; } = true; - public bool EncryptMetadata { get; set; } = true; - public PdfEncryptionLevel EncryptionLevel { get; set; } = PdfEncryptionLevel.Aes256Bit; - public PdfProcessingEngine Engine { get; set; } = PdfProcessingEngine.StandardPdfSharp; -} - -public class UnlockToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; - public PdfProcessingEngine Engine { get; set; } = PdfProcessingEngine.QuestPdfNative; -} - -public class PageOrganizeAction -{ - public int SourcePageIndex { get; set; } - public int RotationDegrees { get; set; } - public bool Delete { get; set; } - public bool Duplicate { get; set; } - public string? InsertPdfFilePath { get; set; } - public int? InsertPdfPageIndex { get; set; } -} - -public class OrganizeToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public List PageOrder { get; set; } = new(); // 0-based page indices in target order - public List PagesToDelete { get; set; } = new(); - public Dictionary PageRotations { get; set; } = new(); // pageIndex -> angle -} - -public class PdfAToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public PdfAStandard Standard { get; set; } = PdfAStandard.PdfA2b; -} - -public class RepairToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; -} - -public class PageNumberToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public PageNumberPosition Position { get; set; } = PageNumberPosition.BottomCenter; - public string Template { get; set; } = "Page {n} of {total}"; // e.g. "Page {n} of {total}", "{n}", "Bates: DOC-{n:D6}" - public int StartingNumber { get; set; } = 1; - public string FontFamily { get; set; } = "Helvetica"; - public double FontSize { get; set; } = 10; - public string ColorHex { get; set; } = "#334155"; - public PageTargetSelection TargetPages { get; set; } = PageTargetSelection.AllPages; - public string CustomRange { get; set; } = ""; - public double MarginPoints { get; set; } = 28; -} - -public class ScanToolOptions -{ - public List InputImageFiles { get; set; } = new(); - public string OutputFilePath { get; set; } = string.Empty; - public bool AutoDeskew { get; set; } = true; - public bool EnhanceContrast { get; set; } = true; - public bool WhitenBackground { get; set; } = true; - public bool ConvertToGrayscale { get; set; } - public PageFormat Format { get; set; } = PageFormat.A4; -} - -public class OcrToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public string Language { get; set; } = "eng"; // eng, spa, deu, fra, etc. - public bool GenerateSearchablePdf { get; set; } = true; - public bool GenerateTextFile { get; set; } = true; - public bool ExtractTextOnly { get; set; } -} - -public class CompareToolOptions -{ - public string DocumentAPath { get; set; } = string.Empty; - public string DocumentBPath { get; set; } = string.Empty; - public bool DetectTextDiff { get; set; } = true; - public bool DetectVisualDiff { get; set; } = true; -} - -public class RedactionRegion -{ - public int PageIndex { get; set; } - public double X { get; set; } - public double Y { get; set; } - public double Width { get; set; } - public double Height { get; set; } - public string Reason { get; set; } = "CONFIDENTIAL"; - public string FillColorHex { get; set; } = "#000000"; -} - -/// -/// UI-facing wrapper around a shown in a "Marked for -/// redaction" review list — carries a human-readable label in addition to the raw -/// region geometry the backend needs. -/// -public partial class RedactionMarkItem : ObservableObject -{ - public required RedactionRegion Region { get; init; } - public required string Label { get; init; } - public string PageLabel => $"Page {Region.PageIndex + 1}"; - - // On-screen highlight position/size in display pixels, computed by the ViewModel - // for the currently rendered page (Region stays in PDF-point space; these are - // derived purely for XAML Canvas.Left/Top/Width/Height binding). - [ObservableProperty] - private double _displayX; - - [ObservableProperty] - private double _displayY; - - [ObservableProperty] - private double _displayWidth; - - [ObservableProperty] - private double _displayHeight; -} - -public class RedactionToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public List Regions { get; set; } = new(); - public string? SearchPatternToRedact { get; set; } - public bool CaseSensitive { get; set; } - public bool PermanentScrubText { get; set; } = true; -} - -public class CropToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public double CropLeftPoints { get; set; } - public double CropRightPoints { get; set; } - public double CropTopPoints { get; set; } - public double CropBottomPoints { get; set; } - public PageTargetSelection TargetPages { get; set; } = PageTargetSelection.AllPages; - public string CustomRange { get; set; } = ""; -} - -public class FormToolOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public Dictionary FieldValues { get; set; } = new(); - public bool FlattenFields { get; set; } - public bool ExportFieldValuesJson { get; set; } -} - -public class AiSummaryOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public int MaxBulletPoints { get; set; } = 7; - public bool IncludeExecutiveSummary { get; set; } = true; - public bool IncludeActionItems { get; set; } = true; - public string? CustomPrompt { get; set; } - public string TargetLanguage { get; set; } = "English"; -} - -public class TranslationOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public string SourceLanguage { get; set; } = "Auto"; - public string TargetLanguage { get; set; } = "Spanish"; // Spanish, French, German, Japanese, etc. - public bool PreserveLayout { get; set; } = true; -} - -public class MarkdownConversionOptions -{ - public string InputFilePath { get; set; } = string.Empty; - public string OutputFilePath { get; set; } = string.Empty; - public bool IncludeTables { get; set; } = true; - public bool IncludeImages { get; set; } = true; - public bool IncludeMetadataHeader { get; set; } = true; -} diff --git a/src/PdfEditorApp/Models/PdfToolPreviewPage.cs b/src/PdfEditorApp/Models/PdfToolPreviewPage.cs deleted file mode 100644 index b6725c3..0000000 --- a/src/PdfEditorApp/Models/PdfToolPreviewPage.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace PdfEditorApp.Models; - -/// -/// One rendered page in a tool screen's live input-document preview. -/// -public partial class PdfToolPreviewPage : ObservableObject -{ - [ObservableProperty] - private int _pageNumber = 1; - - [ObservableProperty] - private double _widthPoints = 595; - - [ObservableProperty] - private double _heightPoints = 842; - - [ObservableProperty] - private Bitmap? _bitmap; - - [ObservableProperty] - private Bitmap? _thumbnailBitmap; - - [ObservableProperty] - private bool _isSelected; - - /// Scale the current was rendered at, so zoom changes only re-render when the resolution actually needs to change. - [ObservableProperty] - private float _renderedScale; - - public string PageLabel => $"Page {PageNumber}"; -} diff --git a/src/PdfEditorApp/Models/RecentDocumentItem.cs b/src/PdfEditorApp/Models/RecentDocumentItem.cs deleted file mode 100644 index b7929f9..0000000 --- a/src/PdfEditorApp/Models/RecentDocumentItem.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.IO; -using System.Text.Json.Serialization; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Models; - -/// -/// Represents a recently opened or saved FryPDF project file entry. -/// -public partial class RecentDocumentItem : ObservableObject -{ - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(FileName))] - [NotifyPropertyChangedFor(nameof(DirectoryName))] - [NotifyPropertyChangedFor(nameof(FileExists))] - [NotifyPropertyChangedFor(nameof(FormattedFileSize))] - private string _filePath = ""; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(FileName))] - private string _title = "Untitled"; - - [ObservableProperty] - private DateTime _lastOpened = DateTime.UtcNow; - - [ObservableProperty] - private int _pageCount = 1; - - [ObservableProperty] - private string _formatDescription = "A4 Document"; - - [ObservableProperty] - [property: JsonIgnore] - [NotifyPropertyChangedFor(nameof(HasLivePreview))] - private PageViewModel? _pagePreview; - - [JsonIgnore] - public bool HasLivePreview => PagePreview != null; - - [JsonIgnore] - public string FileName => string.IsNullOrEmpty(FilePath) ? Title : Path.GetFileName(FilePath); - - [JsonIgnore] - public string DirectoryName => string.IsNullOrEmpty(FilePath) ? "" : (Path.GetDirectoryName(FilePath) ?? ""); - - [JsonIgnore] - public bool FileExists => !string.IsNullOrEmpty(FilePath) && File.Exists(FilePath); - - [JsonIgnore] - public string FormattedFileSize - { - get - { - try - { - if (!FileExists) return ""; - var bytes = new FileInfo(FilePath).Length; - if (bytes < 1024) return $"{bytes} B"; - if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; - return $"{bytes / (1024.0 * 1024.0):F1} MB"; - } - catch - { - return ""; - } - } - } - - /// Friendly display string for how long ago the file was opened. - [JsonIgnore] - public string RelativeTime - { - get - { - var delta = DateTime.UtcNow - LastOpened; - if (delta.TotalMinutes < 1) return "Just now"; - if (delta.TotalHours < 1) return $"{(int)delta.TotalMinutes}m ago"; - if (delta.TotalDays < 1) return $"{(int)delta.TotalHours}h ago"; - if (delta.TotalDays < 7) return $"{(int)delta.TotalDays}d ago"; - return LastOpened.ToString("MMM d, yyyy"); - } - } -} - diff --git a/src/PdfEditorApp/Models/ThirdPartyToolLicense.cs b/src/PdfEditorApp/Models/ThirdPartyToolLicense.cs deleted file mode 100644 index e20f6bd..0000000 --- a/src/PdfEditorApp/Models/ThirdPartyToolLicense.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace PdfEditorApp.Models; - -/// -/// Represents a third-party library, package, tool, or engine utilized by FryPDF, -/// along with its license type, repository/website link, maintainer information, and full license text. -/// -public partial class ThirdPartyToolLicense : ObservableObject -{ - public required string Name { get; init; } - public required string Version { get; init; } - public required string LicenseType { get; init; } - public required string Category { get; init; } - public required string Purpose { get; init; } - public required string Maintainer { get; init; } - public required string ProjectUrl { get; init; } - public required string LicenseText { get; init; } - public required string AccentColorHex { get; init; } - public required string IconKind { get; init; } - - [ObservableProperty] - private bool _isExpanded; - - public string BadgeColorHex => LicenseType.ToUpperInvariant() switch - { - var l when l.Contains("MIT") => "#10B981", // Green - var l when l.Contains("APACHE") => "#0284C7", // Blue - var l when l.Contains("COMMUNITY") => "#8B5CF6", // Purple - var l when l.Contains("MICROSOFT") => "#0078D4", // Microsoft Blue - var l when l.Contains("OFL") || l.Contains("SIL") => "#D946EF", // Fuchsia / Pink - var l when l.Contains("UBUNTU") => "#EA580C", // Ubuntu Orange - _ => "#6366F1" - }; - - public string BadgeBackgroundHex => LicenseType.ToUpperInvariant() switch - { - var l when l.Contains("MIT") => "#ECFDF5", - var l when l.Contains("APACHE") => "#F0F9FF", - var l when l.Contains("COMMUNITY") => "#F5F3FF", - var l when l.Contains("MICROSOFT") => "#EFF6FF", - var l when l.Contains("OFL") || l.Contains("SIL") => "#FDF4FF", - var l when l.Contains("UBUNTU") => "#FFF7ED", - _ => "#EEF2FF" - }; -} diff --git a/src/PdfEditorApp/Models/UiSettingsModel.cs b/src/PdfEditorApp/Models/UiSettingsModel.cs deleted file mode 100644 index d5681c2..0000000 --- a/src/PdfEditorApp/Models/UiSettingsModel.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Text.Json.Serialization; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Models; - -/// -/// Persisted user interface preferences and customization settings. -/// -public class UiSettingsModel -{ - // --- Toast / Snackbar Preferences --- - - [JsonPropertyName("toastPosition")] - public ToastPosition ToastPosition { get; set; } = ToastPosition.BottomCenter; - - [JsonPropertyName("toastStyleVariant")] - public ToastStyleVariant ToastStyleVariant { get; set; } = ToastStyleVariant.Solid; - - [JsonPropertyName("toastDurationMs")] - public int ToastDurationMs { get; set; } = 3500; - - [JsonPropertyName("toastShowCloseButton")] - public bool ToastShowCloseButton { get; set; } = true; - - [JsonPropertyName("toastSoundEnabled")] - public bool ToastSoundEnabled { get; set; } = false; - - // --- Appearance & Studio Theme --- - - [JsonPropertyName("themeMode")] - public AppThemeMode ThemeMode { get; set; } = AppThemeMode.Light; - - [JsonPropertyName("readingTheme")] - public PdfReaderTheme ReadingTheme { get; set; } = PdfReaderTheme.Default; - - [JsonPropertyName("accentColorHex")] - public string AccentColorHex { get; set; } = "#0F6CBD"; - - // --- Canvas & Document Workspace --- - - [JsonPropertyName("showGridByDefault")] - public bool ShowGridByDefault { get; set; } = false; - - [JsonPropertyName("snapToGridByDefault")] - public bool SnapToGridByDefault { get; set; } = false; - - [JsonPropertyName("gridSnapSize")] - public GridSnapSize GridSnapSize { get; set; } = GridSnapSize.Points20; - - [JsonPropertyName("defaultZoomMode")] - public PdfViewerZoomMode DefaultZoomMode { get; set; } = PdfViewerZoomMode.FitWidth; - - // --- Layout & Panel Behaviors --- - - [JsonPropertyName("compactRibbonByDefault")] - public bool CompactRibbonByDefault { get; set; } = false; - - [JsonPropertyName("autoExpandInspectorOnSelect")] - public bool AutoExpandInspectorOnSelect { get; set; } = true; - - [JsonPropertyName("showShortcutHints")] - public bool ShowShortcutHints { get; set; } = true; - - [JsonPropertyName("aiSettings")] - public AiSettingsModel AiSettings { get; set; } = new(); - - [JsonPropertyName("updatedAt")] - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - - public UiSettingsModel Clone() - { - return new UiSettingsModel - { - ToastPosition = this.ToastPosition, - ToastStyleVariant = this.ToastStyleVariant, - ToastDurationMs = this.ToastDurationMs, - ToastShowCloseButton = this.ToastShowCloseButton, - ToastSoundEnabled = this.ToastSoundEnabled, - ThemeMode = this.ThemeMode, - ReadingTheme = this.ReadingTheme, - AccentColorHex = this.AccentColorHex, - ShowGridByDefault = this.ShowGridByDefault, - SnapToGridByDefault = this.SnapToGridByDefault, - GridSnapSize = this.GridSnapSize, - DefaultZoomMode = this.DefaultZoomMode, - CompactRibbonByDefault = this.CompactRibbonByDefault, - AutoExpandInspectorOnSelect = this.AutoExpandInspectorOnSelect, - ShowShortcutHints = this.ShowShortcutHints, - AiSettings = this.AiSettings.Clone(), - UpdatedAt = this.UpdatedAt - }; - } -} diff --git a/src/PdfEditorApp/Models/WorkflowModels.cs b/src/PdfEditorApp/Models/WorkflowModels.cs deleted file mode 100644 index bb41126..0000000 --- a/src/PdfEditorApp/Models/WorkflowModels.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; - -namespace PdfEditorApp.Models; - -public class WorkflowStepDefinition -{ - public string Id { get; set; } = Guid.NewGuid().ToString("N"); - public PdfToolId ToolId { get; set; } - public string StepName { get; set; } = string.Empty; - public string StepDescription { get; set; } = string.Empty; - public bool IsEnabled { get; set; } = true; - public string ParametersJson { get; set; } = "{}"; - - public T GetParameters() where T : class, new() - { - try - { - if (string.IsNullOrWhiteSpace(ParametersJson)) return new T(); - return JsonSerializer.Deserialize(ParametersJson) ?? new T(); - } - catch - { - return new T(); - } - } - - public void SetParameters(T parameters) - { - ParametersJson = JsonSerializer.Serialize(parameters); - } -} - -public class WorkflowDefinition -{ - public string Id { get; set; } = Guid.NewGuid().ToString("N"); - public string Name { get; set; } = "Automated Document Pipeline"; - public string Description { get; set; } = "Chain multiple PDF operations together into a reusable automated pipeline"; - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime ModifiedAt { get; set; } = DateTime.UtcNow; - public string OutputDirectory { get; set; } = string.Empty; - public string OutputFileNamePattern { get; set; } = "{filename}_pipeline.pdf"; - public List Steps { get; set; } = new(); - - public WorkflowDefinition Clone() - { - var clone = new WorkflowDefinition - { - Id = Guid.NewGuid().ToString("N"), - Name = $"{Name} (Copy)", - Description = Description, - CreatedAt = DateTime.UtcNow, - ModifiedAt = DateTime.UtcNow, - OutputDirectory = OutputDirectory, - OutputFileNamePattern = OutputFileNamePattern - }; - - foreach (var s in Steps) - { - clone.Steps.Add(new WorkflowStepDefinition - { - Id = Guid.NewGuid().ToString("N"), - ToolId = s.ToolId, - StepName = s.StepName, - StepDescription = s.StepDescription, - IsEnabled = s.IsEnabled, - ParametersJson = s.ParametersJson - }); - } - - return clone; - } -} - -public record WorkflowProgressInfo -{ - public int CurrentStepIndex { get; init; } - public int TotalSteps { get; init; } - public string CurrentStepName { get; init; } = string.Empty; - public string CurrentFileName { get; init; } = string.Empty; - public double OverallProgressPercentage { get; init; } - public string StatusMessage { get; init; } = string.Empty; -} diff --git a/src/PdfEditorApp/PdfEditorApp.csproj b/src/PdfEditorApp/PdfEditorApp.csproj deleted file mode 100644 index a64b090..0000000 --- a/src/PdfEditorApp/PdfEditorApp.csproj +++ /dev/null @@ -1,52 +0,0 @@ - - - WinExe - net10.0 - enable - true - $(NoWarn);NU1901;NU1902;NU1903;NU1904 - Assets/app-logo.ico - app.manifest - 1.0.0 - FryPDF - FryPDF - Code Fry Dev - Copyright © 2026 Code Fry Dev - Privacy-first, professional desktop PDF creator and editor studio - - - - - - - - - - - - - - - - - - - - None - All - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Plugins/Bundles/AiProvidersBundle.cs b/src/PdfEditorApp/Plugins/Bundles/AiProvidersBundle.cs deleted file mode 100644 index bcbe42a..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/AiProvidersBundle.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Services.AI; -using PdfEditorApp.Services.AI.Providers; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing all built-in AI model providers. -/// -public class AiProvidersBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.AiProviders"; - public string Name => "AI Model Providers Bundle"; - public string Description => "Pluggable AI providers: local Ollama, ultra-fast Groq cloud inference, and OpenAI compatible endpoints."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new OllamaAiPlugin(), - new GroqAiPlugin(), - new OpenAiPlugin() - }; -} - -public class OllamaAiPlugin : IFryPlugin -{ - public string Id => "frypdf.ai.ollama"; - public string Name => "Ollama Local AI Provider"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => new[] { typeof(IAiProviderRegistry) }; - - public IReadOnlyDictionary? SettingsSchema => new Dictionary - { - ["Endpoint"] = new() - { - Type = "string", - Label = "Ollama Endpoint URL", - Description = "HTTP endpoint for local Ollama daemon", - DefaultValue = "http://localhost:11434" - }, - ["Model"] = new() - { - Type = "string", - Label = "Model Tag", - Description = "Default local model tag (e.g. llama3, mistral, phi3)", - DefaultValue = "llama3" - } - }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - var reg = ctx.GetService(); - reg.RegisterProvider(new OllamaAiProvider()); - return Task.CompletedTask; - } -} - -public class GroqAiPlugin : IFryPlugin -{ - public string Id => "frypdf.ai.groq"; - public string Name => "Groq Cloud AI Provider"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => new[] { typeof(IAiProviderRegistry) }; - - public IReadOnlyDictionary? SettingsSchema => new Dictionary - { - ["ApiKey"] = new() - { - Type = "secret", - Label = "Groq API Key", - Description = "API key from console.groq.com for ultra-fast Llama-3 / Mixtral inference", - DefaultValue = "" - }, - ["Model"] = new() - { - Type = "select", - Label = "Model", - Description = "Inference model name", - DefaultValue = "llama-3.3-70b-versatile", - Options = new List { "llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768" } - } - }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - var reg = ctx.GetService(); - reg.RegisterProvider(new GroqAiProvider()); - return Task.CompletedTask; - } -} - -public class OpenAiPlugin : IFryPlugin -{ - public string Id => "frypdf.ai.openai"; - public string Name => "OpenAI / Compatible Provider"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => new[] { typeof(IAiProviderRegistry) }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - var reg = ctx.GetService(); - reg.RegisterProvider(new OpenAiProvider()); - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/CanvasElementsBundle.cs b/src/PdfEditorApp/Plugins/Bundles/CanvasElementsBundle.cs deleted file mode 100644 index 620e9b2..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/CanvasElementsBundle.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing all built-in canvas elements for FryPDF. -/// Enables profile-level enabling/disabling (e.g. minimal viewer vs full publisher). -/// -public class CanvasElementsBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.CanvasElements"; - public string Name => "Canvas Elements Bundle"; - public string Description => "Core canvas elements: typography, geometry, charts, tables, LaTeX equations, form controls, and signatures."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new TextElementPlugin(), - new ShapeElementPlugin(), - new MediaElementPlugin(), - new DataVisualsElementPlugin(), - new MathElementPlugin(), - new FormElementsPlugin(), - new MarkupElementsPlugin() - }; -} - -public class TextElementPlugin : IFryPlugin -{ - public string Id => "frypdf.element.plugin.text"; - public string Name => "Typography Elements"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.text", - DisplayName = "Text Block", - ModelType = typeof(PdfTextElement), - ViewModelType = typeof(TextElementViewModel), - IconKind = "FormatColorText", - DefaultWidth = 200, - DefaultHeight = 60, - CanInsertFromToolbar = true, - InsertionCategory = "Basic", - ShortcutKey = "T", - SortOrder = 10, - Tags = new[] { "text", "typography", "heading", "label" }, - Factory = (sp, m) => new TextElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.divider", - DisplayName = "Divider Line", - ModelType = typeof(PdfDividerElement), - ViewModelType = typeof(DividerElementViewModel), - IconKind = "Minus", - DefaultWidth = 300, - DefaultHeight = 20, - CanInsertFromToolbar = true, - InsertionCategory = "Basic", - SortOrder = 20, - Tags = new[] { "divider", "line", "separator" }, - Factory = (sp, m) => new DividerElementViewModel() - }); - - return Task.CompletedTask; - } -} - -public class ShapeElementPlugin : IFryPlugin -{ - public string Id => "frypdf.element.plugin.shape"; - public string Name => "Geometric Shapes"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.shape", - DisplayName = "Geometric Shape", - ModelType = typeof(PdfShapeElement), - ViewModelType = typeof(ShapeElementViewModel), - IconKind = "ShapeOutline", - DefaultWidth = 150, - DefaultHeight = 100, - CanInsertFromToolbar = true, - InsertionCategory = "Basic", - ShortcutKey = "R", - SortOrder = 30, - Tags = new[] { "shape", "rectangle", "circle", "arrow", "polygon" }, - Factory = (sp, m) => new ShapeElementViewModel() - }); - - return Task.CompletedTask; - } -} - -public class MediaElementPlugin : IFryPlugin -{ - public string Id => "frypdf.element.plugin.media"; - public string Name => "Media Graphics"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.image", - DisplayName = "Image Graphic", - ModelType = typeof(PdfImageElement), - ViewModelType = typeof(ImageElementViewModel), - IconKind = "ImageOutline", - DefaultWidth = 200, - DefaultHeight = 150, - CanInsertFromToolbar = true, - InsertionCategory = "Media", - ShortcutKey = "I", - SortOrder = 40, - Tags = new[] { "image", "photo", "bitmap", "picture" }, - Factory = (sp, m) => new ImageElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.svg", - DisplayName = "Vector SVG Artwork", - ModelType = typeof(PdfSvgElement), - ViewModelType = typeof(SvgElementViewModel), - IconKind = "VectorSquare", - DefaultWidth = 180, - DefaultHeight = 180, - CanInsertFromToolbar = true, - InsertionCategory = "Media", - SortOrder = 45, - Tags = new[] { "svg", "vector", "graphic", "illustration" }, - Factory = (sp, m) => new SvgElementViewModel() - }); - - return Task.CompletedTask; - } -} - -public class DataVisualsElementPlugin : IFryPlugin -{ - public string Id => "frypdf.element.plugin.datavisuals"; - public string Name => "Data Visualizations"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.table", - DisplayName = "Data Table", - ModelType = typeof(PdfTableElement), - ViewModelType = typeof(TableElementViewModel), - IconKind = "TableLarge", - DefaultWidth = 400, - DefaultHeight = 160, - CanInsertFromToolbar = true, - InsertionCategory = "Tables & Data", - SortOrder = 50, - Tags = new[] { "table", "grid", "data", "columns", "rows" }, - Factory = (sp, m) => new TableElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.chart", - DisplayName = "Interactive Chart", - ModelType = typeof(PdfChartElement), - ViewModelType = typeof(ChartElementViewModel), - IconKind = "ChartBar", - DefaultWidth = 400, - DefaultHeight = 250, - CanInsertFromToolbar = true, - InsertionCategory = "Tables & Data", - SortOrder = 60, - Tags = new[] { "chart", "graph", "bar", "pie", "line", "candlestick", "radar", "waterfall" }, - Factory = (sp, m) => new ChartElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.qrcode", - DisplayName = "QR Code", - ModelType = typeof(PdfQrCodeElement), - ViewModelType = typeof(QrCodeElementViewModel), - IconKind = "Qrcode", - DefaultWidth = 120, - DefaultHeight = 120, - CanInsertFromToolbar = true, - InsertionCategory = "Tables & Data", - SortOrder = 70, - Tags = new[] { "qrcode", "barcode", "url", "2d" }, - Factory = (sp, m) => new QrCodeElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.barcode", - DisplayName = "Barcode", - ModelType = typeof(PdfBarcodeElement), - ViewModelType = typeof(BarcodeElementViewModel), - IconKind = "Barcode", - DefaultWidth = 200, - DefaultHeight = 80, - CanInsertFromToolbar = true, - InsertionCategory = "Tables & Data", - SortOrder = 80, - Tags = new[] { "barcode", "code128", "ean", "optical" }, - Factory = (sp, m) => new BarcodeElementViewModel() - }); - - return Task.CompletedTask; - } -} - -public class MathElementPlugin : IFryPlugin -{ - public string Id => "frypdf.element.plugin.math"; - public string Name => "Math Equations (LaTeX)"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.math", - DisplayName = "Math Equation", - ModelType = typeof(PdfMathElement), - ViewModelType = typeof(MathElementViewModel), - IconKind = "Sigma", - DefaultWidth = 240, - DefaultHeight = 60, - CanInsertFromToolbar = true, - InsertionCategory = "Equations", - ShortcutKey = "M", - SortOrder = 90, - Tags = new[] { "math", "formula", "equation", "latex", "integral", "matrix" }, - Factory = (sp, m) => new MathElementViewModel() - }); - - return Task.CompletedTask; - } -} - -public class FormElementsPlugin : IFryPlugin -{ - public string Id => "frypdf.element.plugin.forms"; - public string Name => "Interactive Form Controls"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.formfield", - DisplayName = "Form Field", - ModelType = typeof(PdfFormFieldElement), - ViewModelType = typeof(FormFieldElementViewModel), - IconKind = "TextBoxOutline", - DefaultWidth = 180, - DefaultHeight = 36, - CanInsertFromToolbar = true, - InsertionCategory = "Forms", - SortOrder = 100, - Tags = new[] { "form", "field", "input", "checkbox", "dropdown" }, - Factory = (sp, m) => new FormFieldElementViewModel() - }); - - return Task.CompletedTask; - } -} - -public class MarkupElementsPlugin : IFryPlugin -{ - public string Id => "frypdf.element.plugin.markup"; - public string Name => "Stamps, Redaction & Markup"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.watermark", - DisplayName = "Watermark", - ModelType = typeof(PdfWatermarkElement), - ViewModelType = typeof(WatermarkElementViewModel), - IconKind = "Watermark", - DefaultWidth = 400, - DefaultHeight = 120, - CanInsertFromToolbar = true, - InsertionCategory = "Annotations", - SortOrder = 110, - Tags = new[] { "watermark", "draft", "confidential", "diagonal" }, - Factory = (sp, m) => new WatermarkElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.redaction", - DisplayName = "Redaction Annotation", - ModelType = typeof(PdfRedactionElement), - ViewModelType = typeof(RedactionElementViewModel), - IconKind = "EyeOffOutline", - DefaultWidth = 160, - DefaultHeight = 40, - CanInsertFromToolbar = true, - InsertionCategory = "Annotations", - SortOrder = 120, - Tags = new[] { "redact", "redaction", "blackout", "security" }, - Factory = (sp, m) => new RedactionElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.stickynote", - DisplayName = "Sticky Review Note", - ModelType = typeof(PdfStickyNoteElement), - ViewModelType = typeof(StickyNoteElementViewModel), - IconKind = "NoteOutline", - DefaultWidth = 180, - DefaultHeight = 140, - CanInsertFromToolbar = true, - InsertionCategory = "Annotations", - ShortcutKey = "N", - SortOrder = 130, - Tags = new[] { "note", "comment", "review", "annotation", "sticky" }, - Factory = (sp, m) => new StickyNoteElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.ink", - DisplayName = "Freehand Drawing", - ModelType = typeof(PdfInkElement), - ViewModelType = typeof(InkElementViewModel), - IconKind = "DrawPen", - DefaultWidth = 150, - DefaultHeight = 100, - CanInsertFromToolbar = true, - InsertionCategory = "Annotations", - ShortcutKey = "D", - SortOrder = 140, - Tags = new[] { "ink", "pen", "draw", "highlight", "marker" }, - Factory = (sp, m) => new InkElementViewModel() - }); - - ctx.RegisterCanvasElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.measurement", - DisplayName = "Measurement Dimension", - ModelType = typeof(PdfMeasurementElement), - ViewModelType = typeof(MeasurementElementViewModel), - IconKind = "Ruler", - DefaultWidth = 200, - DefaultHeight = 30, - CanInsertFromToolbar = true, - InsertionCategory = "Annotations", - SortOrder = 150, - Tags = new[] { "measure", "dimension", "ruler", "cad" }, - Factory = (sp, m) => new MeasurementElementViewModel() - }); - - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/CommandPaletteBundle.cs b/src/PdfEditorApp/Plugins/Bundles/CommandPaletteBundle.cs deleted file mode 100644 index f0f21a8..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/CommandPaletteBundle.cs +++ /dev/null @@ -1,288 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing all standard Command Palette (⌘K / Ctrl+K) searchable actions. -/// -public class CommandPaletteBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.CommandPalette"; - public string Name => "Command Palette Actions Bundle"; - public string Description => "Extensible Command Palette (⌘K / Ctrl+K) and keyboard shortcuts for quick action navigation."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new FileCommandsPlugin(), - new EditCommandsPlugin(), - new InsertCommandsPlugin(), - new ViewCommandsPlugin(), - new SecurityCommandsPlugin() - }; -} - -public class FileCommandsPlugin : IFryPlugin -{ - public string Id => "frypdf.commands.file"; - public string Name => "File Operations Commands"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.file.new", - Title = "New Document / Templates", - Subtitle = "Browse executive templates or start a blank document", - Category = "File", - IconKind = "FilePlusOutline", - Shortcut = "⌘N", - Order = 10 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.file.save", - Title = "Save Project", - Subtitle = "Save editable FryPDF project archive (.frypdf)", - Category = "File", - IconKind = "ContentSaveOutline", - Shortcut = "⌘S", - Order = 20 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.file.open", - Title = "Open Project", - Subtitle = "Open existing FryPDF project archive (.frypdf)", - Category = "File", - IconKind = "FolderOpenOutline", - Shortcut = "⌘O", - Order = 30 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.file.export", - Title = "Export Production PDF", - Subtitle = "Compile document to high-resolution vector PDF", - Category = "File", - IconKind = "FilePdfBox", - Shortcut = "⌘E", - Order = 40 - }); - - return Task.CompletedTask; - } -} - -public class EditCommandsPlugin : IFryPlugin -{ - public string Id => "frypdf.commands.edit"; - public string Name => "Edit & History Commands"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.edit.undo", - Title = "Undo Action", - Subtitle = "Revert last canvas or page operation", - Category = "Edit", - IconKind = "Undo", - Shortcut = "⌘Z", - Order = 10 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.edit.redo", - Title = "Redo Action", - Subtitle = "Reapply reverted operation", - Category = "Edit", - IconKind = "Redo", - Shortcut = "⌘Y", - Order = 20 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.edit.cut", - Title = "Cut Element", - Subtitle = "Cut selected element to internal clipboard", - Category = "Edit", - IconKind = "ContentCut", - Shortcut = "⌘X", - Order = 30 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.edit.copy", - Title = "Copy Element", - Subtitle = "Copy selected element to internal clipboard", - Category = "Edit", - IconKind = "ContentCopy", - Shortcut = "⌘C", - Order = 40 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.edit.paste", - Title = "Paste Element", - Subtitle = "Paste element from clipboard to current page", - Category = "Edit", - IconKind = "ContentPaste", - Shortcut = "⌘V", - Order = 50 - }); - - return Task.CompletedTask; - } -} - -public class InsertCommandsPlugin : IFryPlugin -{ - public string Id => "frypdf.commands.insert"; - public string Name => "Insert Elements Commands"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.insert.text", - Title = "Insert Text Block", - Subtitle = "Add multi-line editable rich text block", - Category = "Insert", - IconKind = "FormatColorText", - Shortcut = "T", - Order = 10 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.insert.image", - Title = "Insert Image Graphic", - Subtitle = "Import PNG, JPEG, or WebP graphic from disk", - Category = "Insert", - IconKind = "ImageOutline", - Shortcut = "⌘I", - Order = 20 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.insert.table", - Title = "Insert Data Table", - Subtitle = "Add customizable multi-column data grid", - Category = "Insert", - IconKind = "TableLarge", - Order = 30 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.insert.math", - Title = "Insert Math Equation (LaTeX)", - Subtitle = "Add vector LaTeX mathematical equation", - Category = "Insert", - IconKind = "Sigma", - Shortcut = "⌘M", - Order = 40 - }); - - return Task.CompletedTask; - } -} - -public class ViewCommandsPlugin : IFryPlugin -{ - public string Id => "frypdf.commands.view"; - public string Name => "View & Navigation Commands"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.view.ribbon", - Title = "Toggle Ribbon Toolbar", - Subtitle = "Collapse or expand top ribbon tools panel", - Category = "View", - IconKind = "ViewAgendaOutline", - Shortcut = "⌘F1", - Order = 10 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.view.sidebar", - Title = "Toggle Pages Sidebar", - Subtitle = "Collapse or expand left thumbnails & outline sidebar", - Category = "View", - IconKind = "DockLeft", - Shortcut = "⌘B", - Order = 20 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.view.inspector", - Title = "Toggle Properties Inspector", - Subtitle = "Collapse or expand right formatting & properties panel", - Category = "View", - IconKind = "DockRight", - Shortcut = "⌘⇧P", - Order = 30 - }); - - return Task.CompletedTask; - } -} - -public class SecurityCommandsPlugin : IFryPlugin -{ - public string Id => "frypdf.commands.security"; - public string Name => "Security & Redaction Commands"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.security.sanitize", - Title = "Sanitize Document", - Subtitle = "Scrub author metadata and internal review notes", - Category = "Security", - IconKind = "ShieldCheck", - Order = 10 - }); - - ctx.RegisterCommand(new CommandPaletteDescriptor - { - Id = "cmd.security.redact", - Title = "Search & Redact Pattern", - Subtitle = "Auto-redact text occurrences on current page", - Category = "Security", - IconKind = "DatabaseSearchOutline", - Order = 20 - }); - - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/DataStudioBundle.cs b/src/PdfEditorApp/Plugins/Bundles/DataStudioBundle.cs deleted file mode 100644 index d2e9731..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/DataStudioBundle.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.BatchGeneration; -using PdfEditorApp.ViewModels.DataStudio; - -namespace PdfEditorApp.Plugins.Bundles; - -public class DataStudioBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.DataStudio"; - public string Name => "Data Studio & Batch Generation Bundle"; - public string Description => "Tabular dataset ingestion (Excel, CSV, REST APIs), dynamic data binding, and high-throughput batch PDF generation."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new DataStudioToolPlugin(), - new BatchGenerationToolPlugin() - }; -} - -public class DataStudioToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.datastudio"; - public override string Name => "Data Studio"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.WorkflowBuilder, - Name = Name, - Description = "Connect external datasets (Excel, CSV, REST API) and bind dynamic fields to PDF tables, charts, and text templates.", - Category = "AiAndAutomation", - IconKind = "DatabaseOutline", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".xlsx,.csv,.tsv,.json", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class BatchGenerationToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.batchgeneration"; - public override string Name => "Batch Mail Merge"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.BatchMailMerge, - Name = Name, - Description = "Generate hundreds of personalized PDFs (payslips, certificates, invoices, badges) in one click using Excel, CSV, or REST APIs.", - Category = "AiAndAutomation", - IconKind = "DatabaseArrowDownOutline", - IconColorHex = "#0F6CBD", - BackgroundAccentHex = "#EFF6FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".xlsx,.csv,.tsv,.json", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} diff --git a/src/PdfEditorApp/Plugins/Bundles/DialogsBundle.cs b/src/PdfEditorApp/Plugins/Bundles/DialogsBundle.cs deleted file mode 100644 index 3131595..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/DialogsBundle.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Views.Dialogs; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing all modal dialogs, studio overlays, and floating assistant inspectors. -/// -public class DialogsBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.Dialogs"; - public string Name => "Modal Dialogs & Studios Bundle"; - public string Description => "Modular modal dialogs: New Document, About, Shortcuts Help, Signatures, Security, Preflight Diagnostics, Watermark, Math Studio, and Data Studio."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new CoreAppDialogsPlugin(), - new SecurityDialogsPlugin(), - new DocumentUtilityDialogsPlugin(), - new AdvancedStudioDialogsPlugin() - }; -} - -public class CoreAppDialogsPlugin : IFryPlugin -{ - public string Id => "frypdf.dialogs.core"; - public string Name => "Core Application Dialogs"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.about", - Title = "About FryPDF", - ViewType = typeof(AboutDialog), - ViewFactory = sp => new AboutDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.shortcuts", - Title = "Keyboard Shortcuts", - ViewType = typeof(ShortcutsHelpDialog), - ViewFactory = sp => new ShortcutsHelpDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.plugins", - Title = "Plugins & Extensions Studio", - ViewType = typeof(PluginsDialog), - ViewFactory = sp => new PluginsDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.newdocument", - Title = "Create New Document", - ViewType = typeof(NewDocumentDialog), - ViewFactory = sp => new NewDocumentDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.exportsuccess", - Title = "Export Completed", - ViewType = typeof(ExportSuccessDialog), - ViewFactory = sp => new ExportSuccessDialog() - }); - - return Task.CompletedTask; - } -} - -public class SecurityDialogsPlugin : IFryPlugin -{ - public string Id => "frypdf.dialogs.security"; - public string Name => "Document Security & Privacy Dialogs"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.security", - Title = "Document Security & Permissions", - ViewType = typeof(DocumentSecurityDialog), - ViewFactory = sp => new DocumentSecurityDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.searchredact", - Title = "Search & Redact Sensitive Content", - ViewType = typeof(SearchRedactDialog), - ViewFactory = sp => new SearchRedactDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.watermark", - Title = "Watermark Manager", - ViewType = typeof(WatermarkManagerDialog), - ViewFactory = sp => new WatermarkManagerDialog() - }); - - return Task.CompletedTask; - } -} - -public class DocumentUtilityDialogsPlugin : IFryPlugin -{ - public string Id => "frypdf.dialogs.utilities"; - public string Name => "Document Utility Dialogs"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.preflight", - Title = "Preflight Diagnostics", - ViewType = typeof(PreflightDiagnosticsDialog), - ViewFactory = sp => new PreflightDiagnosticsDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.headerfooter", - Title = "Header & Footer Studio", - ViewType = typeof(HeaderFooterDialog), - ViewFactory = sp => new HeaderFooterDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.customstamp", - Title = "Custom Dynamic Stamps", - ViewType = typeof(CustomStampDialog), - ViewFactory = sp => new CustomStampDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.bates", - Title = "Bates Legal Numbering", - ViewType = typeof(BatesNumberingDialog), - ViewFactory = sp => new BatesNumberingDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.splitextract", - Title = "Split & Extract Pages", - ViewType = typeof(SplitExtractDialog), - ViewFactory = sp => new SplitExtractDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.compare", - Title = "Compare Documents", - ViewType = typeof(CompareDocumentsDialog), - ViewFactory = sp => new CompareDocumentsDialog() - }); - - return Task.CompletedTask; - } -} - -public class AdvancedStudioDialogsPlugin : IFryPlugin -{ - public string Id => "frypdf.dialogs.studios"; - public string Name => "Advanced Floating Studio Dialogs"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.signature", - Title = "Digital Signature Studio", - ViewType = typeof(SignatureStudioDialog), - ViewFactory = sp => new SignatureStudioDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.math", - Title = "LaTeX Math Formula Studio", - ViewType = typeof(MathEquationStudioDialog), - ViewFactory = sp => new MathEquationStudioDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.datastudio", - Title = "Data Studio", - ViewType = typeof(DataStudioDialog), - ViewFactory = sp => new DataStudioDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.workflow", - Title = "PDF Workflow Builder", - ViewType = typeof(WorkflowBuilderDialog), - ViewFactory = sp => new WorkflowBuilderDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.batch", - Title = "Batch Generation Studio", - ViewType = typeof(BatchGenerationDialog), - ViewFactory = sp => new BatchGenerationDialog() - }); - - ctx.RegisterDialog(new DialogDescriptor - { - Id = "frypdf.dialog.aiassistant", - Title = "AI Document Assistant", - ViewType = typeof(AiAssistantDialog), - ViewFactory = sp => new AiAssistantDialog() - }); - - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/DocumentIoBundle.cs b/src/PdfEditorApp/Plugins/Bundles/DocumentIoBundle.cs deleted file mode 100644 index c93f8d9..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/DocumentIoBundle.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Export; -using PdfEditorApp.Services.Import; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing all built-in document importers and multi-format exporters. -/// -public class DocumentIoBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.DocumentIo"; - public string Name => "Document I/O (Import & Export) Bundle"; - public string Description => "Comprehensive document import/export formats: PDF, Images (PNG/JPEG/WebP), Markdown, HTML5, Plain Text, and SVG."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new PdfIoPlugin(), - new ImageIoPlugin(), - new TextIoPlugin(), - new WebVectorIoPlugin() - }; -} - -public class PdfIoPlugin : IFryPlugin -{ - public string Id => "frypdf.io.pdf"; - public string Name => "Standard PDF Engine I/O"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - // Importer - ctx.RegisterImporter(new PdfDeconstructionImporter()); - - // Exporter - var exportService = ctx.GetService(); - ctx.RegisterExporter(new PdfDocumentExporter(exportService)); - - return Task.CompletedTask; - } -} - -public class ImageIoPlugin : IFryPlugin -{ - public string Id => "frypdf.io.images"; - public string Name => "Raster Image Document I/O"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterImporter(new ImageDocumentImporter()); - return Task.CompletedTask; - } -} - -public class TextIoPlugin : IFryPlugin -{ - public string Id => "frypdf.io.text"; - public string Name => "Text & Markdown Document I/O"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterImporter(new PlainTextDocumentImporter()); - ctx.RegisterExporter(new MarkdownDocumentExporter()); - ctx.RegisterExporter(new PlainTextDocumentExporter()); - return Task.CompletedTask; - } -} - -public class WebVectorIoPlugin : IFryPlugin -{ - public string Id => "frypdf.io.webvector"; - public string Name => "HTML5 & SVG Vector Document Exporter"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterExporter(new HtmlDocumentExporter()); - ctx.RegisterExporter(new SvgVectorExporter()); - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/EditorSidebarsBundle.cs b/src/PdfEditorApp/Plugins/Bundles/EditorSidebarsBundle.cs deleted file mode 100644 index 2d16415..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/EditorSidebarsBundle.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing left and right sidebar panels for the document editor workspace. -/// -public class EditorSidebarsBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.EditorSidebars"; - public string Name => "Editor Sidebars Bundle"; - public string Description => "Pluggable sidebar panels for the document editor: Page Thumbnails, Bookmarks Outline, and Review Comments."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new ThumbnailsSidebarPlugin(), - new OutlineSidebarPlugin(), - new CommentsSidebarPlugin() - }; -} - -public class ThumbnailsSidebarPlugin : IFryPlugin -{ - public string Id => "frypdf.sidebar.thumbnails"; - public string Name => "Page Thumbnails Sidebar"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterSidebarTab(new SidebarTabDescriptor - { - Id = "Thumbnails", - Title = "Page Thumbnails", - IconKind = "FileDocumentMultipleOutline", - Tooltip = "Page Thumbnails & Reordering", - Order = 10 - }); - return Task.CompletedTask; - } -} - -public class OutlineSidebarPlugin : IFryPlugin -{ - public string Id => "frypdf.sidebar.outline"; - public string Name => "Bookmarks & Outline Sidebar"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterSidebarTab(new SidebarTabDescriptor - { - Id = "Outline", - Title = "Outline & Bookmarks", - IconKind = "FormatListBulleted", - Tooltip = "Document Outline & Navigation Bookmarks", - Order = 20 - }); - return Task.CompletedTask; - } -} - -public class CommentsSidebarPlugin : IFryPlugin -{ - public string Id => "frypdf.sidebar.comments"; - public string Name => "Comments & Annotations Sidebar"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterSidebarTab(new SidebarTabDescriptor - { - Id = "Comments", - Title = "Comments & Review", - IconKind = "CommentTextMultipleOutline", - Tooltip = "Review Annotations & Sticky Notes", - Order = 30 - }); - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/InspectorBundle.cs b/src/PdfEditorApp/Plugins/Bundles/InspectorBundle.cs deleted file mode 100644 index e2540f2..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/InspectorBundle.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing modular properties inspector sections. -/// -public class InspectorBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.Inspector"; - public string Name => "Properties Inspector Bundle"; - public string Description => "Dynamic inspector property cards: Element Diagnostics, Table Grid Specifications, Chart Series, and Barcode Encoding Details."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new GeometryInspectorPlugin(), - new AppearanceInspectorPlugin(), - new TypographyInspectorPlugin(), - new TableGridInspectorPlugin(), - new ChartVisualizationInspectorPlugin(), - new ElementDiagnosticsInspectorPlugin(), - new BarcodeDetailsInspectorPlugin() - }; -} - -public class GeometryInspectorPlugin : IFryPlugin -{ - public string Id => "frypdf.inspector.geometry"; - public string Name => "Geometry & Transform Inspector Card"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterInspectorSection(new InspectorSectionDescriptor - { - SectionId = "frypdf.inspector.geometry", - Title = "Custom Geometry", - IconKind = "VectorRectangle", - Order = 10, - AppliesTo = target => false, - Factory = (sp, target) => "Geometry specification" - }); - - return Task.CompletedTask; - } -} - -public class AppearanceInspectorPlugin : IFryPlugin -{ - public string Id => "frypdf.inspector.appearance"; - public string Name => "Appearance & Styling Inspector Card"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterInspectorSection(new InspectorSectionDescriptor - { - SectionId = "frypdf.inspector.appearance", - Title = "Custom Appearance", - IconKind = "PaletteOutline", - Order = 20, - AppliesTo = target => false, - Factory = (sp, target) => "Appearance specification" - }); - - return Task.CompletedTask; - } -} - -public class TypographyInspectorPlugin : IFryPlugin -{ - public string Id => "frypdf.inspector.typography"; - public string Name => "Typography & Font Inspector Card"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterInspectorSection(new InspectorSectionDescriptor - { - SectionId = "frypdf.inspector.typography", - Title = "Custom Typography", - IconKind = "FormatFont", - Order = 30, - AppliesTo = target => false, - Factory = (sp, target) => "Typography specification" - }); - - return Task.CompletedTask; - } -} - -public class TableGridInspectorPlugin : IFryPlugin -{ - public string Id => "frypdf.inspector.table"; - public string Name => "Table Grid Inspector Card"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterInspectorSection(new InspectorSectionDescriptor - { - SectionId = "frypdf.inspector.table", - Title = "Table Grid Properties", - IconKind = "TableLarge", - Order = 40, - AppliesTo = target => target is TableElementViewModel, - Factory = (sp, target) => - { - if (target is TableElementViewModel table) - { - return $"Columns: {table.Headers.Count} | Rows: {table.Rows.Count}\nGrid Border: {table.BorderColorHex}"; - } - return "Table specification"; - } - }); - - return Task.CompletedTask; - } -} - -public class ChartVisualizationInspectorPlugin : IFryPlugin -{ - public string Id => "frypdf.inspector.chart"; - public string Name => "Chart Series Inspector Card"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterInspectorSection(new InspectorSectionDescriptor - { - SectionId = "frypdf.inspector.chart", - Title = "Chart Series & Visualization", - IconKind = "ChartLine", - Order = 50, - AppliesTo = target => target is ChartElementViewModel, - Factory = (sp, target) => - { - if (target is ChartElementViewModel chart) - { - return $"Chart Type: {chart.ChartTypeDescription}\nPalette: {chart.Palette}\nLegend: {chart.LegendPosition}"; - } - return "Chart specification"; - } - }); - - return Task.CompletedTask; - } -} - -public class ElementDiagnosticsInspectorPlugin : IFryPlugin -{ - public string Id => "frypdf.inspector.diagnostics"; - public string Name => "Element Diagnostics Inspector Card"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterInspectorSection(new InspectorSectionDescriptor - { - SectionId = "frypdf.inspector.diagnostics", - Title = "Element Diagnostics", - IconKind = "InformationOutline", - Order = 90, - AppliesTo = target => target is ElementViewModelBase, - Factory = (sp, target) => - { - if (target is ElementViewModelBase el) - { - string elType = el.GetType().Name.Replace("ViewModel", ""); - return $"Type: {elType}\nLayer: Z-Index {el.ZIndex}\nBounds: {el.Width:F0} × {el.Height:F0} pt (X: {el.X:F0}, Y: {el.Y:F0})"; - } - return "No element selected"; - } - }); - - return Task.CompletedTask; - } -} - -public class BarcodeDetailsInspectorPlugin : IFryPlugin -{ - public string Id => "frypdf.inspector.barcode"; - public string Name => "Barcode Details Inspector Card"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterInspectorSection(new InspectorSectionDescriptor - { - SectionId = "frypdf.inspector.barcode", - Title = "Barcode & Code Encoding", - IconKind = "Barcode", - Order = 45, - AppliesTo = target => target is BarcodeElementViewModel || target is QrCodeElementViewModel, - Factory = (sp, target) => - { - if (target is QrCodeElementViewModel qr) - { - return $"QR Code Payload:\n{qr.Content}"; - } - if (target is BarcodeElementViewModel bc) - { - return $"Barcode ({bc.BarcodeFormat}):\n{bc.CodeValue}"; - } - return "Encoding details"; - } - }); - - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/OcrEnginesBundle.cs b/src/PdfEditorApp/Plugins/Bundles/OcrEnginesBundle.cs deleted file mode 100644 index 0c41ab0..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/OcrEnginesBundle.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Services.Ocr; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing all built-in OCR engines (Apple Vision, Windows Media, Tesseract). -/// -public class OcrEnginesBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.OcrEngines"; - public string Name => "OCR Engines Bundle"; - public string Description => "Hardware-accelerated optical character recognition: macOS Apple Vision, Windows Media OCR, and Tesseract neural network models."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new AppleVisionOcrPlugin(), - new WindowsMediaOcrPlugin(), - new TesseractOcrPlugin() - }; -} - -public class AppleVisionOcrPlugin : IFryPlugin -{ - public string Id => "frypdf.ocr.applevision"; - public string Name => "Apple Vision Framework OCR (macOS)"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterOcrEngine(new AppleVisionOcrEngine()); - return Task.CompletedTask; - } -} - -public class WindowsMediaOcrPlugin : IFryPlugin -{ - public string Id => "frypdf.ocr.windowsmedia"; - public string Name => "Windows Media OCR Engine (Windows)"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterOcrEngine(new WindowsMediaOcrEngine()); - return Task.CompletedTask; - } -} - -public class TesseractOcrPlugin : IFryPlugin -{ - public string Id => "frypdf.ocr.tesseract"; - public string Name => "Tesseract Neural Network OCR"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - var modelService = ctx.TryGetService(out var ms) ? ms : new TesseractModelService(); - ctx.RegisterOcrEngine(new TesseractOcrEngine(modelService)); - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/ShellOverlaysBundle.cs b/src/PdfEditorApp/Plugins/Bundles/ShellOverlaysBundle.cs deleted file mode 100644 index fc27250..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/ShellOverlaysBundle.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Profiles; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing non-modal, interactive floating shell overlays. -/// Dynamic third-party and arcade overlays are installed on-demand from the FryPDF Marketplace. -/// -public class ShellOverlaysBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.ShellOverlays"; - public string Name => "Shell Overlays & Arcade Bundle"; - public string Description => "Slot provider for non-modal floating widgets and dynamic overlays rendered into the 'shell.overlay' slot."; - - public IReadOnlyList Plugins => Array.Empty(); -} diff --git a/src/PdfEditorApp/Plugins/Bundles/StandardTemplatesBundle.cs b/src/PdfEditorApp/Plugins/Bundles/StandardTemplatesBundle.cs deleted file mode 100644 index 35eb7cd..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/StandardTemplatesBundle.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Templates.Academic; -using PdfEditorApp.Templates.Career; -using PdfEditorApp.Templates.Certificates; -using PdfEditorApp.Templates.Corporate; -using PdfEditorApp.Templates.Education; -using PdfEditorApp.Templates.Events; -using PdfEditorApp.Templates.Finance; -using PdfEditorApp.Templates.General; -using PdfEditorApp.Templates.Technical; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing all 28 executive document templates across 8 professional domains. -/// -public class StandardTemplatesBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.Templates"; - public string Name => "Executive Templates Bundle"; - public string Description => "28 executive templates: Corporate Annual Reports, Payslips, Invoices, CVs, Academic Papers, Certificates, STEM Worksheets, and Invitations."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new CorporateTemplatesPlugin(), - new FinanceTemplatesPlugin(), - new CareerTemplatesPlugin(), - new AcademicTemplatesPlugin(), - new CertificateTemplatesPlugin(), - new EducationTemplatesPlugin(), - new PublishingTemplatesPlugin(), - new TechnicalTemplatesPlugin() - }; -} - -public class CorporateTemplatesPlugin : IFryPlugin -{ - public string Id => "frypdf.templates.corporate"; - public string Name => "Corporate Templates Pack"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTemplate(new AnnualReportTemplate()); - ctx.RegisterTemplate(new InteractiveExecutiveBriefTemplate()); - ctx.RegisterTemplate(new EmployeePayslipTemplate()); - ctx.RegisterTemplate(new OcrAnalysisReportTemplate()); - return Task.CompletedTask; - } -} - -public class FinanceTemplatesPlugin : IFryPlugin -{ - public string Id => "frypdf.templates.finance"; - public string Name => "Finance & Billing Templates Pack"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTemplate(new InvoiceTemplate()); - ctx.RegisterTemplate(new FinanceResearchPaperTemplate()); - return Task.CompletedTask; - } -} - -public class CareerTemplatesPlugin : IFryPlugin -{ - public string Id => "frypdf.templates.career"; - public string Name => "Career & Resume Templates Pack"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTemplate(new ResumeTemplate()); - ctx.RegisterTemplate(new ResumeModernCleanTemplate()); - ctx.RegisterTemplate(new ResumeCreativeMinimalistTemplate()); - ctx.RegisterTemplate(new ResumeAcademicCvTemplate()); - return Task.CompletedTask; - } -} - -public class AcademicTemplatesPlugin : IFryPlugin -{ - public string Id => "frypdf.templates.academic"; - public string Name => "Academic Research Templates Pack"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTemplate(new AcademicPaperTemplate()); - ctx.RegisterTemplate(new MathResearchPaperTemplate()); - ctx.RegisterTemplate(new PhysicsResearchPaperTemplate()); - ctx.RegisterTemplate(new HistoryResearchPaperTemplate()); - return Task.CompletedTask; - } -} - -public class CertificateTemplatesPlugin : IFryPlugin -{ - public string Id => "frypdf.templates.certificates"; - public string Name => "Certificates & Diplomas Templates Pack"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTemplate(new CertificateTemplate()); - ctx.RegisterTemplate(new CertificateNavyGoldTemplate()); - ctx.RegisterTemplate(new DiplomaAcademicTemplate()); - return Task.CompletedTask; - } -} - -public class EducationTemplatesPlugin : IFryPlugin -{ - public string Id => "frypdf.templates.education"; - public string Name => "STEM & Education Worksheets Templates Pack"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTemplate(new MathBODMASWorksheetTemplate()); - ctx.RegisterTemplate(new FactorizationWorksheetTemplate()); - ctx.RegisterTemplate(new BilingualExamPaperTemplate()); - ctx.RegisterTemplate(new StatesOfMatterDiagramNotesTemplate()); - ctx.RegisterTemplate(new QuadrilateralsGuideTemplate()); - return Task.CompletedTask; - } -} - -public class PublishingTemplatesPlugin : IFryPlugin -{ - public string Id => "frypdf.templates.publishing"; - public string Name => "Invitations & Publishing Templates Pack"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTemplate(new WeddingInvitationTraditionalTemplate()); - ctx.RegisterTemplate(new WeddingInvitationRoyalFloralTemplate()); - ctx.RegisterTemplate(new GalaInvitationTemplate()); - ctx.RegisterTemplate(new CreativeTypographyShowcaseTemplate()); - ctx.RegisterTemplate(new RichTextPublishingShowcaseTemplate()); - return Task.CompletedTask; - } -} - -public class TechnicalTemplatesPlugin : IFryPlugin -{ - public string Id => "frypdf.templates.technical"; - public string Name => "Technical Documentation Templates Pack"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTemplate(new ArduinoCheatSheetTemplate()); - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/StatusBarBundle.cs b/src/PdfEditorApp/Plugins/Bundles/StatusBarBundle.cs deleted file mode 100644 index 348ce31..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/StatusBarBundle.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing status bar widgets across the footer of the desktop window. -/// -public class StatusBarBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.StatusBar"; - public string Name => "Status Bar Widgets Bundle"; - public string Description => "Status bar footer indicators: document page statistics, system operational status, and graphics engine memory monitor."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new PageStatsWidgetPlugin(), - new SystemStatusWidgetPlugin(), - new MemoryMonitorWidgetPlugin() - }; -} - -public class PageStatsWidgetPlugin : IFryPlugin -{ - public string Id => "frypdf.status.pagestats"; - public string Name => "Document Page Stats Widget"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterStatusBarWidget(new StatusBarWidgetDescriptor - { - WidgetId = "frypdf.status.pagestats", - Alignment = StatusBarAlignment.Left, - Order = 10, - ToolTip = "Current Page Statistics", - Factory = sp => new ViewModels.StatusBarWidgetViewModel - { - WidgetId = "frypdf.status.pagestats", - Label = "Pages", - IconKind = "FileDocumentOutline", - ToolTip = "Active Document Page Statistics", - IsActive = true - } - }); - - return Task.CompletedTask; - } -} - -public class SystemStatusWidgetPlugin : IFryPlugin -{ - public string Id => "frypdf.status.ready"; - public string Name => "System Operational Status Widget"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterStatusBarWidget(new StatusBarWidgetDescriptor - { - WidgetId = "frypdf.status.ready", - Alignment = StatusBarAlignment.Left, - Order = 20, - ToolTip = "System Operational & Idle", - Factory = sp => new ViewModels.StatusBarWidgetViewModel - { - WidgetId = "frypdf.status.ready", - Label = "Ready", - IconKind = "CheckCircleOutline", - ToolTip = "System Operational & Idle", - IsActive = true - } - }); - - return Task.CompletedTask; - } -} - -public class MemoryMonitorWidgetPlugin : IFryPlugin -{ - public string Id => "frypdf.status.memory"; - public string Name => "Skia Engine Memory Widget"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterStatusBarWidget(new StatusBarWidgetDescriptor - { - WidgetId = "frypdf.status.memory", - Alignment = StatusBarAlignment.Right, - Order = 90, - ToolTip = "SkiaSharp 64-Bit Graphics Engine", - Factory = sp => new ViewModels.StatusBarWidgetViewModel - { - WidgetId = "frypdf.status.memory", - Label = "Skia 64-bit", - IconKind = "Memory", - ToolTip = "SkiaSharp 64-Bit Graphics Engine", - IsActive = true - } - }); - - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Bundles/ToolsConversionBundle.cs b/src/PdfEditorApp/Plugins/Bundles/ToolsConversionBundle.cs deleted file mode 100644 index 6ba9fdb..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/ToolsConversionBundle.cs +++ /dev/null @@ -1,285 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools.Conversion; - -namespace PdfEditorApp.Plugins.Bundles; - -public class ToolsConversionBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.Tools.Conversion"; - public string Name => "PDF Conversion Tools Bundle"; - public string Description => "Bi-directional conversions between PDF and Office (Word, Excel, PowerPoint), Images, HTML, Markdown, and PDF/A."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new PdfToWordToolPlugin(), - new WordToPdfToolPlugin(), - new PdfToPowerPointToolPlugin(), - new PowerPointToPdfToolPlugin(), - new PdfToExcelToolPlugin(), - new ExcelToPdfToolPlugin(), - new PdfToJpgToolPlugin(), - new JpgToPdfToolPlugin(), - new HtmlToPdfToolPlugin(), - new PdfToMarkdownToolPlugin(), - new PdfToPdfAToolPlugin(), - new ScanToPdfToolPlugin() - }; -} - -public class PdfToWordToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.pdftoword"; - public override string Name => "PDF to Word"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PdfToWord, - Name = Name, - Description = "Easily convert your PDF files into easy to edit DOC and DOCX documents.", - Category = "ConvertFromPdf", - IconKind = "FileWordOutline", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class WordToPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.wordtopdf"; - public override string Name => "Word to PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.WordToPdf, - Name = Name, - Description = "Make DOC and DOCX files easy to read by converting them to PDF.", - Category = "ConvertToPdf", - IconKind = "FileWordOutline", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".doc,.docx", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class PdfToPowerPointToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.pdftopowerpoint"; - public override string Name => "PDF to PowerPoint"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PdfToPowerPoint, - Name = Name, - Description = "Turn your PDF files into easy to edit PPT and PPTX slideshows.", - Category = "ConvertFromPdf", - IconKind = "FilePowerpointOutline", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class PowerPointToPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.powerpointtopdf"; - public override string Name => "PowerPoint to PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PowerPointToPdf, - Name = Name, - Description = "Make PPT and PPTX slideshows easy to view by converting them to PDF.", - Category = "ConvertToPdf", - IconKind = "FilePowerpointOutline", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - SupportsMultiFile = true, - AcceptedFileExtensions = ".ppt,.pptx", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class PdfToExcelToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.pdftoexcel"; - public override string Name => "PDF to Excel"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PdfToExcel, - Name = Name, - Description = "Pull data straight from PDFs into Excel spreadsheets in a few short seconds.", - Category = "ConvertFromPdf", - IconKind = "FileExcelOutline", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class ExcelToPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.exceltopdf"; - public override string Name => "Excel to PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.ExcelToPdf, - Name = Name, - Description = "Make EXCEL spreadsheets easy to read by converting them to PDF.", - Category = "ConvertToPdf", - IconKind = "FileExcelOutline", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - SupportsMultiFile = true, - AcceptedFileExtensions = ".xls,.xlsx", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class PdfToJpgToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.pdftojpg"; - public override string Name => "PDF to JPG"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PdfToJpg, - Name = Name, - Description = "Convert each PDF page into a JPG or extract all images contained in a PDF.", - Category = "ConvertFromPdf", - IconKind = "FileImageOutline", - IconColorHex = "#D97706", - BackgroundAccentHex = "#FFFBEB", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class JpgToPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.jpgtopdf"; - public override string Name => "JPG to PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.JpgToPdf, - Name = Name, - Description = "Convert JPG, PNG, BMP, GIF and TIFF images to PDF in seconds with custom orientation and margins.", - Category = "ConvertToPdf", - IconKind = "FileImageOutline", - IconColorHex = "#D97706", - BackgroundAccentHex = "#FFFBEB", - SupportsMultiFile = true, - AcceptedFileExtensions = ".jpg,.jpeg,.png,.bmp,.gif,.webp", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class HtmlToPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.htmltopdf"; - public override string Name => "HTML to PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.HtmlToPdf, - Name = Name, - Description = "Convert webpages or raw HTML files in seconds. Copy and paste the URL or drop local HTML files.", - Category = "ConvertToPdf", - IconKind = "LanguageHtml5", - IconColorHex = "#E11D48", - BackgroundAccentHex = "#FFF1F2", - SupportsMultiFile = false, - AcceptedFileExtensions = ".html,.htm", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class PdfToMarkdownToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.pdftomarkdown"; - public override string Name => "PDF to Markdown"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PdfToMarkdown, - Name = Name, - Description = "Extract clean, readable Markdown content and structured tables from your PDF documents.", - Category = "ConvertFromPdf", - IconKind = "LanguageMarkdownOutline", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class PdfToPdfAToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.pdftopdfa"; - public override string Name => "PDF to PDF/A"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PdfToPdfA, - Name = Name, - Description = "Transform your PDF to PDF/A, the ISO-standardized version of PDF for long-term digital preservation.", - Category = "ConvertFromPdf", - IconKind = "ArchiveOutline", - IconColorHex = "#0D9488", - BackgroundAccentHex = "#F0FDFA", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class ScanToPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.scantopdf"; - public override string Name => "Scan to PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.ScanToPdf, - Name = Name, - Description = "Capture document photos or connect a scanner to convert physical paperwork directly into PDF.", - Category = "ConvertToPdf", - IconKind = "Scanner", - IconColorHex = "#4F46E5", - BackgroundAccentHex = "#EEF2FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".jpg,.jpeg,.png,.bmp,.tiff", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} diff --git a/src/PdfEditorApp/Plugins/Bundles/ToolsIntelligenceBundle.cs b/src/PdfEditorApp/Plugins/Bundles/ToolsIntelligenceBundle.cs deleted file mode 100644 index 3163b5e..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/ToolsIntelligenceBundle.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools.Intelligence; - -namespace PdfEditorApp.Plugins.Bundles; - -public class ToolsIntelligenceBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.Tools.Intelligence"; - public string Name => "PDF Intelligence & Editing Tools Bundle"; - public string Description => "AI summarization, machine translation, OCR text extraction, visual document diffing, and PDF form design."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new AiSummarizerToolPlugin(), - new TranslatePdfToolPlugin(), - new OcrPdfToolPlugin(), - new ComparePdfToolPlugin(), - new EditPdfToolPlugin(), - new PdfFormsToolPlugin() - }; -} - -public class AiSummarizerToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.aisummarizer"; - public override string Name => "AI Summarizer"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.AiSummarizer, - Name = Name, - Description = "Generate comprehensive, structured executive summaries and key takeaway bullet points using AI.", - Category = "AiAndAutomation", - IconKind = "Sparkles", - IconColorHex = "#7C3AED", - BackgroundAccentHex = "#F5F3FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class TranslatePdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.translate"; - public override string Name => "Translate PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.TranslatePdf, - Name = Name, - Description = "Translate entire PDF documents into 50+ world languages while preserving visual layout and geometry.", - Category = "AiAndAutomation", - IconKind = "Translate", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class OcrPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.ocr"; - public override string Name => "OCR PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.OcrPdf, - Name = Name, - Description = "Convert scanned documents and image PDFs into fully searchable, selectable, and copyable text.", - Category = "AiAndAutomation", - IconKind = "TextRecognition", - IconColorHex = "#D97706", - BackgroundAccentHex = "#FFFBEB", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf,.jpg,.jpeg,.png", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class ComparePdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.compare"; - public override string Name => "Compare PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.ComparePdf, - Name = Name, - Description = "Show a side-by-side comparison of two PDF documents and easily spot differences, additions, and deletions.", - Category = "AiAndAutomation", - IconKind = "FileCompare", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class EditPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.edit"; - public override string Name => "Edit PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.EditPdf, - Name = Name, - Description = "Add text, images, shapes or freehand annotations to a PDF document. Edit the size, font, and color of added content.", - Category = "EditAndForms", - IconKind = "SquareEditOutline", - IconColorHex = "#7C3AED", - BackgroundAccentHex = "#F5F3FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class PdfFormsToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.forms"; - public override string Name => "PDF Forms"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PdfForms, - Name = Name, - Description = "Design interactive AcroForms with text inputs, checkboxes, radio options, dropdowns, and digital signature boxes.", - Category = "EditAndForms", - IconKind = "FormSelect", - IconColorHex = "#059669", - BackgroundAccentHex = "#ECFDF5", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} diff --git a/src/PdfEditorApp/Plugins/Bundles/ToolsOrganizeBundle.cs b/src/PdfEditorApp/Plugins/Bundles/ToolsOrganizeBundle.cs deleted file mode 100644 index f361297..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/ToolsOrganizeBundle.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools.Organize; - -namespace PdfEditorApp.Plugins.Bundles; - -public class ToolsOrganizeBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.Tools.Organize"; - public string Name => "PDF Organize & Page Tools Bundle"; - public string Description => "Essential page manipulation and document organizing tools (Merge, Split, Rotate, Organize, Crop, Page Numbers)."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new MergePdfToolPlugin(), - new SplitPdfToolPlugin(), - new RotatePdfToolPlugin(), - new OrganizePdfToolPlugin(), - new CropPdfToolPlugin(), - new PageNumbersToolPlugin() - }; -} - -public class MergePdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.merge"; - public override string Name => "Merge PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.MergePdf, - Name = Name, - Description = "Combine PDFs in the order you want with the easiest PDF merger available.", - Category = "OrganizeAndPage", - IconKind = "CallMerge", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class SplitPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.split"; - public override string Name => "Split PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.SplitPdf, - Name = Name, - Description = "Separate one page or a whole set for easy conversion into independent PDF files.", - Category = "OrganizeAndPage", - IconKind = "CallSplit", - IconColorHex = "#DC2626", - BackgroundAccentHex = "#FEF2F2", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class RotatePdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.rotate"; - public override string Name => "Rotate PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.RotatePdf, - Name = Name, - Description = "Rotate your PDF pages however you'd like. You can even rotate multiple PDFs at once!", - Category = "OrganizeAndPage", - IconKind = "RotateRight", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class OrganizePdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.organize"; - public override string Name => "Organize PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.OrganizePdf, - Name = Name, - Description = "Sort, add and delete PDF pages. Drag and drop the page thumbnails to reorganize them as you wish.", - Category = "OrganizeAndPage", - IconKind = "ViewGridOutline", - IconColorHex = "#7C3AED", - BackgroundAccentHex = "#F5F3FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class CropPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.crop"; - public override string Name => "Crop PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.CropPdf, - Name = Name, - Description = "Trim margins, remove unnecessary whitespace, and adjust canvas bounding boxes.", - Category = "OrganizeAndPage", - IconKind = "Crop", - IconColorHex = "#059669", - BackgroundAccentHex = "#ECFDF5", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class PageNumbersToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.pagenumbers"; - public override string Name => "Page Numbers"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.PageNumbers, - Name = Name, - Description = "Add page numbers into PDFs with ease. Choose position, dimensions, typography and formatting.", - Category = "OrganizeAndPage", - IconKind = "Numeric", - IconColorHex = "#D97706", - BackgroundAccentHex = "#FFFBEB", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} diff --git a/src/PdfEditorApp/Plugins/Bundles/ToolsSecurityBundle.cs b/src/PdfEditorApp/Plugins/Bundles/ToolsSecurityBundle.cs deleted file mode 100644 index 8066bf5..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/ToolsSecurityBundle.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools.Security; - -namespace PdfEditorApp.Plugins.Bundles; - -public class ToolsSecurityBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.Tools.Security"; - public string Name => "PDF Optimization & Security Tools Bundle"; - public string Description => "Document optimization, compression, encryption, digital signatures, redaction and watermarking."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new CompressPdfToolPlugin(), - new RepairPdfToolPlugin(), - new ProtectPdfToolPlugin(), - new UnlockPdfToolPlugin(), - new SignPdfToolPlugin(), - new RedactPdfToolPlugin(), - new WatermarkToolPlugin() - }; -} - -public class CompressPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.compress"; - public override string Name => "Compress PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.CompressPdf, - Name = Name, - Description = "Reduce file size while optimizing for maximal PDF quality.", - Category = "OptimizeAndSecurity", - IconKind = "ArrowCollapseAll", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class RepairPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.repair"; - public override string Name => "Repair PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.RepairPdf, - Name = Name, - Description = "Repair a damaged PDF and recover data from corrupt files. Fix PDF files with our Repair tool.", - Category = "OptimizeAndSecurity", - IconKind = "WrenchOutline", - IconColorHex = "#D97706", - BackgroundAccentHex = "#FFFBEB", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class ProtectPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.protect"; - public override string Name => "Protect PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.ProtectPdf, - Name = Name, - Description = "Encrypt and password protect your PDF files. Prevent unauthorized access to sensitive information.", - Category = "OptimizeAndSecurity", - IconKind = "LockOutline", - IconColorHex = "#DC2626", - BackgroundAccentHex = "#FEF2F2", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class UnlockPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.unlock"; - public override string Name => "Unlock PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.UnlockPdf, - Name = Name, - Description = "Remove PDF password security, giving you the freedom to use your PDFs as you want.", - Category = "OptimizeAndSecurity", - IconKind = "LockOpenOutline", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class SignPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.sign"; - public override string Name => "Sign PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.SignPdf, - Name = Name, - Description = "Sign yourself or request electronic signatures from others. Draw, type, or upload an image of your signature.", - Category = "OptimizeAndSecurity", - IconKind = "Draw", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class RedactPdfToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.redact"; - public override string Name => "Redact PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.RedactPdf, - Name = Name, - Description = "Permanently remove sensitive text, numbers, and images from your PDF documents for compliance.", - Category = "OptimizeAndSecurity", - IconKind = "FormatColorFill", - IconColorHex = "#475569", - BackgroundAccentHex = "#F8FAFC", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} - -public class WatermarkToolPlugin : ToolPluginBase -{ - public override string Id => "frypdf.tool.watermark"; - public override string Name => "Watermark PDF"; - - protected override PdfToolDescriptor CreateDescriptor() => new() - { - Id = Id, - LegacyId = (int)PdfToolId.Watermark, - Name = Name, - Description = "Stamp an image or text over your PDF in seconds. Choose typography, transparency and position.", - Category = "OptimizeAndSecurity", - IconKind = "Watermark", - IconColorHex = "#0D9488", - BackgroundAccentHex = "#F0FDFA", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf", - CreateViewModel = sp => ActivatorUtilities.CreateInstance(sp) - }; -} diff --git a/src/PdfEditorApp/Plugins/Bundles/WorkspacePagesBundle.cs b/src/PdfEditorApp/Plugins/Bundles/WorkspacePagesBundle.cs deleted file mode 100644 index 355d540..0000000 --- a/src/PdfEditorApp/Plugins/Bundles/WorkspacePagesBundle.cs +++ /dev/null @@ -1,404 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.ViewModels; -using PdfEditorApp.Views; - -namespace PdfEditorApp.Plugins.Bundles; - -/// -/// Plugin bundle providing all workspace pages and sidebar navigation sections. -/// -public class WorkspacePagesBundle : IFryPluginBundle -{ - public string Id => "FryPdf.Bundle.WorkspacePages"; - public string Name => "Workspace Pages & Navigation Bundle"; - public string Description => "Modular workspace pages for the studio: Dashboard, PDF Reader, Templates Gallery, Tools Studios, OCR Models, Fonts, and Preferences."; - - public IReadOnlyList Plugins => new IFryPlugin[] - { - new HomeDashboardPagePlugin(), - new PdfReaderLandingPagePlugin(), - new NewDocumentPagePlugin(), - new PdfToolsStudioPagePlugin(), - new ToolCategoryPagesPlugin(), - new StarredToolsPagePlugin(), - new FontManagerPagePlugin(), - new TesseractPagePlugin(), - new TrashCachePagePlugin(), - new HelpGuidePagePlugin(), - new LicensingPagePlugin(), - new PluginsPagePlugin(), - new SettingsPagePlugin() - }; -} - -public class HomeDashboardPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.dashboard"; - public string Name => "Overview Dashboard Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "Home", - Title = "Home", - Group = "Overview", - IconKind = "HomeOutline", - Order = 10, - ViewFactory = null // Built-in view mounted in HomeView.axaml for 0ms instantaneous navigation - }); - return Task.CompletedTask; - } -} - -public class PdfReaderLandingPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.reader"; - public string Name => "PDF Reader Landing Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "PdfReader", - Title = "PDF Reader", - Group = "Overview", - IconKind = "BookOpenPageVariantOutline", - BadgeText = "Read", - BadgeColorHex = "#DC2626", - Order = 20, - ViewFactory = null // Built-in view mounted in HomeView.axaml for 0ms instantaneous navigation - }); - return Task.CompletedTask; - } -} - -public class NewDocumentPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.newdocument"; - public string Name => "New Document & Template Gallery Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "NewDocument", - Title = "New Document", - Group = "Overview", - IconKind = "FileDocumentPlusOutline", - BadgeText = "19", - BadgeColorHex = "#059669", - Order = 30, - ViewFactory = null // Built-in view mounted in HomeView.axaml for 0ms instantaneous navigation - }); - return Task.CompletedTask; - } -} - -public class PdfToolsStudioPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.tools"; - public string Name => "PDF Tools Studio Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "AllTools", - Title = "All Tools", - Group = "Overview", - IconKind = "ViewGridOutline", - BadgeText = "32", - BadgeColorHex = "#4F46E5", - Order = 40, - ViewFactory = null // Built-in view mounted in HomeView.axaml for 0ms instantaneous navigation - }); - return Task.CompletedTask; - } -} - -public class ToolCategoryPagesPlugin : IFryPlugin -{ - public string Id => "frypdf.page.toolcategories"; - public string Name => "Tool Categories Pages"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "OrganizeAndPage", - Title = "Organize & Page", - Group = "Categories", - IconKind = "BookOpenPageVariantOutline", - BadgeText = "6", - Order = 50, - ViewFactory = null - }); - - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "OptimizeAndSecurity", - Title = "Security & Optimize", - Group = "Categories", - IconKind = "ShieldLockOutline", - BadgeText = "7", - Order = 60, - ViewFactory = null - }); - - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "ConvertFromPdf", - Title = "Convert from PDF", - Group = "Categories", - IconKind = "ExportVariant", - BadgeText = "5", - Order = 70, - ViewFactory = null - }); - - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "ConvertToPdf", - Title = "Convert to PDF", - Group = "Categories", - IconKind = "Import", - BadgeText = "6", - Order = 80, - ViewFactory = null - }); - - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "EditAndForms", - Title = "Edit & Forms", - Group = "Categories", - IconKind = "Draw", - BadgeText = "4", - Order = 90, - ViewFactory = null - }); - - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "AiAndAutomation", - Title = "AI & Automation", - Group = "Categories", - IconKind = "AutoFix", - BadgeText = "4", - Order = 100, - ViewFactory = null - }); - - return Task.CompletedTask; - } -} - -public class StarredToolsPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.starred"; - public string Name => "Starred Tools Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "Starred", - Title = "Starred Tools", - Group = "Library", - IconKind = "StarOutline", - Order = 110, - ViewFactory = null - }); - return Task.CompletedTask; - } -} - -public class FontManagerPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.fonts"; - public string Name => "Language & Fonts Studio Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "FontPackages", - Title = "Language & Fonts", - Group = "Library", - IconKind = "Translate", - BadgeText = "10", - BadgeColorHex = "#0284C7", - Order = 120, - ViewFactory = null - }); - return Task.CompletedTask; - } -} - -public class TesseractPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.tesseract"; - public string Name => "OCR Models Manager Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "TesseractData", - Title = "OCR Models", - Group = "Library", - IconKind = "TextRecognition", - Order = 130, - ViewFactory = null - }); - return Task.CompletedTask; - } -} - -public class TrashCachePagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.trash"; - public string Name => "Trash & Cache Cleaner Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "Trash", - Title = "Trash & Cache", - Group = "Library", - IconKind = "TrashCanOutline", - Order = 140, - ViewFactory = null - }); - return Task.CompletedTask; - } -} - -public class HelpGuidePagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.help"; - public string Name => "Help & Guides Studio Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "Help", - Title = "Help & Guides", - Group = "Library", - IconKind = "HelpCircleOutline", - BadgeText = "Guides", - BadgeColorHex = "#0284C7", - Order = 150, - ViewFactory = null - }); - return Task.CompletedTask; - } -} - -public class LicensingPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.licensing"; - public string Name => "Licenses & Tools Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "Licensing", - Title = "Licenses & Tools", - Group = "Library", - IconKind = "CertificateOutline", - BadgeText = "12", - BadgeColorHex = "#4F46E5", - Order = 160, - ViewFactory = null - }); - return Task.CompletedTask; - } -} - -public class SettingsPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.settings"; - public string Name => "Settings & UI Preferences Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "Settings", - Title = "Settings & UI", - Group = "Preferences", - IconKind = "TuneVariant", - BadgeText = "UI", - BadgeColorHex = "#7C3AED", - Order = 170, - ViewFactory = null - }); - return Task.CompletedTask; - } -} - -public class PluginsPagePlugin : IFryPlugin -{ - public string Id => "frypdf.page.plugins"; - public string Name => "Plugins & Extensions Studio Page"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "Plugins", - Title = "Plugins & Extensions", - Group = "Preferences", - IconKind = "PuzzleOutline", - BadgeText = "Store", - BadgeColorHex = "#7C3AED", - Order = 165, - ViewFactory = sp => new PluginsManagerPageView - { - DataContext = (sp?.GetService(typeof(HomeViewModel)) as HomeViewModel)?.PluginsManager - ?? sp?.GetService(typeof(PluginsManagerViewModel)) as PluginsManagerViewModel - ?? new PluginsManagerViewModel( - sp?.GetService(typeof(PluginHost)) as PluginHost, - sp?.GetService(typeof(Core.Plugins.Marketplace.IPluginMarketplaceService)) as Core.Plugins.Marketplace.IPluginMarketplaceService) - } - }); - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Plugins/Cli/HeadlessCliRunner.cs b/src/PdfEditorApp/Plugins/Cli/HeadlessCliRunner.cs deleted file mode 100644 index 073fdb0..0000000 --- a/src/PdfEditorApp/Plugins/Cli/HeadlessCliRunner.cs +++ /dev/null @@ -1,316 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Models; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; - -namespace PdfEditorApp.Plugins.Cli; - -/// -/// Headless Command-Line Runner for FryPDF, enabling batch operations and CI/CD pipelines -/// without launching an Avalonia desktop window. -/// -public static class HeadlessCliRunner -{ - private static readonly HashSet CliFlags = new(StringComparer.OrdinalIgnoreCase) - { - "--tool", "-t", - "--list-tools", - "--list-plugins", - "--profile", - "--help", "-h", - "--version", "-v" - }; - - /// - /// Checks whether the command line arguments indicate a headless CLI execution. - /// - public static bool IsCliInvocation(string[] args) - { - if (args == null || args.Length == 0) return false; - return args.Any(arg => CliFlags.Contains(arg)); - } - - /// - /// Executes the headless command line interface. - /// - public static async Task RunCliAsync(string[] args) - { - Console.WriteLine("=================================================="); - Console.WriteLine(" FryPDF Headless Studio (Plugin-Based CLI)"); - Console.WriteLine(" Powered by .NET 10 & QuestPDF / SkiaSharp"); - Console.WriteLine("=================================================="); - - var parsedArgs = ParseArguments(args); - - if (parsedArgs.ContainsKey("--help") || parsedArgs.ContainsKey("-h")) - { - PrintHelp(); - return 0; - } - - if (parsedArgs.ContainsKey("--version") || parsedArgs.ContainsKey("-v")) - { - Console.WriteLine("FryPDF version 1.0.0 (Plugin Architecture Edition)"); - return 0; - } - - // Initialize Services & Plugin Host - var services = new ServiceCollection(); - ConfigureHeadlessServices(services); - using var sp = services.BuildServiceProvider(); - - var pluginHost = sp.GetRequiredService(); - var toolRegistry = sp.GetRequiredService(); - var operationsService = sp.GetRequiredService(); - - // Load Bundles - var bundles = new IFryPluginBundle[] - { - new ToolsOrganizeBundle(), - new ToolsSecurityBundle(), - new ToolsConversionBundle(), - new ToolsIntelligenceBundle(), - new DataStudioBundle(), - new CanvasElementsBundle(), - new DocumentIoBundle(), - new AiProvidersBundle(), - new OcrEnginesBundle(), - new StandardTemplatesBundle(), - new StatusBarBundle(), - new InspectorBundle(), - new CommandPaletteBundle(), - new WorkspacePagesBundle(), - new DialogsBundle(), - new EditorSidebarsBundle() - }; - - // Determine profile - var profilePath = parsedArgs.GetValueOrDefault("--profile"); - if (!string.IsNullOrWhiteSpace(profilePath) && File.Exists(profilePath)) - { - var profile = ProfileLoader.LoadFromFile(profilePath); - ProfileLoader.ApplyProfile(profile, pluginHost, bundles); - } - else - { - // Default headless setup: register all bundles - foreach (var bundle in bundles) - { - pluginHost.RegisterPlugins(bundle.Plugins); - } - } - - await pluginHost.StartAsync(); - - if (parsedArgs.ContainsKey("--list-plugins")) - { - Console.WriteLine("\nActive Loaded Plugins:"); - foreach (var plugin in pluginHost.LoadedPlugins) - { - Console.WriteLine($" - [{plugin.Id}] {plugin.Name} (v{plugin.Version})"); - } - return 0; - } - - if (parsedArgs.ContainsKey("--list-tools")) - { - Console.WriteLine("\nRegistered PDF Tools:"); - var allTools = toolRegistry.GetAllTools(); - foreach (var tool in allTools) - { - Console.WriteLine($" - {tool.StringId,-25} | {tool.Name,-20} | Category: {tool.Category}"); - } - return 0; - } - - // Tool Execution - var toolId = parsedArgs.GetValueOrDefault("--tool") ?? parsedArgs.GetValueOrDefault("-t"); - if (string.IsNullOrWhiteSpace(toolId)) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Error: No tool specified. Use --tool or --list-tools."); - Console.ResetColor(); - return 1; - } - - var toolDef = toolRegistry.GetTool(toolId); - if (toolDef == null) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: Tool '{toolId}' was not recognized. Run --list-tools to see all available tools."); - Console.ResetColor(); - return 1; - } - - var inputs = parsedArgs.GetValueOrDefault("--input") ?? parsedArgs.GetValueOrDefault("-i"); - var output = parsedArgs.GetValueOrDefault("--output") ?? parsedArgs.GetValueOrDefault("-o"); - - if (string.IsNullOrWhiteSpace(inputs)) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Error: Please provide one or more input files via -i or --input."); - Console.ResetColor(); - return 1; - } - - var inputFiles = inputs.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (var file in inputFiles) - { - if (!File.Exists(file)) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: Input file not found: {file}"); - Console.ResetColor(); - return 1; - } - } - - output ??= $"output_{Path.GetFileName(inputFiles[0])}"; - - Console.WriteLine($"\nExecuting Tool: {toolDef.Name} ({toolDef.StringId})"); - Console.WriteLine($"Inputs: {string.Join(", ", inputFiles)}"); - Console.WriteLine($"Output: {output}\n"); - - var progress = new Progress(p => - { - Console.Write($"\rProgress: {p:F0}%"); - }); - - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - - try - { - ToolExecutionResult result; - - if (string.Equals(toolDef.StringId, "frypdf.tool.merge", StringComparison.OrdinalIgnoreCase) || toolDef.Id == PdfToolId.MergePdf) - { - result = await operationsService.PageService.MergePdfAsync(new MergeToolOptions - { - InputFiles = inputFiles.ToList(), - OutputFilePath = output - }, progress, CancellationToken.None); - } - else if (string.Equals(toolDef.StringId, "frypdf.tool.compress", StringComparison.OrdinalIgnoreCase) || toolDef.Id == PdfToolId.CompressPdf) - { - result = await operationsService.OptimizationService.CompressPdfAsync(new CompressToolOptions - { - InputFilePath = inputFiles[0], - OutputFilePath = output, - Level = PdfCompressionLevel.Balanced - }, progress, CancellationToken.None); - } - else if (string.Equals(toolDef.StringId, "frypdf.tool.split", StringComparison.OrdinalIgnoreCase) || toolDef.Id == PdfToolId.SplitPdf) - { - var outDir = Path.GetDirectoryName(output); - if (string.IsNullOrWhiteSpace(outDir)) outDir = "."; - result = await operationsService.PageService.SplitPdfAsync(new SplitToolOptions - { - InputFilePath = inputFiles[0], - OutputDirectory = outDir, - Mode = SplitExtractMode.SplitEveryNPages, - PagesPerSplit = 1 - }, progress, CancellationToken.None); - } - else - { - Console.WriteLine($"Note: Executing via generic tool pipeline dispatcher for '{toolDef.StringId}'..."); - result = new ToolExecutionResult - { - Success = true, - OutputFilePath = output, - Message = $"Executed {toolDef.Name} successfully." - }; - } - - stopwatch.Stop(); - Console.WriteLine(); - - if (result.Success) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"\n[SUCCESS] Operation completed in {stopwatch.ElapsedMilliseconds}ms!"); - if (!string.IsNullOrEmpty(result.OutputFilePath)) - { - Console.WriteLine($"Generated Output: {result.OutputFilePath}"); - } - if (result.OriginalSizeBytes > 0 && result.OutputSizeBytes > 0) - { - Console.WriteLine($"Saved: {result.SavingsPercentage:F1}% ({result.OriginalSizeBytes:N0} bytes -> {result.OutputSizeBytes:N0} bytes)"); - } - Console.ResetColor(); - return 0; - } - else - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"\n[FAILED] {result.ErrorMessage}"); - Console.ResetColor(); - return 1; - } - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"\n[ERROR] Unhandled exception: {ex.Message}"); - Console.ResetColor(); - return 1; - } - } - - private static Dictionary ParseArguments(string[] args) - { - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - - for (int i = 0; i < args.Length; i++) - { - var arg = args[i]; - if (arg.StartsWith("-")) - { - if (i + 1 < args.Length && !args[i + 1].StartsWith("-")) - { - dict[arg] = args[i + 1]; - i++; - } - else - { - dict[arg] = "true"; - } - } - } - - return dict; - } - - private static void PrintHelp() - { - Console.WriteLine("\nUsage: dotnet run --project src/PdfEditorApp -- [options]\n"); - Console.WriteLine("Options:"); - Console.WriteLine(" --tool, -t Specify tool ID to run (e.g. frypdf.tool.merge)"); - Console.WriteLine(" --input, -i Comma-separated input PDF files"); - Console.WriteLine(" --output, -o Target output PDF or directory"); - Console.WriteLine(" --profile Custom profile JSON path (e.g. profiles/headless.profile.json)"); - Console.WriteLine(" --list-tools List all registered PDF tools"); - Console.WriteLine(" --list-plugins List all loaded plugin components"); - Console.WriteLine(" --version, -v Show version information"); - Console.WriteLine(" --help, -h Display this help menu\n"); - Console.WriteLine("Examples:"); - Console.WriteLine(" dotnet run --project src/PdfEditorApp -- --tool frypdf.tool.merge -i doc1.pdf,doc2.pdf -o merged.pdf"); - Console.WriteLine(" dotnet run --project src/PdfEditorApp -- --tool frypdf.tool.compress -i large.pdf -o compressed.pdf"); - Console.WriteLine(" dotnet run --project src/PdfEditorApp -- --list-tools\n"); - } - - private static void ConfigureHeadlessServices(IServiceCollection services) - { - App.ConfigureServices(services); - } -} diff --git a/src/PdfEditorApp/Plugins/Loader/FryPluginPackageLoader.cs b/src/PdfEditorApp/Plugins/Loader/FryPluginPackageLoader.cs deleted file mode 100644 index c8642f0..0000000 --- a/src/PdfEditorApp/Plugins/Loader/FryPluginPackageLoader.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Text.Json; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Manifests; - -namespace PdfEditorApp.Plugins.Loader; - -/// -/// Result of unpacking and loading a self-contained .fryplugin package. -/// -public sealed class FryPluginPackageResult : IDisposable -{ - public required PluginManifest Manifest { get; init; } - public required PluginAssemblyPackage AssemblyPackage { get; init; } - public required string InstallDirectory { get; init; } - - public void Dispose() - { - AssemblyPackage.Dispose(); - } -} - -/// -/// Manages packing, unpacking, validation, and loading of self-contained .fryplugin distribution archives. -/// A .fryplugin is a standard ZIP archive containing a plugin.json manifest, entry assemblies, dependencies, and assets. -/// -public static class FryPluginPackageLoader -{ - /// - /// Unpacks a .fryplugin package to the target plugins directory and loads its contained plugins. - /// - public static FryPluginPackageResult UnpackAndLoad(string packageFilePath, string? targetPluginsDirectory = null) - { - ArgumentException.ThrowIfNullOrWhiteSpace(packageFilePath); - if (!File.Exists(packageFilePath)) - { - throw new FileNotFoundException($"Plugin package '{packageFilePath}' not found."); - } - - var baseDirectory = targetPluginsDirectory ?? Path.Combine(AppContext.BaseDirectory, "plugins"); - Directory.CreateDirectory(baseDirectory); - - // 1. Read manifest from ZIP before extracting to know target folder name - PluginManifest? manifest = null; - using (var archive = ZipFile.OpenRead(packageFilePath)) - { - var manifestEntry = archive.Entries.FirstOrDefault(e => - string.Equals(e.FullName, "plugin.json", StringComparison.OrdinalIgnoreCase) || - string.Equals(Path.GetFileName(e.FullName), "plugin.json", StringComparison.OrdinalIgnoreCase)); - - if (manifestEntry != null) - { - using var stream = manifestEntry.Open(); - using var reader = new StreamReader(stream); - var json = reader.ReadToEnd(); - manifest = JsonSerializer.Deserialize(json); - } - } - - var pluginId = !string.IsNullOrWhiteSpace(manifest?.Id) - ? manifest.Id - : Path.GetFileNameWithoutExtension(packageFilePath); - - var destinationFolder = Path.Combine(baseDirectory, pluginId); - if (Directory.Exists(destinationFolder)) - { - try - { - Directory.Delete(destinationFolder, recursive: true); - } - catch - { - // If locked, create unique timestamped folder - destinationFolder = Path.Combine(baseDirectory, $"{pluginId}_{DateTime.UtcNow.Ticks}"); - } - } - - Directory.CreateDirectory(destinationFolder); - ZipFile.ExtractToDirectory(packageFilePath, destinationFolder, overwriteFiles: true); - - // 2. Identify entry assembly DLL - string? entryDll = null; - if (!string.IsNullOrWhiteSpace(manifest?.EntryPoint)) - { - var candidate = Path.Combine(destinationFolder, manifest.EntryPoint); - if (File.Exists(candidate)) - { - entryDll = candidate; - } - } - - if (entryDll == null) - { - // Search for DLL matching pluginId or any DLL containing IFryPlugin - var allDlls = Directory.GetFiles(destinationFolder, "*.dll", SearchOption.AllDirectories); - entryDll = allDlls.FirstOrDefault(d => string.Equals(Path.GetFileNameWithoutExtension(d), pluginId, StringComparison.OrdinalIgnoreCase)) - ?? allDlls.FirstOrDefault(); - } - - if (string.IsNullOrWhiteSpace(entryDll) || !File.Exists(entryDll)) - { - throw new InvalidOperationException($"No entry DLL found in package '{packageFilePath}'."); - } - - // 3. Load assembly into isolated collectible ALC - var assemblyPackage = PluginAssemblyLoader.LoadPluginAssembly(entryDll); - - // Create fallback manifest if plugin.json was not packaged - if (manifest == null) - { - var firstPlugin = assemblyPackage.Plugins.FirstOrDefault(); - manifest = new PluginManifest - { - Id = firstPlugin?.Id ?? pluginId, - Name = firstPlugin?.Name ?? pluginId, - Version = firstPlugin?.Version.ToString() ?? "1.0.0", - EntryPoint = Path.GetFileName(entryDll), - Description = "Extracted .fryplugin archive" - }; - } - - return new FryPluginPackageResult - { - Manifest = manifest, - AssemblyPackage = assemblyPackage, - InstallDirectory = destinationFolder - }; - } - - /// - /// Creates a .fryplugin package archive from a directory containing a plugin.json manifest and built DLLs. - /// - public static string CreatePackage(string sourceDirectory, string outputPackagePath) - { - ArgumentException.ThrowIfNullOrWhiteSpace(sourceDirectory); - ArgumentException.ThrowIfNullOrWhiteSpace(outputPackagePath); - - if (!Directory.Exists(sourceDirectory)) - { - throw new DirectoryNotFoundException($"Source directory '{sourceDirectory}' not found."); - } - - var outDir = Path.GetDirectoryName(outputPackagePath); - if (!string.IsNullOrWhiteSpace(outDir)) - { - Directory.CreateDirectory(outDir); - } - - if (File.Exists(outputPackagePath)) - { - File.Delete(outputPackagePath); - } - - ZipFile.CreateFromDirectory(sourceDirectory, outputPackagePath, CompressionLevel.Optimal, includeBaseDirectory: false); - return outputPackagePath; - } -} diff --git a/src/PdfEditorApp/Plugins/Loader/PluginAssemblyLoader.cs b/src/PdfEditorApp/Plugins/Loader/PluginAssemblyLoader.cs deleted file mode 100644 index dd07755..0000000 --- a/src/PdfEditorApp/Plugins/Loader/PluginAssemblyLoader.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.Loader; -using PdfEditorApp.Core.Plugins; - -namespace PdfEditorApp.Plugins.Loader; - -/// -/// Container holding instantiated plugins from an isolated assembly, along with its collectible load context. -/// -public sealed class PluginAssemblyPackage : IDisposable -{ - private readonly CollectiblePluginLoadContext _context; - private bool _isDisposed; - - public IReadOnlyList Plugins { get; } - public string AssemblyPath { get; } - - public PluginAssemblyPackage( - string assemblyPath, - IReadOnlyList plugins, - CollectiblePluginLoadContext context) - { - AssemblyPath = assemblyPath; - Plugins = plugins; - _context = context; - } - - public void Dispose() - { - if (_isDisposed) return; - _isDisposed = true; - - if (_context.IsCollectible) - { - _context.Unload(); - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - } -} - -/// -/// Collectible that allows dynamic plugin DLLs to be loaded and later completely unloaded. -/// -public sealed class CollectiblePluginLoadContext : AssemblyLoadContext -{ - private readonly AssemblyDependencyResolver _resolver; - private readonly string _pluginDirectory; - - public CollectiblePluginLoadContext(string pluginPath, bool isCollectible = false) - : base(name: Path.GetFileNameWithoutExtension(pluginPath), isCollectible: isCollectible) - { - _resolver = new AssemblyDependencyResolver(pluginPath); - _pluginDirectory = Path.GetDirectoryName(pluginPath) ?? string.Empty; - } - - protected override Assembly? Load(AssemblyName assemblyName) - { - string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); - if (assemblyPath != null && File.Exists(assemblyPath)) - { - return LoadFromAssemblyPath(assemblyPath); - } - - // Direct fallback: check for .dll in the plugin directory - if (!string.IsNullOrEmpty(_pluginDirectory)) - { - var candidate = Path.Combine(_pluginDirectory, $"{assemblyName.Name}.dll"); - if (File.Exists(candidate)) - { - return LoadFromAssemblyPath(candidate); - } - - // Also search plugin directory recursively if dependencies are in subfolders - try - { - var match = Directory.GetFiles(_pluginDirectory, $"{assemblyName.Name}.dll", SearchOption.AllDirectories).FirstOrDefault(); - if (match != null && File.Exists(match)) - { - return LoadFromAssemblyPath(match); - } - } - catch { } - } - - return null; - } - - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); - if (libraryPath != null && File.Exists(libraryPath)) - { - return LoadUnmanagedDllFromPath(libraryPath); - } - - // Direct fallback: check in runtimes//native/ or plugin directory - if (!string.IsNullOrEmpty(_pluginDirectory)) - { - string rid = OperatingSystem.IsMacOS() - ? (System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm64 ? "osx-arm64" : "osx-x64") - : OperatingSystem.IsWindows() - ? (System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm64 ? "win-arm64" : "win-x64") - : (System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm64 ? "linux-arm64" : "linux-x64"); - - var candidatePaths = new[] - { - Path.Combine(_pluginDirectory, "runtimes", rid, "native", unmanagedDllName), - Path.Combine(_pluginDirectory, "runtimes", rid, "native", $"{unmanagedDllName}.dylib"), - Path.Combine(_pluginDirectory, "runtimes", rid, "native", $"lib{unmanagedDllName}.dylib"), - Path.Combine(_pluginDirectory, "runtimes", rid, "native", $"{unmanagedDllName}.so"), - Path.Combine(_pluginDirectory, "runtimes", rid, "native", $"lib{unmanagedDllName}.so"), - Path.Combine(_pluginDirectory, "runtimes", rid, "native", $"{unmanagedDllName}.dll"), - Path.Combine(_pluginDirectory, unmanagedDllName), - Path.Combine(_pluginDirectory, $"{unmanagedDllName}.dylib"), - Path.Combine(_pluginDirectory, $"lib{unmanagedDllName}.dylib"), - Path.Combine(_pluginDirectory, $"{unmanagedDllName}.dll") - }; - - foreach (var p in candidatePaths) - { - if (File.Exists(p)) - { - return LoadUnmanagedDllFromPath(p); - } - } - } - - return base.LoadUnmanagedDll(unmanagedDllName); - } -} - -/// -/// Discovers and loads external .NET 10 plugin assemblies at runtime. -/// -public static class PluginAssemblyLoader -{ - private static readonly List _activePackages = new(); - - /// - /// Loads an isolated assembly, instantiates any implementations, and returns a package. - /// - public static PluginAssemblyPackage LoadPluginAssembly(string assemblyPath) - { - ArgumentException.ThrowIfNullOrWhiteSpace(assemblyPath); - if (!File.Exists(assemblyPath)) - { - throw new FileNotFoundException($"Plugin assembly '{assemblyPath}' not found."); - } - - var fullPath = Path.GetFullPath(assemblyPath); - var alc = new CollectiblePluginLoadContext(fullPath); - var assembly = alc.LoadFromAssemblyPath(fullPath); - - var pluginTypes = assembly.GetTypes() - .Where(t => typeof(IFryPlugin).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface) - .ToList(); - - var plugins = new List(); - foreach (var type in pluginTypes) - { - try - { - if (Activator.CreateInstance(type) is IFryPlugin instance) - { - plugins.Add(instance); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginAssemblyLoader] Could not instantiate '{type.FullName}': {ex.Message}"); - } - } - - var package = new PluginAssemblyPackage(fullPath, plugins, alc); - lock (_activePackages) - { - _activePackages.Add(package); - } - return package; - } - - /// - /// Scans a directory for plugin packages (.fryplugin), plugin subdirectories, and standalone DLLs, - /// loading all discovered plugin packages into isolated collectible ALC contexts. - /// - public static IReadOnlyList DiscoverAndLoadDirectory(string pluginsDirectory) - { - if (string.IsNullOrWhiteSpace(pluginsDirectory) || !Directory.Exists(pluginsDirectory)) - { - return Array.Empty(); - } - - var packages = new List(); - var loadedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); - - // 1. Discover and unpack .fryplugin packages - var packageFiles = Directory.GetFiles(pluginsDirectory, "*.fryplugin", SearchOption.TopDirectoryOnly); - foreach (var pkgFile in packageFiles) - { - try - { - var pkgResult = FryPluginPackageLoader.UnpackAndLoad(pkgFile, pluginsDirectory); - if (pkgResult.AssemblyPackage.Plugins.Count > 0) - { - packages.Add(pkgResult.AssemblyPackage); - loadedPaths.Add(pkgResult.AssemblyPackage.AssemblyPath); - } - else - { - pkgResult.Dispose(); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginAssemblyLoader] Failed to unpack/load package '{pkgFile}': {ex.Message}"); - } - } - - // 2. Discover unpacked plugin subdirectories - var subDirectories = Directory.GetDirectories(pluginsDirectory); - foreach (var subDir in subDirectories) - { - try - { - string? entryDll = null; - var manifestFile = Path.Combine(subDir, "plugin.json"); - if (File.Exists(manifestFile)) - { - var json = File.ReadAllText(manifestFile); - var manifest = System.Text.Json.JsonSerializer.Deserialize(json); - if (!string.IsNullOrWhiteSpace(manifest?.EntryPoint)) - { - var candidate = Path.Combine(subDir, manifest.EntryPoint); - if (File.Exists(candidate)) - { - entryDll = candidate; - } - } - } - - if (entryDll == null) - { - var folderName = Path.GetFileName(subDir); - var candidate = Path.Combine(subDir, $"{folderName}.dll"); - if (File.Exists(candidate)) - { - entryDll = candidate; - } - else - { - entryDll = Directory.GetFiles(subDir, "*.dll", SearchOption.TopDirectoryOnly).FirstOrDefault(); - } - } - - if (entryDll != null && !loadedPaths.Contains(entryDll)) - { - var package = LoadPluginAssembly(entryDll); - if (package.Plugins.Count > 0) - { - packages.Add(package); - loadedPaths.Add(entryDll); - } - else - { - package.Dispose(); - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginAssemblyLoader] Failed to load plugin from subdirectory '{subDir}': {ex.Message}"); - } - } - - // 3. Discover top-level standalone DLLs - var dllFiles = Directory.GetFiles(pluginsDirectory, "*.dll", SearchOption.TopDirectoryOnly); - foreach (var dll in dllFiles) - { - if (loadedPaths.Contains(dll)) continue; - - try - { - var package = LoadPluginAssembly(dll); - if (package.Plugins.Count > 0) - { - packages.Add(package); - loadedPaths.Add(dll); - } - else - { - // No plugins in this assembly; unload immediately - package.Dispose(); - } - } - catch - { - // Skip non-.NET or incompatible assemblies - } - } - - return packages; - } -} diff --git a/src/PdfEditorApp/Plugins/ToolPluginBase.cs b/src/PdfEditorApp/Plugins/ToolPluginBase.cs deleted file mode 100644 index 89e65af..0000000 --- a/src/PdfEditorApp/Plugins/ToolPluginBase.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; - -namespace PdfEditorApp.Plugins; - -/// -/// Convenient base class for standard FryPDF tool plugins. -/// Automatically registers its into the active context during . -/// -public abstract class ToolPluginBase : IFryPlugin -{ - public abstract string Id { get; } - public abstract string Name { get; } - public virtual Version Version => new(1, 0, 0); - public virtual IReadOnlyList RequiredServices => Array.Empty(); - public virtual IReadOnlyList ProvidedServices => Array.Empty(); - - protected abstract PdfToolDescriptor CreateDescriptor(); - - public virtual Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTool(CreateDescriptor()); - return Task.CompletedTask; - } -} diff --git a/src/PdfEditorApp/Program.cs b/src/PdfEditorApp/Program.cs deleted file mode 100644 index 973479f..0000000 --- a/src/PdfEditorApp/Program.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Avalonia; -using System; -using PdfEditorApp.Plugins.Cli; - -namespace PdfEditorApp; - -sealed class Program -{ - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called: things aren't initialized - // yet and stuff might break. - [STAThread] - public static int Main(string[] args) - { - if (HeadlessCliRunner.IsCliInvocation(args)) - { - return HeadlessCliRunner.RunCliAsync(args).GetAwaiter().GetResult(); - } - - return BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } - - // Avalonia configuration, don't remove; also used by visual designer. - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() -#if DEBUG - .WithDeveloperTools() -#endif - .WithInterFont() - .LogToTrace(); -} diff --git a/src/PdfEditorApp/Services/AI/AiProviderRegistry.cs b/src/PdfEditorApp/Services/AI/AiProviderRegistry.cs deleted file mode 100644 index f0815d4..0000000 --- a/src/PdfEditorApp/Services/AI/AiProviderRegistry.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Services.AI.Providers; - -namespace PdfEditorApp.Services.AI; - -public class AiProviderRegistry : IAiProviderRegistry -{ - private readonly ConcurrentDictionary _providers = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public AiProviderRegistry() - { - RegisterBuiltInProviders(); - } - - private void RegisterBuiltInProviders() - { - RegisterProvider(new OllamaAiProvider()); - RegisterProvider(new GroqAiProvider()); - RegisterProvider(new OpenAiProvider()); - } - - public IDisposable RegisterProvider(IAiProvider provider) - { - ArgumentNullException.ThrowIfNull(provider); - _providers[provider.ProviderId] = provider; - RegistryChanged?.Invoke(); - - return new DisposableAction(() => - { - _providers.TryRemove(provider.ProviderId, out _); - RegistryChanged?.Invoke(); - }); - } - - public IAiProvider? GetProvider(string providerId) - { - if (string.IsNullOrWhiteSpace(providerId)) return null; - return _providers.GetValueOrDefault(providerId); - } - - public IReadOnlyList GetAllProviders() - { - return _providers.Values.ToList(); - } - - private sealed class DisposableAction(Action action) : IDisposable - { - private Action? _action = action; - public void Dispose() => Interlocked.Exchange(ref _action, null)?.Invoke(); - } -} diff --git a/src/PdfEditorApp/Services/AI/AiService.cs b/src/PdfEditorApp/Services/AI/AiService.cs deleted file mode 100644 index 79fac3d..0000000 --- a/src/PdfEditorApp/Services/AI/AiService.cs +++ /dev/null @@ -1,695 +0,0 @@ -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using OllamaSharp; -using OpenAI; -using OpenAI.Chat; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services.AI; - -/// -/// Core implementation of IAiService leveraging Microsoft.Extensions.AI, OllamaSharp, and OpenAI SDK. -/// -public class AiService : IAiService -{ - private readonly HttpClient _httpClient; - private readonly IAiProviderRegistry _providerRegistry; - private readonly List _ollamaLibraryCache = new(); - private readonly object _libraryLock = new(); - - public AiService(HttpClient? httpClient = null, IAiProviderRegistry? providerRegistry = null) - { - _httpClient = httpClient ?? new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; - _providerRegistry = providerRegistry ?? new AiProviderRegistry(); - if (_httpClient.DefaultRequestHeaders.UserAgent.Count == 0) - { - _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); - } - } - - /// - public async Task> DiscoverOllamaModelsAsync(string endpoint, CancellationToken ct = default) - { - var results = new List(); - - if (string.IsNullOrWhiteSpace(endpoint)) - { - endpoint = "http://localhost:11434"; - } - - bool isRemote = !string.IsNullOrWhiteSpace(endpoint) && - !endpoint.Contains("localhost", StringComparison.OrdinalIgnoreCase) && - !endpoint.Contains("127.0.0.1"); - - try - { - string url = $"{endpoint.TrimEnd('/')}/api/tags"; - var response = await _httpClient.GetFromJsonAsync(url, ct); - - if (response?.Models != null && response.Models.Count > 0) - { - foreach (var m in response.Models) - { - string paramSize = m.Details?.ParameterSize ?? ExtractParamSizeFromName(m.Name); - bool hasRemoteHost = !string.IsNullOrWhiteSpace(m.RemoteHost); - bool isCloudModel = isRemote || - hasRemoteHost || - m.Name.Contains("cloud", StringComparison.OrdinalIgnoreCase) || - (m.Size > 0 && m.Size < 1024 * 1024 && !string.IsNullOrEmpty(paramSize)); - - var modelTier = isCloudModel ? AiModelTier.FreeCloud : AiModelTier.FreeLocal; - string formattedSize = (isCloudModel && m.Size < 1024 * 1024) - ? "Ollama Cloud" - : AiModelInfo.FormatBytes(m.Size); - - var capabilitiesList = new List(); - if (m.Capabilities != null && m.Capabilities.Count > 0) - { - foreach (var cap in m.Capabilities) - { - capabilitiesList.Add(Capitalize(cap)); - } - } - else - { - if (isCloudModel) capabilitiesList.Add("Cloud"); - capabilitiesList.Add("Completion"); - } - string capStr = string.Join(" • ", capabilitiesList); - - string hostNote = hasRemoteHost ? $" [via {m.RemoteHost}]" : string.Empty; - string desc = !string.IsNullOrEmpty(m.Details?.Family) - ? $"{Capitalize(m.Details.Family)} architecture ({paramSize}) [{(isCloudModel ? "Ollama Free Cloud" : "Local Free")}]{hostNote}" - : (isCloudModel ? $"Ollama Free Cloud model{hostNote}" : "Local Ollama model"); - - string category = isCloudModel ? "Ollama Cloud (Installed)" : "Installed on Ollama"; - - results.Add(new AiModelInfo - { - Id = m.Name, - DisplayName = FormatModelDisplayName(m.Name), - Provider = AiProviderType.OllamaLocal, - Tier = modelTier, - Category = category, - Capabilities = capStr, - SizeBytes = m.Size, - FormattedSize = formattedSize, - ParameterSize = paramSize, - Description = desc, - IsInstalledLocally = !isRemote && !hasRemoteHost - }); - } - } - } - catch - { - // If Ollama is not currently running or unreachable, return empty list gracefully - } - - return results; - } - - /// - public IReadOnlyList GetCloudCatalogModels() - { - return new List - { - new() - { - Id = "llama-3.1-8b-instant", - DisplayName = "Llama 3.1 8B (Groq Free Cloud)", - Provider = AiProviderType.CustomOpenAiCompatible, - Tier = AiModelTier.FreeCloud, - Description = "Ultra-fast free cloud tier inference powered by Groq LPU", - ParameterSize = "8B Cloud", - IsInstalledLocally = false - }, - new() - { - Id = "openai/gpt-oss-120b", - DisplayName = "GPT-OSS 120B Reasoning (Groq Cloud)", - Provider = AiProviderType.CustomOpenAiCompatible, - Tier = AiModelTier.FreeCloud, - Description = "Open-weights 120B reasoning model powered by Groq LPUs", - ParameterSize = "120B Cloud", - IsInstalledLocally = false - }, - new() - { - Id = "meta-llama/llama-3.2-3b-instruct:free", - DisplayName = "Llama 3.2 3B (OpenRouter Free Cloud)", - Provider = AiProviderType.CustomOpenAiCompatible, - Tier = AiModelTier.FreeCloud, - Description = "Official free cloud tier model via OpenRouter API", - ParameterSize = "3B Cloud", - IsInstalledLocally = false - }, - new() - { - Id = "gpt-4o-mini", - DisplayName = "GPT-4o Mini (OpenAI)", - Provider = AiProviderType.OpenAiCloud, - Tier = AiModelTier.PaidCloud, - Description = "Fast, lightweight flagship-grade intelligence (recommended for high accuracy)", - ParameterSize = "Cloud API", - IsInstalledLocally = false - }, - new() - { - Id = "gpt-4o", - DisplayName = "GPT-4o (OpenAI)", - Provider = AiProviderType.OpenAiCloud, - Tier = AiModelTier.PaidCloud, - Description = "Omni flagship model with complex layout reasoning and multimodal capabilities", - ParameterSize = "Cloud API", - IsInstalledLocally = false - }, - new() - { - Id = "o1-mini", - DisplayName = "o1-mini Reasoning (OpenAI)", - Provider = AiProviderType.OpenAiCloud, - Tier = AiModelTier.PaidCloud, - Description = "Deep reasoning model for math, complex data tables, and dense structure", - ParameterSize = "Reasoning Cloud", - IsInstalledLocally = false - }, - new() - { - Id = "llama-3.3-70b-versatile", - DisplayName = "Llama 3.3 70B (Groq / OpenRouter)", - Provider = AiProviderType.CustomOpenAiCompatible, - Tier = AiModelTier.PaidCloud, - Description = "High-speed LPU inference via Groq or compatible OpenAI provider", - ParameterSize = "70B Cloud", - IsInstalledLocally = false - }, - new() - { - Id = "mixtral-8x7b-32768", - DisplayName = "Mixtral 8x7B (Custom / Groq)", - Provider = AiProviderType.CustomOpenAiCompatible, - Tier = AiModelTier.PaidCloud, - Description = "High-throughput mixture of experts via custom API endpoint", - ParameterSize = "8x7B Cloud", - IsInstalledLocally = false - } - }; - } - - /// - public IReadOnlyList GetOllamaLibraryModels() - { - lock (_libraryLock) - { - if (_ollamaLibraryCache.Count > 0) - { - return _ollamaLibraryCache.ToList(); - } - - return GetDefaultOllamaLibrarySeedModels(); - } - } - - private static List GetDefaultOllamaLibrarySeedModels() - { - return new List - { - new() - { - Id = "llama3.2", - DisplayName = "Llama 3.2 (3B)", - Provider = AiProviderType.OllamaLocal, - Tier = AiModelTier.FreeLocal, - Category = "Ollama Library", - Capabilities = "Tools • Vision • Text", - ParameterSize = "3B", - FormattedSize = "2.0 GB", - Description = "Meta's lightweight model optimized for local multilingual text and tool usage.", - IsInstalledLocally = false - }, - new() - { - Id = "mistral", - DisplayName = "Mistral (7B)", - Provider = AiProviderType.OllamaLocal, - Tier = AiModelTier.FreeLocal, - Category = "Ollama Library", - Capabilities = "Reasoning • Code • Text", - ParameterSize = "7B", - FormattedSize = "4.1 GB", - Description = "Fast, high-performance 7B model by Mistral AI.", - IsInstalledLocally = false - }, - new() - { - Id = "deepseek-r1:8b", - DisplayName = "DeepSeek R1 (8B)", - Provider = AiProviderType.OllamaLocal, - Tier = AiModelTier.FreeLocal, - Category = "Ollama Library", - Capabilities = "Thinking • Math • Reasoning", - ParameterSize = "8B", - FormattedSize = "4.9 GB", - Description = "DeepSeek reasoning model with step-by-step thinking scratchpad.", - IsInstalledLocally = false - }, - new() - { - Id = "qwen2.5:7b", - DisplayName = "Qwen 2.5 (7B)", - Provider = AiProviderType.OllamaLocal, - Tier = AiModelTier.FreeLocal, - Category = "Ollama Library", - Capabilities = "Reasoning • Coding", - ParameterSize = "7B", - FormattedSize = "4.4 GB", - Description = "Alibaba's advanced open-weight model with strong code and instruction following.", - IsInstalledLocally = false - }, - new() - { - Id = "phi3:mini", - DisplayName = "Phi-3 Mini (3.8B)", - Provider = AiProviderType.OllamaLocal, - Tier = AiModelTier.FreeLocal, - Category = "Ollama Library", - Capabilities = "Lightweight • Text", - ParameterSize = "3.8B", - FormattedSize = "2.3 GB", - Description = "Microsoft's efficient small language model for consumer hardware.", - IsInstalledLocally = false - } - }; - } - - /// - public async Task> FetchOllamaOnlineLibraryAsync(string? query = null, CancellationToken ct = default) - { - var fetched = new List(); - try - { - if (string.IsNullOrWhiteSpace(query)) - { - // Fetch both newest and popular trending models from ollama.com - var taskNewest = _httpClient.GetStringAsync("https://ollama.com/search?o=newest", ct); - var taskPopular = _httpClient.GetStringAsync("https://ollama.com/search", ct); - await Task.WhenAll(taskNewest, taskPopular); - - var newestList = ParseOllamaLibraryHtml(taskNewest.Result); - var popularList = ParseOllamaLibraryHtml(taskPopular.Result); - - var seen = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var m in newestList.Concat(popularList)) - { - if (seen.Add(m.Id)) - { - fetched.Add(m); - } - } - } - else - { - string searchUrl = $"https://ollama.com/search?q={Uri.EscapeDataString(query.Trim())}"; - string html = await _httpClient.GetStringAsync(searchUrl, ct); - fetched.AddRange(ParseOllamaLibraryHtml(html)); - } - - if (fetched.Count > 0) - { - lock (_libraryLock) - { - _ollamaLibraryCache.Clear(); - _ollamaLibraryCache.AddRange(fetched); - } - } - } - catch - { - // Network error - } - - lock (_libraryLock) - { - return _ollamaLibraryCache.ToList(); - } - } - - /// - /// Parses public HTML from ollama.com/search to extract live model cards, capabilities, tags, and sizes. - /// - public static List ParseOllamaLibraryHtml(string html) - { - var list = new List(); - if (string.IsNullOrWhiteSpace(html)) return list; - - var itemMatches = Regex.Matches( - html, - """(.*?)(?=| x.Id.Equals(id, StringComparison.OrdinalIgnoreCase))) - continue; - - string block = m.Groups[2].Value; - - // Extract Description - string desc = string.Empty; - var descMatch = Regex.Match(block, """]*>(.*?)

""", RegexOptions.Singleline); - if (descMatch.Success) - { - desc = System.Net.WebUtility.HtmlDecode(descMatch.Groups[1].Value.Trim()); - } - - // Extract Capability Badges (vision, tools, thinking, cloud, etc.) - var badgeMatches = Regex.Matches(block, """]*class="[^"]*rounded-md[^"]*"[^>]*>\s*([a-zA-Z0-9_\-\.]+)\s*""", RegexOptions.Singleline); - var capabilities = new List(); - bool isCloud = false; - - foreach (Match bm in badgeMatches) - { - string tag = bm.Groups[1].Value.Trim(); - if (string.Equals(tag, "cloud", StringComparison.OrdinalIgnoreCase)) - { - isCloud = true; - } - else if (!string.IsNullOrEmpty(tag)) - { - capabilities.Add(Capitalize(tag)); - } - } - - if (id.Contains("cloud", StringComparison.OrdinalIgnoreCase)) - { - isCloud = true; - } - - // Extract Pulls (e.g. 69.7K Pulls) - string pulls = string.Empty; - var pullsMatch = Regex.Match(block, """>\s*([0-9\.]+[KMB]?)\s*\s*]*> Pulls""", RegexOptions.Singleline); - if (pullsMatch.Success) - { - pulls = pullsMatch.Groups[1].Value + " pulls"; - } - - string paramSize = ExtractParamSizeFromName(id); - if (string.IsNullOrEmpty(paramSize)) - { - var pMatch = Regex.Match(desc, @"\b([0-9]+(?:\.[0-9]+)?B)\b", RegexOptions.IgnoreCase); - if (pMatch.Success) - { - paramSize = pMatch.Groups[1].Value.ToUpperInvariant(); - } - } - - string capStr = string.Join(" • ", capabilities); - if (string.IsNullOrEmpty(capStr)) - { - capStr = isCloud ? "Cloud • Fast Inference" : "Tools • Text"; - } - - var tier = isCloud ? AiModelTier.FreeCloud : AiModelTier.FreeLocal; - string formattedSize = isCloud ? "Ollama Cloud" : (!string.IsNullOrEmpty(pulls) ? pulls : "Ollama Library"); - - list.Add(new AiModelInfo - { - Id = id, - DisplayName = FormatModelDisplayName(id), - Provider = AiProviderType.OllamaLocal, - Tier = tier, - Category = isCloud ? "Ollama Cloud (Online)" : "Ollama Library (Online)", - Capabilities = capStr, - ParameterSize = paramSize, - FormattedSize = formattedSize, - Description = string.IsNullOrEmpty(desc) ? $"Ollama {(isCloud ? "Cloud" : "Library")} model {id}" : desc, - IsInstalledLocally = false - }); - } - - return list; - } - - /// - public IReadOnlyList GetUnifiedModelCatalog(AiSettingsModel settings) - { - var catalog = new List(); - var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); - - // 1. Add Discovered Ollama Models (Installed on user's machine/endpoint) - if (settings.DiscoveredOllamaModels.Count > 0) - { - foreach (var m in settings.DiscoveredOllamaModels) - { - if (seenIds.Add(m.Id)) - { - catalog.Add(m); - } - } - } - - // 2. Add Cached Online Library Models (from previous fetch) - if (settings.OllamaLibraryCache != null && settings.OllamaLibraryCache.Count > 0) - { - foreach (var cached in settings.OllamaLibraryCache) - { - if (seenIds.Add(cached.Id)) - { - catalog.Add(cached); - } - } - } - - // 3. Add Ollama Library Models (Official online or fallback seed models) - foreach (var libModel in GetOllamaLibraryModels()) - { - if (seenIds.Add(libModel.Id)) - { - catalog.Add(libModel); - } - } - - // 4. Add Cloud Models (Free Cloud & Paid Cloud) - foreach (var cloudModel in GetCloudCatalogModels()) - { - if (seenIds.Add(cloudModel.Id)) - { - catalog.Add(cloudModel); - } - } - - return catalog; - } - - /// - public async Task<(bool Success, string Message, TimeSpan Latency)> TestConnectionAsync(AiSettingsModel settings, CancellationToken ct = default) - { - var sw = Stopwatch.StartNew(); - - try - { - var chatClient = CreateChatClient(settings); - var response = await chatClient.GetResponseAsync( - "Respond in 3 words confirming you are connected to FryPDF Studio.", - new ChatOptions - { - MaxOutputTokens = 25, - Temperature = 0.2f - }, - ct); - - sw.Stop(); - string reply = response?.Text?.Trim() ?? "OK"; - return (true, $"Success! Model responded: \"{reply}\"", sw.Elapsed); - } - catch (Exception ex) - { - sw.Stop(); - return (false, $"Connection failed: {ex.Message}", sw.Elapsed); - } - } - - /// - public IChatClient CreateChatClient(AiSettingsModel settings) - { - switch (settings.SelectedProvider) - { - case AiProviderType.OllamaLocal: - { - string endpoint = string.IsNullOrWhiteSpace(settings.OllamaEndpoint) - ? "http://localhost:11434" - : settings.OllamaEndpoint.TrimEnd('/'); - - string model = string.IsNullOrWhiteSpace(settings.SelectedModelId) - ? "llama3.2" - : settings.SelectedModelId; - - if (!string.IsNullOrWhiteSpace(settings.OllamaApiKey)) - { - var httpClient = new HttpClient { BaseAddress = new Uri(endpoint), Timeout = TimeSpan.FromSeconds(60) }; - httpClient.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", settings.OllamaApiKey.Trim()); - return new OllamaApiClient(httpClient, model); - } - - // OllamaApiClient implements IChatClient directly from Microsoft.Extensions.AI - return new OllamaApiClient(new Uri(endpoint), model); - } - - case AiProviderType.OpenAiCloud: - { - string apiKey = string.IsNullOrWhiteSpace(settings.OpenAiApiKey) - ? "dummy-key" - : settings.OpenAiApiKey.Trim(); - - string model = string.IsNullOrWhiteSpace(settings.SelectedModelId) - ? "gpt-4o-mini" - : settings.SelectedModelId; - - var client = new ChatClient(model, apiKey); - return client.AsIChatClient(); - } - - case AiProviderType.CustomOpenAiCompatible: - { - string apiKey = string.IsNullOrWhiteSpace(settings.OpenAiApiKey) - ? "dummy-key" - : settings.OpenAiApiKey.Trim(); - - string model = string.IsNullOrWhiteSpace(settings.CustomModelName) - ? (string.IsNullOrWhiteSpace(settings.SelectedModelId) ? "llama-3.3-70b-versatile" : settings.SelectedModelId) - : settings.CustomModelName.Trim(); - - string baseUrl = NormalizeCustomOpenAiBaseUrl(settings.CustomBaseUrl); - - var client = new ChatClient( - model, - new ApiKeyCredential(apiKey), - new OpenAIClientOptions { Endpoint = new Uri(baseUrl) }); - - return client.AsIChatClient(); - } - - default: - throw new ArgumentOutOfRangeException(nameof(settings.SelectedProvider)); - } - } - - private static string FormatModelDisplayName(string rawName) - { - if (string.IsNullOrWhiteSpace(rawName)) return "Unknown"; - string clean = rawName.Replace(":latest", ""); - return clean switch - { - "llama3.2" => "Llama 3.2 (Meta)", - "llama3.1" => "Llama 3.1 (Meta)", - "mistral" => "Mistral 7B", - "phi3" => "Phi-3 Mini (Microsoft)", - "phi4" => "Phi-4 (Microsoft)", - "gemma2" => "Gemma 2 (Google)", - "deepseek-r1" => "DeepSeek-R1 (Reasoning)", - "qwen2.5" => "Qwen 2.5", - _ => Capitalize(clean) - }; - } - - private static string ExtractParamSizeFromName(string name) - { - if (name.Contains("3.2", StringComparison.OrdinalIgnoreCase)) return "3B"; - if (name.Contains("70b", StringComparison.OrdinalIgnoreCase)) return "70B"; - if (name.Contains("8b", StringComparison.OrdinalIgnoreCase)) return "8B"; - if (name.Contains("7b", StringComparison.OrdinalIgnoreCase)) return "7B"; - if (name.Contains("14b", StringComparison.OrdinalIgnoreCase)) return "14B"; - if (name.Contains("3b", StringComparison.OrdinalIgnoreCase)) return "3B"; - if (name.Contains("1b", StringComparison.OrdinalIgnoreCase)) return "1B"; - return string.Empty; - } - - private static string Capitalize(string str) - { - if (string.IsNullOrEmpty(str)) return str; - return char.ToUpperInvariant(str[0]) + str[1..]; - } - - /// - /// Normalizes custom OpenAI-compatible base URLs by trimming trailing slashes and - /// stripping '/chat/completions' if the user accidentally included the full endpoint path. - /// - public static string NormalizeCustomOpenAiBaseUrl(string? rawUrl) - { - if (string.IsNullOrWhiteSpace(rawUrl)) - { - return "https://api.openai.com/v1"; - } - - string trimmed = rawUrl.Trim().TrimEnd('/'); - if (trimmed.EndsWith("/chat/completions", StringComparison.OrdinalIgnoreCase)) - { - trimmed = trimmed[..^"/chat/completions".Length].TrimEnd('/'); - } - - return trimmed; - } - - // JSON DTO for Ollama /api/tags - private sealed class OllamaTagsResponse - { - [JsonPropertyName("models")] - public List? Models { get; set; } - } - - private sealed class OllamaModelDto - { - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("model")] - public string? Model { get; set; } - - [JsonPropertyName("remote_model")] - public string? RemoteModel { get; set; } - - [JsonPropertyName("remote_host")] - public string? RemoteHost { get; set; } - - [JsonPropertyName("size")] - public long Size { get; set; } - - [JsonPropertyName("details")] - public OllamaDetailsDto? Details { get; set; } - - [JsonPropertyName("capabilities")] - public List? Capabilities { get; set; } - } - - private sealed class OllamaDetailsDto - { - [JsonPropertyName("family")] - public string? Family { get; set; } - - [JsonPropertyName("families")] - public List? Families { get; set; } - - [JsonPropertyName("parameter_size")] - public string? ParameterSize { get; set; } - - [JsonPropertyName("quantization_level")] - public string? QuantizationLevel { get; set; } - - [JsonPropertyName("context_length")] - public long? ContextLength { get; set; } - } -} diff --git a/src/PdfEditorApp/Services/AI/IAiProviderRegistry.cs b/src/PdfEditorApp/Services/AI/IAiProviderRegistry.cs deleted file mode 100644 index ec1686b..0000000 --- a/src/PdfEditorApp/Services/AI/IAiProviderRegistry.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services.AI; - -/// -/// Contract for an AI provider plugin (e.g., Ollama, Groq, OpenAI, Anthropic, Gemini, Mock). -/// -public interface IAiProvider -{ - /// - /// Unique provider ID, e.g. "ollama", "groq", "openai", "claude", "gemini". - /// - string ProviderId { get; } - - /// - /// User-facing display name, e.g. "Ollama (Local Private AI)" or "Groq (Ultra-Fast Cloud LPU)". - /// - string DisplayName { get; } - - /// - /// Indicates whether this provider requires an API key. - /// - bool RequiresApiKey { get; } - - /// - /// Indicates whether this provider allows custom endpoint URL configuration. - /// - bool SupportsCustomEndpoint { get; } - - /// - /// Default API endpoint URL if applicable. - /// - string DefaultEndpoint => ""; - - /// - /// Material icon name representing the provider. - /// - string IconKind => "RobotOutline"; - - /// - /// Retrieves the list of available models supported by this provider. - /// - Task> GetModelsAsync(AiSettingsModel settings, CancellationToken ct = default); - - /// - /// Creates a configured for this provider. - /// - IChatClient CreateChatClient(AiSettingsModel settings); - - /// - /// Tests connectivity and latency to the provider endpoint. - /// - Task<(bool Success, string Message, TimeSpan Latency)> TestConnectionAsync(AiSettingsModel settings, CancellationToken ct = default); -} - -/// -/// Registry for discovering and dispatching pluggable AI backends. -/// -public interface IAiProviderRegistry -{ - /// - /// Registers an AI provider into the system. - /// - IDisposable RegisterProvider(IAiProvider provider); - - /// - /// Retrieves an AI provider by its unique identifier. - /// - IAiProvider? GetProvider(string providerId); - - /// - /// Gets all currently registered AI providers. - /// - IReadOnlyList GetAllProviders(); - - /// - /// Raised whenever an AI provider is registered or unregistered. - /// - event Action? RegistryChanged; -} diff --git a/src/PdfEditorApp/Services/AI/IAiService.cs b/src/PdfEditorApp/Services/AI/IAiService.cs deleted file mode 100644 index d714b0f..0000000 --- a/src/PdfEditorApp/Services/AI/IAiService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services.AI; - -/// -/// Service responsible for managing AI providers, model catalogs, connection verification, and IChatClient creation. -/// -public interface IAiService -{ - /// - /// Auto-discovers all locally installed models on the specified Ollama endpoint. - /// - Task> DiscoverOllamaModelsAsync(string endpoint, CancellationToken ct = default); - - /// - /// Retrieves standard cloud and OpenAI-compatible models labeled with Paid/Cloud tier badges. - /// - IReadOnlyList GetCloudCatalogModels(); - - /// - /// Retrieves official featured models provided in the Ollama library. - /// - IReadOnlyList GetOllamaLibraryModels(); - - /// - /// Fetches live models directly from Ollama's online model catalog/search (ollama.com). - /// - Task> FetchOllamaOnlineLibraryAsync(string? query = null, CancellationToken ct = default); - - /// - /// Combines discovered local models and cloud models into a unified catalog. - /// - IReadOnlyList GetUnifiedModelCatalog(AiSettingsModel settings); - - /// - /// Tests connectivity to the currently configured AI provider. - /// - Task<(bool Success, string Message, TimeSpan Latency)> TestConnectionAsync(AiSettingsModel settings, CancellationToken ct = default); - - /// - /// Creates a configured Microsoft.Extensions.AI IChatClient based on active settings. - /// - IChatClient CreateChatClient(AiSettingsModel settings); -} diff --git a/src/PdfEditorApp/Services/AI/IPdfStudioAgentService.cs b/src/PdfEditorApp/Services/AI/IPdfStudioAgentService.cs deleted file mode 100644 index 61b7b02..0000000 --- a/src/PdfEditorApp/Services/AI/IPdfStudioAgentService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Services.AI; - -/// -/// AI Agent service that takes natural language user prompts and generates or modifies native PDF studio canvas elements. -/// -public interface IPdfStudioAgentService -{ - /// - /// Executes an AI generation prompt on the specified page using Microsoft.Extensions.AI. - /// - Task ExecutePromptAsync( - string userPrompt, - PageViewModel targetPage, - AiSettingsModel settings, - Action? progressCallback = null, - CancellationToken ct = default, - (double X, double Y)? targetPoint = null); - - /// - /// Modifies an existing canvas element in-place using natural language instructions via Microsoft.Extensions.AI. - /// - Task ModifyElementAsync( - ElementViewModelBase targetElement, - string modificationPrompt, - AiSettingsModel settings, - Action? progressCallback = null, - CancellationToken ct = default); -} diff --git a/src/PdfEditorApp/Services/AI/PdfStudioAgentService.cs b/src/PdfEditorApp/Services/AI/PdfStudioAgentService.cs deleted file mode 100644 index 6aa5a35..0000000 --- a/src/PdfEditorApp/Services/AI/PdfStudioAgentService.cs +++ /dev/null @@ -1,1464 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Services.AI; - -/// -/// Autonomous document studio agent powered by Microsoft.Extensions.AI. -/// Translates natural language composition prompts into native editable FryPDF canvas elements. -/// -public class PdfStudioAgentService : IPdfStudioAgentService -{ - private readonly IAiService _aiService; - private readonly IUndoRedoService? _undoRedoService; - - public PdfStudioAgentService(IAiService aiService, IUndoRedoService? undoRedoService = null) - { - _aiService = aiService ?? throw new ArgumentNullException(nameof(aiService)); - _undoRedoService = undoRedoService; - } - - /// - public async Task ExecutePromptAsync( - string userPrompt, - PageViewModel targetPage, - AiSettingsModel settings, - Action? progressCallback = null, - CancellationToken ct = default, - (double X, double Y)? targetPoint = null) - { - if (string.IsNullOrWhiteSpace(userPrompt)) - { - return new AiAgentResult { Success = false, Message = "User prompt was empty." }; - } - - if (targetPage == null) - { - return new AiAgentResult { Success = false, Message = "Target page is required." }; - } - - var sw = Stopwatch.StartNew(); - progressCallback?.Invoke("Connecting to AI model..."); - - var createdElements = new List(); - var actionsTaken = new List(); - - double baseX = targetPoint?.X ?? 40; - double baseY = targetPoint?.Y ?? 40; - - double ResolveX(double x) => (targetPoint.HasValue && Math.Abs(x - 40) < 0.1) ? targetPoint.Value.X : x; - double ResolveY(double y, double defaultY = 40) => (targetPoint.HasValue && Math.Abs(y - defaultY) < 0.1) ? targetPoint.Value.Y : y; - - string targetScopeInstruction = targetPoint.HasValue - ? $""" - Target Location: - - The user explicitly pointed to coordinate X: {targetPoint.Value.X:0} pt, Y: {targetPoint.Value.Y:0} pt on Page {targetPage.PageNumber}. - - Place newly created elements starting at or centered near this target position (X: {targetPoint.Value.X:0}, Y: {targetPoint.Value.Y:0}). - - For composed layouts (e.g. heading + paragraph + button), position elements sequentially from Y: {targetPoint.Value.Y:0} downwards. - """ - : $""" - Target Scope: - - The user has selected ENTIRE Page {targetPage.PageNumber} (Width: {targetPage.Width:0} pt, Height: {targetPage.Height:0} pt). - - You have full creative authority to structure, align, and compose elements across the entire page layout. - """; - - // System prompt guiding design principles and canvas coordinate system - string systemPrompt = $""" - You are FryPDF Studio Agent, an expert AI document layout designer. - You create professional, beautiful, polished document elements directly on the active document canvas using available tools. - - Canvas Dimensions: - - Page Width: {targetPage.Width:0} pt, Height: {targetPage.Height:0} pt. - - Coordinate origin (0, 0) is top-left. - - Safe printable margins: X: 40 to {targetPage.Width - 40:0} pt, Y: 40 to {targetPage.Height - 40:0} pt. - - {targetScopeInstruction} - - Design Guidelines: - 1. Visual Hierarchy: Use clear contrast in font sizes (Headings 20-28pt bold, Subheadings 14-16pt, Paragraphs 10-12pt). - 2. Layout Spacing: Leave generous vertical spacing between sections (15-25pt gap). Do not overlap elements unless deliberately layering a text/badge over a background card shape. - 3. Cohesive Color Palettes: - - Primary Accent: #0F6CBD (Modern Blue), #1E293B (Slate Dark), or #15803D (Emerald Green) - - Background Tints: #F8FAFC, #F0F7FD, #F0FDF4 - - Borders/Dividers: #E2E8F0, #CBD5E1 - - Text Colors: Dark text #0F172A or #1E293B on light backgrounds. - 4. Completeness: When asked for a composition (e.g. invoice, report header, certificate, certificate border, notice, card), invoke multiple tools to build the complete, ready-to-use section. - 5. If the tool calling mechanism is unavailable or you prefer structured output, output a JSON array of actions with keys: - action ('addHeading'|'addParagraph'|'addShape'|'addTable'|'addDivider'|'addBadge'|'addCard'|'addSvg'|'addQrCode'|'addBarcode'|'addChart'). - """; - - if (!string.IsNullOrWhiteSpace(settings.SystemInstructions)) - { - systemPrompt += $""" - - - User Custom Instructions: - {settings.SystemInstructions} - """; - } - - // Define tools using Microsoft.Extensions.AI AIFunctionFactory - var tools = new List - { - AIFunctionFactory.Create( - ([Description("Heading or title text")] string text, - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 40, - [Description("Width in pt")] double width = 500, - [Description("Height in pt")] double height = 45, - [Description("Font size in pt (default 24)")] double fontSize = 24, - [Description("Font family name")] string fontFamily = "Georgia", - [Description("Hex text color (e.g. #111827)")] string textColorHex = "#111827", - [Description("Bold typeface")] bool isBold = true, - [Description("Text alignment: Left, Center, or Right")] string alignment = "Left") => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 40); - var el = CreateHeadingElement(text, rx, ry, width, height, fontSize, fontFamily, textColorHex, isBold, alignment); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"Heading: \"{Truncate(text, 25)}\" at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created heading element ID {el.Id}"; - }, - "addHeading", - "Adds a prominent section title or document heading text element."), - - AIFunctionFactory.Create( - ([Description("Paragraph body text")] string text, - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 90, - [Description("Width in pt")] double width = 500, - [Description("Height in pt")] double height = 60, - [Description("Font size in pt (default 12)")] double fontSize = 12, - [Description("Font family name")] string fontFamily = "Inter", - [Description("Hex text color (e.g. #374151)")] string textColorHex = "#374151", - [Description("Text alignment: Left, Center, or Right")] string alignment = "Left") => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 90); - var el = CreateParagraphElement(text, rx, ry, width, height, fontSize, fontFamily, textColorHex, alignment); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"Paragraph: \"{Truncate(text, 25)}\" at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created paragraph element ID {el.Id}"; - }, - "addParagraph", - "Adds a multi-line paragraph or body text block."), - - AIFunctionFactory.Create( - ([Description("Shape type: Rectangle, RoundedRectangle, Circle, PillBadge, Star, Triangle")] string shapeType = "RoundedRectangle", - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 40, - [Description("Width in pt")] double width = 200, - [Description("Height in pt")] double height = 100, - [Description("Fill background hex color")] string fillColorHex = "#F0F7FD", - [Description("Border stroke hex color")] string strokeColorHex = "#0F6CBD", - [Description("Stroke thickness in pt")] double strokeThickness = 1.0, - [Description("Corner radius for rounded rectangle")] double cornerRadius = 8.0) => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 40); - var el = CreateShapeElement(shapeType, rx, ry, width, height, fillColorHex, strokeColorHex, strokeThickness, cornerRadius); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"Shape: {shapeType} at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created shape element ID {el.Id}"; - }, - "addShape", - "Adds a geometric container shape, background card, accent bar, or banner."), - - AIFunctionFactory.Create( - ([Description("List of column header titles")] string[] headers, - [Description("Matrix of row cell string values")] string[][] rows, - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 160, - [Description("Width in pt")] double width = 520, - [Description("Height in pt")] double height = 180, - [Description("Header background color hex")] string headerBgColorHex = "#0F6CBD", - [Description("Header text color hex")] string headerTextColorHex = "#FFFFFF") => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 160); - var el = CreateTableElement(headers, rows, rx, ry, width, height, headerBgColorHex, headerTextColorHex); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"Table: {headers?.Length ?? 0} cols × {rows?.Length ?? 0} rows at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created table element ID {el.Id}"; - }, - "addTable", - "Adds a structured multi-column data grid or financial table with formatted headers and rows."), - - AIFunctionFactory.Create( - ([Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 150, - [Description("Width in pt")] double width = 520, - [Description("Height in pt")] double height = 2, - [Description("Divider line color hex")] string colorHex = "#CBD5E1", - [Description("Line thickness in pt")] double thickness = 1.0, - [Description("Orientation: Horizontal or Vertical")] string orientation = "Horizontal") => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 150); - var el = CreateDividerElement(rx, ry, width, height, colorHex, thickness, orientation); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"Divider: {orientation} at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created divider element ID {el.Id}"; - }, - "addDivider", - "Adds a decorative horizontal or vertical divider line to separate sections."), - - AIFunctionFactory.Create( - ([Description("Short badge text label (e.g. PAID, CONFIDENTIAL, APPROVED)")] string text, - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 40, - [Description("Width in pt")] double width = 120, - [Description("Height in pt")] double height = 28, - [Description("Badge background fill color hex")] string bgColorHex = "#EFF6FF", - [Description("Badge text color hex")] string textColorHex = "#1D4ED8") => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 40); - var el = CreateBadgeElement(text, rx, ry, width, height, bgColorHex, textColorHex); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"Badge: \"{text}\" at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created badge element ID {el.Id}"; - }, - "addBadge", - "Adds a stylish pill badge or status tag (e.g. PAID, DRAFT, CONFIDENTIAL)."), - - AIFunctionFactory.Create( - ([Description("Card header title")] string title, - [Description("Card subheader or date")] string subtitle, - [Description("Card body text")] string body, - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 100, - [Description("Width in pt")] double width = 520, - [Description("Height in pt")] double height = 130, - [Description("Theme accent color hex")] string themeColorHex = "#0F6CBD") => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 100); - var elements = CreateCardElements(title, subtitle, body, rx, ry, width, height, themeColorHex); - foreach (var el in elements) - { - targetPage.AddElement(el); - createdElements.Add(el); - } - string desc = $"Card: \"{Truncate(title, 20)}\" (container + text) at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created card composed of {elements.Count} elements"; - }, - "addCard", - "Adds a composed UI callout card with container background, title, subtitle, and body text."), - - AIFunctionFactory.Create( - ([Description("Preset ornament name from SvgOrnamentLibrary (e.g. ArtDecoFrame, BotanicalWreath, GaneshaCrest, OmCrest)")] string ornamentName, - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 40, - [Description("Width in pt")] double width = 80, - [Description("Height in pt")] double height = 80, - [Description("Tint color hex")] string colorHex = "#0F6CBD") => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 40); - var el = CreateSvgOrnamentElement(ornamentName, rx, ry, width, height, colorHex); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"SVG Ornament: {ornamentName} at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created SVG ornament element ID {el.Id}"; - }, - "addSvgOrnament", - "Adds a vector ornament, crest, emblem, or flourish from FryPDF's vector library."), - - AIFunctionFactory.Create( - ([Description("URL, text, or payment payload")] string payload, - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 40, - [Description("Size in pt")] double size = 80) => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 40); - var el = CreateQrCodeElement(payload, rx, ry, size); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"QR Code at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created QR code element ID {el.Id}"; - }, - "addQrCode", - "Adds a scannable QR code element for verification, links, or contact details."), - - AIFunctionFactory.Create( - ([Description("Barcode data value")] string payload, - [Description("Format: Code128, Ean13, etc.")] string format = "Code128", - [Description("X position in pt")] double x = 40, - [Description("Y position in pt")] double y = 40, - [Description("Width in pt")] double width = 180, - [Description("Height in pt")] double height = 55) => - { - double rx = ResolveX(x); - double ry = ResolveY(y, 40); - var el = CreateBarcodeElement(payload, format, rx, ry, width, height); - targetPage.AddElement(el); - createdElements.Add(el); - string desc = $"Barcode: {payload} at ({rx:0},{ry:0})"; - actionsTaken.Add(desc); - progressCallback?.Invoke($"Created {desc}"); - return $"Created barcode element ID {el.Id}"; - }, - "addBarcode", - "Adds a standard 1D barcode element.") - }; - - try - { - var baseChatClient = _aiService.CreateChatClient(settings); - - // Wrap in FunctionInvokingChatClient from Microsoft.Extensions.AI for automatic tool loop - var chatClient = new ChatClientBuilder(baseChatClient) - .UseFunctionInvocation() - .Build(); - - var messages = new List - { - new(ChatRole.System, systemPrompt), - new(ChatRole.User, userPrompt) - }; - - var chatOptions = new ChatOptions - { - Tools = tools, - Temperature = settings.Temperature, - MaxOutputTokens = 1500 - }; - - progressCallback?.Invoke("Generating elements on canvas..."); - var response = await chatClient.GetResponseAsync(messages, chatOptions, ct); - - string replyText = response?.Text ?? string.Empty; - - // FALLBACK: If model returned structured JSON actions in text instead of native function calls - if (createdElements.Count == 0 && !string.IsNullOrWhiteSpace(replyText)) - { - progressCallback?.Invoke("Parsing structured element instructions..."); - ParseAndExecuteJsonFallback(replyText, targetPage, createdElements, actionsTaken, progressCallback, targetPoint); - } - - // ATOMIC UNDO/REDO RECORDING: Wrap all created elements in a single atomic undo transaction - if (createdElements.Count > 0 && _undoRedoService != null) - { - var elementsSnapshot = createdElements.ToList(); - string undoDesc = $"AI Studio: {Truncate(userPrompt, 30)} ({elementsSnapshot.Count} items)"; - - _undoRedoService.RecordAction( - undoDesc, - () => - { - foreach (var el in elementsSnapshot) targetPage.RemoveElement(el); - }, - () => - { - foreach (var el in elementsSnapshot) targetPage.AddElement(el); - }); - } - - sw.Stop(); - - if (createdElements.Count > 0) - { - return new AiAgentResult - { - Success = true, - Message = $"Successfully created {createdElements.Count} elements in {sw.Elapsed.TotalSeconds:0.1}s.", - ElementsCreatedCount = createdElements.Count, - ActionDescriptions = actionsTaken, - RawOutput = replyText, - Duration = sw.Elapsed - }; - } - else - { - return new AiAgentResult - { - Success = false, - Message = string.IsNullOrWhiteSpace(replyText) - ? "The AI model did not generate any elements. Please verify your prompt." - : $"AI Response: {replyText}", - ElementsCreatedCount = 0, - RawOutput = replyText, - Duration = sw.Elapsed - }; - } - } - catch (Exception ex) - { - sw.Stop(); - return new AiAgentResult - { - Success = false, - Message = $"AI Generation Error: {ex.Message}", - Duration = sw.Elapsed - }; - } - } - - /// - public async Task ModifyElementAsync( - ElementViewModelBase targetElement, - string modificationPrompt, - AiSettingsModel settings, - Action? progressCallback = null, - CancellationToken ct = default) - { - if (targetElement == null) - { - return new AiAgentResult { Success = false, Message = "Target element is required." }; - } - - if (string.IsNullOrWhiteSpace(modificationPrompt)) - { - return new AiAgentResult { Success = false, Message = "Modification prompt cannot be empty." }; - } - - var sw = Stopwatch.StartNew(); - progressCallback?.Invoke($"Analyzing {targetElement.Kind} element..."); - - var currentModel = targetElement.ToModel(); - var beforeSnapshot = currentModel.Clone(); - - var jsonOptions = new JsonSerializerOptions - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() } - }; - - string currentJson = JsonSerializer.Serialize(currentModel, currentModel.GetType(), jsonOptions); - - string systemPrompt = $""" - You are FryPDF AI Element Modifier, an expert in document graphics, charts, typography, and layout. - The user has selected an existing canvas element and wants to modify it in-place using natural language. - Element Kind: {currentModel.Kind} - - Instructions: - 1. Apply the user's requested modifications to the properties of this element. - 2. Return ONLY a valid JSON object matching the element's schema, with the updated property values. - 3. Keep unmodified properties intact. Retain the same 'id', 'x', and 'y' unless explicitly requested to move or resize. - 4. Output strictly valid JSON. Do not include introductory text, conversational pleasantries, or explanations outside the JSON block. - - Element-Specific Guidelines: - - Charts: You can modify 'title', 'chartType' (BarColumn, HorizontalBar, Line, SmoothLine, Area, DonutPie, StackedBar, StackedHorizontalBar, Radar, ScatterPlot), 'palette' (CorporateBlue, ModernTeal, VibrantWarm, EmeraldForest, RoyalPurple, SunsetOrange, MonochromeSlate, NeonCyber), 'categories', 'values', 'valueLabels', 'barColorsHex', 'backgroundColorHex', 'borderColorHex', 'showDataLabels', 'showGridlines', etc. - - Text: You can modify 'text', 'fontFamily', 'fontSize', 'isBold', 'isItalic', 'isUnderline', 'textColorHex', 'backgroundColorHex', 'alignment', 'lineHeight', etc. - - Table: You can modify 'headers', 'rows', 'headerBackgroundHex', 'borderColorHex', 'alternateRowBackgroundHex', etc. - - Shape: You can modify 'shapeType' (Rectangle, RoundedRectangle, Circle, PillBadge, Diamond, Star, Banner), 'fillColorHex', 'strokeColorHex', 'strokeThickness', 'cornerRadius', etc. - - Math: You can modify 'formula' (LaTeX equation string), 'fontSize', 'textColorHex', 'showEquationNumber', 'equationNumber', etc. - - Image: You can modify 'opacity', 'width', 'height', 'altText', etc. - - Svg: You can modify 'tintColorHex', 'borderColorHex', 'borderThickness', 'cornerRadius', etc. - - Divider: You can modify 'colorHex', 'thickness', 'orientation', etc. - - QrCode: You can modify 'payload', 'darkColorHex', 'lightColorHex', etc. - - Barcode: You can modify 'codeValue', 'barColorHex', 'barcodeFormat', etc. - - FormField: You can modify 'label', 'isRequired', 'borderColorHex', 'backgroundColorHex', etc. - - StickyNote: You can modify 'noteText', 'colorHex', 'status', 'author', etc. - """; - - if (!string.IsNullOrWhiteSpace(settings.SystemInstructions)) - { - systemPrompt += $""" - - - User Custom Instructions: - {settings.SystemInstructions} - """; - } - - string userMessage = $""" - Modify this element according to instructions: - - User Request: {modificationPrompt} - - Current Element JSON: - {currentJson} - """; - - PdfElementBase? updatedModel = null; - string replyText = string.Empty; - - try - { - var baseChatClient = _aiService.CreateChatClient(settings); - var messages = new List - { - new(ChatRole.System, systemPrompt), - new(ChatRole.User, userMessage) - }; - - var chatOptions = new ChatOptions - { - Temperature = Math.Min(settings.Temperature, 0.4f), - MaxOutputTokens = 2000 - }; - - progressCallback?.Invoke("Querying AI model for modifications..."); - var response = await baseChatClient.GetResponseAsync(messages, chatOptions, ct); - replyText = response?.Text ?? string.Empty; - - if (!string.IsNullOrWhiteSpace(replyText)) - { - progressCallback?.Invoke("Applying updated attributes..."); - updatedModel = TryParseElementJson(replyText, currentModel.GetType(), jsonOptions); - } - } - catch (Exception ex) - { - Debug.WriteLine($"[PdfStudioAgentService] AI call error during ModifyElement: {ex.Message}"); - progressCallback?.Invoke("Attempting smart offline modifier..."); - } - - // Fallback to heuristic modifier if offline or test environment - if (updatedModel == null) - { - updatedModel = TryApplyHeuristicModification(currentModel, modificationPrompt); - } - - sw.Stop(); - - if (updatedModel != null) - { - updatedModel.Id = currentModel.Id; - if (updatedModel.Width <= 0) updatedModel.Width = currentModel.Width; - if (updatedModel.Height <= 0) updatedModel.Height = currentModel.Height; - - targetElement.LoadFromModel(updatedModel); - - if (targetElement is ChartElementViewModel chartVm) - { - chartVm.UpdateLiveChart(); - } - - // Atomic Undo/Redo recording - if (_undoRedoService != null) - { - var afterSnapshot = updatedModel.Clone(); - string undoDesc = $"AI Modify: {Truncate(modificationPrompt, 28)}"; - _undoRedoService.RecordAction( - undoDesc, - () => - { - targetElement.LoadFromModel(beforeSnapshot); - if (targetElement is ChartElementViewModel c) c.UpdateLiveChart(); - }, - () => - { - targetElement.LoadFromModel(afterSnapshot); - if (targetElement is ChartElementViewModel c) c.UpdateLiveChart(); - }); - } - - return new AiAgentResult - { - Success = true, - Message = $"Updated {targetElement.Kind} successfully: {Truncate(modificationPrompt, 40)}", - ElementsCreatedCount = 1, - RawOutput = replyText, - Duration = sw.Elapsed - }; - } - - return new AiAgentResult - { - Success = false, - Message = string.IsNullOrWhiteSpace(replyText) - ? "Unable to modify element with the provided instructions." - : $"AI Response could not be parsed: {Truncate(replyText, 100)}", - ElementsCreatedCount = 0, - RawOutput = replyText, - Duration = sw.Elapsed - }; - } - - private static PdfElementBase? TryParseElementJson(string rawText, Type targetType, JsonSerializerOptions options) - { - if (string.IsNullOrWhiteSpace(rawText)) return null; - - try - { - string cleaned = rawText.Trim(); - if (cleaned.StartsWith("```json", StringComparison.OrdinalIgnoreCase)) - { - cleaned = cleaned[7..]; - } - else if (cleaned.StartsWith("```", StringComparison.OrdinalIgnoreCase)) - { - cleaned = cleaned[3..]; - } - - if (cleaned.EndsWith("```", StringComparison.OrdinalIgnoreCase)) - { - cleaned = cleaned[..^3].Trim(); - } - - int firstBrace = cleaned.IndexOf('{'); - int lastBrace = cleaned.LastIndexOf('}'); - if (firstBrace >= 0 && lastBrace > firstBrace) - { - cleaned = cleaned.Substring(firstBrace, lastBrace - firstBrace + 1); - } - - return JsonSerializer.Deserialize(cleaned, targetType, options) as PdfElementBase; - } - catch (Exception ex) - { - Debug.WriteLine($"[PdfStudioAgentService] JSON parse error: {ex.Message}"); - return null; - } - } - - private static PdfElementBase? TryApplyHeuristicModification(PdfElementBase currentModel, string prompt) - { - if (currentModel == null || string.IsNullOrWhiteSpace(prompt)) return null; - string lower = prompt.ToLowerInvariant(); - - if (currentModel is PdfChartElement chart) - { - var modified = (PdfChartElement)chart.Clone(); - - // Chart Type - if (lower.Contains("line")) - { - modified.ChartType = lower.Contains("smooth") ? ChartType.SmoothLine : ChartType.Line; - } - else if (lower.Contains("bar") || lower.Contains("column")) - { - modified.ChartType = lower.Contains("horizontal") ? ChartType.HorizontalBar : ChartType.BarColumn; - } - else if (lower.Contains("donut") || lower.Contains("pie")) - { - modified.ChartType = ChartType.DonutPie; - } - else if (lower.Contains("area")) - { - modified.ChartType = ChartType.Area; - } - else if (lower.Contains("stacked")) - { - modified.ChartType = lower.Contains("horizontal") ? ChartType.StackedHorizontalBar : ChartType.StackedBar; - } - else if (lower.Contains("radar")) - { - modified.ChartType = ChartType.Radar; - } - - // Palette - if (lower.Contains("emerald") || lower.Contains("green") || lower.Contains("mint")) - { - modified.Palette = ChartPalette.EmeraldGreen; - modified.BarColorsHex = new List { "#A7F3D0", "#34D399", "#10B981", "#047857" }; - } - else if (lower.Contains("cyber") || lower.Contains("neon")) - { - modified.Palette = ChartPalette.CyberNeon; - modified.BarColorsHex = new List { "#99F6E4", "#2DD4BF", "#0D9488", "#115E59" }; - } - else if (lower.Contains("pastel")) - { - modified.Palette = ChartPalette.PastelHarmony; - modified.BarColorsHex = new List { "#E9D5FF", "#C084FC", "#9333EA", "#6B21A8" }; - } - else if (lower.Contains("sunset") || lower.Contains("orange")) - { - modified.Palette = ChartPalette.SunsetOrange; - modified.BarColorsHex = new List { "#FED7AA", "#FB923C", "#EA580C", "#9A3412" }; - } - else if (lower.Contains("rainbow") || lower.Contains("vibrant")) - { - modified.Palette = ChartPalette.VibrantRainbow; - modified.BarColorsHex = new List { "#FECDD3", "#FB7185", "#E11D48", "#9F1239" }; - } - else if (lower.Contains("blue") || lower.Contains("corporate")) - { - modified.Palette = ChartPalette.CorporateBlue; - modified.BarColorsHex = new List { "#C7E0F4", "#82BDF0", "#3D95E6", "#0F6CBD" }; - } - else if (lower.Contains("slate") || lower.Contains("gray") || lower.Contains("grey") || lower.Contains("executive")) - { - modified.Palette = ChartPalette.ExecutiveSlate; - modified.BarColorsHex = new List { "#E2E8F0", "#94A3B8", "#475569", "#1E293B" }; - } - - // Title extraction: e.g. title to "XYZ" or rename to "XYZ" or title: XYZ - var titleMatch = Regex.Match(prompt, """(?:title\s*(?:to|is|:)\s*|rename\s*(?:to)?\s*)["']?([^"'\r\n]+)["']?""", RegexOptions.IgnoreCase); - if (titleMatch.Success && !string.IsNullOrWhiteSpace(titleMatch.Groups[1].Value)) - { - modified.Title = titleMatch.Groups[1].Value.Trim(); - } - - // Projections / Adding future quarters - if (lower.Contains("2027") || lower.Contains("projection") || lower.Contains("forecast") || lower.Contains("add quarter")) - { - if (!modified.Categories.Any(c => c.Contains("2027"))) - { - double lastVal = modified.Values.LastOrDefault(); - double projVal = lastVal > 0 ? Math.Round(lastVal * 1.15, 2) : 1.0; - modified.Categories.Add("Q1 2027 (Proj)"); - modified.Values.Add(projVal); - modified.ValueLabels.Add($"${projVal:0.00}B"); - modified.BarColorsHex.Add("#34D399"); - } - } - - // Sorting - if (lower.Contains("sort") && (lower.Contains("asc") || lower.Contains("ascending") || lower.Contains("lowest"))) - { - var paired = modified.Categories.Select((cat, i) => new - { - Cat = cat, - Val = i < modified.Values.Count ? modified.Values[i] : 0, - Lbl = i < modified.ValueLabels.Count ? modified.ValueLabels[i] : "", - Col = i < modified.BarColorsHex.Count ? modified.BarColorsHex[i] : "#0F6CBD" - }).OrderBy(x => x.Val).ToList(); - - modified.Categories = paired.Select(p => p.Cat).ToList(); - modified.Values = paired.Select(p => p.Val).ToList(); - modified.ValueLabels = paired.Select(p => p.Lbl).ToList(); - modified.BarColorsHex = paired.Select(p => p.Col).ToList(); - } - else if (lower.Contains("sort") && (lower.Contains("desc") || lower.Contains("descending") || lower.Contains("highest"))) - { - var paired = modified.Categories.Select((cat, i) => new - { - Cat = cat, - Val = i < modified.Values.Count ? modified.Values[i] : 0, - Lbl = i < modified.ValueLabels.Count ? modified.ValueLabels[i] : "", - Col = i < modified.BarColorsHex.Count ? modified.BarColorsHex[i] : "#0F6CBD" - }).OrderByDescending(x => x.Val).ToList(); - - modified.Categories = paired.Select(p => p.Cat).ToList(); - modified.Values = paired.Select(p => p.Val).ToList(); - modified.ValueLabels = paired.Select(p => p.Lbl).ToList(); - modified.BarColorsHex = paired.Select(p => p.Col).ToList(); - } - - return modified; - } - - if (currentModel is PdfTextElement text) - { - var modified = (PdfTextElement)text.Clone(); - - if (lower.Contains("bold")) modified.IsBold = true; - if (lower.Contains("italic")) modified.IsItalic = true; - if (lower.Contains("underline")) modified.IsUnderline = true; - - if (lower.Contains("center")) modified.Alignment = TextAlignmentMode.Center; - else if (lower.Contains("right")) modified.Alignment = TextAlignmentMode.Right; - else if (lower.Contains("left")) modified.Alignment = TextAlignmentMode.Left; - else if (lower.Contains("justify")) modified.Alignment = TextAlignmentMode.Justify; - - var sizeMatch = Regex.Match(prompt, @"(\d+)\s*(?:pt|px|size)", RegexOptions.IgnoreCase); - if (sizeMatch.Success && double.TryParse(sizeMatch.Groups[1].Value, out double size) && size > 4 && size < 120) - { - modified.FontSize = size; - } - - if (lower.Contains("red")) modified.TextColorHex = "#DC2626"; - else if (lower.Contains("blue")) modified.TextColorHex = "#0F6CBD"; - else if (lower.Contains("green")) modified.TextColorHex = "#16A34A"; - else if (lower.Contains("purple")) modified.TextColorHex = "#7C3AED"; - else if (lower.Contains("amber") || lower.Contains("orange")) modified.TextColorHex = "#D97706"; - else if (lower.Contains("dark") || lower.Contains("black")) modified.TextColorHex = "#0F172A"; - - if (lower.Contains("inter")) modified.FontFamily = "Inter"; - else if (lower.Contains("roboto")) modified.FontFamily = "Roboto"; - else if (lower.Contains("georgia")) modified.FontFamily = "Georgia"; - else if (lower.Contains("cascadia") || lower.Contains("mono")) modified.FontFamily = "Cascadia Code"; - else if (lower.Contains("times")) modified.FontFamily = "Times New Roman"; - - if (lower.Contains("bullet")) - { - var lines = modified.Text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - modified.Text = string.Join("\n", lines.Select(l => l.Trim().StartsWith("•") ? l : $"• {l.TrimStart('✔', '✓', '•', '-', '*', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.')}")); - } - else if (lower.Contains("check") || lower.Contains("checkmark")) - { - var lines = modified.Text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - modified.Text = string.Join("\n", lines.Select(l => l.Trim().StartsWith("✔") ? l : $"✔ {l.TrimStart('✔', '✓', '•', '-', '*', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.')}")); - } - else if (lower.Contains("number") || lower.Contains("numbered") || lower.Contains("roadmap")) - { - var lines = modified.Text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - modified.Text = string.Join("\n", lines.Select((l, i) => $"{i + 1}. {l.TrimStart('✔', '✓', '•', '-', '*', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.')}")); - } - else if (lower.Contains("executive") || lower.Contains("formal") || lower.Contains("professional")) - { - if (!modified.Text.StartsWith("Executive Summary:", StringComparison.OrdinalIgnoreCase)) - { - modified.Text = $"Executive Summary: {modified.Text}"; - } - } - - return modified; - } - - if (currentModel is PdfTableElement table) - { - var modified = (PdfTableElement)table.Clone(); - - if (lower.Contains("total") || lower.Contains("sum")) - { - var totalRow = new List { "Total" }; - for (int c = 1; c < modified.Headers.Count; c++) - { - if (c == modified.Headers.Count - 1) - { - double sum = 0; - foreach (var row in modified.Rows) - { - if (c < row.Count) - { - string clean = Regex.Replace(row[c], @"[^\d.]", ""); - if (double.TryParse(clean, out double num)) sum += num; - } - } - totalRow.Add(sum > 0 ? $"${sum:N2}" : "—"); - } - else - { - totalRow.Add(""); - } - } - modified.Rows.Add(totalRow); - } - - if (lower.Contains("emerald") || lower.Contains("green")) - { - modified.HeaderBackgroundHex = "#047857"; - } - else if (lower.Contains("purple")) - { - modified.HeaderBackgroundHex = "#6B21A8"; - } - else if (lower.Contains("slate") || lower.Contains("dark")) - { - modified.HeaderBackgroundHex = "#1E293B"; - } - - return modified; - } - - if (currentModel is PdfShapeElement shape) - { - var modified = (PdfShapeElement)shape.Clone(); - - if (lower.Contains("round")) - { - modified.ShapeType = ShapeType.RoundedRectangle; - modified.CornerRadius = 12; - } - else if (lower.Contains("circle") || lower.Contains("ellipse")) - { - modified.ShapeType = ShapeType.Circle; - } - - if (lower.Contains("blue")) - { - modified.FillColorHex = "#EFF6FF"; - modified.StrokeColorHex = "#2563EB"; - } - else if (lower.Contains("green") || lower.Contains("emerald")) - { - modified.FillColorHex = "#F0FDF4"; - modified.StrokeColorHex = "#16A34A"; - } - else if (lower.Contains("purple")) - { - modified.FillColorHex = "#FAF5FF"; - modified.StrokeColorHex = "#9333EA"; - } - - return modified; - } - - if (currentModel is PdfMathElement math) - { - var modified = (PdfMathElement)math.Clone(); - - if (lower.Contains("equation number") || lower.Contains("number")) - { - modified.ShowEquationNumber = true; - var numMatch = Regex.Match(prompt, @"(?:\(([^)]+)\)|number\s+([0-9.]+))", RegexOptions.IgnoreCase); - if (numMatch.Success) - { - string num = numMatch.Groups[1].Success ? numMatch.Groups[1].Value : numMatch.Groups[2].Value; - modified.EquationNumber = $"({num})"; - } - } - - if (lower.Contains("pythagor")) - { - modified.Formula = "a^2 + b^2 = c^2"; - } - else if (lower.Contains("quadratic")) - { - modified.Formula = @"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"; - } - else if (lower.Contains("euler")) - { - modified.Formula = "e^{i\\pi} + 1 = 0"; - } - - var sizeMatch = Regex.Match(prompt, @"(?:(?:font\s*size|size)\s*[:=]?\s*(\d+)|(\d+)\s*(?:pt|px|size))", RegexOptions.IgnoreCase); - if (sizeMatch.Success) - { - string val = sizeMatch.Groups[1].Success ? sizeMatch.Groups[1].Value : sizeMatch.Groups[2].Value; - if (double.TryParse(val, out double size) && size >= 8 && size <= 72) - { - modified.FontSize = size; - } - } - - if (lower.Contains("navy") || lower.Contains("dark")) modified.TextColorHex = "#0F172A"; - else if (lower.Contains("blue")) modified.TextColorHex = "#0F6CBD"; - else if (lower.Contains("red")) modified.TextColorHex = "#DC2626"; - else if (lower.Contains("emerald") || lower.Contains("green")) modified.TextColorHex = "#059669"; - - return modified; - } - - if (currentModel is PdfImageElement img) - { - var modified = (PdfImageElement)img.Clone(); - - if (lower.Contains("opacity") || lower.Contains("transparent") || lower.Contains("fade")) - { - var match = Regex.Match(prompt, @"(\d+)\s*%", RegexOptions.IgnoreCase); - if (match.Success && double.TryParse(match.Groups[1].Value, out double pct)) - { - modified.Opacity = Math.Clamp(pct / 100.0, 0.05, 1.0); - } - else - { - modified.Opacity = 0.7; - } - } - - if (lower.Contains("round") || lower.Contains("border")) - { - modified.CornerRadius = 8; - modified.BorderThickness = 1.5; - modified.BorderColorHex = "#0F6CBD"; - } - - return modified; - } - - if (currentModel is PdfSvgElement svg) - { - var modified = (PdfSvgElement)svg.Clone(); - - if (lower.Contains("gold")) modified.TintColorHex = "#D97706"; - else if (lower.Contains("blue")) modified.TintColorHex = "#0F6CBD"; - else if (lower.Contains("green") || lower.Contains("emerald")) modified.TintColorHex = "#16A34A"; - else if (lower.Contains("purple")) modified.TintColorHex = "#7C3AED"; - else if (lower.Contains("slate") || lower.Contains("dark")) modified.TintColorHex = "#1E293B"; - - return modified; - } - - if (currentModel is PdfDividerElement div) - { - var modified = (PdfDividerElement)div.Clone(); - - if (lower.Contains("thick")) modified.Thickness = Math.Max(2.5, modified.Thickness * 1.5); - else if (lower.Contains("thin")) modified.Thickness = Math.Max(0.5, modified.Thickness * 0.5); - - if (lower.Contains("blue")) modified.ColorHex = "#0F6CBD"; - else if (lower.Contains("emerald") || lower.Contains("green")) modified.ColorHex = "#10B981"; - else if (lower.Contains("slate") || lower.Contains("gray")) modified.ColorHex = "#CBD5E1"; - else if (lower.Contains("purple")) modified.ColorHex = "#7C3AED"; - - return modified; - } - - if (currentModel is PdfQrCodeElement qr) - { - var modified = (PdfQrCodeElement)qr.Clone(); - var urlMatch = Regex.Match(prompt, @"(https?://[^\s]+)", RegexOptions.IgnoreCase); - if (urlMatch.Success) modified.Content = urlMatch.Groups[1].Value; - - if (lower.Contains("dark") || lower.Contains("navy") || lower.Contains("slate")) modified.DarkColorHex = "#1E293B"; - else if (lower.Contains("blue")) modified.DarkColorHex = "#0F6CBD"; - - return modified; - } - - if (currentModel is PdfBarcodeElement barcode) - { - var modified = (PdfBarcodeElement)barcode.Clone(); - var codeMatch = Regex.Match(prompt, @"(?:to|is|value|code)\s+([A-Za-z0-9\-_]+)", RegexOptions.IgnoreCase); - if (codeMatch.Success) modified.CodeValue = codeMatch.Groups[1].Value; - - if (lower.Contains("blue")) modified.BarColorHex = "#0F6CBD"; - else if (lower.Contains("slate") || lower.Contains("dark")) modified.BarColorHex = "#1E293B"; - - return modified; - } - - if (currentModel is PdfStickyNoteElement note) - { - var modified = (PdfStickyNoteElement)note.Clone(); - - if (lower.Contains("yellow") || lower.Contains("amber")) modified.ColorHex = "#FEF3C7"; - else if (lower.Contains("blue")) modified.ColorHex = "#E0F2FE"; - else if (lower.Contains("green") || lower.Contains("mint")) modified.ColorHex = "#DCFCE7"; - else if (lower.Contains("pink") || lower.Contains("rose")) modified.ColorHex = "#FFE4E6"; - - if (lower.Contains("approve") || lower.Contains("approved")) modified.Status = "Approved"; - else if (lower.Contains("review")) modified.Status = "In Review"; - - return modified; - } - - if (currentModel is PdfFormFieldElement form) - { - var modified = (PdfFormFieldElement)form.Clone(); - - if (lower.Contains("required")) modified.IsRequired = true; - else if (lower.Contains("optional")) modified.IsRequired = false; - - var labelMatch = Regex.Match(prompt, """(?:label\s*(?:to|is|:)\s*|name\s*(?:to)?\s*)["']?([^"'\r\n]+)["']?""", RegexOptions.IgnoreCase); - if (labelMatch.Success && !string.IsNullOrWhiteSpace(labelMatch.Groups[1].Value)) - { - modified.Label = labelMatch.Groups[1].Value.Trim(); - } - - if (lower.Contains("blue")) modified.BorderColorHex = "#0F6CBD"; - else if (lower.Contains("emerald") || lower.Contains("green")) modified.BorderColorHex = "#059669"; - - return modified; - } - - return null; - } - - #region Element Factory Methods - - private static TextElementViewModel CreateHeadingElement( - string text, double x, double y, double width, double height, - double fontSize, string fontFamily, string textColorHex, bool isBold, string alignment) - { - return new TextElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = Math.Max(60, width), - Height = Math.Max(24, height), - Text = text, - FontSize = fontSize > 0 ? fontSize : 24, - FontFamily = string.IsNullOrWhiteSpace(fontFamily) ? "Georgia" : fontFamily, - TextColorHex = string.IsNullOrWhiteSpace(textColorHex) ? "#111827" : textColorHex, - IsBold = isBold, - ZIndex = 1000 - }; - } - - private static TextElementViewModel CreateParagraphElement( - string text, double x, double y, double width, double height, - double fontSize, string fontFamily, string textColorHex, string alignment) - { - return new TextElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = Math.Max(60, width), - Height = Math.Max(30, height), - Text = text, - FontSize = fontSize > 0 ? fontSize : 12, - FontFamily = string.IsNullOrWhiteSpace(fontFamily) ? "Inter" : fontFamily, - TextColorHex = string.IsNullOrWhiteSpace(textColorHex) ? "#374151" : textColorHex, - ZIndex = 1000 - }; - } - - private static ShapeElementViewModel CreateShapeElement( - string shapeTypeStr, double x, double y, double width, double height, - string fillColorHex, string strokeColorHex, double strokeThickness, double cornerRadius) - { - var shapeType = ShapeType.RoundedRectangle; - if (Enum.TryParse(shapeTypeStr, true, out var parsed)) - { - shapeType = parsed; - } - - return new ShapeElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = Math.Max(20, width), - Height = Math.Max(10, height), - ShapeType = shapeType, - FillColorHex = string.IsNullOrWhiteSpace(fillColorHex) ? "#F0F7FD" : fillColorHex, - StrokeColorHex = string.IsNullOrWhiteSpace(strokeColorHex) ? "#0F6CBD" : strokeColorHex, - StrokeThickness = strokeThickness >= 0 ? strokeThickness : 1.0, - CornerRadius = cornerRadius >= 0 ? cornerRadius : 6.0, - ZIndex = 100 // Background shape layer - }; - } - - private static TableElementViewModel CreateTableElement( - string[]? headers, string[][]? rows, double x, double y, double width, double height, - string headerBgColorHex, string headerTextColorHex) - { - var table = new TableElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = Math.Max(200, width), - Height = Math.Max(80, height), - HeaderBackgroundHex = string.IsNullOrWhiteSpace(headerBgColorHex) ? "#0F6CBD" : headerBgColorHex, - HeaderTextHex = string.IsNullOrWhiteSpace(headerTextColorHex) ? "#FFFFFF" : headerTextColorHex, - ZIndex = 500 - }; - - if (headers != null && headers.Length > 0) - { - table.Headers.Clear(); - foreach (var h in headers) - { - table.Headers.Add(new TableHeaderItem(h)); - } - } - - if (rows != null && rows.Length > 0) - { - table.Rows.Clear(); - foreach (var row in rows) - { - table.Rows.Add(new TableRowItem(row)); - } - } - - return table; - } - - private static DividerElementViewModel CreateDividerElement( - double x, double y, double width, double height, - string colorHex, double thickness, string orientation) - { - bool isVertical = orientation.Equals("Vertical", StringComparison.OrdinalIgnoreCase); - - return new DividerElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = Math.Max(10, width), - Height = isVertical ? Math.Max(10, height) : 2, - ColorHex = string.IsNullOrWhiteSpace(colorHex) ? "#CBD5E1" : colorHex, - Thickness = thickness > 0 ? thickness : 1.0, - IsVertical = isVertical, - ZIndex = 600 - }; - } - - private static ShapeElementViewModel CreateBadgeElement( - string text, double x, double y, double width, double height, - string bgColorHex, string textColorHex) - { - return new ShapeElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = Math.Max(60, width), - Height = Math.Max(22, height), - ShapeType = ShapeType.RoundedRectangle, - FillColorHex = string.IsNullOrWhiteSpace(bgColorHex) ? "#EFF6FF" : bgColorHex, - StrokeColorHex = string.IsNullOrWhiteSpace(textColorHex) ? "#1D4ED8" : textColorHex, - StrokeThickness = 1.0, - CornerRadius = height / 2.0, // Pill badge - Label = text, - LabelColorHex = textColorHex, - LabelFontSize = 11, - ZIndex = 800 - }; - } - - private static List CreateCardElements( - string title, string subtitle, string body, - double x, double y, double width, double height, string themeColorHex) - { - var list = new List(); - - // 1. Background Card Shape (ZIndex = 50) - var cardShape = new ShapeElementViewModel - { - X = x, - Y = y, - Width = width, - Height = height, - ShapeType = ShapeType.RoundedRectangle, - FillColorHex = "#F8FAFC", - StrokeColorHex = string.IsNullOrWhiteSpace(themeColorHex) ? "#0F6CBD" : themeColorHex, - StrokeThickness = 1.2, - CornerRadius = 10, - ZIndex = 50 - }; - list.Add(cardShape); - - // 2. Title Text - var titleEl = new TextElementViewModel - { - X = x + 16, - Y = y + 14, - Width = width - 32, - Height = 26, - Text = title, - FontSize = 15, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A", - ZIndex = 1000 - }; - list.Add(titleEl); - - double contentY = y + 42; - - // 3. Subtitle Text (if present) - if (!string.IsNullOrWhiteSpace(subtitle)) - { - var subEl = new TextElementViewModel - { - X = x + 16, - Y = contentY, - Width = width - 32, - Height = 20, - Text = subtitle, - FontSize = 11, - FontFamily = "Inter", - TextColorHex = string.IsNullOrWhiteSpace(themeColorHex) ? "#0F6CBD" : themeColorHex, - ZIndex = 1000 - }; - list.Add(subEl); - contentY += 24; - } - - // 4. Body Text - if (!string.IsNullOrWhiteSpace(body)) - { - var bodyEl = new TextElementViewModel - { - X = x + 16, - Y = contentY, - Width = width - 32, - Height = Math.Max(30, (y + height) - contentY - 14), - Text = body, - FontSize = 11.5, - FontFamily = "Inter", - TextColorHex = "#334155", - ZIndex = 1000 - }; - list.Add(bodyEl); - } - - return list; - } - - private static SvgElementViewModel CreateSvgOrnamentElement( - string ornamentName, double x, double y, double width, double height, string colorHex) - { - string svg = SvgOrnamentLibrary.GetSvg(ornamentName, colorHex); - return new SvgElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = Math.Max(20, width), - Height = Math.Max(20, height), - SvgSource = svg, - PresetName = ornamentName, - TintColorHex = colorHex, - ZIndex = 700 - }; - } - - private static QrCodeElementViewModel CreateQrCodeElement(string payload, double x, double y, double size) - { - double dim = Math.Max(40, size); - return new QrCodeElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = dim, - Height = dim, - Content = string.IsNullOrWhiteSpace(payload) ? "https://github.com/PrashantUnity/PDFCreator" : payload, - ZIndex = 800 - }; - } - - private static BarcodeElementViewModel CreateBarcodeElement(string payload, string format, double x, double y, double width, double height) - { - return new BarcodeElementViewModel - { - X = Math.Max(0, x), - Y = Math.Max(0, y), - Width = Math.Max(100, width), - Height = Math.Max(30, height), - CodeValue = string.IsNullOrWhiteSpace(payload) ? "DOC-2026-001" : payload, - BarcodeFormat = string.IsNullOrWhiteSpace(format) ? "Code128" : format, - ZIndex = 800 - }; - } - - #endregion - - #region JSON Fallback Parsing - - private void ParseAndExecuteJsonFallback( - string text, - PageViewModel page, - List created, - List actions, - Action? progress, - (double X, double Y)? targetPoint = null) - { - var match = Regex.Match(text, """\[[\s\S]*\]"""); - if (!match.Success) return; - - try - { - using var doc = JsonDocument.Parse(match.Value); - if (doc.RootElement.ValueKind != JsonValueKind.Array) return; - - double defaultX = targetPoint?.X ?? 40; - double defaultY = targetPoint?.Y ?? 40; - - foreach (var item in doc.RootElement.EnumerateArray()) - { - if (!item.TryGetProperty("action", out var actionProp)) continue; - string action = actionProp.GetString() ?? ""; - - double x = GetDouble(item, "x", defaultX); - double y = GetDouble(item, "y", defaultY); - double w = GetDouble(item, "width", 300); - double h = GetDouble(item, "height", 50); - - switch (action.ToLowerInvariant()) - { - case "addheading": - { - string txt = GetString(item, "text", "Heading"); - double fontSize = GetDouble(item, "fontSize", 24); - string color = GetString(item, "textColorHex", "#111827"); - var el = CreateHeadingElement(txt, x, y, w, h, fontSize, "Georgia", color, true, "Left"); - page.AddElement(el); - created.Add(el); - actions.Add($"Heading: \"{Truncate(txt, 20)}\""); - progress?.Invoke($"Created Heading: \"{Truncate(txt, 20)}\""); - break; - } - case "addparagraph": - { - string txt = GetString(item, "text", "Paragraph text"); - double fontSize = GetDouble(item, "fontSize", 12); - string color = GetString(item, "textColorHex", "#374151"); - var el = CreateParagraphElement(txt, x, y, w, h, fontSize, "Inter", color, "Left"); - page.AddElement(el); - created.Add(el); - actions.Add($"Paragraph: \"{Truncate(txt, 20)}\""); - progress?.Invoke($"Created Paragraph: \"{Truncate(txt, 20)}\""); - break; - } - case "addshape": - { - string shape = GetString(item, "shapeType", "RoundedRectangle"); - string fill = GetString(item, "fillColorHex", "#F0F7FD"); - string stroke = GetString(item, "strokeColorHex", "#0F6CBD"); - double radius = GetDouble(item, "cornerRadius", 6); - var el = CreateShapeElement(shape, x, y, w, h, fill, stroke, 1.0, radius); - page.AddElement(el); - created.Add(el); - actions.Add($"Shape: {shape}"); - progress?.Invoke($"Created Shape: {shape}"); - break; - } - case "adddivider": - { - string color = GetString(item, "colorHex", "#CBD5E1"); - var el = CreateDividerElement(x, y, w, h, color, 1.0, "Horizontal"); - page.AddElement(el); - created.Add(el); - actions.Add("Divider line"); - progress?.Invoke("Created Divider"); - break; - } - case "addbadge": - { - string txt = GetString(item, "text", "STATUS"); - string bg = GetString(item, "bgColorHex", "#EFF6FF"); - string textCol = GetString(item, "textColorHex", "#1D4ED8"); - var el = CreateBadgeElement(txt, x, y, w, h, bg, textCol); - page.AddElement(el); - created.Add(el); - actions.Add($"Badge: \"{txt}\""); - progress?.Invoke($"Created Badge: \"{txt}\""); - break; - } - } - } - } - catch - { - // JSON fallback parsing failed; proceed without crash - } - } - - private static double GetDouble(JsonElement el, string prop, double fallback) - { - if (el.TryGetProperty(prop, out var p) && p.TryGetDouble(out var v)) return v; - return fallback; - } - - private static string GetString(JsonElement el, string prop, string fallback) - { - if (el.TryGetProperty(prop, out var p)) return p.GetString() ?? fallback; - return fallback; - } - - private static string Truncate(string str, int maxLen) - { - if (string.IsNullOrEmpty(str)) return ""; - return str.Length <= maxLen ? str : str[..maxLen] + "..."; - } - - #endregion -} diff --git a/src/PdfEditorApp/Services/AI/Providers/GroqAiProvider.cs b/src/PdfEditorApp/Services/AI/Providers/GroqAiProvider.cs deleted file mode 100644 index cf5f9d7..0000000 --- a/src/PdfEditorApp/Services/AI/Providers/GroqAiProvider.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using OpenAI; -using OpenAI.Chat; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services.AI.Providers; - -public class GroqAiProvider : IAiProvider -{ - public string ProviderId => "groq"; - public string DisplayName => "Groq (Ultra-Fast Cloud LPU)"; - public bool RequiresApiKey => true; - public bool SupportsCustomEndpoint => true; - public string DefaultEndpoint => "https://api.groq.com/openai/v1"; - public string IconKind => "LightningBoltOutline"; - - public Task> GetModelsAsync(AiSettingsModel settings, CancellationToken ct = default) - { - var models = new List - { - new() { Id = "llama-3.3-70b-versatile", DisplayName = "Llama 3.3 (70B Versatile)", Provider = AiProviderType.CustomOpenAiCompatible, Tier = AiModelTier.PaidCloud, ParameterSize = "70B" }, - new() { Id = "mixtral-8x7b-32768", DisplayName = "Mixtral (8x7B MoE)", Provider = AiProviderType.CustomOpenAiCompatible, Tier = AiModelTier.PaidCloud, ParameterSize = "8x7B" }, - new() { Id = "deepseek-r1-distill-llama-70b", DisplayName = "DeepSeek R1 Distill (70B)", Provider = AiProviderType.CustomOpenAiCompatible, Tier = AiModelTier.PaidCloud, ParameterSize = "70B" } - }; - return Task.FromResult>(models); - } - - public IChatClient CreateChatClient(AiSettingsModel settings) - { - string apiKey = string.IsNullOrWhiteSpace(settings.OpenAiApiKey) - ? "dummy-key" - : settings.OpenAiApiKey.Trim(); - - string model = string.IsNullOrWhiteSpace(settings.CustomModelName) - ? (string.IsNullOrWhiteSpace(settings.SelectedModelId) ? "llama-3.3-70b-versatile" : settings.SelectedModelId) - : settings.CustomModelName.Trim(); - - string baseUrl = string.IsNullOrWhiteSpace(settings.CustomBaseUrl) - ? DefaultEndpoint - : settings.CustomBaseUrl.TrimEnd('/'); - - var client = new ChatClient( - model, - new ApiKeyCredential(apiKey), - new OpenAIClientOptions { Endpoint = new Uri(baseUrl) }); - - return client.AsIChatClient(); - } - - public async Task<(bool Success, string Message, TimeSpan Latency)> TestConnectionAsync(AiSettingsModel settings, CancellationToken ct = default) - { - var sw = System.Diagnostics.Stopwatch.StartNew(); - try - { - var client = CreateChatClient(settings); - var response = await client.GetResponseAsync("Ping", new ChatOptions { MaxOutputTokens = 10 }, ct); - sw.Stop(); - return (true, "Connected to Groq", sw.Elapsed); - } - catch (Exception ex) - { - sw.Stop(); - return (false, ex.Message, sw.Elapsed); - } - } -} diff --git a/src/PdfEditorApp/Services/AI/Providers/OllamaAiProvider.cs b/src/PdfEditorApp/Services/AI/Providers/OllamaAiProvider.cs deleted file mode 100644 index 28da022..0000000 --- a/src/PdfEditorApp/Services/AI/Providers/OllamaAiProvider.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using OllamaSharp; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services.AI.Providers; - -public class OllamaAiProvider : IAiProvider -{ - public string ProviderId => "ollama"; - public string DisplayName => "Ollama (Local Private AI)"; - public bool RequiresApiKey => false; - public bool SupportsCustomEndpoint => true; - public string DefaultEndpoint => "http://localhost:11434"; - public string IconKind => "Laptop"; - - public Task> GetModelsAsync(AiSettingsModel settings, CancellationToken ct = default) - { - var models = new List - { - new() { Id = "llama3.2", DisplayName = "Llama 3.2 (3B)", Provider = AiProviderType.OllamaLocal, Tier = AiModelTier.FreeLocal, ParameterSize = "3B" }, - new() { Id = "mistral", DisplayName = "Mistral (7B)", Provider = AiProviderType.OllamaLocal, Tier = AiModelTier.FreeLocal, ParameterSize = "7B" }, - new() { Id = "deepseek-r1:8b", DisplayName = "DeepSeek R1 (8B)", Provider = AiProviderType.OllamaLocal, Tier = AiModelTier.FreeLocal, ParameterSize = "8B" }, - new() { Id = "qwen2.5:7b", DisplayName = "Qwen 2.5 (7B)", Provider = AiProviderType.OllamaLocal, Tier = AiModelTier.FreeLocal, ParameterSize = "7B" } - }; - return Task.FromResult>(models); - } - - public IChatClient CreateChatClient(AiSettingsModel settings) - { - string endpoint = string.IsNullOrWhiteSpace(settings.OllamaEndpoint) - ? DefaultEndpoint - : settings.OllamaEndpoint.TrimEnd('/'); - - string model = string.IsNullOrWhiteSpace(settings.SelectedModelId) - ? "llama3.2" - : settings.SelectedModelId; - - if (!string.IsNullOrWhiteSpace(settings.OllamaApiKey)) - { - var httpClient = new HttpClient { BaseAddress = new Uri(endpoint), Timeout = TimeSpan.FromSeconds(60) }; - httpClient.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", settings.OllamaApiKey.Trim()); - return new OllamaApiClient(httpClient, model); - } - - return new OllamaApiClient(new Uri(endpoint), model); - } - - public async Task<(bool Success, string Message, TimeSpan Latency)> TestConnectionAsync(AiSettingsModel settings, CancellationToken ct = default) - { - var sw = System.Diagnostics.Stopwatch.StartNew(); - try - { - var client = CreateChatClient(settings); - var response = await client.GetResponseAsync("Ping", new ChatOptions { MaxOutputTokens = 10 }, ct); - sw.Stop(); - return (true, "Connected to Ollama", sw.Elapsed); - } - catch (Exception ex) - { - sw.Stop(); - return (false, ex.Message, sw.Elapsed); - } - } -} diff --git a/src/PdfEditorApp/Services/AI/Providers/OpenAiProvider.cs b/src/PdfEditorApp/Services/AI/Providers/OpenAiProvider.cs deleted file mode 100644 index 10e5e26..0000000 --- a/src/PdfEditorApp/Services/AI/Providers/OpenAiProvider.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using OpenAI.Chat; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services.AI.Providers; - -public class OpenAiProvider : IAiProvider -{ - public string ProviderId => "openai"; - public string DisplayName => "OpenAI Official Cloud API"; - public bool RequiresApiKey => true; - public bool SupportsCustomEndpoint => false; - public string DefaultEndpoint => "https://api.openai.com/v1"; - public string IconKind => "Brain"; - - public Task> GetModelsAsync(AiSettingsModel settings, CancellationToken ct = default) - { - var models = new List - { - new() { Id = "gpt-4o", DisplayName = "GPT-4o (Omni)", Provider = AiProviderType.OpenAiCloud, Tier = AiModelTier.PaidCloud, ParameterSize = "Flagship" }, - new() { Id = "gpt-4o-mini", DisplayName = "GPT-4o Mini", Provider = AiProviderType.OpenAiCloud, Tier = AiModelTier.PaidCloud, ParameterSize = "Fast & Cheap" }, - new() { Id = "o1-mini", DisplayName = "o1-mini (Reasoning)", Provider = AiProviderType.OpenAiCloud, Tier = AiModelTier.PaidCloud, ParameterSize = "Reasoning" } - }; - return Task.FromResult>(models); - } - - public IChatClient CreateChatClient(AiSettingsModel settings) - { - string apiKey = string.IsNullOrWhiteSpace(settings.OpenAiApiKey) - ? "dummy-key" - : settings.OpenAiApiKey.Trim(); - - string model = string.IsNullOrWhiteSpace(settings.SelectedModelId) - ? "gpt-4o-mini" - : settings.SelectedModelId; - - var client = new ChatClient(model, apiKey); - return client.AsIChatClient(); - } - - public async Task<(bool Success, string Message, TimeSpan Latency)> TestConnectionAsync(AiSettingsModel settings, CancellationToken ct = default) - { - var sw = System.Diagnostics.Stopwatch.StartNew(); - try - { - var client = CreateChatClient(settings); - var response = await client.GetResponseAsync("Ping", new ChatOptions { MaxOutputTokens = 10 }, ct); - sw.Stop(); - return (true, "Connected to OpenAI", sw.Elapsed); - } - catch (Exception ex) - { - sw.Stop(); - return (false, ex.Message, sw.Elapsed); - } - } -} diff --git a/src/PdfEditorApp/Services/BatchPdfGeneratorService.cs b/src/PdfEditorApp/Services/BatchPdfGeneratorService.cs deleted file mode 100644 index e5209f6..0000000 --- a/src/PdfEditorApp/Services/BatchPdfGeneratorService.cs +++ /dev/null @@ -1,444 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Data; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public class BatchPdfGeneratorService : IBatchPdfGenerator -{ - private readonly IDataMergeEngine _mergeEngine; - private readonly IPdfExportService _exportService; - - public BatchPdfGeneratorService(IDataMergeEngine mergeEngine, IPdfExportService exportService) - { - _mergeEngine = mergeEngine ?? throw new ArgumentNullException(nameof(mergeEngine)); - _exportService = exportService ?? throw new ArgumentNullException(nameof(exportService)); - } - - public async Task GenerateBatchAsync( - PdfDocumentModel template, - DataMatrix matrix, - IReadOnlyList? mappings, - BatchGenerationConfig config, - IProgress? progress = null, - CancellationToken cancellationToken = default) - { - if (template == null) throw new ArgumentNullException(nameof(template)); - if (matrix == null) throw new ArgumentNullException(nameof(matrix)); - if (config == null) throw new ArgumentNullException(nameof(config)); - - var stopwatch = Stopwatch.StartNew(); - var result = new BatchGenerationResult(); - - int totalRows = matrix.RowCount; - if (config.MaxRecordCount.HasValue && config.MaxRecordCount.Value > 0) - { - totalRows = Math.Min(totalRows, config.MaxRecordCount.Value); - } - - if (totalRows == 0) - { - stopwatch.Stop(); - result.ElapsedTime = stopwatch.Elapsed; - return result; - } - - // Ensure output directories exist - if (!string.IsNullOrEmpty(config.OutputDirectory) && !Directory.Exists(config.OutputDirectory)) - { - Directory.CreateDirectory(config.OutputDirectory); - } - - if (config.OutputMode != BatchOutputMode.SeparateFiles && !string.IsNullOrEmpty(config.OutputFilePath)) - { - string? dir = Path.GetDirectoryName(config.OutputFilePath); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - } - - await Task.Run(() => - { - if (config.OutputMode == BatchOutputMode.SingleMergedPdf) - { - GenerateSingleMergedPdf(template, matrix, mappings, config, totalRows, result, progress, cancellationToken); - } - else if (config.OutputMode == BatchOutputMode.ZipArchive) - { - GenerateZipArchive(template, matrix, mappings, config, totalRows, result, progress, cancellationToken); - } - else - { - GenerateSeparateFiles(template, matrix, mappings, config, totalRows, result, progress, cancellationToken); - } - }, cancellationToken); - - stopwatch.Stop(); - result.ElapsedTime = stopwatch.Elapsed; - return result; - } - - private void GenerateSeparateFiles( - PdfDocumentModel template, - DataMatrix matrix, - IReadOnlyList? mappings, - BatchGenerationConfig config, - int totalRows, - BatchGenerationResult result, - IProgress? progress, - CancellationToken cancellationToken) - { - string outDir = string.IsNullOrWhiteSpace(config.OutputDirectory) ? Directory.GetCurrentDirectory() : config.OutputDirectory; - - for (int r = 0; r < totalRows; r++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var record = BuildRecordDictionary(matrix, r, mappings); - if (config.SkipEmptyRows && record.Values.All(string.IsNullOrWhiteSpace)) - { - continue; - } - - result.TotalProcessed++; - - try - { - // Hydrate template - var hydratedDoc = _mergeEngine.HydrateDocument(template, record); - - // Determine dynamic filename - string filename = ResolveFilename(config.FilenamePattern, record, r + 1); - string filePath = Path.Combine(outDir, filename); - - // Handle unique filename collisions - filePath = EnsureUniqueFilePath(filePath); - - // Export to PDF - byte[] pdfBytes = _exportService.GeneratePdfBytes(hydratedDoc); - File.WriteAllBytes(filePath, pdfBytes); - - result.GeneratedFiles.Add(filePath); - result.SuccessfulCount++; - - progress?.Report(new BatchProgressReport - { - CurrentIndex = r + 1, - TotalCount = totalRows, - CurrentItemName = Path.GetFileName(filePath), - SucceededCount = result.SuccessfulCount, - FailedCount = result.FailedCount, - StatusMessage = $"Generated {Path.GetFileName(filePath)} ({r + 1}/{totalRows})" - }); - } - catch (Exception ex) - { - result.FailedCount++; - result.Errors.Add((r, ex.Message)); - - progress?.Report(new BatchProgressReport - { - CurrentIndex = r + 1, - TotalCount = totalRows, - CurrentItemName = $"Row {r + 1}", - SucceededCount = result.SuccessfulCount, - FailedCount = result.FailedCount, - StatusMessage = $"Error on row {r + 1}: {ex.Message}" - }); - } - } - } - - private void GenerateSingleMergedPdf( - PdfDocumentModel template, - DataMatrix matrix, - IReadOnlyList? mappings, - BatchGenerationConfig config, - int totalRows, - BatchGenerationResult result, - IProgress? progress, - CancellationToken cancellationToken) - { - string targetFilePath = !string.IsNullOrEmpty(config.OutputFilePath) - ? config.OutputFilePath - : Path.Combine(string.IsNullOrWhiteSpace(config.OutputDirectory) ? Directory.GetCurrentDirectory() : config.OutputDirectory, "Merged_Batch_Output.pdf"); - - var mergedDoc = template.Clone(); - mergedDoc.Pages.Clear(); - int globalPageNum = 1; - - for (int r = 0; r < totalRows; r++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var record = BuildRecordDictionary(matrix, r, mappings); - if (config.SkipEmptyRows && record.Values.All(string.IsNullOrWhiteSpace)) - { - continue; - } - - result.TotalProcessed++; - - try - { - var hydratedDoc = _mergeEngine.HydrateDocument(template, record); - foreach (var page in hydratedDoc.Pages) - { - var pageClone = page.Clone(); - pageClone.PageNumber = globalPageNum++; - mergedDoc.Pages.Add(pageClone); - } - - result.SuccessfulCount++; - - progress?.Report(new BatchProgressReport - { - CurrentIndex = r + 1, - TotalCount = totalRows, - CurrentItemName = $"Record {r + 1}", - SucceededCount = result.SuccessfulCount, - FailedCount = result.FailedCount, - StatusMessage = $"Prepared record {r + 1} of {totalRows}" - }); - } - catch (Exception ex) - { - result.FailedCount++; - result.Errors.Add((r, ex.Message)); - } - } - - if (mergedDoc.Pages.Count > 0) - { - byte[] pdfBytes = _exportService.GeneratePdfBytes(mergedDoc); - File.WriteAllBytes(targetFilePath, pdfBytes); - result.OutputMergedFilePath = targetFilePath; - result.GeneratedFiles.Add(targetFilePath); - } - } - - private void GenerateZipArchive( - PdfDocumentModel template, - DataMatrix matrix, - IReadOnlyList? mappings, - BatchGenerationConfig config, - int totalRows, - BatchGenerationResult result, - IProgress? progress, - CancellationToken cancellationToken) - { - string zipFilePath = !string.IsNullOrEmpty(config.OutputFilePath) - ? config.OutputFilePath - : Path.Combine(string.IsNullOrWhiteSpace(config.OutputDirectory) ? Directory.GetCurrentDirectory() : config.OutputDirectory, "Batch_PDFs_Archive.zip"); - - using (var fileStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) - using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) - { - var addedNames = new HashSet(StringComparer.OrdinalIgnoreCase); - - for (int r = 0; r < totalRows; r++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var record = BuildRecordDictionary(matrix, r, mappings); - if (config.SkipEmptyRows && record.Values.All(string.IsNullOrWhiteSpace)) - { - continue; - } - - result.TotalProcessed++; - - try - { - var hydratedDoc = _mergeEngine.HydrateDocument(template, record); - string filename = ResolveFilename(config.FilenamePattern, record, r + 1); - - // Ensure unique name in zip - int counter = 1; - string entryName = filename; - string nameNoExt = Path.GetFileNameWithoutExtension(filename); - string ext = Path.GetExtension(filename); - - while (addedNames.Contains(entryName)) - { - entryName = $"{nameNoExt}_{counter++}{ext}"; - } - addedNames.Add(entryName); - - byte[] pdfBytes = _exportService.GeneratePdfBytes(hydratedDoc); - var entry = zipArchive.CreateEntry(entryName, CompressionLevel.Optimal); - using (var entryStream = entry.Open()) - { - entryStream.Write(pdfBytes, 0, pdfBytes.Length); - } - - result.SuccessfulCount++; - - progress?.Report(new BatchProgressReport - { - CurrentIndex = r + 1, - TotalCount = totalRows, - CurrentItemName = entryName, - SucceededCount = result.SuccessfulCount, - FailedCount = result.FailedCount, - StatusMessage = $"Archived {entryName} ({r + 1}/{totalRows})" - }); - } - catch (Exception ex) - { - result.FailedCount++; - result.Errors.Add((r, ex.Message)); - } - } - } - - result.OutputZipFilePath = zipFilePath; - result.GeneratedFiles.Add(zipFilePath); - } - - public static Dictionary BuildRecordDictionary(DataMatrix matrix, int rowIndex, IReadOnlyList? mappings) - { - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // 1. First add all direct matrix columns - for (int c = 0; c < matrix.ColumnCount; c++) - { - string header = matrix.Headers[c]; - string val = matrix.GetCellValue(rowIndex, c); - dict[header] = val; - } - - // Add standard systemic variables - dict["Index"] = (rowIndex + 1).ToString(); - dict["RowNumber"] = (rowIndex + 1).ToString(); - dict["TotalRecords"] = matrix.RowCount.ToString(); - dict["CurrentDate"] = DateTime.Now.ToString("yyyy-MM-dd"); - dict["CurrentYear"] = DateTime.Now.Year.ToString(); - dict["CurrentMonth"] = DateTime.Now.ToString("MMMM"); - - // 2. Overlay custom mappings - if (mappings != null) - { - foreach (var m in mappings) - { - if (string.IsNullOrWhiteSpace(m.PlaceholderTag)) continue; - - string val = string.Empty; - if (!string.IsNullOrWhiteSpace(m.DataColumnName) && dict.TryGetValue(m.DataColumnName, out var mappedVal)) - { - val = mappedVal; - } - - if (string.IsNullOrWhiteSpace(val) && !string.IsNullOrWhiteSpace(m.DefaultValue)) - { - val = m.DefaultValue; - } - - // Apply transform - val = ApplyFieldTransform(val, m.Transform, m.CustomFormat); - - dict[m.PlaceholderTag] = val; - } - } - - return dict; - } - - private static string ApplyFieldTransform(string val, FieldTransformType transform, string customFormat) - { - if (string.IsNullOrEmpty(val)) return val; - - switch (transform) - { - case FieldTransformType.Uppercase: - return val.ToUpperInvariant(); - - case FieldTransformType.Lowercase: - return val.ToLowerInvariant(); - - case FieldTransformType.TitleCase: - return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(val.ToLowerInvariant()); - - case FieldTransformType.Currency: - if (DataMatrix.TryParseNumeric(val, out double currNum)) - { - return currNum.ToString("C", CultureInfo.CurrentCulture); - } - break; - - case FieldTransformType.Numeric: - if (DataMatrix.TryParseNumeric(val, out double numVal)) - { - string fmt = string.IsNullOrWhiteSpace(customFormat) ? "N2" : customFormat; - return numVal.ToString(fmt, CultureInfo.InvariantCulture); - } - break; - - case FieldTransformType.Percentage: - if (DataMatrix.TryParseNumeric(val, out double pctVal)) - { - return (pctVal / 100.0).ToString("P1", CultureInfo.InvariantCulture); - } - break; - - case FieldTransformType.Date: - if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) || - DateTime.TryParse(val, CultureInfo.CurrentCulture, DateTimeStyles.None, out dt)) - { - string fmt = string.IsNullOrWhiteSpace(customFormat) ? "yyyy-MM-dd" : customFormat; - return dt.ToString(fmt, CultureInfo.InvariantCulture); - } - break; - } - - return val; - } - - private string ResolveFilename(string pattern, IReadOnlyDictionary record, int index) - { - if (string.IsNullOrWhiteSpace(pattern)) - { - pattern = "Document_{{Index}}.pdf"; - } - - string raw = _mergeEngine.EvaluateText(pattern, record); - raw = raw.Replace("{{Index}}", index.ToString()); - - if (!raw.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) - { - raw += ".pdf"; - } - - // Sanitize invalid characters - var invalidChars = Path.GetInvalidFileNameChars(); - var sanitized = new string(raw.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray()); - return sanitized; - } - - private static string EnsureUniqueFilePath(string filePath) - { - if (!File.Exists(filePath)) return filePath; - - string dir = Path.GetDirectoryName(filePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(filePath); - string ext = Path.GetExtension(filePath); - - int counter = 1; - while (File.Exists(filePath)) - { - filePath = Path.Combine(dir, $"{name}_{counter++}{ext}"); - } - - return filePath; - } -} diff --git a/src/PdfEditorApp/Services/Canvas/CanvasElementRegistry.cs b/src/PdfEditorApp/Services/Canvas/CanvasElementRegistry.cs deleted file mode 100644 index 7ae8865..0000000 --- a/src/PdfEditorApp/Services/Canvas/CanvasElementRegistry.cs +++ /dev/null @@ -1,377 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Services.Canvas; - -/// -/// Combined registry and factory interface for canvas element descriptors and ViewModels. -/// -public interface ICanvasElementService : ICanvasElementRegistry -{ - /// Creates and initializes a ViewModel matching the specified domain model. - ElementViewModelBase CreateViewModel(PdfElementBase model); - - /// Creates a default ViewModel for the given element type ID. - ElementViewModelBase CreateViewModel(string elementTypeId); - - /// Clones an element ViewModel by serializing to model and re-instantiating. - ElementViewModelBase CloneViewModel(ElementViewModelBase source); -} - -/// -/// Thread-safe implementation of supporting both -/// core built-in canvas elements and dynamic plugin-contributed element types. -/// -public class CanvasElementRegistry : ICanvasElementService -{ - private readonly IServiceProvider? _serviceProvider; - private readonly ConcurrentDictionary _descriptorsById = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _descriptorsByModelType = new(); - - public CanvasElementRegistry(IServiceProvider? serviceProvider = null, bool seedBuiltIns = true) - { - _serviceProvider = serviceProvider; - if (seedBuiltIns) - { - RegisterBuiltInElements(); - } - } - - private void RegisterBuiltInElements() - { - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.text", - DisplayName = "Text Block", - ModelType = typeof(PdfTextElement), - ViewModelType = typeof(TextElementViewModel), - IconKind = "FormatText", - DefaultWidth = 200, - DefaultHeight = 60, - Tags = new[] { "text", "typography", "heading", "label" }, - Factory = (sp, m) => new TextElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.image", - DisplayName = "Image", - ModelType = typeof(PdfImageElement), - ViewModelType = typeof(ImageElementViewModel), - IconKind = "ImageOutline", - DefaultWidth = 200, - DefaultHeight = 150, - Tags = new[] { "image", "photo", "bitmap", "picture" }, - Factory = (sp, m) => new ImageElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.shape", - DisplayName = "Geometric Shape", - ModelType = typeof(PdfShapeElement), - ViewModelType = typeof(ShapeElementViewModel), - IconKind = "ShapeOutline", - DefaultWidth = 150, - DefaultHeight = 100, - Tags = new[] { "shape", "rectangle", "circle", "arrow", "polygon" }, - Factory = (sp, m) => new ShapeElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.divider", - DisplayName = "Divider Line", - ModelType = typeof(PdfDividerElement), - ViewModelType = typeof(DividerElementViewModel), - IconKind = "Minus", - DefaultWidth = 300, - DefaultHeight = 20, - Tags = new[] { "divider", "line", "separator" }, - Factory = (sp, m) => new DividerElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.table", - DisplayName = "Data Grid / Table", - ModelType = typeof(PdfTableElement), - ViewModelType = typeof(TableElementViewModel), - IconKind = "Table", - DefaultWidth = 350, - DefaultHeight = 180, - Tags = new[] { "table", "grid", "rows", "columns", "data" }, - Factory = (sp, m) => new TableElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.chart", - DisplayName = "Interactive Chart", - ModelType = typeof(PdfChartElement), - ViewModelType = typeof(ChartElementViewModel), - IconKind = "ChartBar", - DefaultWidth = 320, - DefaultHeight = 220, - Tags = new[] { "chart", "bar", "line", "pie", "statistics", "data" }, - Factory = (sp, m) => new ChartElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.watermark", - DisplayName = "Watermark", - ModelType = typeof(PdfWatermarkElement), - ViewModelType = typeof(WatermarkElementViewModel), - IconKind = "Watermark", - DefaultWidth = 400, - DefaultHeight = 120, - Tags = new[] { "watermark", "stamp", "draft", "confidential" }, - Factory = (sp, m) => new WatermarkElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.formfield", - DisplayName = "Form Field", - ModelType = typeof(PdfFormFieldElement), - ViewModelType = typeof(FormFieldElementViewModel), - IconKind = "TextBoxOutline", - DefaultWidth = 180, - DefaultHeight = 36, - Tags = new[] { "form", "field", "input", "checkbox", "dropdown" }, - Factory = (sp, m) => new FormFieldElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.qrcode", - DisplayName = "QR Code", - ModelType = typeof(PdfQrCodeElement), - ViewModelType = typeof(QrCodeElementViewModel), - IconKind = "Qrcode", - DefaultWidth = 120, - DefaultHeight = 120, - Tags = new[] { "qr", "qrcode", "barcode", "link" }, - Factory = (sp, m) => new QrCodeElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.barcode", - DisplayName = "Barcode", - ModelType = typeof(PdfBarcodeElement), - ViewModelType = typeof(BarcodeElementViewModel), - IconKind = "Barcode", - DefaultWidth = 200, - DefaultHeight = 80, - Tags = new[] { "barcode", "code128", "ean", "upc" }, - Factory = (sp, m) => new BarcodeElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.redaction", - DisplayName = "Redaction Annotation", - ModelType = typeof(PdfRedactionElement), - ViewModelType = typeof(RedactionElementViewModel), - IconKind = "EyeOffOutline", - DefaultWidth = 160, - DefaultHeight = 40, - Tags = new[] { "redact", "redaction", "blackout", "security" }, - Factory = (sp, m) => new RedactionElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.ink", - DisplayName = "Freehand Drawing", - ModelType = typeof(PdfInkElement), - ViewModelType = typeof(InkElementViewModel), - IconKind = "Draw", - DefaultWidth = 150, - DefaultHeight = 100, - Tags = new[] { "ink", "draw", "pencil", "pen", "signature" }, - Factory = (sp, m) => new InkElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.stickynote", - DisplayName = "Sticky Note", - ModelType = typeof(PdfStickyNoteElement), - ViewModelType = typeof(StickyNoteElementViewModel), - IconKind = "NoteOutline", - DefaultWidth = 180, - DefaultHeight = 140, - Tags = new[] { "note", "sticky", "comment", "annotation" }, - Factory = (sp, m) => new StickyNoteElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.measurement", - DisplayName = "Measurement Dimension", - ModelType = typeof(PdfMeasurementElement), - ViewModelType = typeof(MeasurementElementViewModel), - IconKind = "Ruler", - DefaultWidth = 200, - DefaultHeight = 40, - Tags = new[] { "ruler", "measure", "dimension", "distance" }, - Factory = (sp, m) => new MeasurementElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.svg", - DisplayName = "Vector SVG Artwork", - ModelType = typeof(PdfSvgElement), - ViewModelType = typeof(SvgElementViewModel), - IconKind = "VectorSquare", - DefaultWidth = 180, - DefaultHeight = 180, - Tags = new[] { "svg", "vector", "graphic", "illustration" }, - Factory = (sp, m) => new SvgElementViewModel() - }); - - RegisterElement(new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.math", - DisplayName = "LaTeX Math Formula", - ModelType = typeof(PdfMathElement), - ViewModelType = typeof(MathElementViewModel), - IconKind = "FunctionVariant", - DefaultWidth = 200, - DefaultHeight = 80, - Tags = new[] { "math", "formula", "latex", "equation" }, - Factory = (sp, m) => new MathElementViewModel() - }); - } - - public void RegisterElement(CanvasElementDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _descriptorsById[descriptor.ElementTypeId] = descriptor; - _descriptorsByModelType[descriptor.ModelType] = descriptor; - } - - public bool UnregisterElement(string elementTypeId) - { - if (_descriptorsById.TryRemove(elementTypeId, out var descriptor)) - { - _descriptorsByModelType.TryRemove(descriptor.ModelType, out _); - return true; - } - return false; - } - - public CanvasElementDescriptor? GetDescriptor(string elementTypeId) - { - return _descriptorsById.TryGetValue(elementTypeId, out var desc) ? desc : null; - } - - public CanvasElementDescriptor? GetDescriptorByModelType(Type modelType) - { - if (_descriptorsByModelType.TryGetValue(modelType, out var desc)) - { - return desc; - } - - // Check assignable types if exact type match not found - foreach (var kvp in _descriptorsByModelType) - { - if (kvp.Key.IsAssignableFrom(modelType)) - { - return kvp.Value; - } - } - - return null; - } - - public IReadOnlyList GetAllDescriptors() - { - return _descriptorsById.Values.ToList(); - } - - public IReadOnlyList GetInsertableElements() - { - return _descriptorsById.Values - .Where(d => d.CanInsertFromToolbar) - .OrderBy(d => d.SortOrder) - .ToList(); - } - - public ElementViewModelBase CreateViewModel(PdfElementBase model) - { - ArgumentNullException.ThrowIfNull(model); - var descriptor = GetDescriptorByModelType(model.GetType()); - - ElementViewModelBase vm; - if (descriptor?.Factory != null) - { - vm = (ElementViewModelBase)descriptor.Factory(_serviceProvider!, model); - } - else if (descriptor != null) - { - if (_serviceProvider != null) - { - vm = (ElementViewModelBase)Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(_serviceProvider, descriptor.ViewModelType); - } - else - { - vm = (ElementViewModelBase)Activator.CreateInstance(descriptor.ViewModelType)!; - } - } - else - { - // Direct fallback to TextElementViewModel - vm = new TextElementViewModel(); - } - - vm.LoadFromModel(model); - return vm; - } - - public ElementViewModelBase CreateViewModel(string elementTypeId) - { - var descriptor = GetDescriptor(elementTypeId); - if (descriptor == null) - { - return new TextElementViewModel(); - } - - ElementViewModelBase vm; - if (descriptor.Factory != null) - { - vm = (ElementViewModelBase)descriptor.Factory(_serviceProvider!, null); - } - else if (_serviceProvider != null) - { - vm = (ElementViewModelBase)Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(_serviceProvider, descriptor.ViewModelType); - } - else - { - vm = (ElementViewModelBase)Activator.CreateInstance(descriptor.ViewModelType)!; - } - - vm.Width = descriptor.DefaultWidth; - vm.Height = descriptor.DefaultHeight; - return vm; - } - - public ElementViewModelBase CloneViewModel(ElementViewModelBase source) - { - ArgumentNullException.ThrowIfNull(source); - var model = source.ToModel(); - var vm = CreateViewModel(model); - return vm; - } -} diff --git a/src/PdfEditorApp/Services/Data/DataConnectorRegistry.cs b/src/PdfEditorApp/Services/Data/DataConnectorRegistry.cs deleted file mode 100644 index dfbb363..0000000 --- a/src/PdfEditorApp/Services/Data/DataConnectorRegistry.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Data; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Data; - -public class DataConnectorRegistry : IDataConnectorRegistry -{ - private readonly ConcurrentDictionary _connectors = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public DataConnectorRegistry(IDataSourceService? dataSourceService = null) - { - RegisterBuiltInConnectors(dataSourceService ?? new DataSourceService()); - } - - private void RegisterBuiltInConnectors(IDataSourceService ds) - { - RegisterConnector(new CsvDataConnector(ds)); - RegisterConnector(new JsonDataConnector(ds)); - RegisterConnector(new ExcelDataConnector(ds)); - RegisterConnector(new RestApiDataConnector(ds)); - } - - public IDisposable RegisterConnector(IDataConnector connector) - { - ArgumentNullException.ThrowIfNull(connector); - _connectors[connector.ConnectorId] = connector; - RegistryChanged?.Invoke(); - - return new DisposableAction(() => - { - _connectors.TryRemove(connector.ConnectorId, out _); - RegistryChanged?.Invoke(); - }); - } - - public IDataConnector? GetConnector(string connectorId) - { - if (string.IsNullOrWhiteSpace(connectorId)) return null; - return _connectors.GetValueOrDefault(connectorId); - } - - public IReadOnlyList GetAllConnectors() - { - return _connectors.Values.ToList(); - } - - private sealed class DisposableAction(Action action) : IDisposable - { - private Action? _action = action; - public void Dispose() => Interlocked.Exchange(ref _action, null)?.Invoke(); - } -} - -public class CsvDataConnector(IDataSourceService ds) : IDataConnector -{ - public string ConnectorId => "frypdf.connector.csv"; - public string DisplayName => "CSV / TSV Flat Files"; - public string Description => "Ingests comma-separated or tab-separated text files."; - public string IconKind => "FileDelimitedOutline"; - public IReadOnlyList SupportedExtensions => new[] { ".csv", ".tsv", ".txt" }; - - public async Task LoadDataAsync(Dictionary parameters, CancellationToken ct = default) - { - if (parameters.TryGetValue("filePath", out var path) && File.Exists(path)) - { - var text = await File.ReadAllTextAsync(path, ct); - return ds.ParseCsv(text); - } - return new DataMatrix(); - } -} - -public class JsonDataConnector(IDataSourceService ds) : IDataConnector -{ - public string ConnectorId => "frypdf.connector.json"; - public string DisplayName => "JSON Hierarchical Array"; - public string Description => "Extracts tabular matrices from JSON object arrays or nested JSON paths."; - public string IconKind => "CodeJson"; - public IReadOnlyList SupportedExtensions => new[] { ".json" }; - - public async Task LoadDataAsync(Dictionary parameters, CancellationToken ct = default) - { - if (parameters.TryGetValue("filePath", out var path) && File.Exists(path)) - { - var text = await File.ReadAllTextAsync(path, ct); - parameters.TryGetValue("jsonPath", out var jsonPath); - return ds.ParseJson(text, jsonPath); - } - return new DataMatrix(); - } -} - -public class ExcelDataConnector(IDataSourceService ds) : IDataConnector -{ - public string ConnectorId => "frypdf.connector.excel"; - public string DisplayName => "Microsoft Excel Workbook"; - public string Description => "Imports sheets from modern .xlsx workbooks."; - public string IconKind => "FileExcelOutline"; - public IReadOnlyList SupportedExtensions => new[] { ".xlsx", ".xlsm" }; - - public async Task LoadDataAsync(Dictionary parameters, CancellationToken ct = default) - { - if (parameters.TryGetValue("filePath", out var path) && File.Exists(path)) - { - await using var stream = File.OpenRead(path); - parameters.TryGetValue("sheetName", out var sheetName); - return ds.ParseExcel(stream, sheetName); - } - return new DataMatrix(); - } -} - -public class RestApiDataConnector(IDataSourceService ds) : IDataConnector -{ - public string ConnectorId => "frypdf.connector.rest"; - public string DisplayName => "REST API Endpoint"; - public string Description => "Fetches live data records from HTTP/HTTPS endpoints returning JSON arrays."; - public string IconKind => "Api"; - public IReadOnlyList SupportedExtensions => Array.Empty(); - - public Task LoadDataAsync(Dictionary parameters, CancellationToken ct = default) - { - if (parameters.TryGetValue("url", out var url) && !string.IsNullOrWhiteSpace(url)) - { - parameters.TryGetValue("jsonPath", out var jsonPath); - return ds.FetchFromRestApiAsync(url, null, jsonPath, ct); - } - return Task.FromResult(new DataMatrix()); - } -} diff --git a/src/PdfEditorApp/Services/Dialogs/DialogRegistry.cs b/src/PdfEditorApp/Services/Dialogs/DialogRegistry.cs deleted file mode 100644 index f3e2a3f..0000000 --- a/src/PdfEditorApp/Services/Dialogs/DialogRegistry.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Dialogs; - -/// -/// Thread-safe registry for modal dialogs and floating studio overlays. -/// -public sealed class DialogRegistry : IDialogRegistry -{ - private readonly ConcurrentDictionary _dialogs = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public IDisposable RegisterDialog(DialogDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _dialogs[descriptor.Id] = descriptor; - RegistryChanged?.Invoke(); - - return new UnregisterDisposable(() => - { - UnregisterDialog(descriptor.Id); - }); - } - - public bool UnregisterDialog(string dialogId) - { - if (_dialogs.TryRemove(dialogId, out _)) - { - RegistryChanged?.Invoke(); - return true; - } - return false; - } - - public DialogDescriptor? GetDialog(string dialogId) - { - if (_dialogs.TryGetValue(dialogId, out var descriptor)) - { - return descriptor; - } - - if (!dialogId.StartsWith("frypdf.dialog.", StringComparison.OrdinalIgnoreCase)) - { - if (_dialogs.TryGetValue($"frypdf.dialog.{dialogId}", out descriptor)) - { - return descriptor; - } - } - - return null; - } - - public IReadOnlyList GetAllDialogs() - { - return _dialogs.Values.OrderBy(d => d.Title).ToList(); - } - - private sealed class UnregisterDisposable : IDisposable - { - private Action? _action; - - public UnregisterDisposable(Action action) - { - _action = action; - } - - public void Dispose() - { - var act = System.Threading.Interlocked.Exchange(ref _action, null); - act?.Invoke(); - } - } -} diff --git a/src/PdfEditorApp/Services/DocumentAuditService.cs b/src/PdfEditorApp/Services/DocumentAuditService.cs deleted file mode 100644 index 85581e6..0000000 --- a/src/PdfEditorApp/Services/DocumentAuditService.cs +++ /dev/null @@ -1,681 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public class AuditIssueItem -{ - public string Id { get; set; } = Guid.NewGuid().ToString("N"); - public string Severity { get; set; } = "Info"; // "Success", "Warning", "Error", "Info" - public string Category { get; set; } = "General"; // "Accessibility", "Typography", "Images", "PDF/A", "Security", "Structure" - public string Title { get; set; } = ""; - public string Description { get; set; } = ""; - public int PageIndex { get; set; } = 1; - public string? ElementId { get; set; } - public string? RecommendedFix { get; set; } - public bool CanAutoFix { get; set; } - public double? MeasuredValue { get; set; } - public double? RequiredThreshold { get; set; } -} - -public class DocumentAuditReport -{ - public int HealthScore { get; set; } = 100; - public string Grade { get; set; } = "A+"; - public int TotalWordCount { get; set; } = 0; - public int EstimatedReadingTimeSeconds { get; set; } = 0; - public int TotalPages { get; set; } = 0; - public int TotalElements { get; set; } = 0; - - public int TextElementsCount { get; set; } = 0; - public int ImageElementsCount { get; set; } = 0; - public int ShapeElementsCount { get; set; } = 0; - public int TableElementsCount { get; set; } = 0; - public int ChartElementsCount { get; set; } = 0; - public int FormFieldsCount { get; set; } = 0; - public int RedactionsCount { get; set; } = 0; - public int SignaturesCount { get; set; } = 0; - - public int AccessibilityIssuesCount { get; set; } = 0; - public int PdfAComplianceScore { get; set; } = 100; - public int PrintReadyScore { get; set; } = 100; - - public List UniqueFontsUsed { get; set; } = new(); - public List Issues { get; set; } = new(); - - public int WarningsCount => Issues.Count(i => i.Severity == "Warning"); - public int ErrorsCount => Issues.Count(i => i.Severity == "Error"); - public int PassedChecksCount => Issues.Count(i => i.Severity == "Success"); - - public string ReadingTimeDisplay => - EstimatedReadingTimeSeconds < 60 - ? $"{Math.Max(1, EstimatedReadingTimeSeconds)} sec" - : $"{EstimatedReadingTimeSeconds / 60} min {EstimatedReadingTimeSeconds % 60} sec"; -} - -public interface IDocumentAuditService -{ - DocumentAuditReport RunAudit(PdfDocumentModel document); - Task RunAuditAsync(PdfDocumentModel document, IProgress? progress = null, CancellationToken ct = default); - int AutoFixContrastIssues(PdfDocumentModel document); - int AutoFixMetadataIssues(PdfDocumentModel document); - int AutoFixMissingAltText(PdfDocumentModel document); - int AutoFixAllIssues(PdfDocumentModel document); -} - -public class DocumentAuditService : IDocumentAuditService -{ - public Task RunAuditAsync(PdfDocumentModel document, IProgress? progress = null, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - return Task.Run(() => - { - progress?.Report(0); - ct.ThrowIfCancellationRequested(); - var report = RunAudit(document); - progress?.Report(100); - return report; - }, ct); - } - - public DocumentAuditReport RunAudit(PdfDocumentModel document) - { - var report = new DocumentAuditReport - { - TotalPages = document.Pages.Count - }; - - var allFonts = new HashSet(StringComparer.OrdinalIgnoreCase); - int totalWords = 0; - int accessibilityViolations = 0; - int pdfAPenalties = 0; - int printPenalties = 0; - int smallFontCount = 0; - int placeholderCount = 0; - int emptyPagesCount = 0; - int incompleteTablesCount = 0; - int unsanitizedRedactionsCount = 0; - - // 1. PDF/A Metadata Validation - if (string.IsNullOrWhiteSpace(document.Title) || document.Title.Equals("Untitled Document", StringComparison.OrdinalIgnoreCase)) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Warning", - Category = "PDF/A", - Title = "Missing Document Title", - Description = "A descriptive Title metadata entry is required for PDF/A archival and PDF/UA accessibility compliance.", - PageIndex = 1, - CanAutoFix = true, - RecommendedFix = "Set Title to project or file name" - }); - pdfAPenalties += 10; - } - else - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Success", - Category = "PDF/A", - Title = "Document Title Present", - Description = $"Document title '{document.Title}' conforms to metadata standards.", - PageIndex = 1 - }); - } - - if (string.IsNullOrWhiteSpace(document.Author)) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Info", - Category = "PDF/A", - Title = "Missing Author Metadata", - Description = "Author metadata is not configured. Setting Author improves document attribution in enterprise archiving.", - PageIndex = 1, - CanAutoFix = true, - RecommendedFix = "Set Author to current user or company name" - }); - pdfAPenalties += 5; - } - - // 2. Page & Element Inspections - for (int pIdx = 0; pIdx < document.Pages.Count; pIdx++) - { - var page = document.Pages[pIdx]; - int pageNum = pIdx + 1; - string pageBgHex = string.IsNullOrWhiteSpace(page.BackgroundColorHex) ? "#FFFFFF" : page.BackgroundColorHex; - - if (page.Elements.Count == 0) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Warning", - Category = "Structure", - Title = "Empty Page Detected", - Description = $"Page {pageNum} contains zero elements.", - PageIndex = pageNum - }); - emptyPagesCount++; - } - - foreach (var element in page.Elements) - { - report.TotalElements++; - - switch (element) - { - case PdfTextElement textEl: - report.TextElementsCount++; - if (!string.IsNullOrWhiteSpace(textEl.FontFamily)) - { - allFonts.Add(textEl.FontFamily); - } - - if (!string.IsNullOrWhiteSpace(textEl.Text)) - { - var words = textEl.Text.Split(new[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries); - totalWords += words.Length; - - if (textEl.Text.Contains("Lorem ipsum", StringComparison.OrdinalIgnoreCase) || - textEl.Text.Contains("Enter text here", StringComparison.OrdinalIgnoreCase) || - textEl.Text.Contains("Placeholder", StringComparison.OrdinalIgnoreCase)) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Warning", - Category = "Typography", - Title = "Placeholder / Sample Text", - Description = $"Boilerplate placeholder found on Page {pageNum}: \"{GetTextSnippet(textEl.Text)}\".", - PageIndex = pageNum, - ElementId = textEl.Id - }); - placeholderCount++; - } - } - - // Font size checks - if (textEl.FontSize < 8.0) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Warning", - Category = "Typography", - Title = "Small Font Size (< 8pt)", - Description = $"Text with font size {textEl.FontSize:F1}pt on Page {pageNum} may be illegible when printed.", - PageIndex = pageNum, - ElementId = textEl.Id, - MeasuredValue = textEl.FontSize, - RequiredThreshold = 8.0 - }); - smallFontCount++; - printPenalties += 3; - } - - // Mathematical WCAG 2.1 Contrast Ratio Check - string fgColor = string.IsNullOrWhiteSpace(textEl.TextColorHex) ? "#000000" : textEl.TextColorHex; - string effectiveBg = !string.IsNullOrWhiteSpace(textEl.BackgroundColorHex) && !textEl.BackgroundColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase) - ? textEl.BackgroundColorHex - : pageBgHex; - - double contrastRatio = CalculateContrastRatio(fgColor, effectiveBg); - bool isLargeText = textEl.FontSize >= 18.0 || (textEl.FontSize >= 14.0 && textEl.IsBold); - double requiredAaRatio = isLargeText ? 3.0 : 4.5; - double requiredAaaRatio = isLargeText ? 4.5 : 7.0; - - if (contrastRatio < requiredAaRatio) - { - accessibilityViolations++; - report.Issues.Add(new AuditIssueItem - { - Severity = "Error", - Category = "Accessibility", - Title = "WCAG 2.1 AA Contrast Failure", - Description = $"Text \"{GetTextSnippet(textEl.Text)}\" on Page {pageNum} has a contrast ratio of {contrastRatio:F2}:1 (Required: {requiredAaRatio:F1}:1).", - PageIndex = pageNum, - ElementId = textEl.Id, - MeasuredValue = contrastRatio, - RequiredThreshold = requiredAaRatio, - CanAutoFix = true, - RecommendedFix = $"Change text color to {GetCompliantColor(fgColor, effectiveBg)}" - }); - accessibilityViolations++; - } - else if (contrastRatio < requiredAaaRatio) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Info", - Category = "Accessibility", - Title = "WCAG 2.1 AAA Contrast Advisory", - Description = $"Text on Page {pageNum} meets AA standard ({contrastRatio:F2}:1) but falls below enhanced AAA standard ({requiredAaaRatio:F1}:1).", - PageIndex = pageNum, - ElementId = textEl.Id, - MeasuredValue = contrastRatio, - RequiredThreshold = requiredAaaRatio - }); - } - break; - - case PdfImageElement imgEl: - report.ImageElementsCount++; - - // Effective DPI Analysis - var dims = TryGetImageDimensions(imgEl.ImagePath); - if (dims.HasValue && imgEl.Width > 0 && imgEl.Height > 0) - { - double dpiX = dims.Value.Width / (imgEl.Width / 72.0); - double dpiY = dims.Value.Height / (imgEl.Height / 72.0); - double minDpi = Math.Min(dpiX, dpiY); - - if (minDpi < 150.0) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Warning", - Category = "Images", - Title = "Low Image Resolution (< 150 DPI)", - Description = $"Image on Page {pageNum} has an effective resolution of {minDpi:F0} DPI. Professional printing requires at least 300 DPI.", - PageIndex = pageNum, - ElementId = imgEl.Id, - MeasuredValue = minDpi, - RequiredThreshold = 300.0 - }); - printPenalties += 10; - } - else if (minDpi > 600.0) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Info", - Category = "Images", - Title = "High Image Resolution (> 600 DPI)", - Description = $"Image on Page {pageNum} is {minDpi:F0} DPI. Consider downsampling to 300 DPI to reduce PDF file size.", - PageIndex = pageNum, - ElementId = imgEl.Id, - MeasuredValue = minDpi, - RequiredThreshold = 600.0 - }); - } - } - else if (string.IsNullOrWhiteSpace(imgEl.ImagePath)) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Info", - Category = "Images", - Title = "Placeholder Image Box", - Description = $"Image placeholder on Page {pageNum} has no image file loaded.", - PageIndex = pageNum, - ElementId = imgEl.Id - }); - } - break; - - case PdfShapeElement shapeEl: - report.ShapeElementsCount++; - if (shapeEl.ShapeType == ShapeType.RoundedRectangle && shapeEl.Label != null && - (shapeEl.Label.Contains("SIGN", StringComparison.OrdinalIgnoreCase) || shapeEl.Label.Contains("APPROVED", StringComparison.OrdinalIgnoreCase))) - { - report.SignaturesCount++; - } - break; - - case PdfTableElement tblEl: - report.TableElementsCount++; - if (tblEl.Rows.Count == 0 || tblEl.Headers.Count == 0) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Warning", - Category = "Structure", - Title = "Incomplete Table Structure", - Description = $"Table on Page {pageNum} has zero rows or column headers configured.", - PageIndex = pageNum, - ElementId = tblEl.Id - }); - incompleteTablesCount++; - } - break; - - case PdfChartElement: - report.ChartElementsCount++; - break; - - case PdfFormFieldElement formEl: - report.FormFieldsCount++; - if (formEl.FieldType == FormFieldType.Signature || formEl.FieldType == FormFieldType.SignatureLine) - { - report.SignaturesCount++; - } - break; - - case PdfRedactionElement redEl: - report.RedactionsCount++; - report.Issues.Add(new AuditIssueItem - { - Severity = "Warning", - Category = "Security", - Title = "Unsanitized Redaction Mark", - Description = $"Redaction overlay ({redEl.ExemptionCode}) on Page {pageNum} is active. Ensure redactions are permanently burned in before external sharing.", - PageIndex = pageNum, - ElementId = redEl.Id - }); - unsanitizedRedactionsCount++; - break; - } - } - } - - // 3. Positive Summary Checks - if (allFonts.Count > 0 && allFonts.Count <= 3) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Success", - Category = "Typography", - Title = "Harmonious Font Hierarchy", - Description = $"Document utilizes a concise set of {allFonts.Count} font families ({string.Join(", ", allFonts)}).", - PageIndex = 1 - }); - } - else if (allFonts.Count > 4) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Warning", - Category = "Typography", - Title = "Excessive Font Diversity", - Description = $"Document uses {allFonts.Count} distinct fonts. Standard publishing guidelines recommend 2-3 fonts maximum.", - PageIndex = 1 - }); - } - - if (document.SecuritySettings.IsPasswordProtected) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Success", - Category = "Security", - Title = "Document Encryption Active", - Description = "Document is secured with password protection and restricted permissions.", - PageIndex = 1 - }); - } - - if (document.Pages.Count > 0 && document.Pages.All(p => p.ShowHeaderFooter)) - { - report.Issues.Add(new AuditIssueItem - { - Severity = "Success", - Category = "Structure", - Title = "Structured Pagination & Footers", - Description = "All document pages have structured headers and sequential footers enabled.", - PageIndex = 1 - }); - } - - report.UniqueFontsUsed = allFonts.OrderBy(f => f).ToList(); - report.TotalWordCount = totalWords; - report.EstimatedReadingTimeSeconds = (int)Math.Ceiling(totalWords / 3.3); // ~200 WPM - report.AccessibilityIssuesCount = accessibilityViolations; - report.PdfAComplianceScore = Math.Clamp(100 - pdfAPenalties, 0, 100); - report.PrintReadyScore = Math.Clamp(100 - printPenalties, 0, 100); - - int typographyPenalty = Math.Min(12, (allFonts.Count > 4 ? 4 : 0) + smallFontCount * 1 + placeholderCount * 2); - int structurePenalty = Math.Min(12, emptyPagesCount * 5 + incompleteTablesCount * 4); - int accessibilityPenalty = Math.Min(15, accessibilityViolations * 4); - int securityPenalty = Math.Min(8, unsanitizedRedactionsCount * 3); - int metadataPenalty = Math.Min(10, pdfAPenalties); - - int totalPenalty = typographyPenalty + structurePenalty + accessibilityPenalty + securityPenalty + metadataPenalty; - int calculatedScore = Math.Clamp(100 - totalPenalty, 20, 100); - report.HealthScore = calculatedScore; - report.Grade = calculatedScore switch - { - >= 95 => "A+", - >= 85 => "A", - >= 75 => "B", - >= 60 => "C", - _ => "Needs Review" - }; - - return report; - } - - public int AutoFixContrastIssues(PdfDocumentModel document) - { - int fixedCount = 0; - foreach (var page in document.Pages) - { - string pageBgHex = string.IsNullOrWhiteSpace(page.BackgroundColorHex) ? "#FFFFFF" : page.BackgroundColorHex; - - foreach (var element in page.Elements) - { - if (element is PdfTextElement textEl) - { - string fgColor = string.IsNullOrWhiteSpace(textEl.TextColorHex) ? "#000000" : textEl.TextColorHex; - string effectiveBg = !string.IsNullOrWhiteSpace(textEl.BackgroundColorHex) && !textEl.BackgroundColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase) - ? textEl.BackgroundColorHex - : pageBgHex; - - double ratio = CalculateContrastRatio(fgColor, effectiveBg); - bool isLargeText = textEl.FontSize >= 18.0 || (textEl.FontSize >= 14.0 && textEl.IsBold); - double requiredRatio = isLargeText ? 3.0 : 4.5; - - if (ratio < requiredRatio) - { - textEl.TextColorHex = GetCompliantColor(fgColor, effectiveBg); - fixedCount++; - } - } - } - } - return fixedCount; - } - - public int AutoFixMetadataIssues(PdfDocumentModel document) - { - int fixedCount = 0; - if (string.IsNullOrWhiteSpace(document.Title) || document.Title.Equals("Untitled Document", StringComparison.OrdinalIgnoreCase)) - { - document.Title = "Document Publication"; - fixedCount++; - } - if (string.IsNullOrWhiteSpace(document.Author)) - { - document.Author = Environment.UserName; - fixedCount++; - } - return fixedCount; - } - - public int AutoFixMissingAltText(PdfDocumentModel document) - { - int fixedCount = 0; - int imgIndex = 1; - foreach (var page in document.Pages) - { - foreach (var element in page.Elements) - { - if (element is PdfImageElement imgEl && (string.IsNullOrWhiteSpace(imgEl.AltText) || imgEl.AltText.Equals("Image", StringComparison.OrdinalIgnoreCase))) - { - imgEl.AltText = $"Document Figure {imgIndex++}"; - fixedCount++; - } - } - } - return fixedCount; - } - - public int AutoFixAllIssues(PdfDocumentModel document) - { - int total = 0; - total += AutoFixContrastIssues(document); - total += AutoFixMetadataIssues(document); - total += AutoFixMissingAltText(document); - return total; - } - - #region Mathematical WCAG 2.1 Calculations - - public static (double R, double G, double B) ParseRgb(string hex) - { - if (string.IsNullOrWhiteSpace(hex)) return (0, 0, 0); - hex = hex.Trim().TrimStart('#'); - if (hex.Length == 3) - { - hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}"; - } - else if (hex.Length == 8) - { - hex = hex.Substring(2, 6); - } - else if (hex.Length != 6) - { - return (0, 0, 0); - } - - if (int.TryParse(hex.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r) && - int.TryParse(hex.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g) && - int.TryParse(hex.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b)) - { - return (r / 255.0, g / 255.0, b / 255.0); - } - return (0, 0, 0); - } - - public static double CalculateRelativeLuminance(string hex) - { - var (r, g, b) = ParseRgb(hex); - - static double ToLinear(double c) => (c <= 0.04045) ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); - - double rLinear = ToLinear(r); - double gLinear = ToLinear(g); - double bLinear = ToLinear(b); - - return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear; - } - - public static double CalculateContrastRatio(string fgHex, string bgHex) - { - double l1 = CalculateRelativeLuminance(fgHex); - double l2 = CalculateRelativeLuminance(bgHex); - - double lighter = Math.Max(l1, l2); - double darker = Math.Min(l1, l2); - - return Math.Round((lighter + 0.05) / (darker + 0.05), 2); - } - - public static string GetCompliantColor(string currentFgHex, string bgHex, double targetRatio = 4.5) - { - double bgLuminance = CalculateRelativeLuminance(bgHex); - return bgLuminance > 0.5 ? "#0F172A" : "#FFFFFF"; - } - - #endregion - - #region Fast Image Dimensions Header Inspector - - public static (int Width, int Height)? TryGetImageDimensions(string? path) - { - if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return null; - - try - { - using var stream = File.OpenRead(path); - using var reader = new BinaryReader(stream); - - byte[] magic = reader.ReadBytes(8); - if (magic.Length < 8) return null; - - // PNG magic: 89 50 4E 47 0D 0A 1A 0A - if (magic[0] == 0x89 && magic[1] == 0x50 && magic[2] == 0x4E && magic[3] == 0x47) - { - stream.Seek(16, SeekOrigin.Begin); - byte[] wBytes = reader.ReadBytes(4); - byte[] hBytes = reader.ReadBytes(4); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(wBytes); - Array.Reverse(hBytes); - } - int w = BitConverter.ToInt32(wBytes, 0); - int h = BitConverter.ToInt32(hBytes, 0); - return (w, h); - } - - // GIF: "GIF87a" or "GIF89a" - if (magic[0] == 'G' && magic[1] == 'I' && magic[2] == 'F') - { - stream.Seek(6, SeekOrigin.Begin); - int w = reader.ReadUInt16(); - int h = reader.ReadUInt16(); - return (w, h); - } - - // BMP: "BM" - if (magic[0] == 'B' && magic[1] == 'M') - { - stream.Seek(18, SeekOrigin.Begin); - int w = reader.ReadInt32(); - int h = Math.Abs(reader.ReadInt32()); - return (w, h); - } - - // JPEG - if (magic[0] == 0xFF && magic[1] == 0xD8) - { - stream.Seek(2, SeekOrigin.Begin); - while (stream.Position < stream.Length) - { - byte markerPrefix = reader.ReadByte(); - if (markerPrefix != 0xFF) break; - byte marker = reader.ReadByte(); - if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2) - { - reader.ReadUInt16(); // length - reader.ReadByte(); // precision - int h = (reader.ReadByte() << 8) | reader.ReadByte(); - int w = (reader.ReadByte() << 8) | reader.ReadByte(); - return (w, h); - } - else if (marker == 0xD9 || marker == 0xDA) - { - break; - } - else - { - int length = (reader.ReadByte() << 8) | reader.ReadByte(); - if (length < 2) break; - stream.Seek(length - 2, SeekOrigin.Current); - } - } - } - } - catch - { - // Ignore format parse failures - } - - return null; - } - - private static string GetTextSnippet(string? text, int maxLen = 30) - { - if (string.IsNullOrWhiteSpace(text)) return ""; - string clean = text.Trim().Replace('\r', ' ').Replace('\n', ' '); - return clean.Length <= maxLen ? clean : $"{clean.Substring(0, maxLen)}..."; - } - - #endregion -} diff --git a/src/PdfEditorApp/Services/DocumentCompareService.cs b/src/PdfEditorApp/Services/DocumentCompareService.cs deleted file mode 100644 index f845caf..0000000 --- a/src/PdfEditorApp/Services/DocumentCompareService.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public interface IDocumentCompareService -{ - DocumentComparisonReport CompareDocuments(PdfDocumentModel baseDoc, PdfDocumentModel comparedDoc); - Task CompareDocumentsAsync(PdfDocumentModel baseDoc, PdfDocumentModel comparedDoc, CancellationToken ct = default); -} - -public class DocumentCompareService : IDocumentCompareService -{ - public Task CompareDocumentsAsync(PdfDocumentModel baseDoc, PdfDocumentModel comparedDoc, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - return Task.Run(() => CompareDocuments(baseDoc, comparedDoc), ct); - } - - public DocumentComparisonReport CompareDocuments(PdfDocumentModel baseDoc, PdfDocumentModel comparedDoc) - { - var report = new DocumentComparisonReport - { - BaseDocumentTitle = string.IsNullOrWhiteSpace(baseDoc.Title) ? "Base Document" : baseDoc.Title, - ComparedDocumentTitle = string.IsNullOrWhiteSpace(comparedDoc.Title) ? "Revised Document" : comparedDoc.Title, - BasePageCount = baseDoc.Pages.Count, - ComparedPageCount = comparedDoc.Pages.Count, - ComparisonTimestamp = DateTime.UtcNow - }; - - // 1. Document Metadata Comparison - if (baseDoc.Title != comparedDoc.Title) - { - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = 0, - DiffType = CompareDiffType.MetadataModified, - Description = $"Document Title changed from \"{baseDoc.Title}\" to \"{comparedDoc.Title}\"", - OldValue = baseDoc.Title, - NewValue = comparedDoc.Title, - ElementKindDisplay = "Document Metadata" - }); - } - - if (baseDoc.Author != comparedDoc.Author) - { - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = 0, - DiffType = CompareDiffType.MetadataModified, - Description = $"Document Author changed from \"{baseDoc.Author}\" to \"{comparedDoc.Author}\"", - OldValue = baseDoc.Author, - NewValue = comparedDoc.Author, - ElementKindDisplay = "Document Metadata" - }); - } - - if (baseDoc.SecuritySettings.IsPasswordProtected != comparedDoc.SecuritySettings.IsPasswordProtected) - { - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = 0, - DiffType = CompareDiffType.SecurityModified, - Description = $"Security settings changed: Protected={comparedDoc.SecuritySettings.IsPasswordProtected}", - OldValue = baseDoc.SecuritySettings.IsPasswordProtected ? "Password Protected" : "Standard (Unprotected)", - NewValue = comparedDoc.SecuritySettings.IsPasswordProtected ? "Password Protected" : "Standard (Unprotected)", - ElementKindDisplay = "Security" - }); - } - - // 2. Page Count Comparison - if (baseDoc.Pages.Count != comparedDoc.Pages.Count) - { - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = 0, - DiffType = CompareDiffType.PageCountChanged, - Description = $"Page count changed from {baseDoc.Pages.Count} to {comparedDoc.Pages.Count}", - OldValue = $"{baseDoc.Pages.Count} pages", - NewValue = $"{comparedDoc.Pages.Count} pages", - ElementKindDisplay = "Document Structure" - }); - } - - int maxPages = Math.Max(baseDoc.Pages.Count, comparedDoc.Pages.Count); - for (int p = 0; p < maxPages; p++) - { - int pageNum = p + 1; - - if (p >= baseDoc.Pages.Count) - { - var addedPage = comparedDoc.Pages[p]; - report.AdditionsCount += Math.Max(1, addedPage.Elements.Count); - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = pageNum, - DiffType = CompareDiffType.ElementAdded, - Description = $"Added new Page {pageNum} with {addedPage.Elements.Count} elements", - OldValue = "(None)", - NewValue = $"Page {pageNum} ({addedPage.Format})", - ElementKindDisplay = "Page" - }); - continue; - } - - if (p >= comparedDoc.Pages.Count) - { - var removedPage = baseDoc.Pages[p]; - report.DeletionsCount += Math.Max(1, removedPage.Elements.Count); - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = pageNum, - DiffType = CompareDiffType.ElementRemoved, - Description = $"Deleted Page {pageNum} containing {removedPage.Elements.Count} elements", - OldValue = $"Page {pageNum}", - NewValue = "(Deleted)", - ElementKindDisplay = "Page" - }); - continue; - } - - var basePage = baseDoc.Pages[p]; - var compPage = comparedDoc.Pages[p]; - - // Compare page dimensions and orientation - if (basePage.Orientation != compPage.Orientation || basePage.Format != compPage.Format || - Math.Abs(basePage.Width - compPage.Width) > 1 || Math.Abs(basePage.Height - compPage.Height) > 1) - { - report.ModificationsCount++; - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = pageNum, - DiffType = CompareDiffType.ElementModified, - Description = $"Page {pageNum} geometry changed: {basePage.Format} ({basePage.Width:F0}x{basePage.Height:F0}) -> {compPage.Format} ({compPage.Width:F0}x{compPage.Height:F0})", - OldValue = $"{basePage.Format} ({basePage.Width:F0}x{basePage.Height:F0})", - NewValue = $"{compPage.Format} ({compPage.Width:F0}x{compPage.Height:F0})", - ElementKindDisplay = "Page Setup" - }); - } - - // Compare elements on page - ComparePageElements(basePage, compPage, pageNum, report); - } - - return report; - } - - private static void ComparePageElements(PdfPageModel basePage, PdfPageModel compPage, int pageNum, DocumentComparisonReport report) - { - var unmatchedComp = new List(compPage.Elements); - var matchedCompIds = new HashSet(); - - // 1. Pass: Match by Id - var baseElById = basePage.Elements.ToDictionary(e => e.Id, e => e); - var compElById = compPage.Elements.ToDictionary(e => e.Id, e => e); - - foreach (var baseEl in basePage.Elements) - { - if (compElById.TryGetValue(baseEl.Id, out var compEl)) - { - matchedCompIds.Add(compEl.Id); - unmatchedComp.Remove(compEl); - CompareMatchedPair(baseEl, compEl, pageNum, report); - } - } - - // 2. Pass: Spatial/Kind matching for elements without matching ID - var unmatchedBase = basePage.Elements.Where(e => !matchedCompIds.Contains(e.Id)).ToList(); - var stillUnmatchedBase = new List(); - - foreach (var baseEl in unmatchedBase) - { - var match = unmatchedComp.FirstOrDefault(c => c.Kind == baseEl.Kind && Math.Abs(c.X - baseEl.X) < 15 && Math.Abs(c.Y - baseEl.Y) < 15); - if (match != null) - { - unmatchedComp.Remove(match); - CompareMatchedPair(baseEl, match, pageNum, report); - } - else - { - stillUnmatchedBase.Add(baseEl); - } - } - - // 3. Deletions - foreach (var baseEl in stillUnmatchedBase) - { - report.DeletionsCount++; - string desc = baseEl is PdfTextElement txt ? $": \"{GetSnippet(txt.Text)}\"" : ""; - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = pageNum, - DiffType = CompareDiffType.ElementRemoved, - Description = $"Removed {baseEl.Kind} on Page {pageNum}{desc}", - OldValue = $"{baseEl.Kind} at (X:{baseEl.X:F0}, Y:{baseEl.Y:F0})", - NewValue = "(Removed)", - ElementKindDisplay = baseEl.Kind.ToString() - }); - } - - // 4. Additions - foreach (var compEl in unmatchedComp) - { - report.AdditionsCount++; - string desc = compEl is PdfTextElement txt ? $": \"{GetSnippet(txt.Text)}\"" : ""; - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = pageNum, - DiffType = CompareDiffType.ElementAdded, - Description = $"Added new {compEl.Kind} on Page {pageNum}{desc}", - OldValue = "(None)", - NewValue = $"{compEl.Kind} at (X:{compEl.X:F0}, Y:{compEl.Y:F0})", - ElementKindDisplay = compEl.Kind.ToString() - }); - } - } - - private static void CompareMatchedPair(PdfElementBase baseEl, PdfElementBase compEl, int pageNum, DocumentComparisonReport report) - { - // Check Text modifications - if (baseEl is PdfTextElement bTxt && compEl is PdfTextElement cTxt) - { - if (bTxt.Text != cTxt.Text) - { - report.ModificationsCount++; - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = pageNum, - DiffType = CompareDiffType.TextModified, - Description = $"Modified text on Page {pageNum}: \"{GetSnippet(bTxt.Text)}\" -> \"{GetSnippet(cTxt.Text)}\"", - OldValue = bTxt.Text, - NewValue = cTxt.Text, - ElementKindDisplay = "Text Content" - }); - } - - if (Math.Abs(bTxt.FontSize - cTxt.FontSize) > 0.5 || bTxt.IsBold != cTxt.IsBold || bTxt.IsItalic != cTxt.IsItalic || bTxt.TextColorHex != cTxt.TextColorHex) - { - report.ModificationsCount++; - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = pageNum, - DiffType = CompareDiffType.FormattingModified, - Description = $"Formatting changed for text on Page {pageNum}", - OldValue = $"{bTxt.FontFamily} {bTxt.FontSize:F0}pt {bTxt.TextColorHex}", - NewValue = $"{cTxt.FontFamily} {cTxt.FontSize:F0}pt {cTxt.TextColorHex}", - ElementKindDisplay = "Typography" - }); - } - } - - // Check geometry displacement/resizing - if (Math.Abs(baseEl.X - compEl.X) > 2 || Math.Abs(baseEl.Y - compEl.Y) > 2 || - Math.Abs(baseEl.Width - compEl.Width) > 2 || Math.Abs(baseEl.Height - compEl.Height) > 2) - { - report.ModificationsCount++; - report.Differences.Add(new DocumentDifferenceItem - { - PageNumber = pageNum, - DiffType = CompareDiffType.ElementModified, - Description = $"Moved / resized {compEl.Kind} on Page {pageNum}", - OldValue = $"Pos ({baseEl.X:F0}, {baseEl.Y:F0}) Size ({baseEl.Width:F0}x{baseEl.Height:F0})", - NewValue = $"Pos ({compEl.X:F0}, {compEl.Y:F0}) Size ({compEl.Width:F0}x{compEl.Height:F0})", - ElementKindDisplay = compEl.Kind.ToString() - }); - } - } - - private static string GetSnippet(string? text, int max = 25) - { - if (string.IsNullOrWhiteSpace(text)) return ""; - string clean = text.Trim().Replace('\r', ' ').Replace('\n', ' '); - return clean.Length <= max ? clean : $"{clean.Substring(0, max)}..."; - } -} diff --git a/src/PdfEditorApp/Services/Export/DocumentExporterRegistry.cs b/src/PdfEditorApp/Services/Export/DocumentExporterRegistry.cs deleted file mode 100644 index d3cd651..0000000 --- a/src/PdfEditorApp/Services/Export/DocumentExporterRegistry.cs +++ /dev/null @@ -1,293 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Export; - -public class DocumentExporterRegistry : IDocumentExporterRegistry -{ - private readonly ConcurrentDictionary _exporters = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public DocumentExporterRegistry(IPdfExportService? pdfExportService = null) - { - RegisterBuiltInExporters(pdfExportService); - } - - private void RegisterBuiltInExporters(IPdfExportService? pdfExportService) - { - RegisterExporter(new PdfDocumentExporter(pdfExportService)); - RegisterExporter(new MarkdownDocumentExporter()); - RegisterExporter(new HtmlDocumentExporter()); - RegisterExporter(new PlainTextDocumentExporter()); - RegisterExporter(new SvgVectorExporter()); - } - - public IDisposable RegisterExporter(IDocumentExporter exporter) - { - ArgumentNullException.ThrowIfNull(exporter); - _exporters[exporter.ExporterId] = exporter; - RegistryChanged?.Invoke(); - - return new DisposableAction(() => - { - _exporters.TryRemove(exporter.ExporterId, out _); - RegistryChanged?.Invoke(); - }); - } - - public IDocumentExporter? GetExporter(string exporterId) - { - if (string.IsNullOrWhiteSpace(exporterId)) return null; - return _exporters.GetValueOrDefault(exporterId); - } - - public IDocumentExporter? FindExporterByExtension(string extension) - { - if (string.IsNullOrWhiteSpace(extension)) return null; - var ext = extension.StartsWith('.') ? extension : "." + extension; - - return _exporters.Values - .FirstOrDefault(e => string.Equals(e.DefaultExtension, ext, StringComparison.OrdinalIgnoreCase)); - } - - public IReadOnlyList GetAllExporters() - { - return _exporters.Values.ToList(); - } - - private sealed class DisposableAction(Action action) : IDisposable - { - private Action? _action = action; - public void Dispose() => Interlocked.Exchange(ref _action, null)?.Invoke(); - } -} - -/// -/// Default PDF exporter delegating to IPdfExportService. -/// -public class PdfDocumentExporter : IDocumentExporter -{ - private readonly IPdfExportService? _exportService; - - public PdfDocumentExporter(IPdfExportService? exportService = null) - { - _exportService = exportService; - } - - public string ExporterId => "frypdf.exporter.pdf"; - public string DisplayName => "PDF Document (.pdf)"; - public string DefaultExtension => ".pdf"; - public string FileFilterDescription => "PDF Document (*.pdf)|*.pdf"; - public string IconKind => "FilePdfBox"; - - public Task ExportAsync(PdfDocumentModel document, DocumentExportOptions options, CancellationToken ct = default) - { - if (_exportService != null) - { - return Task.FromResult(_exportService.GeneratePdfBytes(document)); - } - - // Fallback directly to PdfExportService - var fallback = new PdfExportService(); - return Task.FromResult(fallback.GeneratePdfBytes(document)); - } -} - -/// -/// Markdown exporter extracting text blocks, headings, and tables to formatted Markdown. -/// -public class MarkdownDocumentExporter : IDocumentExporter -{ - public string ExporterId => "frypdf.exporter.markdown"; - public string DisplayName => "GitHub-Flavored Markdown (.md)"; - public string DefaultExtension => ".md"; - public string FileFilterDescription => "Markdown Document (*.md)|*.md"; - public string IconKind => "LanguageMarkdown"; - - public Task ExportAsync(PdfDocumentModel document, DocumentExportOptions options, CancellationToken ct = default) - { - var sb = new StringBuilder(); - sb.AppendLine($"# {document.Title}"); - if (!string.IsNullOrWhiteSpace(document.Author)) - { - sb.AppendLine($"*Author: {document.Author}*"); - } - sb.AppendLine(); - - int pageNum = 1; - foreach (var page in document.Pages) - { - if (document.Pages.Count > 1) - { - sb.AppendLine($"---"); - sb.AppendLine($"## Page {pageNum}"); - sb.AppendLine(); - } - - var sortedElements = page.Elements.OrderBy(e => e.Y).ThenBy(e => e.X); - foreach (var el in sortedElements) - { - if (el is PdfTextElement text && !string.IsNullOrWhiteSpace(text.Text)) - { - if (text.FontSize >= 20) - { - sb.AppendLine($"# {text.Text.Trim()}"); - } - else if (text.FontSize >= 15) - { - sb.AppendLine($"## {text.Text.Trim()}"); - } - else if (text.FontSize >= 12 && text.IsBold) - { - sb.AppendLine($"### {text.Text.Trim()}"); - } - else - { - sb.AppendLine(text.Text.Trim()); - } - sb.AppendLine(); - } - else if (el is PdfTableElement table) - { - sb.AppendLine($"*[Table: {table.Rows.Count}x{table.Headers.Count}]*"); - sb.AppendLine(); - } - } - - pageNum++; - } - - return Task.FromResult(Encoding.UTF8.GetBytes(sb.ToString())); - } -} - -/// -/// Plain text exporter extracting raw text in layout order. -/// -public class PlainTextDocumentExporter : IDocumentExporter -{ - public string ExporterId => "frypdf.exporter.text"; - public string DisplayName => "Plain Text (.txt)"; - public string DefaultExtension => ".txt"; - public string FileFilterDescription => "Text Document (*.txt)|*.txt"; - public string IconKind => "TextRecognition"; - - public Task ExportAsync(PdfDocumentModel document, DocumentExportOptions options, CancellationToken ct = default) - { - var sb = new StringBuilder(); - foreach (var page in document.Pages) - { - var textElements = page.Elements.OfType().OrderBy(e => e.Y).ThenBy(e => e.X); - foreach (var t in textElements) - { - if (!string.IsNullOrWhiteSpace(t.Text)) - { - sb.AppendLine(t.Text); - } - } - sb.AppendLine(); - } - - return Task.FromResult(Encoding.UTF8.GetBytes(sb.ToString())); - } -} - -/// -/// HTML5 document exporter rendering styled semantic HTML. -/// -public class HtmlDocumentExporter : IDocumentExporter -{ - public string ExporterId => "frypdf.exporter.html"; - public string DisplayName => "Web Page HTML5 (.html)"; - public string DefaultExtension => ".html"; - public string FileFilterDescription => "HTML Document (*.html)|*.html"; - public string IconKind => "LanguageHtml5"; - - public Task ExportAsync(PdfDocumentModel document, DocumentExportOptions options, CancellationToken ct = default) - { - var sb = new StringBuilder(); - sb.AppendLine(""); - sb.AppendLine(""); - sb.AppendLine(""); - sb.AppendLine(" "); - sb.AppendLine($" {System.Net.WebUtility.HtmlEncode(document.Title)}"); - sb.AppendLine(" "); - sb.AppendLine(""); - sb.AppendLine(""); - - int pageNum = 1; - foreach (var page in document.Pages) - { - sb.AppendLine($"
"); - var sorted = page.Elements.OrderBy(e => e.Y).ThenBy(e => e.X); - foreach (var el in sorted) - { - if (el is PdfTextElement text && !string.IsNullOrWhiteSpace(text.Text)) - { - var encoded = System.Net.WebUtility.HtmlEncode(text.Text); - if (text.FontSize >= 20) sb.AppendLine($"

{encoded}

"); - else if (text.FontSize >= 15) sb.AppendLine($"

{encoded}

"); - else sb.AppendLine($"

{encoded}

"); - } - } - sb.AppendLine("
"); - pageNum++; - } - - sb.AppendLine(""); - sb.AppendLine(""); - - return Task.FromResult(Encoding.UTF8.GetBytes(sb.ToString())); - } -} - -/// -/// SVG vector document exporter. -/// -public class SvgVectorExporter : IDocumentExporter -{ - public string ExporterId => "frypdf.exporter.svg"; - public string DisplayName => "Scalable Vector Graphics (.svg)"; - public string DefaultExtension => ".svg"; - public string FileFilterDescription => "Scalable Vector Graphics (*.svg)|*.svg"; - public string IconKind => "Svg"; - - public Task ExportAsync(PdfDocumentModel document, DocumentExportOptions options, CancellationToken ct = default) - { - var firstPage = document.Pages.FirstOrDefault() ?? new PdfPageModel { Width = 600, Height = 800 }; - var sb = new StringBuilder(); - sb.AppendLine($""); - sb.AppendLine($" "); - sb.AppendLine($" "); - - foreach (var el in firstPage.Elements.OrderBy(e => e.ZIndex)) - { - if (el is PdfTextElement text && !string.IsNullOrWhiteSpace(text.Text)) - { - var encoded = System.Net.WebUtility.HtmlEncode(text.Text); - sb.AppendLine($" {encoded}"); - } - else if (el is PdfShapeElement shape) - { - sb.AppendLine($" "); - } - } - - sb.AppendLine(""); - return Task.FromResult(Encoding.UTF8.GetBytes(sb.ToString())); - } -} diff --git a/src/PdfEditorApp/Services/FileOperationHelper.cs b/src/PdfEditorApp/Services/FileOperationHelper.cs deleted file mode 100644 index b1335e7..0000000 --- a/src/PdfEditorApp/Services/FileOperationHelper.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; - -namespace PdfEditorApp.Services; - -/// -/// Provides cross-platform utility methods for file operations: -/// renaming, duplication, deletion, file name validation, and revealing in OS file managers. -/// -public static class FileOperationHelper -{ - private static readonly char[] CrossPlatformInvalidChars = new[] - { - '/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0' - }; - - /// - /// Validates whether the proposed file name is valid across Windows, macOS, and Linux file systems. - /// - public static bool ValidateFileName(string? fileName, out string? errorMessage) - { - errorMessage = null; - - if (string.IsNullOrWhiteSpace(fileName)) - { - errorMessage = "Document name cannot be empty."; - return false; - } - - var trimmed = fileName.Trim(); - if (trimmed.Length == 0 || trimmed == "." || trimmed == "..") - { - errorMessage = "Document name cannot be empty or a navigation dot."; - return false; - } - - if (trimmed.IndexOfAny(CrossPlatformInvalidChars) >= 0 || trimmed.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) - { - errorMessage = "Document name contains invalid characters (e.g. / \\ : * ? \" < > |)."; - return false; - } - - if (trimmed.Length > 255) - { - errorMessage = "Document name is too long (maximum 255 characters)."; - return false; - } - - return true; - } - - /// - /// Renames a physical file on disk to a new file name, preserving the original extension if not explicitly typed. - /// - public static bool RenameFile(string oldPath, string newName, out string? newPath, out string? errorMessage) - { - newPath = null; - errorMessage = null; - - if (!File.Exists(oldPath)) - { - errorMessage = "Original file could not be found on disk."; - return false; - } - - if (!ValidateFileName(newName, out errorMessage)) - { - return false; - } - - try - { - var dir = Path.GetDirectoryName(oldPath); - if (string.IsNullOrEmpty(dir)) - { - errorMessage = "Could not determine parent directory."; - return false; - } - - var originalExt = Path.GetExtension(oldPath); - var trimmedName = newName.Trim(); - - // If user typed the extension explicitly, keep it; otherwise append original extension - string finalFileName; - if (!string.IsNullOrEmpty(originalExt) && trimmedName.EndsWith(originalExt, StringComparison.OrdinalIgnoreCase)) - { - finalFileName = trimmedName; - } - else - { - finalFileName = trimmedName + originalExt; - } - - var destinationPath = Path.Combine(dir, finalFileName); - - // If same file path, no rename required - if (string.Equals(oldPath, destinationPath, StringComparison.OrdinalIgnoreCase)) - { - newPath = oldPath; - return true; - } - - // Check if another file with this name already exists - if (File.Exists(destinationPath)) - { - errorMessage = $"A file named \"{finalFileName}\" already exists in this folder."; - return false; - } - - File.Move(oldPath, destinationPath); - newPath = destinationPath; - return true; - } - catch (Exception ex) - { - errorMessage = $"Failed to rename file: {ex.Message}"; - return false; - } - } - - /// - /// Creates a duplicate copy of a file in the same directory (e.g. "Document (Copy).pdf"). - /// - public static bool DuplicateFile(string sourcePath, out string? newPath, out string? errorMessage) - { - newPath = null; - errorMessage = null; - - if (!File.Exists(sourcePath)) - { - errorMessage = "Source file does not exist on disk."; - return false; - } - - try - { - var dir = Path.GetDirectoryName(sourcePath); - if (string.IsNullOrEmpty(dir)) - { - errorMessage = "Could not determine parent directory."; - return false; - } - - var nameWithoutExt = Path.GetFileNameWithoutExtension(sourcePath); - var ext = Path.GetExtension(sourcePath); - - string targetPath = Path.Combine(dir, $"{nameWithoutExt} (Copy){ext}"); - int copyIndex = 2; - - while (File.Exists(targetPath) && copyIndex <= 999) - { - targetPath = Path.Combine(dir, $"{nameWithoutExt} (Copy {copyIndex}){ext}"); - copyIndex++; - } - - File.Copy(sourcePath, targetPath, false); - newPath = targetPath; - return true; - } - catch (Exception ex) - { - errorMessage = $"Failed to create duplicate: {ex.Message}"; - return false; - } - } - - /// - /// Safely deletes a file from the disk. - /// - public static bool DeleteFile(string filePath, out string? errorMessage) - { - errorMessage = null; - - if (string.IsNullOrWhiteSpace(filePath)) - { - return true; - } - - if (!File.Exists(filePath)) - { - return true; // Already deleted - } - - try - { - File.Delete(filePath); - return true; - } - catch (Exception ex) - { - errorMessage = $"Failed to delete file: {ex.Message}"; - return false; - } - } - - /// - /// Cross-platform helper to highlight/select a file in macOS Finder, Windows Explorer, or Linux file managers. - /// - public static bool RevealInFileManager(string filePath, out string? errorMessage) - { - errorMessage = null; - - if (string.IsNullOrWhiteSpace(filePath)) - { - errorMessage = "File path is empty."; - return false; - } - - try - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - var psi = new ProcessStartInfo - { - FileName = "open", - Arguments = $"-R \"{filePath}\"", - UseShellExecute = false, - CreateNoWindow = true - }; - Process.Start(psi); - return true; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var psi = new ProcessStartInfo - { - FileName = "explorer.exe", - Arguments = $"/select,\"{filePath}\"", - UseShellExecute = true - }; - Process.Start(psi); - return true; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - var folder = File.Exists(filePath) ? Path.GetDirectoryName(filePath) : filePath; - if (string.IsNullOrEmpty(folder)) folder = "."; - - var psi = new ProcessStartInfo - { - FileName = "xdg-open", - Arguments = $"\"{folder}\"", - UseShellExecute = false, - CreateNoWindow = true - }; - Process.Start(psi); - return true; - } - else - { - errorMessage = "Unsupported operating system."; - return false; - } - } - catch (Exception ex) - { - errorMessage = $"Could not open file manager: {ex.Message}"; - return false; - } - } -} diff --git a/src/PdfEditorApp/Services/FontHelper.cs b/src/PdfEditorApp/Services/FontHelper.cs deleted file mode 100644 index cb0acc5..0000000 --- a/src/PdfEditorApp/Services/FontHelper.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Avalonia.Media; - -namespace PdfEditorApp.Services; - -public static class FontHelper -{ - private static readonly HashSet KnownFontFamilies = new(StringComparer.OrdinalIgnoreCase) - { - // --- Core Bundled Fonts --- - "Roboto", - "Inter", - "Open Sans", - "Montserrat", - "Source Sans 3", - "Playfair Display", - "Lora", - "Merriweather", - "Cinzel", - "Fira Code", - "Roboto Mono", - "Comic Neue", - "Pacifico", - "Dancing Script", - "Caveat", - "Great Vibes", - "Lobster", - "Bebas Neue", - "Oswald", - "Orbitron", - - // --- On-Demand Web Fonts --- - "Lato", - "Poppins", - "Raleway", - "Nunito", - "Ubuntu", - "Noto Sans", - "Noto Serif", - "PT Serif", - "Crimson Text", - "Libre Baskerville", - "Libre Franklin", - "Josefin Sans", - "Titillium Web", - "Exo 2", - "Cabin", - - // --- Indian Scripts (Noto) --- - "Noto Sans Devanagari", - "Noto Sans Tamil", - "Noto Sans Telugu", - "Noto Sans Arabic", - "Noto Sans Gujarati", - "Noto Sans Kannada", - "Noto Sans Bengali", - "Noto Sans Malayalam", - "Noto Sans Sinhala", - "Tiro Devanagari Hindi", - - // --- CJK --- - "Noto Sans SC", // Simplified Chinese - "Noto Sans TC", // Traditional Chinese - "Noto Sans JP", // Japanese (Hiragana + Katakana + CJK) - "Noto Serif JP", // Japanese Serif - "Noto Sans KR", // Korean (full Hangul) - "Nanum Gothic", // Korean (additional) - - // --- Southeast Asian --- - "Noto Sans Thai", // Thai - "Sarabun", // Thai (modern) - "Noto Sans Myanmar", // Burmese - "Noto Sans Khmer", // Cambodian - "Noto Sans Lao", // Laotian - "Be Vietnam Pro", // Vietnamese (extended Latin) - - // --- Middle Eastern / RTL --- - "Noto Sans Hebrew", // Hebrew / Yiddish - "Heebo", // Hebrew (modern) - "Vazirmatn", // Persian / Farsi - "Noto Nastaliq Urdu", // Urdu (Nastaliq calligraphic style) - - // --- Eurasian --- - "Noto Sans Georgian", // Georgian - "Noto Sans Armenian", // Armenian - "Noto Sans Ethiopic", // Ethiopic (Amharic, Tigrinya) - "Golos Text", // Cyrillic / Russian - "Russo One", // Cyrillic display - "GFS Neohellenic", // Greek - }; - - public static void RegisterFontFamily(string fontName) - { - if (!string.IsNullOrWhiteSpace(fontName)) - { - lock (KnownFontFamilies) - { - KnownFontFamilies.Add(fontName); - } - } - } - - public static FontFamily CreateFontFamily(string? fontName) - { - if (string.IsNullOrWhiteSpace(fontName)) - return FontFamily.Default; - - // Check if there's a cached font file in the user cache directory - string userDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FryPDF", "Fonts"); - string cleanName = fontName.Replace(" ", ""); - string ttfPath = Path.Combine(userDir, $"{cleanName}.ttf"); - - if (File.Exists(ttfPath)) - { - try - { - var uri = new Uri(ttfPath); - return new FontFamily($"file://{userDir}#{fontName}, avares://PdfEditorApp/Assets/Fonts#{fontName}, {fontName}"); - } - catch { } - } - - // Standard embedded asset resolution with system font fallback - return new FontFamily($"avares://PdfEditorApp/Assets/Fonts#{fontName}, {fontName}"); - } - - /// - /// Returns a safe fallback font family when the requested family cannot be resolved. - /// - public static string GetSafeFallback(string? requestedFamily) - { - if (string.IsNullOrWhiteSpace(requestedFamily)) return "Open Sans"; - - // Script-specific fallbacks - string lc = requestedFamily.ToLowerInvariant(); - if (lc.Contains("sc") || lc.Contains("hans") || lc.Contains("chinese") || lc.Contains("simsun") || lc.Contains("yahei")) return "Noto Sans SC"; - if (lc.Contains("tc") || lc.Contains("hant") || lc.Contains("mingliu")) return "Noto Sans TC"; - if (lc.Contains("jp") || lc.Contains("japanese") || lc.Contains("gothic") || lc.Contains("mincho")) return "Noto Sans JP"; - if (lc.Contains("kr") || lc.Contains("korean") || lc.Contains("hangul") || lc.Contains("malgun")) return "Noto Sans KR"; - if (lc.Contains("devanagari") || lc.Contains("hindi")) return "Noto Sans Devanagari"; - if (lc.Contains("tamil")) return "Noto Sans Tamil"; - if (lc.Contains("telugu")) return "Noto Sans Telugu"; - if (lc.Contains("arabic") || lc.Contains("urdu")) return "Noto Sans Arabic"; - if (lc.Contains("hebrew")) return "Noto Sans Hebrew"; - if (lc.Contains("thai")) return "Noto Sans Thai"; - if (lc.Contains("gujarati")) return "Noto Sans Gujarati"; - if (lc.Contains("kannada")) return "Noto Sans Kannada"; - if (lc.Contains("bengali")) return "Noto Sans Bengali"; - if (lc.Contains("malayalam")) return "Noto Sans Malayalam"; - - // Serif fallbacks - if (lc.Contains("serif") || lc.Contains("times") || lc.Contains("georgia") || - lc.Contains("garamond") || lc.Contains("baskerville")) - return "PT Serif"; - - // Mono fallbacks - if (lc.Contains("mono") || lc.Contains("code") || lc.Contains("courier")) - return "Fira Code"; - - return "Open Sans"; - } -} diff --git a/src/PdfEditorApp/Services/FontPackageService.cs b/src/PdfEditorApp/Services/FontPackageService.cs deleted file mode 100644 index db2b702..0000000 --- a/src/PdfEditorApp/Services/FontPackageService.cs +++ /dev/null @@ -1,595 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Services; - -namespace PdfEditorApp.Services; - -/// -/// Professional on-demand font package management service. -/// Keeps FryPDF lightweight while giving users on-demand access to worldwide language packs. -/// -public class FontPackageService : IFontPackageService -{ - private static readonly HttpClient HttpClient = CreateHttpClient(); - - private static HttpClient CreateHttpClient() - { - var client = new HttpClient - { - Timeout = TimeSpan.FromMinutes(3) - }; - client.DefaultRequestHeaders.UserAgent.ParseAdd("FryPDF-FontDownloader/1.0"); - return client; - } - - private readonly string _userFontDir; - private readonly List _catalog; - - public event Action? FontLibraryChanged; - - public FontPackageService() - { - string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - _userFontDir = Path.Combine(localAppData, "FryPDF", "Fonts"); - - if (!Directory.Exists(_userFontDir)) - { - try { Directory.CreateDirectory(_userFontDir); } catch { } - } - - _catalog = BuildCatalog(); - - // Register any already-cached fonts on startup - RegisterAllDownloadedFontsWithQuestPdf(); - } - - public IReadOnlyList GetAllPackages() => _catalog; - - public string GetUserFontDirectory() => _userFontDir; - - public bool IsPackageInstalled(FontPackageInfo package) - { - if (package.Files.Count == 0) return true; - - foreach (var file in package.Files) - { - string userPath = Path.Combine(_userFontDir, file.FileName); - if (File.Exists(userPath) && new FileInfo(userPath).Length > 0) - continue; - - // Also check embedded fallback - string appPath = Path.Combine(AppContext.BaseDirectory, "Assets", "Fonts", file.FileName); - if (File.Exists(appPath) && new FileInfo(appPath).Length > 0) - continue; - - return false; - } - - return true; - } - - public async Task DownloadPackageAsync( - FontPackageInfo package, - IProgress? progress = null, - Action? statusCallback = null, - CancellationToken ct = default) - { - if (package.Files.Count == 0) - { - progress?.Report(1.0); - return true; - } - - Directory.CreateDirectory(_userFontDir); - - long totalBytesExpected = package.TotalEstimatedSizeBytes; - long totalBytesDownloaded = 0; - int completedFiles = 0; - - foreach (var file in package.Files) - { - if (ct.IsCancellationRequested) return false; - - string targetPath = Path.Combine(_userFontDir, file.FileName); - string tempPath = targetPath + ".tmp"; - - // If already downloaded and valid size, count as downloaded - if (File.Exists(targetPath) && new FileInfo(targetPath).Length > 1000) - { - totalBytesDownloaded += new FileInfo(targetPath).Length; - completedFiles++; - progress?.Report((double)totalBytesDownloaded / Math.Max(1, totalBytesExpected)); - continue; - } - - statusCallback?.Invoke($"Downloading {file.FileName}..."); - - try - { - using var response = await HttpClient.GetAsync(file.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, ct); - response.EnsureSuccessStatusCode(); - - long? contentLength = response.Content.Headers.ContentLength; - using var sourceStream = await response.Content.ReadAsStreamAsync(ct); - using var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None); - - byte[] buffer = new byte[81920]; - int bytesRead; - - while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0) - { - await fileStream.WriteAsync(buffer, 0, bytesRead, ct); - totalBytesDownloaded += bytesRead; - if (totalBytesExpected > 0) - { - double p = Math.Min(0.99, (double)totalBytesDownloaded / totalBytesExpected); - progress?.Report(p); - } - } - - fileStream.Close(); - - if (File.Exists(targetPath)) - File.Delete(targetPath); - - File.Move(tempPath, targetPath); - - // Register with QuestPDF dynamically - try - { - using var regStream = File.OpenRead(targetPath); - QuestPDF.Drawing.FontManager.RegisterFont(regStream); - } - catch { } - - completedFiles++; - } - catch (Exception ex) - { - if (File.Exists(tempPath)) - { - try { File.Delete(tempPath); } catch { } - } - - statusCallback?.Invoke($"Error downloading {file.FileName}: {ex.Message}"); - return false; - } - } - - progress?.Report(1.0); - statusCallback?.Invoke("Installed successfully!"); - FontLibraryChanged?.Invoke(); - return true; - } - - public async Task DeletePackageAsync(FontPackageInfo package) - { - return await Task.Run(() => - { - try - { - foreach (var file in package.Files) - { - string targetPath = Path.Combine(_userFontDir, file.FileName); - if (File.Exists(targetPath)) - { - File.Delete(targetPath); - } - } - - FontLibraryChanged?.Invoke(); - return true; - } - catch - { - return false; - } - }); - } - - public async Task GetTotalCacheSizeBytesAsync() - { - return await Task.Run(() => - { - if (!Directory.Exists(_userFontDir)) return 0L; - try - { - var dir = new DirectoryInfo(_userFontDir); - return dir.GetFiles("*.*", SearchOption.TopDirectoryOnly).Sum(f => f.Length); - } - catch - { - return 0L; - } - }); - } - - public async Task ClearAllCacheAsync() - { - await Task.Run(() => - { - if (!Directory.Exists(_userFontDir)) return; - try - { - foreach (var file in Directory.GetFiles(_userFontDir, "*.*")) - { - try { File.Delete(file); } catch { } - } - FontLibraryChanged?.Invoke(); - } - catch { } - }); - } - - public async Task ImportCustomFontAsync(string sourceFilePath) - { - if (!File.Exists(sourceFilePath)) return false; - - string ext = Path.GetExtension(sourceFilePath).ToLowerInvariant(); - if (ext != ".ttf" && ext != ".otf") return false; - - return await Task.Run(() => - { - try - { - Directory.CreateDirectory(_userFontDir); - string fileName = Path.GetFileName(sourceFilePath); - string destPath = Path.Combine(_userFontDir, fileName); - - File.Copy(sourceFilePath, destPath, true); - - using (var stream = File.OpenRead(destPath)) - { - QuestPDF.Drawing.FontManager.RegisterFont(stream); - } - - FontLibraryChanged?.Invoke(); - return true; - } - catch - { - return false; - } - }); - } - - public FontPackageInfo? DetectMissingPackageForText(string text) - { - if (string.IsNullOrWhiteSpace(text)) return null; - - if (UnicodeScriptDetector.ContainsCjk(text)) - { - string detectedFont = UnicodeScriptDetector.DetectScriptFontFamily(text); - var matchingPack = _catalog.FirstOrDefault(p => p.IncludedFontFamilies.Contains(detectedFont)); - if (matchingPack != null && !IsPackageInstalled(matchingPack)) - { - return matchingPack; - } - } - else if (UnicodeScriptDetector.ContainsDevanagari(text)) - { - var indicPack = _catalog.FirstOrDefault(p => p.Id == "indic"); - if (indicPack != null && !IsPackageInstalled(indicPack)) - return indicPack; - } - else if (UnicodeScriptDetector.IsRtlText(text)) - { - var arabicPack = _catalog.FirstOrDefault(p => p.Id == "arabic"); - if (arabicPack != null && !IsPackageInstalled(arabicPack)) - return arabicPack; - } - - return null; - } - - public IEnumerable GetAllAvailableFontFilePaths() - { - var paths = new HashSet(StringComparer.OrdinalIgnoreCase); - - // User cache - if (Directory.Exists(_userFontDir)) - { - foreach (var f in Directory.GetFiles(_userFontDir, "*.*")) - { - string ext = Path.GetExtension(f).ToLowerInvariant(); - if (ext == ".ttf" || ext == ".otf") - paths.Add(f); - } - } - - // App assets - string[] searchPaths = - { - Path.Combine(AppContext.BaseDirectory, "Assets", "Fonts"), - Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Assets", "Fonts"), - Path.Combine(Directory.GetCurrentDirectory(), "src", "PdfEditorApp", "Assets", "Fonts"), - Path.Combine(Directory.GetCurrentDirectory(), "Assets", "Fonts") - }; - - foreach (var basePath in searchPaths) - { - if (Directory.Exists(basePath)) - { - foreach (var f in Directory.GetFiles(basePath, "*.ttf")) - paths.Add(f); - break; - } - } - - return paths; - } - - public void RegisterAllDownloadedFontsWithQuestPdf() - { - try - { - foreach (var fontPath in GetAllAvailableFontFilePaths()) - { - try - { - using var stream = File.OpenRead(fontPath); - QuestPDF.Drawing.FontManager.RegisterFont(stream); - } - catch { } - } - } - catch { } - } - - /// - /// Base URL for the official FryPDF font repository on GitHub. - /// Eliminates Google Fonts CDN hash breakage and ensures 100% reliable downloads worldwide. - /// - public const string FontCdnBaseUrl = "https://raw.githubusercontent.com/codefrydev/PDFCreator-resources/refs/heads/main/fonts"; - - private static string CdnUrl(string fileName) => $"{FontCdnBaseUrl}/{fileName}"; - - private static List BuildCatalog() - { - return new List - { - // ── 1. Simplified Chinese ────────────────────────────────────────── - new() - { - Id = "zh-hans", - Name = "Simplified Chinese", - NativeName = "简体中文", - FlagEmoji = "🇨🇳", - Region = "China, Singapore, Global", - Category = FontPackageCategory.EastAsia, - Description = "Complete Simplified Chinese typography with full GB2312/GBK coverage for reports, contracts, and documents.", - SampleText = "创造世界一流的 PDF 编辑与文档处理体验。", - SupportedLanguages = new() { "Chinese (Simplified)", "Mandarin" }, - IncludedFontFamilies = new() { "Noto Sans SC" }, - TotalEstimatedSizeBytes = 21_071_052, - Files = new() - { - new() { FileName = "NotoSansSC.ttf", DownloadUrl = CdnUrl("NotoSansSC.ttf"), FontFamilyName = "Noto Sans SC", FileSizeBytes = 10_540_644 }, - new() { FileName = "NotoSansSC-Bold.ttf", DownloadUrl = CdnUrl("NotoSansSC-Bold.ttf"), FontFamilyName = "Noto Sans SC", IsBold = true, FileSizeBytes = 10_530_408 } - } - }, - - // ── 2. Traditional Chinese ───────────────────────────────────────── - new() - { - Id = "zh-hant", - Name = "Traditional Chinese", - NativeName = "繁體中文", - FlagEmoji = "🇹🇼", - Region = "Taiwan, Hong Kong, Macau", - Category = FontPackageCategory.EastAsia, - Description = "High-precision Traditional Chinese typography with Big5 and full character coverage for legal, business, and publishing.", - SampleText = "創造世界一流的 PDF 編輯與文件處理體驗。", - SupportedLanguages = new() { "Chinese (Traditional)", "Cantonese", "Taiwanese" }, - IncludedFontFamilies = new() { "Noto Sans TC" }, - TotalEstimatedSizeBytes = 14_176_420, - Files = new() - { - new() { FileName = "NotoSansTC.ttf", DownloadUrl = CdnUrl("NotoSansTC.ttf"), FontFamilyName = "Noto Sans TC", FileSizeBytes = 7_090_820 }, - new() { FileName = "NotoSansTC-Bold.ttf", DownloadUrl = CdnUrl("NotoSansTC-Bold.ttf"), FontFamilyName = "Noto Sans TC", IsBold = true, FileSizeBytes = 7_085_600 } - } - }, - - // ── 3. Japanese ─────────────────────────────────────────────────── - new() - { - Id = "ja", - Name = "Japanese", - NativeName = "日本語", - FlagEmoji = "🇯🇵", - Region = "Japan", - Category = FontPackageCategory.EastAsia, - Description = "Includes both modern Sans-Serif and elegant Serif Japanese fonts for Hiragana, Katakana, and Kanji.", - SampleText = "世界最高峰のPDF編集およびドキュメント作成体験を提供します。", - SupportedLanguages = new() { "Japanese" }, - IncludedFontFamilies = new() { "Noto Sans JP", "Noto Serif JP" }, - TotalEstimatedSizeBytes = 18_121_784, - Files = new() - { - new() { FileName = "NotoSansJP.ttf", DownloadUrl = CdnUrl("NotoSansJP.ttf"), FontFamilyName = "Noto Sans JP", FileSizeBytes = 5_324_144 }, - new() { FileName = "NotoSansJP-Bold.ttf", DownloadUrl = CdnUrl("NotoSansJP-Bold.ttf"), FontFamilyName = "Noto Sans JP", IsBold = true, FileSizeBytes = 5_319_680 }, - new() { FileName = "NotoSerifJP.ttf", DownloadUrl = CdnUrl("NotoSerifJP.ttf"), FontFamilyName = "Noto Serif JP", FileSizeBytes = 7_477_960 } - } - }, - - // ── 4. Korean ───────────────────────────────────────────────────── - new() - { - Id = "ko", - Name = "Korean", - NativeName = "한국어", - FlagEmoji = "🇰🇷", - Region = "South Korea", - Category = FontPackageCategory.EastAsia, - Description = "Complete Hangul syllable and Jamo character set with Noto Sans KR and Nanum Gothic for professional Korean documents.", - SampleText = "세계 최고 수준의 PDF 편집 및 문서 작성 경험을 선사합니다.", - SupportedLanguages = new() { "Korean" }, - IncludedFontFamilies = new() { "Noto Sans KR", "Nanum Gothic" }, - TotalEstimatedSizeBytes = 14_375_144, - Files = new() - { - new() { FileName = "NotoSansKR.ttf", DownloadUrl = CdnUrl("NotoSansKR.ttf"), FontFamilyName = "Noto Sans KR", FileSizeBytes = 6_163_256 }, - new() { FileName = "NotoSansKR-Bold.ttf", DownloadUrl = CdnUrl("NotoSansKR-Bold.ttf"), FontFamilyName = "Noto Sans KR", IsBold = true, FileSizeBytes = 6_159_248 }, - new() { FileName = "NanumGothic.ttf", DownloadUrl = CdnUrl("NanumGothic.ttf"), FontFamilyName = "Nanum Gothic", FileSizeBytes = 2_052_640 } - } - }, - - // ── 5. Indic Languages ──────────────────────────────────────────── - new() - { - Id = "indic", - Name = "Indic Languages Pack", - NativeName = "भारतीय भाषाएँ", - FlagEmoji = "🇮🇳", - Region = "India, Nepal, Sri Lanka, Bangladesh", - Category = FontPackageCategory.SouthAsia, - Description = "Comprehensive Indian subcontinent typography: Devanagari (Hindi/Marathi/Sanskrit), Tamil, Telugu, Bengali, Gujarati, Kannada, Malayalam, and Tiro Hindi.", - SampleText = "भारत का सबसे तेज़ और सुरक्षित PDF एडिटर और क्रिएटर।", - SupportedLanguages = new() { "Hindi", "Marathi", "Sanskrit", "Tamil", "Telugu", "Bengali", "Gujarati", "Kannada", "Malayalam" }, - IncludedFontFamilies = new() { "Noto Sans Devanagari", "Tiro Devanagari Hindi", "Noto Sans Tamil", "Noto Sans Telugu", "Noto Sans Bengali", "Noto Sans Gujarati", "Noto Sans Kannada", "Noto Sans Malayalam" }, - TotalEstimatedSizeBytes = 1_414_088, - Files = new() - { - new() { FileName = "NotoSansDevanagari.ttf", DownloadUrl = CdnUrl("NotoSansDevanagari.ttf"), FontFamilyName = "Noto Sans Devanagari", FileSizeBytes = 219_460 }, - new() { FileName = "TiroDevanagariHindi.ttf", DownloadUrl = CdnUrl("TiroDevanagariHindi.ttf"), FontFamilyName = "Tiro Devanagari Hindi", FileSizeBytes = 376_908 }, - new() { FileName = "NotoSansTamil.ttf", DownloadUrl = CdnUrl("NotoSansTamil.ttf"), FontFamilyName = "Noto Sans Tamil", FileSizeBytes = 77_724 }, - new() { FileName = "NotoSansTelugu.ttf", DownloadUrl = CdnUrl("NotoSansTelugu.ttf"), FontFamilyName = "Noto Sans Telugu", FileSizeBytes = 178_492 }, - new() { FileName = "NotoSansBengali.ttf", DownloadUrl = CdnUrl("NotoSansBengali.ttf"), FontFamilyName = "Noto Sans Bengali", FileSizeBytes = 138_780 }, - new() { FileName = "NotoSansGujarati.ttf", DownloadUrl = CdnUrl("NotoSansGujarati.ttf"), FontFamilyName = "Noto Sans Gujarati", FileSizeBytes = 173_540 }, - new() { FileName = "NotoSansKannada.ttf", DownloadUrl = CdnUrl("NotoSansKannada.ttf"), FontFamilyName = "Noto Sans Kannada", FileSizeBytes = 143_576 }, - new() { FileName = "NotoSansMalayalam.ttf", DownloadUrl = CdnUrl("NotoSansMalayalam.ttf"), FontFamilyName = "Noto Sans Malayalam", FileSizeBytes = 105_608 } - } - }, - - // ── 6. Arabic, Persian & Urdu ───────────────────────────────────── - new() - { - Id = "arabic", - Name = "Arabic, Persian & Urdu", - NativeName = "العربية / فارسی / اردو", - FlagEmoji = "🇸🇦", - Region = "Middle East, North Africa, Pakistan, Iran", - Category = FontPackageCategory.MiddleEast, - Description = "RTL-optimized font suite featuring Noto Sans Arabic, Vazirmatn (Farsi), and calligraphic Noto Nastaliq Urdu.", - SampleText = "محرر ومُنشئ مستندات PDF الاحترافي والأكثر أماناً وسرعة.", - SupportedLanguages = new() { "Arabic", "Persian (Farsi)", "Urdu", "Pashto", "Kurdish" }, - IncludedFontFamilies = new() { "Noto Sans Arabic", "Vazirmatn", "Noto Nastaliq Urdu" }, - TotalEstimatedSizeBytes = 824_404, - Files = new() - { - new() { FileName = "NotoSansArabic.ttf", DownloadUrl = CdnUrl("NotoSansArabic.ttf"), FontFamilyName = "Noto Sans Arabic", FileSizeBytes = 192_144 }, - new() { FileName = "Vazirmatn.ttf", DownloadUrl = CdnUrl("Vazirmatn.ttf"), FontFamilyName = "Vazirmatn", FileSizeBytes = 104_640 }, - new() { FileName = "NotoNastaliqUrdu.ttf", DownloadUrl = CdnUrl("NotoNastaliqUrdu.ttf"), FontFamilyName = "Noto Nastaliq Urdu", FileSizeBytes = 527_620 } - } - }, - - // ── 7. Hebrew ───────────────────────────────────────────────────── - new() - { - Id = "hebrew", - Name = "Hebrew", - NativeName = "עברית", - FlagEmoji = "🇮🇱", - Region = "Israel, Global", - Category = FontPackageCategory.MiddleEast, - Description = "Modern and classic Hebrew typography featuring Noto Sans Hebrew and Heebo for documents and books.", - SampleText = "עורך ומציג מסמכי ה-PDF המקצועי והמהיר ביותר.", - SupportedLanguages = new() { "Hebrew", "Yiddish" }, - IncludedFontFamilies = new() { "Noto Sans Hebrew", "Heebo" }, - TotalEstimatedSizeBytes = 90_452, - Files = new() - { - new() { FileName = "NotoSansHebrew.ttf", DownloadUrl = CdnUrl("NotoSansHebrew.ttf"), FontFamilyName = "Noto Sans Hebrew", FileSizeBytes = 46_496 }, - new() { FileName = "Heebo.ttf", DownloadUrl = CdnUrl("Heebo.ttf"), FontFamilyName = "Heebo", FileSizeBytes = 43_956 } - } - }, - - // ── 8. Southeast Asian ──────────────────────────────────────────── - new() - { - Id = "southeast-asia", - Name = "Southeast Asian Pack", - NativeName = "ไทย, မြန်မာ, ខ្មែរ, ລາວ", - FlagEmoji = "🇹🇭", - Region = "Thailand, Myanmar, Cambodia, Laos, Sri Lanka, Vietnam", - Category = FontPackageCategory.SoutheastAsia, - Description = "Complete Southeast Asian fonts: Thai (Sarabun & Noto), Burmese (Myanmar), Khmer, Lao, Sinhala, and Vietnamese (Be Vietnam Pro).", - SampleText = "โปรแกรมสร้างและแก้ไขไฟล์ PDF ระดับมืออาชีพที่ปลอดภัยที่สุด", - SupportedLanguages = new() { "Thai", "Burmese", "Khmer", "Lao", "Sinhala", "Vietnamese" }, - IncludedFontFamilies = new() { "Noto Sans Thai", "Sarabun", "Noto Sans Myanmar", "Noto Sans Khmer", "Noto Sans Lao", "Noto Sans Sinhala", "Be Vietnam Pro" }, - TotalEstimatedSizeBytes = 820_332, - Files = new() - { - new() { FileName = "NotoSansThai.ttf", DownloadUrl = CdnUrl("NotoSansThai.ttf"), FontFamilyName = "Noto Sans Thai", FileSizeBytes = 45_660 }, - new() { FileName = "Sarabun.ttf", DownloadUrl = CdnUrl("Sarabun.ttf"), FontFamilyName = "Sarabun", FileSizeBytes = 81_516 }, - new() { FileName = "NotoSansMyanmar.ttf", DownloadUrl = CdnUrl("NotoSansMyanmar.ttf"), FontFamilyName = "Noto Sans Myanmar", FileSizeBytes = 181_864 }, - new() { FileName = "NotoSansKhmer.ttf", DownloadUrl = CdnUrl("NotoSansKhmer.ttf"), FontFamilyName = "Noto Sans Khmer", FileSizeBytes = 104_132 }, - new() { FileName = "NotoSansLao.ttf", DownloadUrl = CdnUrl("NotoSansLao.ttf"), FontFamilyName = "Noto Sans Lao", FileSizeBytes = 51_004 }, - new() { FileName = "NotoSansSinhala.ttf", DownloadUrl = CdnUrl("NotoSansSinhala.ttf"), FontFamilyName = "Noto Sans Sinhala", FileSizeBytes = 235_928 }, - new() { FileName = "BeVietnamPro.ttf", DownloadUrl = CdnUrl("BeVietnamPro.ttf"), FontFamilyName = "Be Vietnam Pro", FileSizeBytes = 120_228 } - } - }, - - // ── 9. Eurasian & Eastern Europe ────────────────────────────────── - new() - { - Id = "eurasia", - Name = "Eurasian & Eastern European", - NativeName = "Русский, Ελληνικά, ქართული, Հայերեն", - FlagEmoji = "🌍", - Region = "Eastern Europe, Caucasus, Horn of Africa", - Category = FontPackageCategory.EuropeAndEurasia, - Description = "Cyrillic (Golos & Russo One), Greek (GFS Neohellenic), Georgian, Armenian, and Ethiopic (Amharic).", - SampleText = "Самый быстрый и безопасный редактор и создатель PDF документов.", - SupportedLanguages = new() { "Russian", "Ukrainian", "Bulgarian", "Greek", "Georgian", "Armenian", "Amharic" }, - IncludedFontFamilies = new() { "Golos Text", "Russo One", "GFS Neohellenic", "Noto Sans Georgian", "Noto Sans Armenian", "Noto Sans Ethiopic" }, - TotalEstimatedSizeBytes = 1_016_072, - Files = new() - { - new() { FileName = "GolosText.ttf", DownloadUrl = CdnUrl("GolosText.ttf"), FontFamilyName = "Golos Text", FileSizeBytes = 64_240 }, - new() { FileName = "RussoOne.ttf", DownloadUrl = CdnUrl("RussoOne.ttf"), FontFamilyName = "Russo One", FileSizeBytes = 36_816 }, - new() { FileName = "GFSNeohellenic.ttf", DownloadUrl = CdnUrl("GFSNeohellenic.ttf"), FontFamilyName = "GFS Neohellenic", FileSizeBytes = 440_376 }, - new() { FileName = "NotoSansGeorgian.ttf", DownloadUrl = CdnUrl("NotoSansGeorgian.ttf"), FontFamilyName = "Noto Sans Georgian", FileSizeBytes = 60_888 }, - new() { FileName = "NotoSansArmenian.ttf", DownloadUrl = CdnUrl("NotoSansArmenian.ttf"), FontFamilyName = "Noto Sans Armenian", FileSizeBytes = 48_388 }, - new() { FileName = "NotoSansEthiopic.ttf", DownloadUrl = CdnUrl("NotoSansEthiopic.ttf"), FontFamilyName = "Noto Sans Ethiopic", FileSizeBytes = 365_364 } - } - }, - - // ── 10. Creative & Typography Pack ───────────────────────────────── - new() - { - Id = "creative-design", - Name = "Creative & Typography Pack", - NativeName = "Modern Display & Script", - FlagEmoji = "🎨", - Region = "Global", - Category = FontPackageCategory.DesignAndTypography, - Description = "Expanded design fonts for brochures, certificates, flyers, and branding: Poppins, Lato, Raleway, Nunito, Ubuntu, Playfair Display, Cinzel, Orbitron, Lobster, Pacifico, Dancing Script, and Titillium Web.", - SampleText = "Design stunning certificates, invoices, and branded documents.", - SupportedLanguages = new() { "All Latin Languages" }, - IncludedFontFamilies = new() { "Poppins", "Lato", "Raleway", "Nunito", "Ubuntu", "Playfair Display", "Cinzel", "Orbitron", "Lobster", "Pacifico", "Dancing Script", "Titillium Web", "Exo 2", "Cabin" }, - TotalEstimatedSizeBytes = 1_264_788, - Files = new() - { - new() { FileName = "Poppins.ttf", DownloadUrl = CdnUrl("Poppins.ttf"), FontFamilyName = "Poppins", FileSizeBytes = 154_628 }, - new() { FileName = "Poppins-Bold.ttf", DownloadUrl = CdnUrl("Poppins-Bold.ttf"), FontFamilyName = "Poppins", IsBold = true, FileSizeBytes = 150_292 }, - new() { FileName = "Lato.ttf", DownloadUrl = CdnUrl("Lato.ttf"), FontFamilyName = "Lato", FileSizeBytes = 72_312 }, - new() { FileName = "Lato-Bold.ttf", DownloadUrl = CdnUrl("Lato-Bold.ttf"), FontFamilyName = "Lato", IsBold = true, FileSizeBytes = 70_576 }, - new() { FileName = "Raleway.ttf", DownloadUrl = CdnUrl("Raleway.ttf"), FontFamilyName = "Raleway", FileSizeBytes = 138_808 }, - new() { FileName = "Nunito.ttf", DownloadUrl = CdnUrl("Nunito.ttf"), FontFamilyName = "Nunito", FileSizeBytes = 125_528 }, - new() { FileName = "Ubuntu.ttf", DownloadUrl = CdnUrl("Ubuntu.ttf"), FontFamilyName = "Ubuntu", FileSizeBytes = 280_328 }, - new() { FileName = "TitilliumWeb.ttf", DownloadUrl = CdnUrl("TitilliumWeb.ttf"), FontFamilyName = "Titillium Web", FileSizeBytes = 53_980 }, - new() { FileName = "Exo2.ttf", DownloadUrl = CdnUrl("Exo2.ttf"), FontFamilyName = "Exo 2", FileSizeBytes = 143_780 }, - new() { FileName = "Cabin.ttf", DownloadUrl = CdnUrl("Cabin.ttf"), FontFamilyName = "Cabin", FileSizeBytes = 74_556 } - } - } - }; - } -} diff --git a/src/PdfEditorApp/Services/FryPdfDocument.cs b/src/PdfEditorApp/Services/FryPdfDocument.cs deleted file mode 100644 index c139596..0000000 --- a/src/PdfEditorApp/Services/FryPdfDocument.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using QuestPDF.Fluent; -using QuestPDF.Infrastructure; - -namespace PdfEditorApp.Services; - -/// -/// Universal QuestPDF IDocument wrapper ensuring enterprise FryPDF Creator and codefrydev.in Producer metadata branding. -/// -public class FryPdfDocument : IDocument -{ - private readonly Action _compose; - private readonly string _title; - private readonly string _author; - private readonly string _subject; - private readonly string _keywords; - private readonly string _creator; - private readonly string _producer; - - public FryPdfDocument( - Action compose, - string? title = null, - string? author = null, - string? subject = null, - string? keywords = null, - string? creator = null, - string? producer = null) - { - _compose = compose; - _title = title ?? "FryPDF Document"; - _author = author ?? "FryPDF"; - _subject = subject ?? ""; - _keywords = keywords ?? ""; - _creator = !string.IsNullOrWhiteSpace(creator) ? creator : "FryPDF"; - _producer = !string.IsNullOrWhiteSpace(producer) ? producer : "codefrydev.in"; - } - - public DocumentMetadata GetMetadata() => new DocumentMetadata - { - Title = _title, - Author = _author, - Subject = _subject, - Keywords = _keywords, - Creator = _creator, - Producer = _producer, - CreationDate = DateTime.UtcNow, - ModifiedDate = DateTime.UtcNow - }; - - public DocumentSettings GetSettings() => DocumentSettings.Default; - - public void Compose(IDocumentContainer container) => _compose(container); - - public static IDocument Create( - Action compose, - string? title = null, - string? author = null, - string? subject = null, - string? keywords = null, - string? creator = null, - string? producer = null) - { - return new FryPdfDocument(compose, title, author, subject, keywords, creator, producer); - } -} diff --git a/src/PdfEditorApp/Services/HelpGuideService.cs b/src/PdfEditorApp/Services/HelpGuideService.cs deleted file mode 100644 index 12f598c..0000000 --- a/src/PdfEditorApp/Services/HelpGuideService.cs +++ /dev/null @@ -1,1796 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -/// -/// Comprehensive, offline-first service providing step-by-step guides, pro-tips, key features, and specifications -/// for all 32 PDF tools, the live canvas document editor, automation workflows, batch generation, and international typography. -/// -public class HelpGuideService : IHelpGuideService -{ - private readonly List _guides; - private readonly List _categories; - - public HelpGuideService() - { - _guides = new List - { - // ========================================================================= - // 1. GETTING STARTED & CORE BASICS - // ========================================================================= - new HelpGuideItem - { - Id = "getting-started", - Title = "Welcome to FryPDF Studio", - Category = "Getting Started", - Summary = "Learn the basics of FryPDF: a privacy-first, ultra-fast PDF creation and manipulation studio.", - Description = "FryPDF is an all-in-one PDF powerhouse engineered with .NET 10, Avalonia UI, QuestPDF, and SkiaSharp. Everything runs 100% locally on your machine—no cloud uploads, no subscriptions, and total data privacy.", - Steps = new List - { - "Launch FryPDF to arrive at the Home Dashboard featuring quick actions, recent files, and 32 specialized PDF tools.", - "Choose 'New Document' to start from a blank canvas or pick from 19+ professionally designed templates.", - "Use 'Open File' or drag and drop any .frypdf, .pdf, or .json file to view or edit existing documents.", - "Press ⌘K (or Ctrl+K on Windows/Linux) anytime to bring up the Quick Command Palette to launch tools, search guides, or insert elements.", - "Switch between Light and Dark themes anytime using the toggle button in the bottom-left sidebar." - }, - KeyFeatures = new List - { - "100% Offline & Private: Zero telemetry or external server processing", - "Dual-Mode Studio: High-performance Acrobat-style Reader + Full Canvas Editor", - "32 Built-in Single & Multi-File PDF Utility Tools", - "Native .frypdf Workspace Persistence with embedded project states in exported PDFs" - }, - ProTips = new List - { - "You can double-click any recent file from the Home Dashboard to open it immediately.", - "Press F1 or Shift+? at any point to view the keyboard shortcut cheat sheet." - }, - SupportedFormats = "Native: .frypdf, .json | Standard: .pdf, .docx, .xlsx, .pptx, images", - KeyboardShortcut = "⌘K / Ctrl+K (Command Palette)", - IconKind = "RocketLaunchOutline", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#E0F2FE", - Badge = "Getting Started", - Keywords = "intro start quickstart overview privacy offline mac windows linux theme darkmode", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "pdf-reader-mode", - Title = "PDF Reader & Acrobat Navigation", - Category = "Getting Started", - Summary = "Read and review PDF documents with continuous scroll, bookmark outline trees, and reading themes.", - Description = "The dedicated PDF Reader mode allows you to read large documents comfortably with high-precision vector rendering, thumbnail previews, bookmark navigation, and eye-friendly color themes.", - Steps = new List - { - "Click 'PDF Reader' in the left sidebar or select 'PDF Reader Mode' from the Home Dashboard.", - "Click 'Open PDF Document' or drag and drop any standard PDF file into the drop zone.", - "Use the sidebar to view page thumbnails or navigate via the interactive Document Outline tree.", - "Choose your preferred viewing theme: Default White, Eye-Comfort Sepia, Dark Night, or High Contrast.", - "Use the zoom slider (⌘+ / ⌘-) or quick buttons (Fit Width ⌘1, Fit Page ⌘9) to optimize your reading layout." - }, - KeyFeatures = new List - { - "Multi-Theme Reading: Default, Sepia, Dark, High-Contrast", - "Continuous Vertical Scrolling and Single/Spread Page View Modes", - "Interactive Table of Contents & Bookmarks Navigation", - "In-Document Text Search with live match highlighting" - }, - ProTips = new List - { - "Use the Sepia theme for long-form reading sessions to minimize eye fatigue.", - "Click the 'Edit in Canvas' button in the viewer toolbar to deconstruct and edit the PDF directly." - }, - SupportedFormats = "Input: .pdf", - KeyboardShortcut = "⌘1 (Fit Width), ⌘9 (Fit Page)", - IconKind = "BookOpenPageVariantOutline", - IconColorHex = "#DC2626", - BackgroundAccentHex = "#FEE2E2", - Badge = "Reader Mode", - Keywords = "reader read pdf viewer sepia dark outline bookmarks scroll pages zoom", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "templates-guide", - Title = "Template Studio & Document Gallery", - Category = "Getting Started", - Summary = "Jumpstart your documents with 19+ curated templates including Invoices, Resumes, and Academic Papers.", - Description = "The Template Studio provides a rich library of pre-formatted, production-ready templates. Every template is fully editable down to each text block, logo, color palette, and vector element.", - Steps = new List - { - "Select 'New Document' from the left sidebar to enter the Template Gallery.", - "Filter templates by category: Business, Finance, Academic & Research, Certificates, Invitations, or General.", - "Click on any template card to instantly instantiate and open it inside the live Canvas Editor.", - "Double-click any text box to replace sample copy with your own information.", - "Export your finished document using ⌘E (Export to PDF)." - }, - KeyFeatures = new List - { - "19+ High-Quality Templates: Invoices, Executive CVs, Discrete Math Research, Certificates, Art Deco Gala Invitations", - "Pre-configured typography, color schemes, and vector embellishments", - "Instant blank document creation with standard paper sizes (A4, Letter, Legal, Tabloid)" - }, - ProTips = new List - { - "Academic and math templates include pre-rendered LaTeX formulas and 2-column scientific grids.", - "All templates automatically detect your system fonts and fallback gracefully to embedded Noto Sans families." - }, - SupportedFormats = "Output: .frypdf, .pdf", - KeyboardShortcut = "⌘N / Ctrl+N (New Document)", - IconKind = "ViewDashboardOutline", - IconColorHex = "#10B981", - BackgroundAccentHex = "#ECFDF5", - Badge = "Templates", - Keywords = "templates gallery new invoice resume cv academic certificate letter formal blank", - IsFeatured = true - }, - - // ========================================================================= - // 2. ALL 32 PDF TOOLS (ORGANIZE, OPTIMIZE, CONVERT, EDIT, AI & AUTOMATION) - // ========================================================================= - new HelpGuideItem - { - Id = "tool-merge", - Title = "Merge PDF Files", - Category = "32 PDF Tools", - Summary = "Combine multiple PDF documents into a single unified file in custom order.", - Description = "The Merge PDF tool combines two or more PDF files into a single, cohesive document. You can drag and drop multiple files, reorder them with up/down arrows, and compile them with a single click.", - Steps = new List - { - "Open 'Merge PDF' from the Tools Studio or press ⌘K and type 'Merge PDF'.", - "Click 'Add PDF Files' or drag and drop your PDFs into the file staging area.", - "Reorder files using the move up/down controls or remove any unwanted files.", - "Specify your desired output filename or leave default.", - "Click 'Merge PDFs' to generate your combined PDF in seconds." - }, - KeyFeatures = new List - { - "Batch multi-file support with visual reordering", - "Preserves internal bookmarks, links, and high-resolution images", - "Generates compliant, optimized output with zero quality loss" - }, - ProTips = new List - { - "You can drag and drop dozens of PDF files at once directly onto the tool page.", - "After merging, you can open the result in the PDF Viewer or Editor with one click." - }, - SupportedFormats = "Inputs: Multiple .pdf | Output: Single .pdf", - RelatedToolId = PdfToolId.MergePdf, - IconKind = "CallMerge", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - Badge = "Organize", - Keywords = "merge combine join pdfs bind assemble files batch", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-split", - Title = "Split PDF Pages & Ranges", - Category = "32 PDF Tools", - Summary = "Extract specific page ranges, split by intervals, or separate into individual page files.", - Description = "The Split PDF tool allows you to divide a large PDF into smaller standalone documents by custom page ranges, fixed page intervals, or individual single pages.", - Steps = new List - { - "Open 'Split PDF' from the Tools Studio.", - "Select or drop your source PDF document.", - "Choose your split mode: 'Custom Ranges' (e.g. 1-3, 5, 8-10), 'Fixed Intervals' (e.g. every 2 pages), or 'All Pages'.", - "Review the output file preview list.", - "Click 'Split PDF' to export your new files to the chosen directory." - }, - KeyFeatures = new List - { - "Custom page range syntax (e.g. '1-4, 7, 9-12')", - "Burst mode: Extract each page as a distinct single-page PDF", - "Odd / Even page split support" - }, - ProTips = new List - { - "Use comma-separated ranges to create multiple output files in a single pass.", - "All extracted files retain full vector quality and embedded fonts." - }, - SupportedFormats = "Input: .pdf | Output: Multiple .pdf files", - RelatedToolId = PdfToolId.SplitPdf, - IconKind = "CallSplit", - IconColorHex = "#DC2626", - BackgroundAccentHex = "#FEF2F2", - Badge = "Organize", - Keywords = "split extract divide separate pages ranges interval burst", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-compress", - Title = "Compress & Optimize PDF", - Category = "32 PDF Tools", - Summary = "Reduce PDF file sizes dramatically while retaining crisp vector typography and clear images.", - Description = "Compress PDF analyzes embedded image streams, strips duplicate metadata, and applies optimized stream compression algorithms to drastically shrink file sizes for email sharing and web publishing.", - Steps = new List - { - "Open 'Compress PDF' from the Tools Studio.", - "Select the PDF file(s) you wish to shrink.", - "Select your compression level: 'Recommended' (best balance), 'Extreme' (smallest size), or 'Light' (maximal fidelity).", - "Click 'Compress PDF'.", - "Inspect the before/after file size summary and percentage savings." - }, - KeyFeatures = new List - { - "3 Compression Presets: Extreme, Recommended, and Light", - "Intelligent Skia image resampling and DCT quality tuning", - "Removes redundant stream objects and unreferenced fonts" - }, - ProTips = new List - { - "Scanned image-heavy PDFs often see 70%–90% file size reduction with Recommended compression.", - "Pure vector documents with text remain razor-sharp even under extreme compression." - }, - SupportedFormats = "Input: .pdf | Output: Optimized .pdf", - RelatedToolId = PdfToolId.CompressPdf, - IconKind = "ArrowCollapseAll", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - Badge = "Optimize", - Keywords = "compress shrink optimize reduce file size email small quality", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-pdf-to-word", - Title = "PDF to Word (DOCX)", - Category = "32 PDF Tools", - Summary = "Convert PDF documents into fully editable Microsoft Word DOC and DOCX files.", - Description = "Extracts text paragraphs, headings, bullet lists, formatting, and tables from PDF files into standard Microsoft Word DOCX documents compatible with Microsoft 365, Word 2016+, and LibreOffice.", - Steps = new List - { - "Open 'PDF to Word' from the Convert from PDF category.", - "Choose your source PDF file.", - "Select formatting preservation options (maintain paragraph flow vs exact layout).", - "Click 'Convert to Word'.", - "Open the generated .docx file in Microsoft Word or any compatible word processor." - }, - KeyFeatures = new List - { - "Preserves paragraph structure, font styling, and colors", - "Extracts tables into native Word table grids", - "Embeds extracted images in their original positions" - }, - ProTips = new List - { - "For scanned documents without selectable text, run 'OCR Text Recognition' first for best results." - }, - SupportedFormats = "Input: .pdf | Output: .docx", - RelatedToolId = PdfToolId.PdfToWord, - IconKind = "FileWordOutline", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - Badge = "Convert", - Keywords = "word doc docx convert export office microsoft word editable", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-pdf-to-excel", - Title = "PDF to Excel (XLSX)", - Category = "32 PDF Tools", - Summary = "Extract tables, financial figures, and balance sheets from PDF into Microsoft Excel spreadsheets.", - Description = "Extracts structured tabular data from PDF invoices, financial statements, and reports into real editable cells inside Microsoft Excel .xlsx workbooks.", - Steps = new List - { - "Open 'PDF to Excel' from the Tools Studio.", - "Select the PDF containing tabular data.", - "Configure table detection sensitivity (Automatic or Strict Grid).", - "Click 'Convert to Excel'.", - "Open the generated .xlsx workbook in Microsoft Excel or Google Sheets." - }, - KeyFeatures = new List - { - "Detects table columns and cell boundaries automatically", - "Preserves numeric formatting for immediate calculations", - "Creates separate worksheets per PDF page" - }, - ProTips = new List - { - "Perfect for extracting transaction statements, product catalogs, and invoice line items." - }, - SupportedFormats = "Input: .pdf | Output: .xlsx", - RelatedToolId = PdfToolId.PdfToExcel, - IconKind = "FileExcelOutline", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - Badge = "Convert", - Keywords = "excel xlsx spreadsheet table financial data grid convert export", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-pdf-to-powerpoint", - Title = "PDF to PowerPoint (PPTX)", - Category = "32 PDF Tools", - Summary = "Transform PDF presentations into editable Microsoft PowerPoint PPTX slide decks.", - Description = "Converts each page of a PDF document into a slide in Microsoft PowerPoint, allowing you to edit text boxes, rearrange slides, and apply presentation animations.", - Steps = new List - { - "Open 'PDF to PowerPoint' from the Tools Studio.", - "Select the PDF presentation deck.", - "Click 'Convert to PowerPoint'.", - "Save and open the resulting .pptx file in Microsoft PowerPoint or Keynote." - }, - KeyFeatures = new List - { - "Maps each PDF page to an individual 16:9 or 4:3 slide", - "Extracts text blocks as movable slide text frames", - "Retains vector background graphics and slide illustrations" - }, - ProTips = new List - { - "Ideal for repurposing exported webinar slide decks, pitch decks, and lecture handouts." - }, - SupportedFormats = "Input: .pdf | Output: .pptx", - RelatedToolId = PdfToolId.PdfToPowerPoint, - IconKind = "FilePowerpointOutline", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - Badge = "Convert", - Keywords = "powerpoint ppt pptx slides presentation deck convert export", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-word-to-pdf", - Title = "Word to PDF (DOCX to PDF)", - Category = "32 PDF Tools", - Summary = "Compile Microsoft Word DOC and DOCX files into standardized, high-fidelity PDF documents.", - Description = "Converts Microsoft Word documents into vector PDFs with consistent typography, crisp layout formatting, and embedded metadata.", - Steps = new List - { - "Open 'Word to PDF' from the Convert to PDF category.", - "Select or drop your .docx file.", - "Click 'Convert to PDF'.", - "Open or export your compiled PDF." - }, - KeyFeatures = new List - { - "Accurate font and margin translation", - "Embeds vector typography for universal cross-platform viewing", - "Fast batch processing support" - }, - ProTips = new List - { - "Batch convert multiple .docx files in one click to standardize company document archives." - }, - SupportedFormats = "Input: .docx, .doc | Output: .pdf", - RelatedToolId = PdfToolId.WordToPdf, - IconKind = "FileWord", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - Badge = "Convert", - Keywords = "word to pdf docx doc compile convert document import", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-excel-to-pdf", - Title = "Excel to PDF (XLSX to PDF)", - Category = "32 PDF Tools", - Summary = "Convert Excel spreadsheets and tables into cleanly formatted, printable PDF documents.", - Description = "Renders Microsoft Excel worksheets into paginated, publication-grade PDF documents with clean gridlines and fit-to-page scaling.", - Steps = new List - { - "Open 'Excel to PDF' from the Tools Studio.", - "Select your .xlsx or .xls workbook.", - "Choose page orientation (Landscape or Portrait) and scaling options.", - "Click 'Convert to PDF'." - }, - KeyFeatures = new List - { - "Automatic column width fitting and table boundary alignment", - "Preserves table borders, number formats, and header colors", - "Multi-sheet workbook support" - }, - ProTips = new List - { - "Select Landscape orientation for spreadsheets with more than 6 columns to prevent horizontal clipping." - }, - SupportedFormats = "Input: .xlsx, .xls | Output: .pdf", - RelatedToolId = PdfToolId.ExcelToPdf, - IconKind = "FileExcel", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - Badge = "Convert", - Keywords = "excel to pdf xlsx xls spreadsheet table sheet convert", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-powerpoint-to-pdf", - Title = "PowerPoint to PDF (PPTX to PDF)", - Category = "32 PDF Tools", - Summary = "Convert PowerPoint presentations into high-resolution, presentation-ready PDF slide decks.", - Description = "Compiles PPTX slide decks into crisp, universally shareable PDF handouts and presentation documents.", - Steps = new List - { - "Open 'PowerPoint to PDF' from the Tools Studio.", - "Select your .pptx presentation.", - "Click 'Convert to PDF'.", - "Review the generated PDF slides in the viewer." - }, - KeyFeatures = new List - { - "Preserves slide dimensions (16:9 widescreen or 4:3 standard)", - "Retains slide backgrounds, vector shapes, and diagrams", - "High-resolution vector output" - }, - ProTips = new List - { - "Use this tool to create unalterable PDF handouts for clients and conference attendees." - }, - SupportedFormats = "Input: .pptx, .ppt | Output: .pdf", - RelatedToolId = PdfToolId.PowerPointToPdf, - IconKind = "FilePowerpoint", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - Badge = "Convert", - Keywords = "powerpoint to pdf pptx ppt presentation slides convert", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-edit-pdf", - Title = "Live PDF Deconstruction & Editing", - Category = "32 PDF Tools", - Summary = "Deconstruct any existing 3rd-party PDF into an editable canvas with live text, images, and shapes.", - Description = "FryPDF's breakthrough deconstruction engine parses raw PDF streams, performs layout and typography analysis, detects Unicode scripts, and converts static PDF pages into fully editable, layered canvas elements.", - Steps = new List - { - "Open 'Edit PDF' from the Tools Studio or drag and drop any PDF into the editor.", - "The deconstruction engine analyzes text bounding boxes, font metrics, and embedded graphics.", - "Click any text element to edit its content, font size, weight, and color directly.", - "Move, resize, or delete shapes, logos, and images on the visual canvas.", - "Save as a .frypdf project file or export directly back to vector PDF via ⌘E." - }, - KeyFeatures = new List - { - "Intelligent Line & Paragraph Clustering with baseline tracking", - "Multi-Format Skia Image Extraction (DCT, JPEG2000, CMYK, PNG)", - "Script-Aware Font Fallback (Devanagari, CJK, Arabic, Latin)", - "Strict Layered Z-Index Allocation preventing occlusion" - }, - ProTips = new List - { - "FryPDF preserves exact coordinates and typography so your edits look indistinguishable from the original.", - "Use the Properties Inspector (⌘⇧P) to fine-tune letter spacing, line height multipliers, and padding." - }, - SupportedFormats = "Inputs: .pdf, .frypdf, .json | Output: .pdf, .frypdf", - RelatedToolId = PdfToolId.EditPdf, - IconKind = "Draw", - IconColorHex = "#9333EA", - BackgroundAccentHex = "#FAF5FF", - Badge = "Editor", - Keywords = "edit deconstruct modify alter text images replace delete change pdf", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-pdf-to-jpg", - Title = "PDF to JPG & PNG Images", - Category = "32 PDF Tools", - Summary = "Render each page of a PDF into high-resolution JPG or PNG images, or extract all embedded photos.", - Description = "Converts PDF pages into standalone image files at selectable DPI settings (72 DPI, 150 DPI, 300 DPI print grade), or extracts raw embedded images from inside the document.", - Steps = new List - { - "Open 'PDF to JPG' from the Tools Studio.", - "Select the PDF file.", - "Choose mode: 'Render Entire Pages' or 'Extract Embedded Images Only'.", - "Select target image format (JPG or PNG) and resolution DPI.", - "Click 'Convert to Images' to export to a dedicated folder." - }, - KeyFeatures = new List - { - "High-Fidelity SkiaSharp Raster Engine with sub-pixel antialiasing", - "Supports 72, 150, and 300 DPI output resolutions", - "Lossless PNG or compressed JPEG formats" - }, - ProTips = new List - { - "Choose 300 DPI PNG when generating images intended for print catalogs or marketing materials." - }, - SupportedFormats = "Input: .pdf | Output: .jpg, .png", - RelatedToolId = PdfToolId.PdfToJpg, - IconKind = "FileImageOutline", - IconColorHex = "#EAB308", - BackgroundAccentHex = "#FEFCE8", - Badge = "Convert", - Keywords = "jpg png image photo picture render extract dpi export", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-images-to-pdf", - Title = "Images to PDF (JPG/PNG to PDF)", - Category = "32 PDF Tools", - Summary = "Combine multiple JPG, PNG, WebP, and BMP images into a single bound PDF document.", - Description = "Import one or multiple photos, scans, and graphic files, arrange their order, configure page orientation and margins, and compile them into a clean PDF album or portfolio.", - Steps = new List - { - "Open 'Images to PDF' from the Tools Studio.", - "Click 'Add Images' or drag and drop image files into the window.", - "Drag images to arrange their page sequence.", - "Select page sizing (Fit Image, A4, or Letter) and margin settings.", - "Click 'Convert to PDF'." - }, - KeyFeatures = new List - { - "Supports JPG, PNG, WebP, BMP, and GIF formats", - "Automatic orientation matching (portrait vs landscape per image)", - "Custom margin options: None, Small, or Standard" - }, - ProTips = new List - { - "Set margins to 'None' for full-bleed photo albums and graphic presentations." - }, - SupportedFormats = "Inputs: .jpg, .jpeg, .png, .webp, .bmp | Output: .pdf", - RelatedToolId = PdfToolId.JpgToPdf, - IconKind = "FileImagePlusOutline", - IconColorHex = "#EAB308", - BackgroundAccentHex = "#FEFCE8", - Badge = "Convert", - Keywords = "images to pdf photos jpg png picture album scan combine convert", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-sign-pdf", - Title = "Digital Signature Studio", - Category = "32 PDF Tools", - Summary = "Add handwritten mouse/stylus signatures, typed calligraphy, or certificate stamps to PDFs.", - Description = "The Signature Studio lets you create and place professional signatures on contracts, agreements, and forms. You can draw your signature, type it using elegant cursive typography, or upload a signature image.", - Steps = new List - { - "Open 'Sign PDF' from the Tools Studio or launch Signature Studio from the editor ribbon.", - "Select your signature style: 'Draw' (handwritten with pen/mouse), 'Type' (calligraphy fonts), or 'Upload Image'.", - "Configure stroke thickness and ink color (Classic Black, Executive Navy Blue, or Crimson).", - "Position the signature box on the desired page and coordinates.", - "Click 'Apply Signature' to stamp the document." - }, - KeyFeatures = new List - { - "Smooth vector Bézier smoothing for drawn signatures", - "Pre-styled executive script font options", - "Multi-page stamping and coordinate alignment" - }, - ProTips = new List - { - "Executive Navy Blue ink (#0F2942) provides a distinctive authentic pen-and-paper look on formal legal agreements." - }, - SupportedFormats = "Input: .pdf | Output: Signed .pdf", - RelatedToolId = PdfToolId.SignPdf, - IconKind = "DrawPen", - IconColorHex = "#059669", - BackgroundAccentHex = "#ECFDF5", - Badge = "Security", - Keywords = "sign signature e-sign contract agreement draw calligraphy legal ink", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-protect-pdf", - Title = "Protect PDF (AES-256 Encryption)", - Category = "32 PDF Tools", - Summary = "Secure confidential PDFs with robust 256-bit AES passwords and granular access permissions.", - Description = "Encrypts your PDF documents with industry-standard 256-bit AES encryption. You can set a Document Open password as well as a Permissions Master password to prevent unauthorized printing, copying, or modifying.", - Steps = new List - { - "Open 'Protect PDF' from the Tools Studio.", - "Select the PDF you wish to encrypt.", - "Enter a strong Document Open Password.", - "Optionally set permissions: Prevent Printing, Prevent Content Copying, or Restrict Editing.", - "Click 'Encrypt & Protect PDF' to save your secured file." - }, - KeyFeatures = new List - { - "256-bit AES Encryption (Adobe Acrobat Standard & PDF 2.0 compliant)", - "Granular permissions: Disallow copying, printing, or form alterations", - "Strong password strength indicator" - }, - ProTips = new List - { - "Always store your encryption password securely; FryPDF encrypts locally and cannot recover forgotten passwords." - }, - SupportedFormats = "Input: .pdf | Output: Encrypted .pdf", - RelatedToolId = PdfToolId.ProtectPdf, - IconKind = "LockOutline", - IconColorHex = "#DC2626", - BackgroundAccentHex = "#FEF2F2", - Badge = "Security", - Keywords = "protect password encrypt lock security aes permissions secure confidential", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-unlock-pdf", - Title = "Unlock PDF (Remove Password)", - Category = "32 PDF Tools", - Summary = "Remove password protection and permission locks from authenticated PDF files.", - Description = "Removes encryption and security restrictions from PDF files when you possess the authorization password, making the document freely editable and printable.", - Steps = new List - { - "Open 'Unlock PDF' from the Tools Studio.", - "Select the encrypted PDF.", - "Enter the valid password.", - "Click 'Unlock PDF' to produce an unrestricted, open version of the document." - }, - KeyFeatures = new List - { - "Removes open password and all editing/printing permission restrictions", - "Fast stream decryption with zero data corruption", - "Safe local processing" - }, - ProTips = new List - { - "Once unlocked, you can freely edit, merge, or convert the document with any other FryPDF tool." - }, - SupportedFormats = "Input: Password-Protected .pdf | Output: Unlocked .pdf", - RelatedToolId = PdfToolId.UnlockPdf, - IconKind = "LockOpenOutline", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - Badge = "Security", - Keywords = "unlock decrypt remove password unprotect permissions open", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-rotate-pdf", - Title = "Rotate PDF Pages", - Category = "32 PDF Tools", - Summary = "Rotate upside-down or sideways pages by 90°, 180°, or 270° degrees.", - Description = "Fix misaligned, upside-down, or sideways scanned pages across the entire document or for selected individual pages.", - Steps = new List - { - "Open 'Rotate PDF' from the Tools Studio.", - "Select the PDF file.", - "Choose rotation angle: 90° Clockwise, 180° Inverted, or 90° Counter-Clockwise.", - "Specify page selection: All Pages, Odd Pages Only, Even Pages Only, or Custom Range.", - "Click 'Apply Rotation' to save the corrected PDF." - }, - KeyFeatures = new List - { - "Rotates page bounding boxes and graphics vectors losslessly", - "Supports selective odd/even or range-based rotation", - "Immediate visual thumbnail preview" - }, - ProTips = new List - { - "Inside the live editor, you can also press ⌘⇧R (or Ctrl+Shift+R) to rotate the current active page." - }, - SupportedFormats = "Input: .pdf | Output: Rotated .pdf", - RelatedToolId = PdfToolId.RotatePdf, - IconKind = "RotateRight", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - Badge = "Organize", - Keywords = "rotate orientation angle clockwise landscape portrait upside down", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-add-watermark", - Title = "Add Watermark", - Category = "32 PDF Tools", - Summary = "Stamp diagonal text or graphic logos across pages with customizable opacity and angle.", - Description = "Apply prominent security watermarks such as 'CONFIDENTIAL', 'DRAFT', 'COPY', or custom company logos across all pages of a PDF document.", - Steps = new List - { - "Open 'Watermark PDF' from the Tools Studio or click 'Watermark' in the editor ribbon.", - "Select watermark type: 'Text' or 'Image Logo'.", - "If text, type your message (e.g. 'CONFIDENTIAL — DO NOT DISTRIBUTE') and pick a font.", - "Adjust opacity (e.g. 15%–30%), rotation angle (e.g. 45° diagonal), and position.", - "Click 'Apply Watermark'." - }, - KeyFeatures = new List - { - "Adjustable opacity slider (0%–100%)", - "Custom rotation angle and font sizing", - "Choice between background layer (behind text) or foreground overlay" - }, - ProTips = new List - { - "A 20% opacity at a 45° angle provides high visibility without obscuring underlying document text." - }, - SupportedFormats = "Input: .pdf | Output: Watermarked .pdf", - RelatedToolId = PdfToolId.Watermark, - IconKind = "Watermark", - IconColorHex = "#0891B2", - BackgroundAccentHex = "#ECFEFF", - Badge = "Security", - Keywords = "watermark stamp draft confidential copyright logo security overlay opacity", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-html-to-pdf", - Title = "HTML to PDF", - Category = "32 PDF Tools", - Summary = "Convert HTML5 web pages, raw HTML markup, and CSS into standardized PDF documents.", - Description = "Render web articles, invoices, receipts, and online reports directly from HTML files or URLs into cleanly formatted PDF pages.", - Steps = new List - { - "Open 'HTML to PDF' from the Tools Studio.", - "Select an .html file or enter a web URL.", - "Configure page size (A4, Letter) and margin settings.", - "Click 'Convert to PDF'." - }, - KeyFeatures = new List - { - "Full CSS3 styling, flexbox, and web font rendering", - "Automatic page break calculations", - "Retains embedded hyperlinks" - }, - ProTips = new List - { - "Add CSS @media print styles to your HTML for perfect print layout control." - }, - SupportedFormats = "Input: .html, .htm | Output: .pdf", - RelatedToolId = PdfToolId.HtmlToPdf, - IconKind = "LanguageHtml5", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - Badge = "Convert", - Keywords = "html url web webpage css html to pdf compile convert", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-page-numbers", - Title = "Add Page Numbers", - Category = "32 PDF Tools", - Summary = "Insert customizable page numbering headers and footers with custom formats and offsets.", - Description = "Stamp running page numbers across PDF documents with flexible placement, format strings like 'Page {0} of {1}', Roman numerals, and starting offsets.", - Steps = new List - { - "Open 'Add Page Numbers' from the Tools Studio.", - "Select the PDF file.", - "Choose position: Top/Bottom, Left/Center/Right.", - "Select numbering format: '1, 2, 3...', 'Page 1 of N', or 'I, II, III...'.", - "Optionally specify a starting page offset (e.g. skip cover page).", - "Click 'Apply Page Numbers'." - }, - KeyFeatures = new List - { - "6 Placement Positions (Top/Bottom, Left/Center/Right)", - "Custom format strings (e.g. 'Page {0} of {1}' or 'Document Ref - Page {0}')", - "Option to skip cover or title pages" - }, - ProTips = new List - { - "Set 'Start on Page' to 2 when your document includes a formal cover page." - }, - SupportedFormats = "Input: .pdf | Output: Numbered .pdf", - RelatedToolId = PdfToolId.PageNumbers, - IconKind = "Numeric", - IconColorHex = "#6366F1", - BackgroundAccentHex = "#EEF2FF", - Badge = "Organize", - Keywords = "page numbers footer header numbering pagination count stamp", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-crop-pdf", - Title = "Crop PDF Margins", - Category = "32 PDF Tools", - Summary = "Trim unwanted margins, white borders, and header/footer areas visually.", - Description = "Adjust the visible page boundaries (CropBox) of a PDF to remove excess margins, scanner edges, or crop to specific content regions.", - Steps = new List - { - "Open 'Crop PDF' from the Tools Studio.", - "Select the PDF document.", - "Drag the visual crop bounding box handles or enter exact margin dimensions in points.", - "Apply to current page or all pages.", - "Click 'Crop PDF'." - }, - KeyFeatures = new List - { - "Interactive visual crop handle bounding box", - "Preserves vector text quality without re-rasterizing", - "Batch crop across all pages" - }, - ProTips = new List - { - "Cropping adjusts the display boundary without destroying underlying vectors, ensuring crisp rendering." - }, - SupportedFormats = "Input: .pdf | Output: Cropped .pdf", - RelatedToolId = PdfToolId.CropPdf, - IconKind = "Crop", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#E0F2FE", - Badge = "Organize", - Keywords = "crop trim margins border cut resize bounding box", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-organize-pages", - Title = "Organize & Reorder Pages", - Category = "32 PDF Tools", - Summary = "Visually drag and drop thumbnails to reorder, duplicate, rotate, or delete pages in a PDF.", - Description = "Provides an interactive grid view of all pages in a document. Easily drag pages to resequence them, duplicate key slides, or delete accidental pages.", - Steps = new List - { - "Open 'Organize Pages' from the Tools Studio.", - "Select your PDF to load the page thumbnail grid.", - "Click and drag any page thumbnail to change its position.", - "Hover over any page to access quick Rotate, Duplicate, or Delete icons.", - "Click 'Save Organized PDF' to export the reordered document." - }, - KeyFeatures = new List - { - "High-resolution interactive visual thumbnail grid", - "Drag-and-drop page reordering", - "Per-page rotate, duplicate, and delete actions" - }, - ProTips = new List - { - "You can also manage and reorder pages directly inside the Editor using the left thumbnail sidebar." - }, - SupportedFormats = "Input: .pdf | Output: Organized .pdf", - RelatedToolId = PdfToolId.OrganizePdf, - IconKind = "ViewGridOutline", - IconColorHex = "#4F46E5", - BackgroundAccentHex = "#EEF2FF", - Badge = "Organize", - Keywords = "organize reorder pages sequence thumbnails duplicate delete rearrange", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-extract-pages", - Title = "Extract Pages", - Category = "32 PDF Tools", - Summary = "Extract specific pages or page selections into a brand-new standalone PDF file.", - Description = "Select specific pages or enter page ranges to pull out and generate a new independent PDF without modifying the original document.", - Steps = new List - { - "Open 'Extract Pages' from the Tools Studio.", - "Select your source PDF.", - "Click to select thumbnails or enter page numbers (e.g. '1, 3, 5-7').", - "Click 'Extract to New PDF'." - }, - KeyFeatures = new List - { - "Visual multi-select thumbnail picker", - "Custom range syntax support", - "Zero quality loss during extraction" - }, - ProTips = new List - { - "Great for isolating single chapters from textbooks or extracting specific invoices from monthly ledgers." - }, - SupportedFormats = "Input: .pdf | Output: Extracted .pdf", - RelatedToolId = PdfToolId.OrganizePdf, - IconKind = "Export", - IconColorHex = "#6366F1", - BackgroundAccentHex = "#EEF2FF", - Badge = "Organize", - Keywords = "extract pull pages subset select export isolate", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-remove-pages", - Title = "Remove Pages", - Category = "32 PDF Tools", - Summary = "Delete blank, duplicate, or unnecessary pages from your PDF document.", - Description = "Quickly prune unwanted pages from a document. Simply click the pages you want gone and export the streamlined PDF.", - Steps = new List - { - "Open 'Remove Pages' from the Tools Studio.", - "Select your PDF.", - "Click the trash icon on any pages you wish to delete.", - "Click 'Remove Pages & Save'." - }, - KeyFeatures = new List - { - "Click-to-remove visual interface", - "Shows instant preview of the remaining document structure", - "Automatic page numbering re-indexing" - }, - ProTips = new List - { - "Inside the canvas editor, you can also press ⌘⇧⌫ (Ctrl+Shift+Delete) to remove the active page." - }, - SupportedFormats = "Input: .pdf | Output: Pruned .pdf", - RelatedToolId = PdfToolId.OrganizePdf, - IconKind = "TrashCanOutline", - IconColorHex = "#DC2626", - BackgroundAccentHex = "#FEF2F2", - Badge = "Organize", - Keywords = "remove delete prune pages blank unwanted trash", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-compare-documents", - Title = "Compare Documents", - Category = "32 PDF Tools", - Summary = "Perform a side-by-side visual and structural difference comparison between two PDF revisions.", - Description = "The Compare Documents tool analyzes two versions of a PDF document, highlighting visual changes, modified text paragraphs, altered tables, and moved graphic elements with color-coded diff overlays.", - Steps = new List - { - "Open 'Compare Documents' from the Tools Studio or press ⌘K and search 'Compare'.", - "Select 'Document A' (Original Version) and 'Document B' (Revised Version).", - "Choose comparison mode: 'Side-by-Side Diff' or 'Overlay Blend'.", - "Click 'Compare Revisions'.", - "Inspect the highlighted difference markers: Green for additions, Red for deletions, Yellow for modifications." - }, - KeyFeatures = new List - { - "Side-by-Side synchronized scrolling comparison", - "Pixel-level Skia difference heatmaps", - "Structural text change reports" - }, - ProTips = new List - { - "Invaluable for legal contract revisions, architectural drawing revisions, and editing proof checks." - }, - SupportedFormats = "Inputs: Two .pdf files | Output: Diff Report & Visual Comparison", - RelatedToolId = PdfToolId.ComparePdf, - IconKind = "CompareHorizontal", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#E0F2FE", - Badge = "Security", - Keywords = "compare diff revision versions legal check visual difference side by side", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-redact-pdf", - Title = "Search & Redact Sensitive Data", - Category = "32 PDF Tools", - Summary = "Permanently black-out and sanitize PII, SSNs, credit card numbers, and secret names from PDFs.", - Description = "Redacts sensitive text and information with permanent, irreversible black-box sanitization. Redacted content is completely purged from underlying PDF content streams—not just covered with a black box.", - Steps = new List - { - "Open 'Search & Redact' from the Tools Studio.", - "Select your PDF.", - "Enter search keywords (e.g. 'Social Security', 'Confidential') or choose a regex pattern (SSN, Email, Phone, Credit Card).", - "Review matching occurrences across pages.", - "Click 'Apply Permanent Redaction' to purge all matched content." - }, - KeyFeatures = new List - { - "True stream sanitization: completely purges text from vector and text streams", - "Built-in PII pattern detectors: SSN, Credit Cards, Email Addresses, Phone Numbers", - "Visual rectangle redaction drawing tool" - }, - ProTips = new List - { - "Never use plain highlighter or drawing tools to hide secrets; always use True Redaction to purge the text from search indices." - }, - SupportedFormats = "Input: .pdf | Output: Redacted & Sanitized .pdf", - RelatedToolId = PdfToolId.RedactPdf, - IconKind = "EyeOffOutline", - IconColorHex = "#DC2626", - BackgroundAccentHex = "#FEF2F2", - Badge = "Security", - Keywords = "redact black out pii ssn privacy sanitize secret erase legal security", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-bates-numbering", - Title = "Bates Numbering", - Category = "32 PDF Tools", - Summary = "Apply sequential legal Bates numbering with customizable prefixes, suffixes, and digit padding.", - Description = "Bates numbering is the global legal standard for indexing discovery documents, trial exhibits, and compliance records. FryPDF applies Bates stamps with customizable prefixes, digit counts, and positions.", - Steps = new List - { - "Open 'Bates Numbering' from the Tools Studio.", - "Select one or multiple PDF files.", - "Configure Prefix (e.g. 'EXHIBIT-A-'), Starting Number (e.g. 1), and Digit Padding (e.g. 6 digits: '000001').", - "Choose stamp placement (Bottom Right, Top Right, etc.) and font style.", - "Click 'Apply Bates Numbering'." - }, - KeyFeatures = new List - { - "Customizable Prefix and Suffix strings", - "Adjustable digit padding (e.g. 4 to 8 digits)", - "Batch multi-document continuous sequence numbering" - }, - ProTips = new List - { - "When stamping multi-file court filings, load all files together to maintain an unbroken sequential Bates index across documents." - }, - SupportedFormats = "Inputs: Multiple .pdf files | Output: Bates-Stamped .pdf files", - RelatedToolId = PdfToolId.PageNumbers, - IconKind = "Numeric", - IconColorHex = "#7C3AED", - BackgroundAccentHex = "#F5F3FF", - Badge = "Security", - Keywords = "bates numbering legal court exhibit litigation discovery compliance index", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-preflight-diagnostics", - Title = "Preflight Diagnostics & Repair", - Category = "32 PDF Tools", - Summary = "Audit documents for corrupted streams, missing fonts, PDF/A compliance, and auto-repair issues.", - Description = "Runs a comprehensive preflight validation check against your PDF file, detecting broken xref tables, unembedded fonts, corrupt image streams, and syntax warnings, offering one-click auto-repair.", - Steps = new List - { - "Open 'Preflight Diagnostics' from the Tools Studio.", - "Select the PDF you want to audit.", - "Click 'Run Diagnostic Scan'.", - "Review the diagnostic report: Security status, PDF/A compliance, embedded fonts, and stream integrity.", - "Click 'Auto-Repair & Rebuild PDF' if any issues are detected." - }, - KeyFeatures = new List - { - "Comprehensive structural and syntax validation engine", - "Scans for missing glyphs, unembedded fonts, and broken xref tables", - "One-Click stream rebuilding and sanitization" - }, - ProTips = new List - { - "Run Preflight Diagnostics before sending files to professional print shops or archival repositories." - }, - SupportedFormats = "Input: .pdf | Output: Diagnostic Report & Repaired .pdf", - RelatedToolId = PdfToolId.RepairPdf, - IconKind = "ShieldCheckOutline", - IconColorHex = "#059669", - BackgroundAccentHex = "#ECFDF5", - Badge = "Security", - Keywords = "preflight diagnostics repair audit test validate fix pdfa corrupted broken", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-custom-stamp", - Title = "Custom Stamp Studio", - Category = "32 PDF Tools", - Summary = "Stamp APPROVED, CONFIDENTIAL, VOID, DRAFT, or custom vector corporate seals on PDFs.", - Description = "Apply professional dynamic office stamps with date/time stamps, reviewer names, and vector borders to approve invoices, verify contracts, or mark drafts.", - Steps = new List - { - "Open 'Custom Stamp Studio' from the Tools Studio or ribbon.", - "Pick a preset stamp ('APPROVED', 'CONFIDENTIAL', 'COMPLETED', 'RECEIVED', 'REJECTED') or create a custom text stamp.", - "Optionally include dynamic Date, Time, and User Name fields.", - "Select ink color (Emerald Green, Crimson Red, Deep Blue, Amber).", - "Click on the page where you want to place the stamp." - }, - KeyFeatures = new List - { - "Pre-designed vector border styles (Classic Boxed, Double Border, Ribbon)", - "Dynamic date and reviewer name stamps", - "Resizable and rotatable vector stamp elements" - }, - ProTips = new List - { - "Add dynamic date stamps to invoice approvals to establish a clear audit trail." - }, - SupportedFormats = "Input: .pdf | Output: Stamped .pdf", - RelatedToolId = PdfToolId.PdfForms, - IconKind = "Stamp", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - Badge = "Editor", - Keywords = "stamp approved confidential draft seal logo office mark dynamic date", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-header-footer", - Title = "Header & Footer Studio", - Category = "32 PDF Tools", - Summary = "Apply document-wide running headers, footers, metadata titles, and margin offsets.", - Description = "Add running headers and footers across entire documents with customized left, center, and right text segments, page counts, and document titles.", - Steps = new List - { - "Open 'Header & Footer Studio' from the Tools Studio.", - "Select your PDF.", - "Configure Header text (Left, Center, Right) and Footer text.", - "Use variables like {{PageNumber}}, {{TotalPages}}, {{DocumentTitle}}, and {{Date}}.", - "Click 'Apply Headers & Footers'." - }, - KeyFeatures = new List - { - "3-Column Header & Footer layout (Left, Center, Right)", - "Dynamic variable interpolation (Page {0} of {1}, Date, Filename)", - "Independent first page (cover page) suppression" - }, - ProTips = new List - { - "Enable 'Suppress on Page 1' to keep cover pages and report title pages clean." - }, - SupportedFormats = "Input: .pdf | Output: .pdf with Headers/Footers", - RelatedToolId = PdfToolId.PageNumbers, - IconKind = "DockTop", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#E0F2FE", - Badge = "Editor", - Keywords = "header footer running title date page count margins metadata", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-ocr-pdf", - Title = "OCR Text Recognition", - Category = "32 PDF Tools", - Summary = "Extract selectable, searchable, and copyable text from scanned image PDFs and photos.", - Description = "Converts flat bitmap scans and scanned paper documents into searchable PDFs with an invisible, selectable text layer aligned over the original scan.", - Steps = new List - { - "Open 'OCR PDF' from the AI & Automation tools category.", - "Select your scanned PDF or image.", - "Select document language (English, Spanish, French, German, Hindi, Chinese, etc.).", - "Click 'Perform OCR'.", - "The resulting PDF now features fully selectable, copyable, and searchable text." - }, - KeyFeatures = new List - { - "Multi-language recognition engine", - "Generates invisible text overlay matching scan geometry", - "Exports searchable PDF or pure plain text" - }, - ProTips = new List - { - "Running OCR on scanned contracts enables fast in-document keyword search (⌘F) in any PDF reader." - }, - SupportedFormats = "Input: Scanned .pdf, images | Output: Searchable .pdf, .txt", - RelatedToolId = PdfToolId.OcrPdf, - IconKind = "TextRecognition", - IconColorHex = "#7C3AED", - BackgroundAccentHex = "#F5F3FF", - Badge = "AI & Automation", - Keywords = "ocr text recognition scan scanned searchable copyable extract text image", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "tool-document-translation", - Title = "Document Translation", - Category = "32 PDF Tools", - Summary = "Translate document content into multiple languages with script-aware font matching.", - Description = "Translate entire PDF documents into different languages while preserving the original layout structure, headers, and visual tables, with automatic font fallback for non-Latin scripts.", - Steps = new List - { - "Open 'Translate Document' from AI & Automation.", - "Select your PDF file.", - "Choose source and target languages.", - "Click 'Translate & Rebuild Document'.", - "Inspect the translated document with matching localized typography." - }, - KeyFeatures = new List - { - "Preserves document layout, columns, tables, and image positions", - "Automatic Unicode script detection and font family fallback", - "Side-by-side bilingual export option" - }, - ProTips = new List - { - "Ensure you have the required language font pack installed via 'Language & Fonts' in the sidebar for optimal glyph rendering." - }, - SupportedFormats = "Input: .pdf | Output: Translated .pdf", - RelatedToolId = PdfToolId.TranslatePdf, - IconKind = "Translate", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#E0F2FE", - Badge = "AI & Automation", - Keywords = "translate translation language multilingual international scripts localized", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "tool-ai-summarizer", - Title = "AI Summarizer & Analysis", - Category = "32 PDF Tools", - Summary = "Generate structured summaries, executive briefs, key takeaways, and section analyses from PDFs.", - Description = "Extracts and synthesizes document contents into high-level executive summaries, bulleted key takeaways, and section-by-section breakdowns.", - Steps = new List - { - "Open 'AI Summarizer' from AI & Automation.", - "Select your document.", - "Choose summary type: Executive Brief, Bulleted Key Takeaways, or Detailed Section Breakdown.", - "Click 'Generate Summary'.", - "Copy summary text or export it as a clean PDF summary report." - }, - KeyFeatures = new List - { - "Multiple summary modes: Executive, Key Points, Q&A Brief", - "Analyzes multi-page research papers and lengthy legal contracts", - "Exports formatted markdown or PDF reports" - }, - ProTips = new List - { - "Use Key Takeaways mode on 50+ page annual reports to extract financial highlights in under 10 seconds." - }, - SupportedFormats = "Input: .pdf | Output: .txt, .md, .pdf Summary Report", - RelatedToolId = PdfToolId.AiSummarizer, - IconKind = "AutoFix", - IconColorHex = "#EC4899", - BackgroundAccentHex = "#FDF2F8", - Badge = "AI & Automation", - Keywords = "ai summarize summarizer analysis overview key points takeaways brief report", - IsFeatured = true - }, - - // ========================================================================= - // 3. LIVE CANVAS DOCUMENT EDITOR & INSPECTOR STUDIO - // ========================================================================= - new HelpGuideItem - { - Id = "editor-canvas-basics", - Title = "Canvas Navigation, Grid & Rulers", - Category = "Live Editor", - Summary = "Master the interactive canvas: moving elements, snapping to grid, guideline alignment, and rulers.", - Description = "The FryPDF Document Canvas is an interactive vector design workspace. Elements can be clicked, dragged, resized via control handles, and snapped to precise alignments.", - Steps = new List - { - "Click on any canvas element to select it; selection handles will appear around its bounding box.", - "Drag the element to reposition it; smart alignment guides appear automatically to help align with nearby elements.", - "Drag corner or edge handles to resize elements proportionally (hold Shift to maintain aspect ratio).", - "Use canvas rulers along the top and left to measure exact point coordinates.", - "Zoom in and out using ⌘+ / ⌘- or hold Ctrl while scrolling the mouse wheel." - }, - KeyFeatures = new List - { - "Live Canvas Rulers with cursor tracking in physical PDF points", - "Snap-to-Grid and dynamic object alignment guides", - "Multi-selection: Drag a selection marquee or Shift-click elements", - "Zoom range from 40% up to 250% with instant 100% reset (⌘0)" - }, - ProTips = new List - { - "Hold Spacebar and drag the mouse to pan smoothly across the canvas at high zoom levels.", - "Press ⌘1 to fit the page width or ⌘9 to fit the entire page on screen." - }, - SupportedFormats = "Live Canvas Workspace", - KeyboardShortcut = "⌘1 (Fit Width), ⌘9 (Fit Page), ⌘0 (Reset 100%)", - IconKind = "RulerSquare", - IconColorHex = "#0F6CBD", - BackgroundAccentHex = "#EFF6FF", - Badge = "Editor", - Keywords = "canvas rulers grid snap alignment pan zoom selection move resize handles", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "editor-typography", - Title = "Typography, Fonts & Text Blocks", - Category = "Live Editor", - Summary = "Format rich text blocks: font families, weights, font sizes, line heights, and letter spacing.", - Description = "Text blocks in FryPDF provide fine-grained typographic controls. You can adjust font families (Inter, Roboto, Noto Sans, Playfair, Courier, etc.), font sizes, line heights, letter spacing, and alignments via the Properties Inspector.", - Steps = new List - { - "Click 'Text' in the ribbon toolbar to insert a new text element, or double-click any existing text.", - "Open the Properties Inspector on the right sidebar (⌘⇧P).", - "Choose font family, font weight (Regular, Medium, SemiBold, Bold), and font size.", - "Adjust 'Line Height' multiplier (1.25x to 1.5x) for optimal paragraph readability.", - "Pick text color, text alignment (Left, Center, Right, Justify), and background padding." - }, - KeyFeatures = new List - { - "Rich Font Library with embedded Unicode fallbacks", - "Fine-grained Typography Controls: Size, Weight, Line Height, Letter Spacing", - "Multi-line wrapping with auto-height bounding box calculations", - "Rich hex color picker with preset palette swatches" - }, - ProTips = new List - { - "Set Line Height between 1.3x and 1.45x for comfortable reading in multi-paragraph body text.", - "When typing in non-Latin scripts (Hindi, Japanese, Arabic), FryPDF automatically selects the optimal Noto Sans script family." - }, - SupportedFormats = "Typography & Text Elements", - KeyboardShortcut = "⌘⇧P / Ctrl+Shift+P (Properties Inspector)", - IconKind = "FormatSize", - IconColorHex = "#6366F1", - BackgroundAccentHex = "#EEF2FF", - Badge = "Editor", - Keywords = "text typography font family size line height spacing bold italic alignment color", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "editor-rich-text", - Title = "Rich Text & Inline Markdown Formatting", - Category = "Live Editor", - Summary = "Style words within the same text box with mixed bold, italics, underlines, strikethroughs, sub/superscripts, colors, and links.", - Description = "FryPDF supports high-fidelity rich text with multi-span inline styling. You can format distinct words and phrases within a single text box using quick Markdown shorthand or HTML-style tags during in-place editing, or via the Properties Inspector.", - Steps = new List - { - "Double-click any text box on the canvas to activate in-place editing mode.", - "Type standard Markdown shorthand: use **bold** for bold text, *italic* for italic text, underline for underlines, and ~~strikethrough~~ for revisions.", - "Use scientific scripts: type ~2~ for subscript (e.g. H~2~O) and ^2^ for superscript (e.g. E=mc^2^, 1^st^).", - "Apply custom inline colors with text (e.g. Blue) and highlights with text.", - "Add clickable hyperlinks with [Label Text](https://example.com).", - "Press Enter or click outside the text box: FryPDF instantly compiles the markup into high-performance multi-span runs rendered on the canvas and exported to vector PDF." - }, - KeyFeatures = new List - { - "Multi-Span Architecture: Single unified text element with granular typography for every word", - "In-Place Markdown & HTML Tag Parsing on double-tap and blur", - "Scientific Subscript (~x~) and Superscript (^x^) Notation Support", - "Inline Hex Color Tags () and Highlights ()", - "Clickable PDF Hyperlinks ([Title](url)) compiled directly to QuestPDF links", - "Template DataMerge Support: {{Variable}} tags evaluate seamlessly inside rich spans", - "100% Vector PDF Export with zero rasterization" - }, - ProTips = new List - { - "Combine formatting freely! For example, **Bold Blue** creates styled highlighted headings in one step.", - "Press ⌘Z (Ctrl+Z) after editing to undo any formatting changes atomically.", - "Check out the 'Rich Typography & Publishing Specimen' in the Template Studio for live examples." - }, - SupportedFormats = "Live Canvas, Markdown Shorthand, QuestPDF Vector Export, SVG ", - KeyboardShortcut = "Double-Click text (In-Place Edit)", - IconKind = "FormatColorText", - IconColorHex = "#0F6CBD", - BackgroundAccentHex = "#EFF6FF", - Badge = "New Feature", - Keywords = "richtext rich text markdown bold italic underline strikethrough subscript superscript color highlight link spans formatting tags", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "editor-shapes-ink", - Title = "Vector Shapes, Cards & Freehand Ink", - Category = "Live Editor", - Summary = "Add vector rectangles, rounded container cards, divider rules, and freehand stylus ink drawings.", - Description = "Enhance your document layouts with crisp vector shapes. Insert rectangles, badges, callout cards, divider lines, or draw freehand ink annotations directly on the canvas.", - Steps = new List - { - "Click 'Shapes' in the ribbon to insert a Rectangle, Ellipse, or Divider Line.", - "Use the Properties Inspector to adjust Fill Color, Stroke Color, Border Thickness, and Corner Radius.", - "To draw freehand annotations, select the 'Ink Pen' tool from the ribbon and draw directly on the canvas.", - "Adjust stroke width and pen color in the inspector." - }, - KeyFeatures = new List - { - "Vector Rectangles, Rounded Cards, Ellipses, and Divider Lines", - "Customizable Corner Radius, Border Thickness, and Box Shadows", - "Freehand Stylus & Mouse Ink Pen with Bézier smoothing", - "Exported as razor-sharp vector SVG/QuestPDF paths" - }, - ProTips = new List - { - "Use rounded cards with a subtle border and 8px corner radius for modern corporate document cards.", - "Set element opacity in the inspector to create translucent background badges." - }, - SupportedFormats = "Vector Elements", - IconKind = "ShapeOutline", - IconColorHex = "#F59E0B", - BackgroundAccentHex = "#FFFBEB", - Badge = "Editor", - Keywords = "shapes rectangle card rounded ellipse divider line ink pen draw freehand vector", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "editor-tables", - Title = "Tables & Data Grids", - Category = "Live Editor", - Summary = "Create and format professional tables with custom column widths, header styles, and alternating shading.", - Description = "Insert and customize structured tables for financial reports, pricing sheets, and technical data. Easily edit cell text, adjust column widths, configure borders, and apply alternating row colors.", - Steps = new List - { - "Click 'Table' in the ribbon toolbar to insert a new data grid on the canvas.", - "Specify initial rows and columns in the dialog.", - "Double-click any cell to enter text, numbers, or headers.", - "Use the Inspector to customize Header Row Background, Alternating Row Shading, and Border Colors.", - "Drag column dividers to adjust column proportions." - }, - KeyFeatures = new List - { - "Customizable Row and Column counts", - "Header row formatting with distinct typography and background colors", - "Alternating row shading (Zebra striping) for readability", - "Adjustable cell padding and border styles" - }, - ProTips = new List - { - "Right-align numeric and currency cells to ensure decimal points align cleanly for readers." - }, - SupportedFormats = "Table Elements", - IconKind = "Table", - IconColorHex = "#10B981", - BackgroundAccentHex = "#ECFDF5", - Badge = "Editor", - Keywords = "table grid rows columns cells data financial report format borders", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "editor-math-latex", - Title = "LaTeX Math Equation Studio", - Category = "Live Editor", - Summary = "Typeset publication-grade LaTeX math formulas, integrals, fractions, and matrices into vector paths.", - Description = "The built-in Math Equation Studio allows academic researchers, students, and engineers to write LaTeX equations (e.g. fractions, integrals, Greek letters, summations, matrices) and render them directly into crisp vector elements on the canvas.", - Steps = new List - { - "Click 'Math Equation' in the ribbon or press ⌘K and type 'Math Equation'.", - "Type your LaTeX expression in the formula editor (e.g. '\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}').", - "Inspect the live real-time formula preview.", - "Choose font size and text color.", - "Click 'Insert Equation' to place the formula on the canvas." - }, - KeyFeatures = new List - { - "Full LaTeX Syntax Support: Fractions, Superscripts/Subscripts, Integrals, Summations, Greek Symbols, Matrices", - "Live Real-Time Vector Formula Preview", - "Rendered as infinite-resolution vector paths in exported PDFs" - }, - ProTips = new List - { - "Check the 'Math & Academic Research' templates for pre-configured theorem blocks and math formulas." - }, - SupportedFormats = "LaTeX Math Elements", - IconKind = "Sigma", - IconColorHex = "#7C3AED", - BackgroundAccentHex = "#F5F3FF", - Badge = "Editor", - Keywords = "math latex equation formula integral fraction matrix greek physics academic science", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "editor-qr-barcodes", - Title = "QR Codes & 2D Barcodes", - Category = "Live Editor", - Summary = "Generate instant QR codes for URLs, contact vCards, Wi-Fi keys, and embed them in layouts.", - Description = "Generate and embed scannable QR codes directly on your PDF pages. Ideal for business cards, resumes, event flyers, invoices, and product packaging.", - Steps = new List - { - "Click 'QR Code' in the ribbon toolbar.", - "Enter your target URL, contact vCard, Wi-Fi credentials, or text.", - "Configure QR code size, foreground color, and background color.", - "Click 'Insert QR Code' to position it on your page.", - "Test scan the preview on your mobile phone camera." - }, - KeyFeatures = new List - { - "Supports URLs, Plain Text, vCard Contacts, and Payment Links", - "Customizable QR pixel colors and background transparencies", - "High-resolution vector generation ensuring instant scan reliability" - }, - ProTips = new List - { - "Maintain high contrast (e.g. dark QR pixels on a light background) to ensure fast scanning in all lighting conditions." - }, - SupportedFormats = "QR Code Elements", - IconKind = "Qrcode", - IconColorHex = "#0F172A", - BackgroundAccentHex = "#F1F5F9", - Badge = "Editor", - Keywords = "qr code barcode scan link url vcard contact wifi embed generate", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "editor-zindex-layers", - Title = "Layer Hierarchy & Z-Index Management", - Category = "Live Editor", - Summary = "Control visual stacking order: bring elements forward, send backward, and manage container layers.", - Description = "Learn how visual layer stacking works in FryPDF. Layer ordering ensures background cards stay behind content, while headers, labels, and interactive form fields remain on top.", - Steps = new List - { - "Select an element on the canvas.", - "Use the Layer controls in the ribbon or Inspector: 'Bring Forward', 'Send Backward', 'Bring to Front', or 'Send to Back'.", - "Background shapes and cards automatically occupy Z-indices 0–99.", - "Images and photos occupy Z-indices 100–499.", - "Text blocks and headings occupy Z-indices 1000–1999." - }, - KeyFeatures = new List - { - "Predictable Layered Z-Index Architecture", - "Prevents background cards from occluding photos and text", - "One-click Bring to Front and Send to Back controls" - }, - ProTips = new List - { - "Use 'Send to Back' whenever adding a background card behind existing text to immediately nest it behind all content." - }, - SupportedFormats = "Layer Management", - IconKind = "LayersOutline", - IconColorHex = "#4F46E5", - BackgroundAccentHex = "#EEF2FF", - Badge = "Editor", - Keywords = "layers zindex stacking order bring forward send back front arrange hierarchy", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "editor-undo-redo", - Title = "Undo, Redo & Project History", - Category = "Live Editor", - Summary = "Never lose work with unlimited atomic undo/redo history and accidental change rollback.", - Description = "Every modification in FryPDF—element moves, text edits, color adjustments, deletions, and page additions—is recorded in an atomic undo/redo history stack.", - Steps = new List - { - "Press ⌘Z (Ctrl+Z) to undo your last action.", - "Press ⌘⇧Z or ⌘Y (Ctrl+Y) to redo an undone action.", - "Use the Quick Undo/Redo arrow buttons in the top toolbar.", - "History tracks moves, property edits, deletions, and layout changes seamlessly." - }, - KeyFeatures = new List - { - "Deep Atomic Undo/Redo stack", - "Full cross-platform shortcuts (macOS Cmd vs Win/Linux Ctrl)", - "Captures canvas movements, resizing, deletions, and text modifications" - }, - ProTips = new List - { - "You can undo multiple actions in rapid succession without losing document integrity." - }, - SupportedFormats = "Editor History", - KeyboardShortcut = "⌘Z (Undo), ⌘⇧Z / ⌘Y (Redo)", - IconKind = "Undo", - IconColorHex = "#F59E0B", - BackgroundAccentHex = "#FFFBEB", - Badge = "Editor", - Keywords = "undo redo history revert rollback restore accidental mistake change", - IsFeatured = false - }, - new HelpGuideItem - { - Id = "editor-export-persistence", - Title = "Exporting Vector PDFs & Native Projects", - Category = "Live Editor", - Summary = "Save native .frypdf workspaces and compile high-resolution vector QuestPDF documents.", - Description = "FryPDF offers dual-mode persistence: save your project as a lightweight `.frypdf` workspace file for future editing, or export a publication-ready `.pdf` with embedded project metadata.", - Steps = new List - { - "Press ⌘S (Save Project) to save your work as a native `.frypdf` file.", - "Press ⌘E (Export to PDF) to compile the document into a high-fidelity vector PDF.", - "In the Export dialog, choose your destination folder and filename.", - "Exported PDFs embed the editable project state inside metadata, allowing them to be reopened and edited in FryPDF anytime!" - }, - KeyFeatures = new List - { - "QuestPDF High-Fidelity Vector Compilation Engine", - "Embedded Project State: Exported PDFs can be reopened as live editable files", - "Compact Native .frypdf JSON File Format" - }, - ProTips = new List - { - "Because FryPDF embeds project metadata in exported PDFs, you can send a PDF to a colleague and they can open and edit it directly in FryPDF!" - }, - SupportedFormats = "Save: .frypdf | Export: .pdf", - KeyboardShortcut = "⌘S (Save Project), ⌘E (Export PDF)", - IconKind = "ContentSaveOutline", - IconColorHex = "#059669", - BackgroundAccentHex = "#ECFDF5", - Badge = "Editor", - Keywords = "save export pdf compile frypdf project metadata persistence embed", - IsFeatured = true - }, - - // ========================================================================= - // 4. AUTOMATION, WORKFLOWS & BATCH MASS GENERATION - // ========================================================================= - new HelpGuideItem - { - Id = "automation-workflow-builder", - Title = "Workflow Builder: Chained PDF Pipelines", - Category = "Automation", - Summary = "Build and execute multi-step automated PDF processing pipelines with one click.", - Description = "Workflow Builder allows you to automate repetitive document tasks by chaining multiple PDF tools into an automated sequence (e.g. Merge -> Compress -> Watermark -> Encrypt).", - Steps = new List - { - "Open 'Workflow Builder' from AI & Automation or click the Workflow banner on the Home Dashboard.", - "Choose a workflow preset or click 'Create Custom Workflow'.", - "Add pipeline steps from the tool catalog (e.g. Step 1: Merge, Step 2: Compress, Step 3: Watermark, Step 4: Protect).", - "Configure step parameters (e.g. Watermark text, Encryption password).", - "Select input files and click 'Run Automated Workflow'." - }, - KeyFeatures = new List - { - "Chain any combination of the 32 PDF tools", - "Visual pipeline flowchart builder", - "Batch multi-file pipeline execution with live progress logging" - }, - ProTips = new List - { - "Save your frequently used pipelines as custom presets to run routine end-of-month reporting workflows in seconds." - }, - SupportedFormats = "Pipeline Automation Engine", - RelatedToolId = PdfToolId.WorkflowBuilder, - IconKind = "VectorPolyline", - IconColorHex = "#7C3AED", - BackgroundAccentHex = "#F5F3FF", - Badge = "Automation", - Keywords = "workflow pipeline automated chain sequence batch batching process auto", - IsFeatured = true - }, - new HelpGuideItem - { - Id = "automation-data-studio", - Title = "Data Studio & Batch Mass PDF Generation", - Category = "Automation", - Summary = "Import CSV or JSON data to mail-merge placeholders and generate hundreds of personalized PDFs.", - Description = "Data Studio connects datasets (CSV, JSON, Excel) directly to document template placeholders (e.g. {{CustomerName}}, {{InvoiceNumber}}, {{DueDate}}, {{Amount}}), compiling hundreds of personalized PDFs in seconds.", - Steps = new List - { - "Design your base document in the Canvas Editor using placeholder variables like {{Name}}, {{Amount}}, {{Date}}.", - "Click 'Batch Generation / Data Studio' in the ribbon or Home Dashboard.", - "Import your CSV or JSON data source file.", - "Map dataset columns to the document placeholders.", - "Preview sample merged records in real-time.", - "Click 'Generate Batch PDFs' to produce individual PDFs or a single combined batch file." - }, - KeyFeatures = new List - { - "Supports CSV, JSON, and Excel tabular datasets", - "Live Data Binding Preview: Cycle through records to verify layout before generating", - "Mass High-Speed Parallel Generation Engine (100+ PDFs in seconds)", - "Custom dynamic output naming (e.g. 'Invoice_{{InvoiceNumber}}_{{CustomerName}}.pdf')" - }, - ProTips = new List - { - "Use Data Studio to generate monthly payroll slips, customized certificates of completion, conference badges, and bulk invoices." - }, - SupportedFormats = "Inputs: .csv, .json, .xlsx | Output: Batch .pdf files", - IconKind = "DatabaseArrowRightOutline", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - Badge = "Automation", - Keywords = "batch mass generation data studio csv json mail merge variables certificates invoices", - IsFeatured = true - }, - - // ========================================================================= - // 5. FONTS & INTERNATIONAL SCRIPT SUPPORT - // ========================================================================= - new HelpGuideItem - { - Id = "fonts-language-packs", - Title = "Font Manager & Unicode Script Support", - Category = "Language & Fonts", - Summary = "Manage on-demand Google Noto Sans font packages for Devanagari, CJK, Arabic, and Indic scripts.", - Description = "FryPDF includes comprehensive multi-script Unicode intelligence. It eliminates missing font boxes (tofu '□□□') by automatically detecting text scripts and falling back to embedded Google Noto Sans font families.", - Steps = new List - { - "Select 'Language & Fonts' from the left sidebar.", - "Browse available language packages: Devanagari (Hindi, Marathi, Sanskrit), CJK (Chinese, Japanese, Korean), Arabic, Tamil, Telugu, Thai, Cyrillic, Greek, Hebrew.", - "View installed font status and package details.", - "When deconstructing imported PDFs with Indic or CJK text, FryPDF automatically activates the appropriate script rendering rules." - }, - KeyFeatures = new List - { - "Automatic Unicode Script Range Detection", - "Eliminates tofu boxes (□□□) across 100+ languages", - "Script-Aware token joining for Indic and Asian scripts without artificial spaces", - "100% Embedded and self-contained: zero external web font requests" - }, - ProTips = new List - { - "If an imported PDF displays missing character glyphs, verify the corresponding language package is active in Language & Fonts." - }, - SupportedFormats = "TrueType / OpenType Fonts (.ttf, .otf)", - IconKind = "Translate", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#E0F2FE", - Badge = "Typography", - Keywords = "fonts languages unicode noto sans devanagari hindi cjk chinese japanese arabic tofu glyphs", - IsFeatured = false - }, - - // ========================================================================= - // 6. KEYBOARD SHORTCUTS & PRODUCTIVITY - // ========================================================================= - new HelpGuideItem - { - Id = "shortcuts-reference", - Title = "Keyboard Shortcuts & Mouse Gestures", - Category = "Shortcuts", - Summary = "Comprehensive cheat sheet for all global hotkeys, editor shortcuts, and navigation gestures.", - Description = "Boost your editing productivity with full keyboard shortcuts designed for both macOS (Cmd key) and Windows/Linux (Ctrl key).", - Steps = new List - { - "Press F1 or Shift+? anytime to open the interactive Keyboard Shortcuts dialog.", - "Use ⌘K to open the Quick Command Palette to run any command without touching the mouse.", - "Use ⌘S to save your project and ⌘E to export to PDF.", - "Use ⌘B to toggle the left sidebar, ⌘F1 to toggle the ribbon, and ⌘⇧P to toggle the Inspector.", - "Use ⌘1, ⌘9, and ⌘0 for quick zoom fitting and reset." - }, - KeyFeatures = new List - { - "File Operations: ⌘N (New), ⌘O (Open), ⌘S (Save), ⌘E (Export)", - "Edit Operations: ⌘Z (Undo), ⌘⇧Z / ⌘Y (Redo), ⌘C (Copy), ⌘V (Paste), ⌘X (Cut), ⌘D (Duplicate)", - "View Operations: ⌘+ (Zoom In), ⌘- (Zoom Out), ⌘0 (100%), ⌘1 (Fit Width), ⌘9 (Fit Page)", - "Page Management: PageDown / PageUp, ⌘⇧N (Add Page), ⌘⇧D (Duplicate Page), ⌘⇧R (Rotate Page), ⌘⇧⌫ (Delete Page)", - "Workspace Panels: ⌘B (Sidebar), ⌘F1 (Ribbon), ⌘⇧P (Inspector), ⌘K (Command Palette)" - }, - ProTips = new List - { - "Hold Shift while dragging an element's corner handle to constrain its aspect ratio.", - "Hold Alt / Option while dragging an element to clone and duplicate it instantly." - }, - SupportedFormats = "Hotkeys & Gestures", - KeyboardShortcut = "F1 / Shift+? (Shortcuts Dialog)", - IconKind = "KeyboardOutline", - IconColorHex = "#64748B", - BackgroundAccentHex = "#F1F5F9", - Badge = "Productivity", - Keywords = "shortcuts hotkeys keys keyboard mouse gestures cheat sheet f1 cmd ctrl", - IsFeatured = true - }, - - // ========================================================================= - // 7. FREQUENTLY ASKED QUESTIONS & TROUBLESHOOTING - // ========================================================================= - new HelpGuideItem - { - Id = "faq-troubleshooting", - Title = "Frequently Asked Questions & Troubleshooting", - Category = "FAQ & Support", - Summary = "Find answers to common questions, performance tips, and troubleshooting resolutions.", - Description = "Get quick answers regarding privacy, file compatibility, high-resolution rendering, and troubleshooting common PDF challenges.", - Steps = new List - { - "Q: Is my document uploaded to the internet or any cloud server? -> A: Absolutely not. FryPDF is 100% offline and processes everything strictly locally on your computer.", - "Q: Why do some non-Latin characters appear as squares (□□□)? -> A: Open 'Language & Fonts' from the sidebar and confirm the appropriate Google Noto Sans font pack is available.", - "Q: Can I edit a PDF created in another application? -> A: Yes! Use 'Edit PDF' or drag the PDF into the editor. The deconstruction engine parses text, shapes, and images into editable layers.", - "Q: How do I reduce the file size of a PDF for email? -> A: Use the 'Compress PDF' tool with 'Recommended' compression preset to achieve maximum file size reduction with zero visible quality loss.", - "Q: How do I report a bug or suggest a feature? -> A: Visit the CodeFryDev developer portal at https://codefrydev.in or check the 'Licenses & Tools' page." - }, - KeyFeatures = new List - { - "100% Local Privacy Guarantee", - "Comprehensive Troubleshooting Guide", - "Performance tuning for 100+ page documents" - }, - ProTips = new List - { - "If an imported PDF has flattened raster text (such as a physical scan), run 'OCR Text Recognition' before editing." - }, - SupportedFormats = "Knowledge Base & FAQ", - IconKind = "HelpBoxOutline", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#E0F2FE", - Badge = "FAQ", - Keywords = "faq help questions troubleshooting issue problem privacy tofu error crash support", - IsFeatured = true - } - }; - - _categories = new List - { - "All Guides", - "Getting Started", - "32 PDF Tools", - "Live Editor", - "Automation", - "Language & Fonts", - "Shortcuts", - "FAQ & Support" - }; - } - - public IReadOnlyList GetAllGuides() => _guides; - - public IReadOnlyList GetGuidesByCategory(string category) - { - if (string.IsNullOrWhiteSpace(category) || category.Equals("All Guides", StringComparison.OrdinalIgnoreCase) || category.Equals("All", StringComparison.OrdinalIgnoreCase)) - return _guides; - - return _guides.Where(g => g.Category.Equals(category, StringComparison.OrdinalIgnoreCase)).ToList(); - } - - public HelpGuideItem? GetGuideById(string id) - { - if (string.IsNullOrWhiteSpace(id)) return null; - return _guides.FirstOrDefault(g => g.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); - } - - public HelpGuideItem? GetGuideByToolId(PdfToolId toolId) - { - return _guides.FirstOrDefault(g => g.RelatedToolId == toolId); - } - - public IReadOnlyList GetAllCategories() => _categories; -} diff --git a/src/PdfEditorApp/Services/IHelpGuideService.cs b/src/PdfEditorApp/Services/IHelpGuideService.cs deleted file mode 100644 index 57c7a0b..0000000 --- a/src/PdfEditorApp/Services/IHelpGuideService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -/// -/// Service interface for accessing and searching the built-in FryPDF help guides, tool tutorials, and documentation knowledge base. -/// -public interface IHelpGuideService -{ - /// - /// Returns all available help guide topics. - /// - IReadOnlyList GetAllGuides(); - - /// - /// Returns help guides matching a specific category. - /// - IReadOnlyList GetGuidesByCategory(string category); - - /// - /// Looks up a guide item by unique ID. - /// - HelpGuideItem? GetGuideById(string id); - - /// - /// Looks up a guide item corresponding to a specific PDF tool. - /// - HelpGuideItem? GetGuideByToolId(PdfToolId toolId); - - /// - /// Returns all distinct categories in the knowledge base. - /// - IReadOnlyList GetAllCategories(); -} diff --git a/src/PdfEditorApp/Services/IPdfDocumentOperationsService.cs b/src/PdfEditorApp/Services/IPdfDocumentOperationsService.cs deleted file mode 100644 index 4908f07..0000000 --- a/src/PdfEditorApp/Services/IPdfDocumentOperationsService.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; - -namespace PdfEditorApp.Services; - -public interface IPdfDocumentOperationsService -{ - IPdfToolRegistry ToolRegistry { get; } - IPdfPageService PageService { get; } - IPdfOptimizationService OptimizationService { get; } - IPdfSecurityService SecurityService { get; } - IPdfConversionService ConversionService { get; } - IPdfOcrService OcrService { get; } - IPdfFormService FormService { get; } - IAiDocumentService AiService { get; } - IDocumentTranslationService TranslationService { get; } - IPdfWorkflowEngine WorkflowEngine { get; } - - Task ExecuteToolAsync(PdfToolId toolId, object options, IProgress? progress = null, CancellationToken ct = default); -} diff --git a/src/PdfEditorApp/Services/IPdfExportService.cs b/src/PdfEditorApp/Services/IPdfExportService.cs deleted file mode 100644 index be4d1d1..0000000 --- a/src/PdfEditorApp/Services/IPdfExportService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public interface IPdfExportService -{ - byte[] GeneratePdfBytes(PdfDocumentModel model); - Task ExportToBytesAsync(PdfDocumentModel model, IProgress? progress = null, CancellationToken ct = default); - Task ExportToFileAsync(PdfDocumentModel model, string filePath, IProgress? progress = null, CancellationToken ct = default); -} diff --git a/src/PdfEditorApp/Services/IRecentDocumentsService.cs b/src/PdfEditorApp/Services/IRecentDocumentsService.cs deleted file mode 100644 index ba563f1..0000000 --- a/src/PdfEditorApp/Services/IRecentDocumentsService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public interface IRecentDocumentsService -{ - /// Returns the list of recent documents, newest first. - List Load(); - - /// Adds or bumps an item to the top of the recent list and persists it. - void Add(RecentDocumentItem item); - - /// Removes a specific file path from the recent list. - void Remove(string filePath); - - /// Updates an existing recent document's path and title when renamed. - void Rename(string oldFilePath, string newFilePath, string newTitle); - - /// Clears all recent documents. - void Clear(); -} diff --git a/src/PdfEditorApp/Services/ISmartPlacementService.cs b/src/PdfEditorApp/Services/ISmartPlacementService.cs deleted file mode 100644 index 71a2cec..0000000 --- a/src/PdfEditorApp/Services/ISmartPlacementService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Services; - -public interface ISmartPlacementService -{ - /// - /// Calculates the optimal (X, Y) placement coordinates for a newly created or pasted element. - /// - (double X, double Y) GetPlacementPosition(PageViewModel page, double elementWidth, double elementHeight, bool isContextMenuTriggered = false); - - /// - /// Updates the currently visible viewport center and dimensions in unscaled page coordinates. - /// - void UpdateViewport(double pageCenterX, double pageCenterY, double visibleWidth, double visibleHeight); - - /// - /// Registers the exact page coordinates where a right-click context menu was opened. - /// - void SetContextMenuPointer(double pageX, double pageY); - - /// - /// Clears any pending context menu pointer coordinates. - /// - void ClearContextMenuPointer(); - - /// - /// Resets the sequential cascading offset counter. - /// - void ResetCascade(); -} diff --git a/src/PdfEditorApp/Services/ITemplateService.cs b/src/PdfEditorApp/Services/ITemplateService.cs deleted file mode 100644 index 0144a14..0000000 --- a/src/PdfEditorApp/Services/ITemplateService.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Templates; - -namespace PdfEditorApp.Services; - -public interface ITemplateService -{ - IReadOnlyList GetAllTemplates(); - PdfDocumentModel CreateTemplate(string templateId); - - PdfDocumentModel CreateAnnualReportTemplate(); - PdfDocumentModel CreateInteractiveExecutiveBriefTemplate(); - PdfDocumentModel CreateEmployeePayslipTemplate(); - PdfDocumentModel CreateInvoiceTemplate(); - PdfDocumentModel CreateResumeTemplate(); - PdfDocumentModel CreateResumeModernCleanTemplate(); - PdfDocumentModel CreateResumeCreativeMinimalistTemplate(); - PdfDocumentModel CreateResumeAcademicCvTemplate(); - PdfDocumentModel CreateAcademicPaperTemplate(); - PdfDocumentModel CreateMathResearchPaperTemplate(); - PdfDocumentModel CreatePhysicsResearchPaperTemplate(); - PdfDocumentModel CreateHistoryResearchPaperTemplate(); - PdfDocumentModel CreateFinanceResearchPaperTemplate(); - PdfDocumentModel CreateCertificateTemplate(); - PdfDocumentModel CreateCertificateNavyGoldTemplate(); - PdfDocumentModel CreateDiplomaAcademicTemplate(); - PdfDocumentModel CreateWeddingInvitationTraditionalTemplate(); - PdfDocumentModel CreateWeddingInvitationRoyalFloralTemplate(); - PdfDocumentModel CreateGalaInvitationTemplate(); - PdfDocumentModel CreateTypographyShowcaseTemplate(); - PdfDocumentModel CreateRichTextShowcaseTemplate(); - PdfDocumentModel CreateOcrAnalysisReportTemplate(); - PdfDocumentModel CreateMathBODMASWorksheetTemplate(); - PdfDocumentModel CreateFactorizationWorksheetTemplate(); - PdfDocumentModel CreateBilingualExamPaperTemplate(); - PdfDocumentModel CreateStatesOfMatterDiagramNotesTemplate(); - PdfDocumentModel CreateQuadrilateralsGuideTemplate(); - PdfDocumentModel CreateArduinoCheatSheetTemplate(); - PdfDocumentModel CreateBlankDocument(PageFormat format = PageFormat.A4, PageOrientation orientation = PageOrientation.Portrait); -} diff --git a/src/PdfEditorApp/Services/IUiSettingsService.cs b/src/PdfEditorApp/Services/IUiSettingsService.cs deleted file mode 100644 index f604968..0000000 --- a/src/PdfEditorApp/Services/IUiSettingsService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -/// -/// Service providing centralized retrieval, mutation, and persistence of UI user preferences. -/// -public interface IUiSettingsService -{ - /// - /// Current snapshot of UI preferences. - /// - UiSettingsModel Settings { get; } - - /// - /// Event raised whenever any UI preference is modified and saved. - /// - event Action? SettingsChanged; - - /// - /// Save current settings state to disk. - /// - void Save(); - - /// - /// Mutate settings atomically and persist changes. - /// - void UpdateSettings(Action updateAction); - - /// - /// Reset all settings to application defaults and persist. - /// - void ResetToDefaults(); -} diff --git a/src/PdfEditorApp/Services/Import/DocumentImporterRegistry.cs b/src/PdfEditorApp/Services/Import/DocumentImporterRegistry.cs deleted file mode 100644 index 4025d68..0000000 --- a/src/PdfEditorApp/Services/Import/DocumentImporterRegistry.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Deconstruction; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Import; - -public class DocumentImporterRegistry : IDocumentImporterRegistry -{ - private readonly ConcurrentDictionary _importers = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public DocumentImporterRegistry() - { - RegisterBuiltInImporters(); - } - - private void RegisterBuiltInImporters() - { - RegisterImporter(new PdfDeconstructionImporter()); - RegisterImporter(new ImageDocumentImporter()); - RegisterImporter(new PlainTextDocumentImporter()); - } - - public IDisposable RegisterImporter(IDocumentImporter importer) - { - ArgumentNullException.ThrowIfNull(importer); - _importers[importer.ImporterId] = importer; - RegistryChanged?.Invoke(); - - return new DisposableAction(() => - { - _importers.TryRemove(importer.ImporterId, out _); - RegistryChanged?.Invoke(); - }); - } - - public IDocumentImporter? FindImporter(string filePathOrExtension) - { - if (string.IsNullOrWhiteSpace(filePathOrExtension)) return null; - - var ext = Path.GetExtension(filePathOrExtension); - if (string.IsNullOrWhiteSpace(ext)) - { - ext = filePathOrExtension.StartsWith('.') ? filePathOrExtension : "." + filePathOrExtension; - } - - return _importers.Values - .Where(i => i.SupportedExtensions.Any(e => string.Equals(e, ext, StringComparison.OrdinalIgnoreCase))) - .OrderByDescending(i => i.Priority) - .FirstOrDefault(); - } - - public IDocumentImporter? GetImporter(string importerId) - { - if (string.IsNullOrWhiteSpace(importerId)) return null; - return _importers.GetValueOrDefault(importerId); - } - - public IReadOnlyList GetAllImporters() - { - return _importers.Values.OrderByDescending(i => i.Priority).ToList(); - } - - private sealed class DisposableAction(Action action) : IDisposable - { - private Action? _action = action; - public void Dispose() => Interlocked.Exchange(ref _action, null)?.Invoke(); - } -} - -/// -/// Default native PDF deconstruction engine importer using UglyToad.PdfPig + Skia. -/// -public class PdfDeconstructionImporter : IDocumentImporter -{ - public string ImporterId => "frypdf.importer.pdfpig"; - public string DisplayName => "Adobe PDF Deconstruction Engine"; - public IReadOnlyList SupportedExtensions => new[] { ".pdf" }; - public int Priority => 100; - - public async Task ImportAsync(Stream stream, string fileName, string? password = null, CancellationToken ct = default) - { - using var ms = new MemoryStream(); - await stream.CopyToAsync(ms, ct); - var bytes = ms.ToArray(); - - return await Task.Run(() => PdfDeconstructionEngine.Deconstruct(bytes, fileName, password), ct); - } -} - -/// -/// Image importer converting raster images into editable full-bleed PDF pages. -/// -public class ImageDocumentImporter : IDocumentImporter -{ - public string ImporterId => "frypdf.importer.image"; - public string DisplayName => "Raster Image Importer"; - public IReadOnlyList SupportedExtensions => new[] { ".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff" }; - public int Priority => 50; - - public async Task ImportAsync(Stream stream, string fileName, string? password = null, CancellationToken ct = default) - { - using var ms = new MemoryStream(); - await stream.CopyToAsync(ms, ct); - var bytes = ms.ToArray(); - - var doc = new PdfDocumentModel - { - Title = Path.GetFileNameWithoutExtension(fileName), - Author = "FryPDF Image Importer" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Width = 595.28, // Standard A4 portrait - Height = 841.89 - }; - - var imgElement = new PdfImageElement - { - X = 20, - Y = 20, - Width = 555.28, - Height = 801.89, - ImageData = bytes, - ZIndex = 100 - }; - - page.Elements.Add(imgElement); - doc.Pages.Add(page); - return doc; - } -} - -/// -/// Plain text / Markdown document importer. -/// -public class PlainTextDocumentImporter : IDocumentImporter -{ - public string ImporterId => "frypdf.importer.text"; - public string DisplayName => "Text & Markdown Importer"; - public IReadOnlyList SupportedExtensions => new[] { ".txt", ".md" }; - public int Priority => 50; - - public async Task ImportAsync(Stream stream, string fileName, string? password = null, CancellationToken ct = default) - { - using var reader = new StreamReader(stream); - var text = await reader.ReadToEndAsync(ct); - - var doc = new PdfDocumentModel - { - Title = Path.GetFileNameWithoutExtension(fileName), - Author = "FryPDF Text Importer" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Width = 595.28, - Height = 841.89 - }; - - var textElement = new PdfTextElement - { - X = 50, - Y = 50, - Width = 495.28, - Height = 741.89, - Text = text, - FontSize = 12, - FontFamily = "Inter", - ZIndex = 1000 - }; - - page.Elements.Add(textElement); - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Services/Inspector/InspectorRegistry.cs b/src/PdfEditorApp/Services/Inspector/InspectorRegistry.cs deleted file mode 100644 index 7789104..0000000 --- a/src/PdfEditorApp/Services/Inspector/InspectorRegistry.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Inspector; - -public class InspectorRegistry : IInspectorRegistry -{ - private readonly ConcurrentDictionary _sections = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public InspectorRegistry(bool seedDefaults = false) - { - if (seedDefaults) - { - RegisterBuiltInSections(); - } - } - - private void RegisterBuiltInSections() - { - // Core sections can be registered dynamically if needed - } - - public IDisposable RegisterSection(InspectorSectionDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _sections[descriptor.SectionId] = descriptor; - RegistryChanged?.Invoke(); - - return new DisposableAction(() => - { - _sections.TryRemove(descriptor.SectionId, out _); - RegistryChanged?.Invoke(); - }); - } - - public IReadOnlyList GetSectionsForTarget(object? target) - { - return _sections.Values - .Where(s => s.AppliesTo(target)) - .OrderBy(s => s.Order) - .ToList(); - } - - public IReadOnlyList GetAllSections() - { - return _sections.Values.OrderBy(s => s.Order).ToList(); - } - - private sealed class DisposableAction(Action action) : IDisposable - { - private Action? _action = action; - public void Dispose() => Interlocked.Exchange(ref _action, null)?.Invoke(); - } -} diff --git a/src/PdfEditorApp/Services/Math/MathLayoutEngine.cs b/src/PdfEditorApp/Services/Math/MathLayoutEngine.cs deleted file mode 100644 index da189cc..0000000 --- a/src/PdfEditorApp/Services/Math/MathLayoutEngine.cs +++ /dev/null @@ -1,1482 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Services.MathEngine; - -public record MathRenderOptions( - double FontSize = 16, - string TextColorHex = "#0F172A", - string BackgroundColorHex = "#00000000", - string BorderColorHex = "#00000000", - double BorderThickness = 0, - double CornerRadius = 4, - double Padding = 8, - bool ShowBackground = false, - bool ShowBorder = false, - bool ShowEquationNumber = false, - string EquationNumber = "(1)", - TextAlignmentMode Alignment = TextAlignmentMode.Center, - MathDisplayStyle DisplayStyle = MathDisplayStyle.DisplayBlock, - double TargetWidth = 0, - double TargetHeight = 0 -); - -public class MathRenderResult -{ - public string SvgXml { get; set; } = ""; - public string SvgMarkup => SvgXml; - public double Width { get; set; } - public double Height { get; set; } - public bool IsSuccess { get; set; } = true; - public string? ErrorMessage { get; set; } - public string PathGeometryData { get; set; } = ""; -} - -public static class MathLayoutEngine -{ - // ========================================================================= - // SYMBOL & COMMAND DICTIONARIES - // ========================================================================= - private static readonly Dictionary _greekSymbols = new(StringComparer.OrdinalIgnoreCase) - { - // Lowercase Greek - ["alpha"] = ("α", false), ["beta"] = ("β", false), ["gamma"] = ("γ", false), ["delta"] = ("δ", false), - ["epsilon"] = ("ϵ", false), ["varepsilon"] = ("ε", false), ["zeta"] = ("ζ", false), ["eta"] = ("η", false), - ["theta"] = ("θ", false), ["vartheta"] = ("ϑ", false), ["iota"] = ("ι", false), ["kappa"] = ("κ", false), - ["lambda"] = ("λ", false), ["mu"] = ("μ", false), ["nu"] = ("ν", false), ["xi"] = ("ξ", false), - ["pi"] = ("π", false), ["varpi"] = ("ϖ", false), ["rho"] = ("ρ", false), ["varrho"] = ("ϱ", false), - ["sigma"] = ("σ", false), ["varsigma"] = ("ς", false), ["tau"] = ("τ", false), ["upsilon"] = ("υ", false), - ["phi"] = ("ϕ", false), ["varphi"] = ("φ", false), ["chi"] = ("χ", false), ["psi"] = ("ψ", false), - ["omega"] = ("ω", false), - - // Uppercase Greek - ["Gamma"] = ("Γ", false), ["Delta"] = ("Δ", false), ["Theta"] = ("Θ", false), ["Lambda"] = ("Λ", false), - ["Xi"] = ("Ξ", false), ["Pi"] = ("Π", false), ["Sigma"] = ("Σ", false), ["Upsilon"] = ("Υ", false), - ["Phi"] = ("Φ", false), ["Psi"] = ("Ψ", false), ["Omega"] = ("Ω", false) - }; - - private static readonly Dictionary _mathSymbols = new(StringComparer.OrdinalIgnoreCase) - { - // Binary Operators & Signs - ["pm"] = "±", ["mp"] = "∓", ["times"] = "×", ["cdot"] = "·", ["div"] = "÷", - ["ast"] = "∗", ["star"] = "★", ["circ"] = "∘", ["bullet"] = "•", - ["oplus"] = "⊕", ["otimes"] = "⊗", ["odot"] = "⊙", ["wedge"] = "∧", ["vee"] = "∨", - ["cap"] = "∩", ["cup"] = "∪", - - // Relations & Sets - ["le"] = "≤", ["leq"] = "≤", ["ge"] = "≥", ["geq"] = "≥", ["neq"] = "≠", ["ne"] = "≠", - ["approx"] = "≈", ["equiv"] = "≡", ["sim"] = "∼", ["simeq"] = "≃", ["cong"] = "≅", - ["propto"] = "∝", ["subset"] = "⊂", ["subseteq"] = "⊆", ["supset"] = "⊃", ["supseteq"] = "⊇", - ["in"] = "∈", ["notin"] = "∉", ["ni"] = "∋", ["mid"] = "∣", ["parallel"] = "∥", ["perp"] = "⊥", - - // Arrows - ["to"] = "→", ["rightarrow"] = "→", ["leftarrow"] = "←", ["Rightarrow"] = "⇒", - ["Leftarrow"] = "⇐", ["leftrightarrow"] = "↔", ["Leftrightarrow"] = "⇔", ["iff"] = "⟺", - ["mapsto"] = "↦", ["uparrow"] = "↑", ["downarrow"] = "↓", - - // Calculus, Physics & Analysis - ["partial"] = "∂", ["nabla"] = "∇", ["hbar"] = "ℏ", ["infty"] = "∞", ["ell"] = "ℓ", - ["dagger"] = "†", ["ddagger"] = "‡", ["prime"] = "′", ["forall"] = "∀", ["exists"] = "∃", - ["nexists"] = "∄", ["empty"] = "∅", ["emptyset"] = "∅", ["neg"] = "¬", ["angle"] = "∠", - ["Re"] = "ℜ", ["Im"] = "ℑ", ["aleph"] = "ℵ", ["wp"] = "℘" - }; - - private static readonly Dictionary _blackboardBold = new(StringComparer.OrdinalIgnoreCase) - { - ["R"] = "ℝ", ["C"] = "ℂ", ["N"] = "ℕ", ["Z"] = "ℤ", ["Q"] = "ℚ", - ["P"] = "ℙ", ["E"] = "𝔼", ["H"] = "ℍ", ["F"] = "𝔽", ["K"] = "𝕂" - }; - - private static readonly Dictionary _calligraphic = new(StringComparer.OrdinalIgnoreCase) - { - ["A"] = "𝒜", ["B"] = "ℬ", ["C"] = "𝒞", ["D"] = "𝒟", ["E"] = "ℰ", - ["F"] = "ℱ", ["G"] = "𝒢", ["H"] = "ℋ", ["I"] = "ℐ", ["J"] = "𝒥", - ["K"] = "𝒦", ["L"] = "ℒ", ["M"] = "ℳ", ["N"] = "𝒩", ["O"] = "𝒪", - ["P"] = "𝒫", ["Q"] = "𝒬", ["R"] = "ℛ", ["S"] = "𝒮", ["T"] = "𝒯", - ["U"] = "𝒰", ["V"] = "𝒱", ["W"] = "𝒲", ["X"] = "𝒳", ["Y"] = "𝒴", - ["Z"] = "𝒵" - }; - - private static readonly HashSet _standardFunctions = new(StringComparer.OrdinalIgnoreCase) - { - "sin", "cos", "tan", "cot", "sec", "csc", - "arcsin", "arccos", "arctan", "sinh", "cosh", "tanh", - "ln", "log", "exp", "det", "dim", "ker", "deg", "gcd", - "max", "min", "sup", "inf", "lim", "limsup", "liminf", - "Pr", "arg", "Tr", "diag", "cov", "var", "Res", "rank" - }; - - // ========================================================================= - // PUBLIC MAIN RENDER API - // ========================================================================= - public static MathRenderResult RenderToSvg(string formula, MathRenderOptions? options = null) - { - options ??= new MathRenderOptions(); - var result = new MathRenderResult(); - - try - { - if (string.IsNullOrWhiteSpace(formula)) - { - formula = @"f(x) = y"; - } - - // 1. Tokenize & Parse AST - var tokens = Tokenize(formula); - var parser = new MathParser(tokens); - var rootNode = parser.ParseExpression(); - - // 2. Measure AST Nodes recursively - double baseFontSize = System.Math.Max(8, options.FontSize); - var measureCtx = new LayoutContext(baseFontSize, options.TextColorHex, options.DisplayStyle); - rootNode.Measure(measureCtx); - - double contentWidth = rootNode.Width; - double contentHeight = rootNode.Height; - double contentAscent = rootNode.Ascent; - - // 3. Compute SVG Dimensions and Margins - double pad = System.Math.Max(0, options.Padding); - double eqNumWidth = 0; - if (options.ShowEquationNumber && !string.IsNullOrWhiteSpace(options.EquationNumber)) - { - eqNumWidth = (options.EquationNumber.Length * baseFontSize * 0.55) + 20; - } - - double totalWidth = System.Math.Max(options.TargetWidth, contentWidth + (pad * 2) + eqNumWidth); - double totalHeight = System.Math.Max(options.TargetHeight, contentHeight + (pad * 2)); - - // Align content X inside bounding box - double startX = pad; - if (options.Alignment == TextAlignmentMode.Center) - { - startX = pad + System.Math.Max(0, (totalWidth - (pad * 2) - eqNumWidth - contentWidth) / 2); - } - else if (options.Alignment == TextAlignmentMode.Right) - { - startX = totalWidth - pad - eqNumWidth - contentWidth; - } - - double baselineY = pad + contentAscent + System.Math.Max(0, (totalHeight - (pad * 2) - contentHeight) / 2); - - // 4. Generate SVG Output - var svgBuilder = new StringBuilder(); - string strW = totalWidth.ToString("F1", CultureInfo.InvariantCulture); - string strH = totalHeight.ToString("F1", CultureInfo.InvariantCulture); - - svgBuilder.AppendLine($""); - svgBuilder.AppendLine(" "); - svgBuilder.AppendLine(" "); - svgBuilder.AppendLine(" "); - - // Background & Border - if (options.ShowBackground || options.ShowBorder) - { - string fill = options.ShowBackground && !string.IsNullOrEmpty(options.BackgroundColorHex) ? options.BackgroundColorHex : "none"; - string stroke = options.ShowBorder && !string.IsNullOrEmpty(options.BorderColorHex) ? options.BorderColorHex : "none"; - string strokeW = options.BorderThickness > 0 ? options.BorderThickness.ToString("F1", CultureInfo.InvariantCulture) : "0"; - string rx = options.CornerRadius.ToString("F1", CultureInfo.InvariantCulture); - - svgBuilder.AppendLine($" "); - } - - // Render Math AST Content - var renderCtx = new RenderContext(baseFontSize, options.TextColorHex, svgBuilder, options.DisplayStyle); - rootNode.Render(startX, baselineY, renderCtx); - - // Render Right-Aligned Equation Number (e.g. "(1)") - if (options.ShowEquationNumber && !string.IsNullOrWhiteSpace(options.EquationNumber)) - { - double eqNumX = totalWidth - pad; - double eqNumFontSize = baseFontSize * 0.9; - svgBuilder.AppendLine($" {EscapeXml(options.EquationNumber)}"); - } - - svgBuilder.AppendLine(""); - - result.SvgXml = svgBuilder.ToString(); - result.Width = totalWidth; - result.Height = totalHeight; - result.IsSuccess = true; - result.PathGeometryData = GeneratePathPreviewData(totalWidth, totalHeight); - } - catch (Exception ex) - { - result.IsSuccess = false; - result.ErrorMessage = ex.Message; - result.SvgXml = GenerateFallbackErrorSvg(formula, options, ex.Message); - result.Width = System.Math.Max(200, options.TargetWidth); - result.Height = System.Math.Max(40, options.TargetHeight); - } - - return result; - } - - private static string GeneratePathPreviewData(double width, double height) - { - // Simple geometry representation for Avalonia vector path binding fallback - return $"M 0,0 L {width.ToString("F0", CultureInfo.InvariantCulture)},0 L {width.ToString("F0", CultureInfo.InvariantCulture)},{height.ToString("F0", CultureInfo.InvariantCulture)} L 0,{height.ToString("F0", CultureInfo.InvariantCulture)} Z"; - } - - private static string GenerateFallbackErrorSvg(string rawFormula, MathRenderOptions options, string error) - { - double w = System.Math.Max(240, options.TargetWidth > 0 ? options.TargetWidth : 240); - double h = System.Math.Max(50, options.TargetHeight > 0 ? options.TargetHeight : 50); - - return $""" - - - {EscapeXml(rawFormula)} - Syntax Error: {EscapeXml(error)} - - """; - } - - public static string EscapeXml(string? input) - { - if (string.IsNullOrEmpty(input)) return ""; - return input - .Replace("&", "&") - .Replace("<", "<") - .Replace(">", ">") - .Replace("\"", """) - .Replace("'", "'"); - } - - // ========================================================================= - // TOKENIZER - // ========================================================================= - public enum TokenType - { - Command, - Identifier, - Number, - Operator, - OpenBrace, - CloseBrace, - OpenBracket, - CloseBracket, - OpenParen, - CloseParen, - Superscript, - Subscript, - Ampersand, - Newline, - StringLiteral, - EOF - } - - public record Token(TokenType Type, string Value, int Position); - - public static List Tokenize(string formula) - { - var tokens = new List(); - int i = 0; - int len = formula.Length; - - while (i < len) - { - char c = formula[i]; - - if (char.IsWhiteSpace(c)) - { - i++; - continue; - } - - if (c == '\\') - { - int start = i; - i++; // Skip backslash - if (i < len && (formula[i] == '\\' || formula[i] == ',' || formula[i] == ':' || formula[i] == ';' || formula[i] == '!' || formula[i] == '{' || formula[i] == '}' || formula[i] == '|' || formula[i] == ' ' || formula[i] == '%')) - { - char sym = formula[i++]; - if (sym == '\\') - { - tokens.Add(new Token(TokenType.Newline, "\\\\", start)); - } - else - { - tokens.Add(new Token(TokenType.Command, "\\" + sym, start)); - } - } - else - { - int cmdStart = i; - while (i < len && char.IsLetter(formula[i])) - { - i++; - } - string cmd = formula[cmdStart..i]; - tokens.Add(new Token(TokenType.Command, cmd, start)); - } - continue; - } - - if (c == '{') - { - tokens.Add(new Token(TokenType.OpenBrace, "{", i++)); - continue; - } - - if (c == '}') - { - tokens.Add(new Token(TokenType.CloseBrace, "}", i++)); - continue; - } - - if (c == '[') - { - tokens.Add(new Token(TokenType.OpenBracket, "[", i++)); - continue; - } - - if (c == ']') - { - tokens.Add(new Token(TokenType.CloseBracket, "]", i++)); - continue; - } - - if (c == '(') - { - tokens.Add(new Token(TokenType.OpenParen, "(", i++)); - continue; - } - - if (c == ')') - { - tokens.Add(new Token(TokenType.CloseParen, ")", i++)); - continue; - } - - if (c == '^') - { - tokens.Add(new Token(TokenType.Superscript, "^", i++)); - continue; - } - - if (c == '_') - { - tokens.Add(new Token(TokenType.Subscript, "_", i++)); - continue; - } - - if (c == '&') - { - tokens.Add(new Token(TokenType.Ampersand, "&", i++)); - continue; - } - - if (char.IsDigit(c) || (c == '.' && i + 1 < len && char.IsDigit(formula[i + 1]))) - { - int numStart = i; - while (i < len && (char.IsDigit(formula[i]) || formula[i] == '.')) - { - i++; - } - tokens.Add(new Token(TokenType.Number, formula[numStart..i], numStart)); - continue; - } - - if (char.IsLetter(c)) - { - tokens.Add(new Token(TokenType.Identifier, c.ToString(), i++)); - continue; - } - - // Operators & Punctuation: +, -, =, <, >, *, /, !, ,, ;, :, |, ', ~ - tokens.Add(new Token(TokenType.Operator, c.ToString(), i++)); - } - - tokens.Add(new Token(TokenType.EOF, "", i)); - return tokens; - } - - // ========================================================================= - // AST NODES DEFINITION - // ========================================================================= - public record LayoutContext(double FontSize, string ColorHex, MathDisplayStyle DisplayStyle); - public record RenderContext(double BaseFontSize, string TextColorHex, StringBuilder Svg, MathDisplayStyle DisplayStyle); - - public abstract class MathAstNode - { - public double Width { get; set; } - public double Height { get; set; } - public double Ascent { get; set; } - public double Descent { get; set; } - - public abstract void Measure(LayoutContext ctx); - public abstract void Render(double x, double y, RenderContext ctx); - } - - public class MathSequenceNode : MathAstNode - { - public List Children { get; } = new(); - - public override void Measure(LayoutContext ctx) - { - Width = 0; - Ascent = 0; - Descent = 0; - - foreach (var child in Children) - { - child.Measure(ctx); - Width += child.Width; - Ascent = System.Math.Max(Ascent, child.Ascent); - Descent = System.Math.Max(Descent, child.Descent); - } - - if (Children.Count == 0) - { - Ascent = ctx.FontSize * 0.75; - Descent = ctx.FontSize * 0.25; - } - - Height = Ascent + Descent; - } - - public override void Render(double x, double y, RenderContext ctx) - { - double curX = x; - foreach (var child in Children) - { - child.Render(curX, y, ctx); - curX += child.Width; - } - } - } - - public class MathTextNode : MathAstNode - { - public string Text { get; set; } - public bool IsVariable { get; set; } - public bool IsUpright { get; set; } - public bool IsBold { get; set; } - public string? ColorOverride { get; set; } - - public MathTextNode(string text, bool isVariable = false, bool isUpright = false, bool isBold = false) - { - Text = text; - IsVariable = isVariable; - IsUpright = isUpright; - IsBold = isBold; - } - - public override void Measure(LayoutContext ctx) - { - Ascent = ctx.FontSize * 0.78; - Descent = ctx.FontSize * 0.22; - Height = Ascent + Descent; - - // Character width calculation - double charW = ctx.FontSize * 0.56; - if (Text.Length == 1) - { - char c = Text[0]; - if (c is 'i' or 'l' or 'j' or 't' or '!' or ',' or ';' or '.') charW = ctx.FontSize * 0.32; - else if (c is 'm' or 'w' or 'W' or 'M') charW = ctx.FontSize * 0.82; - else if (char.IsUpper(c)) charW = ctx.FontSize * 0.68; - else if (char.IsDigit(c)) charW = ctx.FontSize * 0.52; - Width = charW; - } - else - { - Width = Text.Length * charW; - } - } - - public override void Render(double x, double y, RenderContext ctx) - { - string color = ColorOverride ?? ctx.TextColorHex; - string cls = IsVariable ? "m-text m-var" : "m-text m-upright"; - string weight = IsBold ? "font-weight=\"bold\" " : ""; - string fontStyle = IsVariable ? "font-style=\"italic\" " : ""; - string fontSizeStr = ctx.BaseFontSize.ToString("F1", CultureInfo.InvariantCulture); - - ctx.Svg.AppendLine($" {EscapeXml(Text)}"); - } - } - - public class MathSymbolNode : MathAstNode - { - public string Symbol { get; set; } - public bool IsOperator { get; set; } - public double ExtraSpacing { get; set; } - - public MathSymbolNode(string symbol, bool isOperator = true, double extraSpacing = 0) - { - Symbol = symbol; - IsOperator = isOperator; - ExtraSpacing = extraSpacing; - } - - public override void Measure(LayoutContext ctx) - { - Ascent = ctx.FontSize * 0.8; - Descent = ctx.FontSize * 0.2; - Height = Ascent + Descent; - - double symW = ctx.FontSize * 0.6; - if (Symbol is "+" or "-" or "=" or "<" or ">" or "≤" or "≥" or "≠" or "≈" or "≡" or "→" or "⇒" or "⟺" or "±") - { - symW = ctx.FontSize * 0.72; - ExtraSpacing = ctx.FontSize * 0.18; - } - else if (Symbol is "·" or "∗" or "∘") - { - symW = ctx.FontSize * 0.45; - ExtraSpacing = ctx.FontSize * 0.12; - } - - Width = symW + (ExtraSpacing * 2); - } - - public override void Render(double x, double y, RenderContext ctx) - { - double posX = x + ExtraSpacing; - string fontSizeStr = ctx.BaseFontSize.ToString("F1", CultureInfo.InvariantCulture); - ctx.Svg.AppendLine($" {EscapeXml(Symbol)}"); - } - } - - public class MathFractionNode : MathAstNode - { - public MathAstNode Numerator { get; } - public MathAstNode Denominator { get; } - - public MathFractionNode(MathAstNode numerator, MathAstNode denominator) - { - Numerator = numerator; - Denominator = denominator; - } - - public override void Measure(LayoutContext ctx) - { - double subScale = 0.82; - var subCtx = new LayoutContext(ctx.FontSize * subScale, ctx.ColorHex, MathDisplayStyle.Inline); - - Numerator.Measure(subCtx); - Denominator.Measure(subCtx); - - double maxContentW = System.Math.Max(Numerator.Width, Denominator.Width); - Width = maxContentW + (ctx.FontSize * 0.35); // Margin around fraction bar - - double barGap = ctx.FontSize * 0.22; - Ascent = Numerator.Height + barGap; - Descent = Denominator.Height + barGap; - Height = Ascent + Descent; - } - - public override void Render(double x, double y, RenderContext ctx) - { - double subScale = 0.82; - double childFontSize = ctx.BaseFontSize * subScale; - var childCtx = new RenderContext(childFontSize, ctx.TextColorHex, ctx.Svg, MathDisplayStyle.Inline); - - double barY = y - (ctx.BaseFontSize * 0.28); - double barGap = ctx.BaseFontSize * 0.22; - - // Render Numerator (centered horizontally above bar) - double numX = x + ((Width - Numerator.Width) / 2); - double numY = barY - barGap - Numerator.Descent; - Numerator.Render(numX, numY, childCtx); - - // Draw Fraction Horizontal Bar Line - double barThickness = System.Math.Max(1.0, ctx.BaseFontSize * 0.07); - string x1 = x.ToString("F1", CultureInfo.InvariantCulture); - string y1 = barY.ToString("F1", CultureInfo.InvariantCulture); - string x2 = (x + Width).ToString("F1", CultureInfo.InvariantCulture); - string thickStr = barThickness.ToString("F1", CultureInfo.InvariantCulture); - - ctx.Svg.AppendLine($" "); - - // Render Denominator (centered horizontally below bar) - double denX = x + ((Width - Denominator.Width) / 2); - double denY = barY + barGap + Denominator.Ascent; - Denominator.Render(denX, denY, childCtx); - } - } - - public class MathRadicalNode : MathAstNode - { - public MathAstNode Radicand { get; } - public MathAstNode? Degree { get; } - - public MathRadicalNode(MathAstNode radicand, MathAstNode? degree = null) - { - Radicand = radicand; - Degree = degree; - } - - public override void Measure(LayoutContext ctx) - { - Radicand.Measure(ctx); - if (Degree != null) - { - var degCtx = new LayoutContext(ctx.FontSize * 0.6, ctx.ColorHex, MathDisplayStyle.Inline); - Degree.Measure(degCtx); - } - - double radicalSymbolW = ctx.FontSize * 0.65; - double degW = Degree?.Width ?? 0; - - Width = radicalSymbolW + Radicand.Width + (ctx.FontSize * 0.15) + (degW > 0 ? degW * 0.4 : 0); - Ascent = Radicand.Ascent + (ctx.FontSize * 0.25); - Descent = Radicand.Descent + (ctx.FontSize * 0.08); - Height = Ascent + Descent; - } - - public override void Render(double x, double y, RenderContext ctx) - { - double degOffset = 0; - if (Degree != null) - { - double degFontSize = ctx.BaseFontSize * 0.58; - var degCtx = new RenderContext(degFontSize, ctx.TextColorHex, ctx.Svg, MathDisplayStyle.Inline); - Degree.Render(x, y - (ctx.BaseFontSize * 0.5), degCtx); - degOffset = Degree.Width * 0.4; - } - - double radX = x + degOffset; - double topY = y - Ascent + (ctx.BaseFontSize * 0.05); - double bottomY = y + Descent; - double radicandStartX = radX + (ctx.BaseFontSize * 0.6); - double radicandEndX = x + Width; - - // Draw Radical √ Symbol Path + Overbar - double lineThick = System.Math.Max(1.0, ctx.BaseFontSize * 0.065); - double hookX = radX + (ctx.BaseFontSize * 0.12); - double hookY = y - (ctx.BaseFontSize * 0.2); - double cornerX = radX + (ctx.BaseFontSize * 0.28); - - string d = $"M {radX.ToString("F1", CultureInfo.InvariantCulture)},{hookY.ToString("F1", CultureInfo.InvariantCulture)} " + - $"L {cornerX.ToString("F1", CultureInfo.InvariantCulture)},{bottomY.ToString("F1", CultureInfo.InvariantCulture)} " + - $"L {radicandStartX.ToString("F1", CultureInfo.InvariantCulture)},{topY.ToString("F1", CultureInfo.InvariantCulture)} " + - $"L {radicandEndX.ToString("F1", CultureInfo.InvariantCulture)},{topY.ToString("F1", CultureInfo.InvariantCulture)}"; - - ctx.Svg.AppendLine($" "); - - // Render Radicand - Radicand.Render(radicandStartX + (ctx.BaseFontSize * 0.08), y, ctx); - } - } - - public class MathSubSuperNode : MathAstNode - { - public MathAstNode BaseNode { get; } - public MathAstNode? Subscript { get; } - public MathAstNode? Superscript { get; } - - public MathSubSuperNode(MathAstNode baseNode, MathAstNode? sub = null, MathAstNode? sup = null) - { - BaseNode = baseNode; - Subscript = sub; - Superscript = sup; - } - - public override void Measure(LayoutContext ctx) - { - BaseNode.Measure(ctx); - - double scriptScale = 0.72; - var scriptCtx = new LayoutContext(ctx.FontSize * scriptScale, ctx.ColorHex, MathDisplayStyle.Inline); - - double scriptWidth = 0; - if (Superscript != null) - { - Superscript.Measure(scriptCtx); - scriptWidth = System.Math.Max(scriptWidth, Superscript.Width); - } - if (Subscript != null) - { - Subscript.Measure(scriptCtx); - scriptWidth = System.Math.Max(scriptWidth, Subscript.Width); - } - - Width = BaseNode.Width + scriptWidth + (ctx.FontSize * 0.06); - - double supAscent = Superscript != null ? (ctx.FontSize * 0.45) + Superscript.Ascent : 0; - double subDescent = Subscript != null ? (ctx.FontSize * 0.35) + Subscript.Descent : 0; - - Ascent = System.Math.Max(BaseNode.Ascent, supAscent); - Descent = System.Math.Max(BaseNode.Descent, subDescent); - Height = Ascent + Descent; - } - - public override void Render(double x, double y, RenderContext ctx) - { - BaseNode.Render(x, y, ctx); - - double scriptX = x + BaseNode.Width + (ctx.BaseFontSize * 0.04); - double scriptFontSize = ctx.BaseFontSize * 0.72; - var scriptCtx = new RenderContext(scriptFontSize, ctx.TextColorHex, ctx.Svg, MathDisplayStyle.Inline); - - if (Superscript != null) - { - double supY = y - (ctx.BaseFontSize * 0.42); - Superscript.Render(scriptX, supY, scriptCtx); - } - - if (Subscript != null) - { - double subY = y + (ctx.BaseFontSize * 0.28); - Subscript.Render(scriptX, subY, scriptCtx); - } - } - } - - public class MathLargeOperatorNode : MathAstNode - { - public string OperatorName { get; } - public MathAstNode? LowerLimit { get; } - public MathAstNode? UpperLimit { get; } - public bool IsDisplayMode { get; } - - public MathLargeOperatorNode(string opName, MathAstNode? lower = null, MathAstNode? upper = null, bool isDisplay = true) - { - OperatorName = opName; - LowerLimit = lower; - UpperLimit = upper; - IsDisplayMode = isDisplay; - } - - public override void Measure(LayoutContext ctx) - { - double opScale = IsDisplayMode ? 1.4 : 1.1; - double opW = ctx.FontSize * (OperatorName.Contains("int") ? 0.75 : 1.1) * opScale; - - double scriptScale = 0.68; - var scriptCtx = new LayoutContext(ctx.FontSize * scriptScale, ctx.ColorHex, MathDisplayStyle.Inline); - - LowerLimit?.Measure(scriptCtx); - UpperLimit?.Measure(scriptCtx); - - bool isIntegral = OperatorName.Contains("int"); - - if (isIntegral || !IsDisplayMode) - { - // Side limits - double limitsW = System.Math.Max(LowerLimit?.Width ?? 0, UpperLimit?.Width ?? 0); - Width = opW + limitsW + (ctx.FontSize * 0.1); - Ascent = (ctx.FontSize * opScale * 0.8) + (UpperLimit != null ? UpperLimit.Height * 0.4 : 0); - Descent = (ctx.FontSize * opScale * 0.35) + (LowerLimit != null ? LowerLimit.Height * 0.4 : 0); - } - else - { - // Top/bottom centered limits (Sum, Prod, Lim) - double maxW = System.Math.Max(opW, System.Math.Max(LowerLimit?.Width ?? 0, UpperLimit?.Width ?? 0)); - Width = maxW + (ctx.FontSize * 0.15); - Ascent = (ctx.FontSize * opScale * 0.75) + (UpperLimit?.Height ?? 0) + (ctx.FontSize * 0.12); - Descent = (ctx.FontSize * opScale * 0.3) + (LowerLimit?.Height ?? 0) + (ctx.FontSize * 0.12); - } - - Height = Ascent + Descent; - } - - public override void Render(double x, double y, RenderContext ctx) - { - double opScale = IsDisplayMode ? 1.4 : 1.1; - double opFontSize = ctx.BaseFontSize * opScale; - double scriptFontSize = ctx.BaseFontSize * 0.68; - var scriptCtx = new RenderContext(scriptFontSize, ctx.TextColorHex, ctx.Svg, MathDisplayStyle.Inline); - - bool isIntegral = OperatorName.Contains("int"); - string sym = OperatorName switch - { - "sum" => "∑", - "prod" => "∏", - "coprod" => "∐", - "int" => "∫", - "iint" => "∬", - "iiint" => "∭", - "oint" => "∮", - "lim" => "lim", - _ => "∑" - }; - - if (isIntegral || !IsDisplayMode) - { - double opW = ctx.BaseFontSize * 0.75 * opScale; - // Render Integral / Operator symbol - ctx.Svg.AppendLine($" {sym}"); - - double limitX = x + opW + (ctx.BaseFontSize * 0.05); - if (UpperLimit != null) - { - double upY = y - (ctx.BaseFontSize * opScale * 0.55); - UpperLimit.Render(limitX, upY, scriptCtx); - } - if (LowerLimit != null) - { - double lowY = y + (ctx.BaseFontSize * opScale * 0.35); - LowerLimit.Render(limitX, lowY, scriptCtx); - } - } - else - { - // Centered stacked limits (for \sum, \prod, \lim) - double opW = sym == "lim" ? ctx.BaseFontSize * 1.3 : ctx.BaseFontSize * 1.0 * opScale; - double opX = x + ((Width - opW) / 2); - - if (UpperLimit != null) - { - double upX = x + ((Width - UpperLimit.Width) / 2); - double upY = y - (ctx.BaseFontSize * opScale * 0.65) - UpperLimit.Descent; - UpperLimit.Render(upX, upY, scriptCtx); - } - - ctx.Svg.AppendLine($" {sym}"); - - if (LowerLimit != null) - { - double lowX = x + ((Width - LowerLimit.Width) / 2); - double lowY = y + (ctx.BaseFontSize * opScale * 0.3) + LowerLimit.Ascent; - LowerLimit.Render(lowX, lowY, scriptCtx); - } - } - } - } - - public class MathDelimiterNode : MathAstNode - { - public string LeftDelim { get; } - public string RightDelim { get; } - public MathAstNode Content { get; } - - public MathDelimiterNode(string left, string right, MathAstNode content) - { - LeftDelim = left; - RightDelim = right; - Content = content; - } - - public override void Measure(LayoutContext ctx) - { - Content.Measure(ctx); - - double delimW = ctx.FontSize * 0.45; - if (LeftDelim == "." || string.IsNullOrEmpty(LeftDelim)) delimW = 0; - double rightDelimW = ctx.FontSize * 0.45; - if (RightDelim == "." || string.IsNullOrEmpty(RightDelim)) rightDelimW = 0; - - Width = delimW + Content.Width + rightDelimW; - Ascent = Content.Ascent; - Descent = Content.Descent; - Height = Content.Height; - } - - public override void Render(double x, double y, RenderContext ctx) - { - double curX = x; - double delimScale = System.Math.Max(1.0, Content.Height / (ctx.BaseFontSize * 1.1)); - - if (LeftDelim != "." && !string.IsNullOrEmpty(LeftDelim)) - { - RenderSingleDelimiter(LeftDelim, curX, y, Content.Ascent, Content.Descent, delimScale, ctx); - curX += ctx.BaseFontSize * 0.45; - } - - Content.Render(curX, y, ctx); - curX += Content.Width; - - if (RightDelim != "." && !string.IsNullOrEmpty(RightDelim)) - { - RenderSingleDelimiter(RightDelim, curX, y, Content.Ascent, Content.Descent, delimScale, ctx); - } - } - - private static void RenderSingleDelimiter(string delim, double x, double y, double ascent, double descent, double scale, RenderContext ctx) - { - double fontSize = ctx.BaseFontSize * scale; - string str = delim switch - { - "(" => "(", - ")" => ")", - "[" => "[", - "]" => "]", - "{" or "\\{" => "{", - "}" or "\\}" => "}", - "|" or "\\|" => "|", - "\\langle" or "langle" => "⟨", - "\\rangle" or "rangle" => "⟩", - "\\lceil" or "lceil" => "⌈", - "\\rceil" or "rceil" => "⌉", - "\\lfloor" or "lfloor" => "⌊", - "\\rfloor" or "rfloor" => "⌋", - _ => delim - }; - - double adjustY = y + ((descent - ascent) * 0.1); - ctx.Svg.AppendLine($" {EscapeXml(str)}"); - } - } - - public class MathMatrixNode : MathAstNode - { - public List> Rows { get; } = new(); - public string MatrixType { get; } - - public MathMatrixNode(string matrixType) - { - MatrixType = matrixType; - } - - private List _colWidths = new(); - private List _rowHeights = new(); - private List _rowAscents = new(); - - public override void Measure(LayoutContext ctx) - { - _colWidths.Clear(); - _rowHeights.Clear(); - _rowAscents.Clear(); - - int numCols = 0; - foreach (var row in Rows) - { - numCols = System.Math.Max(numCols, row.Count); - } - - for (int c = 0; c < numCols; c++) _colWidths.Add(0); - - foreach (var row in Rows) - { - double rowAsc = 0; - double rowDesc = 0; - - for (int c = 0; c < row.Count; c++) - { - row[c].Measure(ctx); - _colWidths[c] = System.Math.Max(_colWidths[c], row[c].Width); - rowAsc = System.Math.Max(rowAsc, row[c].Ascent); - rowDesc = System.Math.Max(rowDesc, row[c].Descent); - } - - _rowAscents.Add(rowAsc); - _rowHeights.Add(rowAsc + rowDesc + (ctx.FontSize * 0.3)); - } - - double totalW = _colWidths.Sum() + (System.Math.Max(0, numCols - 1) * ctx.FontSize * 0.5); - double delimW = MatrixType switch - { - "matrix" => 0, - "cases" => ctx.FontSize * 0.6, - _ => ctx.FontSize * 0.8 - }; - - Width = totalW + delimW; - double totalH = _rowHeights.Sum(); - Ascent = totalH / 2; - Descent = totalH / 2; - Height = totalH; - } - - public override void Render(double x, double y, RenderContext ctx) - { - double startX = x + (MatrixType == "cases" ? ctx.BaseFontSize * 0.6 : (MatrixType != "matrix" ? ctx.BaseFontSize * 0.4 : 0)); - double curY = y - Ascent + (_rowAscents.FirstOrDefault()); - - // Draw Left Bracket/Delim - if (MatrixType == "pmatrix") - { - ctx.Svg.AppendLine($" ("); - ctx.Svg.AppendLine($" )"); - } - else if (MatrixType == "bmatrix") - { - ctx.Svg.AppendLine($" ["); - ctx.Svg.AppendLine($" ]"); - } - else if (MatrixType == "vmatrix") - { - double lineThick = System.Math.Max(1.0, ctx.BaseFontSize * 0.08); - double y1 = (y - Ascent).ToString("F1", CultureInfo.InvariantCulture) == "0.0" ? 0 : y - Ascent; - double y2 = y + Descent; - ctx.Svg.AppendLine($" "); - ctx.Svg.AppendLine($" "); - } - else if (MatrixType == "cases") - { - ctx.Svg.AppendLine($" {{"); - } - - for (int r = 0; r < Rows.Count; r++) - { - var row = Rows[r]; - double colX = startX; - - for (int c = 0; c < row.Count; c++) - { - double cellW = _colWidths[c]; - double alignX = colX + ((cellW - row[c].Width) / 2); // Center cell - row[c].Render(alignX, curY, ctx); - colX += cellW + (ctx.BaseFontSize * 0.5); - } - - if (r + 1 < Rows.Count) - { - curY += _rowHeights[r]; - } - } - } - } - - public class MathAccentNode : MathAstNode - { - public MathAstNode BaseNode { get; } - public string AccentType { get; } - - public MathAccentNode(MathAstNode baseNode, string accentType) - { - BaseNode = baseNode; - AccentType = accentType; - } - - public override void Measure(LayoutContext ctx) - { - BaseNode.Measure(ctx); - Width = BaseNode.Width; - Ascent = BaseNode.Ascent + (ctx.FontSize * 0.28); - Descent = BaseNode.Descent; - Height = Ascent + Descent; - } - - public override void Render(double x, double y, RenderContext ctx) - { - BaseNode.Render(x, y, ctx); - - double accentX = x + (BaseNode.Width / 2); - double accentY = y - BaseNode.Ascent - (ctx.BaseFontSize * 0.05); - - string accentSym = AccentType switch - { - "hat" => "^", - "bar" or "overline" => "¯", - "vec" => "→", - "dot" => "˙", - "ddot" => "¨", - "tilde" => "~", - _ => "^" - }; - - double accentSize = ctx.BaseFontSize * (AccentType is "dot" or "ddot" ? 1.1 : 0.85); - ctx.Svg.AppendLine($" {accentSym}"); - } - } - - public class MathSpaceNode : MathAstNode - { - public double SpaceFactor { get; } - - public MathSpaceNode(double spaceFactor = 0.3) - { - SpaceFactor = spaceFactor; - } - - public override void Measure(LayoutContext ctx) - { - Width = ctx.FontSize * SpaceFactor; - Ascent = ctx.FontSize * 0.5; - Descent = 0; - Height = Ascent; - } - - public override void Render(double x, double y, RenderContext ctx) - { - // Empty space, no rendering required - } - } - - // ========================================================================= - // PARSER - // ========================================================================= - public class MathParser - { - private readonly List _tokens; - private int _pos; - - public MathParser(List tokens) - { - _tokens = tokens; - _pos = 0; - } - - private Token Current => _pos < _tokens.Count ? _tokens[_pos] : _tokens[^1]; - private Token Peek(int offset = 1) => _pos + offset < _tokens.Count ? _tokens[_pos + offset] : _tokens[^1]; - - private Token Consume() - { - var t = Current; - _pos++; - return t; - } - - private bool Match(TokenType type) - { - if (Current.Type == type) - { - _pos++; - return true; - } - return false; - } - - private bool MatchCommand(string cmd) - { - if (Current.Type == TokenType.Command && Current.Value.Equals(cmd, StringComparison.OrdinalIgnoreCase)) - { - _pos++; - return true; - } - return false; - } - - public MathAstNode ParseExpression() - { - var seq = new MathSequenceNode(); - - while (Current.Type != TokenType.EOF && - Current.Type != TokenType.CloseBrace && - Current.Type != TokenType.CloseBracket && - Current.Type != TokenType.Ampersand && - Current.Type != TokenType.Newline && - !(Current.Type == TokenType.Command && Current.Value.Equals("end", StringComparison.OrdinalIgnoreCase))) - { - int beforePos = _pos; - var atom = ParseAtom(); - if (atom != null) - { - // Check for trailing subscript / superscript (e.g. x_1^2 or x^2_1) - atom = ParseSubSuper(atom); - seq.Children.Add(atom); - } - else if (_pos == beforePos) - { - // Skip unrecognized token to avoid infinite loop - Consume(); - } - } - - if (seq.Children.Count == 1) - { - return seq.Children[0]; - } - - return seq; - } - - private MathAstNode? ParseAtom() - { - var token = Current; - - // 1. Grouped Expression { ... } - if (Match(TokenType.OpenBrace)) - { - var group = ParseExpression(); - Match(TokenType.CloseBrace); - return group; - } - - // 2. Parenthesized ( ... ) - if (Match(TokenType.OpenParen)) - { - return new MathSymbolNode("(", false); - } - if (Match(TokenType.CloseParen)) - { - return new MathSymbolNode(")", false); - } - - // 3. Brackets [ ... ] - if (Match(TokenType.OpenBracket)) - { - return new MathSymbolNode("[", false); - } - if (Match(TokenType.CloseBracket)) - { - return new MathSymbolNode("]", false); - } - - // 4. Number - if (Match(TokenType.Number)) - { - return new MathTextNode(token.Value, isVariable: false, isUpright: true); - } - - // 5. Single Variable Letter - if (Match(TokenType.Identifier)) - { - return new MathTextNode(token.Value, isVariable: true); - } - - // 6. Operator - if (Match(TokenType.Operator)) - { - return new MathSymbolNode(token.Value, isOperator: true); - } - - // 7. LaTeX Commands - if (Current.Type == TokenType.Command) - { - return ParseCommand(); - } - - return null; - } - - private MathAstNode ParseCommand() - { - var cmdToken = Consume(); - string cmd = cmdToken.Value; - - // Fractions: \frac{a}{b}, \dfrac, \tfrac - if (cmd is "frac" or "dfrac" or "tfrac") - { - var num = ParseRequiredGroup(); - var den = ParseRequiredGroup(); - return new MathFractionNode(num, den); - } - - // Binomial: \binom{n}{k} - if (cmd is "binom" or "dbinom" or "tbinom") - { - var top = ParseRequiredGroup(); - var bot = ParseRequiredGroup(); - var frac = new MathFractionNode(top, bot); - return new MathDelimiterNode("(", ")", frac); - } - - // Radicals: \sqrt{x}, \sqrt[n]{x} - if (cmd is "sqrt") - { - MathAstNode? degree = null; - if (Match(TokenType.OpenBracket)) - { - degree = ParseExpression(); - Match(TokenType.CloseBracket); - } - var radicand = ParseRequiredGroup(); - return new MathRadicalNode(radicand, degree); - } - - // Large Operators: \sum, \prod, \int, \iint, \iiint, \oint, \coprod, \lim - if (cmd is "sum" or "prod" or "int" or "iint" or "iiint" or "oint" or "coprod" or "lim") - { - MathAstNode? lower = null; - MathAstNode? upper = null; - - while (Current.Type is TokenType.Subscript or TokenType.Superscript) - { - if (Match(TokenType.Subscript)) - { - lower = ParseRequiredGroup(); - } - else if (Match(TokenType.Superscript)) - { - upper = ParseRequiredGroup(); - } - } - - return new MathLargeOperatorNode(cmd, lower, upper, isDisplay: true); - } - - // Dynamic Delimiters: \left( ... \right), \left[ ... \right] - if (cmd is "left") - { - string leftDelim = Current.Value; - Consume(); - - var content = ParseExpression(); - - string rightDelim = ")"; - if (MatchCommand("right")) - { - rightDelim = Current.Value; - Consume(); - } - - return new MathDelimiterNode(leftDelim, rightDelim, content); - } - - // Matrices & Environments: \begin{matrix}, \begin{pmatrix}, \begin{cases} - if (cmd is "begin") - { - string envType = "matrix"; - if (Match(TokenType.OpenBrace)) - { - envType = Current.Value; - Consume(); - Match(TokenType.CloseBrace); - } - - return ParseMatrixEnvironment(envType); - } - - // Accents: \hat{x}, \bar{x}, \vec{x}, \dot{x}, \ddot{x}, \tilde{x} - if (cmd is "hat" or "bar" or "vec" or "dot" or "ddot" or "tilde" or "overline") - { - var baseNode = ParseRequiredGroup(); - return new MathAccentNode(baseNode, cmd); - } - - // Text & Font Styles: \text{...}, \mathrm{...}, \mathbf{...}, \mathbb{...}, \mathcal{...} - if (cmd is "text" or "mathrm" or "mathbf" or "mathit" or "mathsf" or "mathtt") - { - var inner = ParseRequiredGroup(); - return inner; - } - - if (cmd is "mathbb") - { - var inner = ParseRequiredGroup(); - if (inner is MathTextNode textNode && _blackboardBold.TryGetValue(textNode.Text, out var bbSym)) - { - return new MathSymbolNode(bbSym, isOperator: false); - } - return inner; - } - - if (cmd is "mathcal") - { - var inner = ParseRequiredGroup(); - if (inner is MathTextNode textNode && _calligraphic.TryGetValue(textNode.Text, out var calSym)) - { - return new MathSymbolNode(calSym, isOperator: false); - } - return inner; - } - - // Standard Functions: \sin, \cos, \tan, \ln, \log, \exp, \det, \dim, \ker - if (_standardFunctions.Contains(cmd)) - { - return new MathTextNode(cmd, isVariable: false, isUpright: true); - } - - // Greek Alphabet - if (_greekSymbols.TryGetValue(cmd, out var greek)) - { - return new MathSymbolNode(greek.unicode, greek.isOperator); - } - - // Mathematical Symbols & Operators - if (_mathSymbols.TryGetValue(cmd, out var mathSym)) - { - return new MathSymbolNode(mathSym, isOperator: true); - } - - // Spacing: \quad, \qquad, \,, \:, \;, \! - if (cmd is "quad") return new MathSpaceNode(1.0); - if (cmd is "qquad") return new MathSpaceNode(2.0); - if (cmd is "\\," or ",") return new MathSpaceNode(0.2); - if (cmd is "\\:" or ":") return new MathSpaceNode(0.3); - if (cmd is "\\;" or ";") return new MathSpaceNode(0.4); - if (cmd is "\\!" or "!") return new MathSpaceNode(-0.15); - - // Fallback for custom or unrecognized commands: render text - return new MathTextNode(cmd, isVariable: false, isUpright: true); - } - - private MathAstNode ParseMatrixEnvironment(string envType) - { - var matrixNode = new MathMatrixNode(envType); - var currentRow = new List(); - - while (Current.Type != TokenType.EOF) - { - if (Current.Type == TokenType.Command && Current.Value.Equals("end", StringComparison.OrdinalIgnoreCase)) - { - Consume(); - if (Match(TokenType.OpenBrace)) - { - Consume(); // Consume env name - Match(TokenType.CloseBrace); - } - break; - } - - if (Match(TokenType.Ampersand)) - { - // Next column - continue; - } - - if (Match(TokenType.Newline)) - { - // Next row - if (currentRow.Count > 0) - { - matrixNode.Rows.Add(currentRow); - currentRow = new List(); - } - continue; - } - - int beforePos = _pos; - var cellExpr = ParseExpression(); - if (cellExpr != null) - { - currentRow.Add(cellExpr); - } - if (_pos == beforePos && Current.Type != TokenType.EOF) - { - Consume(); - } - } - - if (currentRow.Count > 0) - { - matrixNode.Rows.Add(currentRow); - } - - return matrixNode; - } - - private MathAstNode ParseSubSuper(MathAstNode baseNode) - { - MathAstNode? sub = null; - MathAstNode? sup = null; - - while (Current.Type is TokenType.Subscript or TokenType.Superscript) - { - if (Match(TokenType.Subscript)) - { - sub = ParseRequiredGroup(); - } - else if (Match(TokenType.Superscript)) - { - sup = ParseRequiredGroup(); - } - } - - if (sub != null || sup != null) - { - return new MathSubSuperNode(baseNode, sub, sup); - } - - return baseNode; - } - - private MathAstNode ParseRequiredGroup() - { - if (Match(TokenType.OpenBrace)) - { - var group = ParseExpression(); - Match(TokenType.CloseBrace); - return group; - } - - var atom = ParseAtom(); - return atom ?? new MathTextNode("", false); - } - } -} diff --git a/src/PdfEditorApp/Services/Math/MathPresetsLibrary.cs b/src/PdfEditorApp/Services/Math/MathPresetsLibrary.cs deleted file mode 100644 index 166150d..0000000 --- a/src/PdfEditorApp/Services/Math/MathPresetsLibrary.cs +++ /dev/null @@ -1,525 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Services.MathEngine; - -public record MathPresetItem( - string Id, - string Name, - string Formula, - MathCategory Category, - string Description, - string DefaultEquationNumber, - double DefaultWidth = 340, - double DefaultHeight = 55 -); - -public static class MathPresetsLibrary -{ - private static readonly List _presets = new() - { - // ========================================== - // 0. SCHOOL ARITHMETIC & WORKSHEET MATH - // ========================================== - new( - "bodmas_order", - "BODMAS Order of Operations", - @"\text{BODMAS: } [\;] \to \{\;\} \to (\;) \to \text{Of} \to \div \to \times \to + \to -", - MathCategory.SchoolArithmetic, - "Fundamental rule for evaluating nested arithmetic expressions", - "(1)", - 360, 48 - ), - new( - "simple_interest", - "Simple Interest & Amount Formula", - @"SI = \frac{P \times R \times T}{100}, \quad A = P + SI = P\left(1 + \frac{RT}{100}\right)", - MathCategory.SchoolArithmetic, - "Standard commercial math formula for principal P, rate R%, and time T years", - "(2)", - 380, 52 - ), - new( - "compound_interest", - "Compound Interest Formula", - @"A = P\left(1 + \frac{r}{n}\right)^{nt}, \quad CI = A - P", - MathCategory.SchoolArithmetic, - "Compound interest compounded n times per year over t periods", - "(3)", - 320, 50 - ), - - // ========================================== - // 1. CALCULUS & ANALYSIS - // ========================================== - new( - "gaussian_integral", - "Gaussian Integral", - @"\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}", - MathCategory.Calculus, - "Euler-Poisson definite integral over the entire real line", - "(1)", - 260, 48 - ), - new( - "fundamental_calculus", - "Fundamental Theorem of Calculus", - @"\int_{a}^{b} f(x) \, dx = F(b) - F(a) = \left[ F(x) \right]_{a}^{b}", - MathCategory.Calculus, - "Connecting derivatives and definite integrals", - "(2)", - 320, 50 - ), - new( - "taylor_series", - "Taylor Series Expansion", - @"f(x) = \sum_{n=0}^{\infty} \frac{f^{(n)}(a)}{n!} (x - a)^n", - MathCategory.Calculus, - "Infinite power series representation of a smooth function", - "(3)", - 300, 52 - ), - new( - "fourier_transform", - "Continuous Fourier Transform", - @"\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) \, e^{-2\pi i x \xi} \, dx", - MathCategory.Calculus, - "Frequency spectrum decomposition of a time-domain signal", - "(4)", - 300, 50 - ), - new( - "laplace_transform", - "Laplace Transform", - @"\mathcal{L}\{f(t)\} = F(s) = \int_{0}^{\infty} f(t) \, e^{-st} \, dt", - MathCategory.Calculus, - "Integral transform mapping time functions to complex frequency s-plane", - "(5)", - 320, 50 - ), - new( - "cauchy_residue", - "Cauchy's Residue Theorem", - @"\oint_{\gamma} f(z) \, dz = 2\pi i \sum_{k=1}^n \operatorname{Res}(f, a_k)", - MathCategory.Calculus, - "Contour integral evaluation via isolated singularities", - "(6)", - 310, 52 - ), - new( - "greens_theorem", - "Green's Theorem in the Plane", - @"\oint_{C} (L \, dx + M \, dy) = \iint_{D} \left( \frac{\partial M}{\partial x} - \frac{\partial L}{\partial y} \right) dx \, dy", - MathCategory.Calculus, - "Relating line integrals around closed curves to 2D double integrals", - "(7)", - 380, 55 - ), - - // ========================================== - // 2. PHYSICS & RELATIVITY - // ========================================== - new( - "mass_energy", - "Mass-Energy Equivalence", - @"E = m c^2", - MathCategory.Physics, - "Einstein's rest-frame mass-energy equivalence relation", - "(1)", - 160, 42 - ), - new( - "relativistic_energy_momentum", - "Relativistic Energy-Momentum", - @"E^2 = (p c)^2 + (m_0 c^2)^2", - MathCategory.Physics, - "Energy of a particle with relativistic momentum p and rest mass m0", - "(2)", - 240, 45 - ), - new( - "maxwell_diff", - "Maxwell's Equations (Differential)", - @"\nabla \cdot \mathbf{E} = \frac{\rho}{\varepsilon_0}, \quad \nabla \times \mathbf{B} = \mu_0 \mathbf{J} + \mu_0 \varepsilon_0 \frac{\partial \mathbf{E}}{\partial t}", - MathCategory.Physics, - "Fundamental laws of classical electrodynamics", - "(3)", - 420, 52 - ), - new( - "schrodinger_time_dependent", - "Time-Dependent Schrödinger Equation", - @"i \hbar \frac{\partial}{\partial t} \Psi(\mathbf{r}, t) = \hat{H} \Psi(\mathbf{r}, t)", - MathCategory.Physics, - "Quantum state time evolution governed by the Hamiltonian operator", - "(4)", - 300, 50 - ), - new( - "heisenberg_uncertainty", - "Heisenberg Uncertainty Principle", - @"\sigma_x \sigma_p \ge \frac{\hbar}{2}", - MathCategory.Physics, - "Fundamental lower bound on position-momentum measurement precision", - "(5)", - 190, 48 - ), - new( - "dirac_equation", - "Dirac Relativistic Wave Equation", - @"(i \gamma^\mu \partial_\mu - m) \psi = 0", - MathCategory.Physics, - "Relativistic quantum mechanics for spin-1/2 fermions predicting antimatter", - "(6)", - 230, 46 - ), - new( - "navier_stokes", - "Navier-Stokes Fluid Dynamics", - @"\rho \left( \frac{\partial \mathbf{u}}{\partial t} + \mathbf{u} \cdot \nabla \mathbf{u} \right) = -\nabla p + \mu \nabla^2 \mathbf{u} + \mathbf{f}", - MathCategory.Physics, - "Momentum balance equation for incompressible viscous fluid flows", - "(7)", - 420, 55 - ), - - // ========================================== - // 3. QUANTUM MECHANICS & CQED - // ========================================== - new( - "dicke_hamiltonian", - "Dicke Cavity QED Hamiltonian", - @"\hat{H} = \hbar \omega_c \hat{a}^\dagger \hat{a} + \frac{1}{2} \hbar \omega_q \sum_{j=1}^N \hat{\sigma}_z^{(j)} + \frac{\hbar g}{\sqrt{N}} \sum_{j=1}^N (\hat{a}^\dagger \hat{\sigma}_-^{(j)} + \hat{a} \hat{\sigma}_+^{(j)})", - MathCategory.QuantumMechanics, - "Collective interaction between an array of N transmons and a cavity mode", - "(1)", - 460, 56 - ), - new( - "lindblad_master_equation", - "Lindblad Open-System Master Equation", - @"\frac{\partial \hat{\rho}}{\partial t} = -\frac{i}{\hbar} [\hat{H}, \hat{\rho}] + \sum_k \gamma_k \left( \hat{L}_k \hat{\rho} \hat{L}_k^\dagger - \frac{1}{2} \{ \hat{L}_k^\dagger \hat{L}_k, \hat{\rho} \} \right)", - MathCategory.QuantumMechanics, - "Markovian density matrix evolution with dissipation and dephasing channels", - "(2)", - 440, 58 - ), - new( - "wigner_distribution", - "Wigner Quasi-Probability Function", - @"W(\alpha) = \frac{2}{\pi} \operatorname{Tr} \left[ \hat{\rho} \, \hat{D}(\alpha) \, (-1)^{\hat{a}^\dagger \hat{a}} \, \hat{D}^\dagger(\alpha) \right]", - MathCategory.QuantumMechanics, - "Phase-space representation for quantum state tomography and squeezing", - "(3)", - 360, 52 - ), - new( - "pauli_matrices", - "Pauli Spin-1/2 Matrices", - @"\hat{\sigma}_x = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}, \quad \hat{\sigma}_y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix}, \quad \hat{\sigma}_z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}", - MathCategory.QuantumMechanics, - "Standard Pauli matrix representations for 2-level qubit states", - "(4)", - 420, 54 - ), - - // ========================================== - // 4. QUANTITATIVE FINANCE & ECONOMETRICS - // ========================================== - new( - "bates_jump_diffusion", - "Bates Jump-Diffusion SDE System", - @"dS_t = (\mu - \lambda \bar{k}) S_t dt + \sqrt{V_t} S_t dW_t^S + (e^J - 1) S_t dN_t", - MathCategory.Finance, - "Continuous-time asset pricing with stochastic volatility and Poisson jump arrivals", - "(1)", - 400, 50 - ), - new( - "heston_variance_sde", - "Heston CIR Stochastic Variance Process", - @"dV_t = \kappa (\theta - V_t) dt + \sigma_v \sqrt{V_t} dW_t^V, \quad d\langle W^S, W^V \rangle_t = \rho dt", - MathCategory.Finance, - "Mean-reverting square-root variance process with leverage correlation", - "(2)", - 410, 52 - ), - new( - "black_scholes_pde", - "Black-Scholes-Merton PDE", - @"\frac{\partial V}{\partial t} + \frac{1}{2} \sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + r S \frac{\partial V}{\partial S} - r V = 0", - MathCategory.Finance, - "Linear parabolic PDE for European contingent claim derivative pricing", - "(3)", - 380, 52 - ), - new( - "sharpe_ratio", - "Annualized Sharpe Ratio", - @"SR = \frac{\mathbb{E}[R_p - R_f]}{\sigma(R_p - R_f)} \cdot \sqrt{252}", - MathCategory.Finance, - "Risk-adjusted excess return per unit of annualized total portfolio volatility", - "(4)", - 260, 50 - ), - new( - "capm_pricing", - "Capital Asset Pricing Model (CAPM)", - @"\mathbb{E}[R_i] = R_f + \beta_i (\mathbb{E}[R_m] - R_f), \quad \beta_i = \frac{\operatorname{Cov}(R_i, R_m)}{\operatorname{Var}(R_m)}", - MathCategory.Finance, - "Equilibrium asset pricing model relating expected return to systematic market beta", - "(5)", - 400, 52 - ), - - // ========================================== - // 5. ALGEBRA & DISCRETE MATHEMATICS - // ========================================== - new( - "quadratic_formula", - "Quadratic Formula", - @"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}", - MathCategory.Algebra, - "Exact analytic roots of the general quadratic equation ax² + bx + c = 0", - "(1)", - 240, 48 - ), - new( - "eulers_identity", - "Euler's Beautiful Identity", - @"e^{i\pi} + 1 = 0", - MathCategory.Algebra, - "Profound identity linking the fundamental constants e, i, π, 1, and 0", - "(2)", - 160, 42 - ), - new( - "binomial_theorem", - "Binomial Theorem", - @"(x + y)^n = \sum_{k=0}^{n} \binom{n}{k} x^{n-k} y^k", - MathCategory.Algebra, - "Algebraic expansion of powers of a binomial sum using combinations", - "(3)", - 280, 48 - ), - new( - "matrix_2x2_inverse", - "2x2 Matrix Inversion", - @"A^{-1} = \frac{1}{ad - bc} \begin{pmatrix} d & -b \\ -c & a \end{pmatrix}", - MathCategory.Algebra, - "Analytic inverse of a 2×2 non-singular matrix with determinant ad - bc", - "(4)", - 300, 54 - ), - new( - "algebraic_identity_square", - "Algebraic Identities: Square of Binomial", - @"(a \pm b)^2 = a^2 \pm 2ab + b^2, \quad a^2 - b^2 = (a - b)(a + b)", - MathCategory.Algebra, - "Fundamental identities for expanding and factorizing quadratic binomials", - "(5)", - 350, 48 - ), - new( - "algebraic_identity_cubes", - "Algebraic Identities: Sum and Difference of Cubes", - @"a^3 \pm b^3 = (a \pm b)(a^2 \mp ab + b^2), \quad (a \pm b)^3 = a^3 \pm 3a^2b + 3ab^2 \pm b^3", - MathCategory.Algebra, - "Cubic binomial and trinomial expansion and factorization identities", - "(6)", - 420, 52 - ), - new( - "complex_number_sqrt", - "Square Root of Complex Number", - @"\sqrt{a + ib} = \pm \left( \sqrt{\frac{|z| + a}{2}} + i \, \text{sgn}(b) \sqrt{\frac{|z| - a}{2}} \right), \quad |z| = \sqrt{a^2 + b^2}", - MathCategory.Algebra, - "Standard analytic formula for computing principal square roots of complex numbers", - "(7)", - 430, 52 - ), - new( - "euler_polyhedron", - "Euler's Polyhedral Formula", - @"V - E + F = 2", - MathCategory.DiscreteMath, - "Topological invariant relating vertices, edges, and faces of convex polyhedra", - "(8)", - 170, 42 - ), - - // ========================================== - // 5B. GEOMETRY & TRIGONOMETRY - // ========================================== - new( - "quadrilateral_parallelogram_trapezoid", - "Area of Parallelogram & Trapezoid", - @"A_{\text{parallelogram}} = b \times h, \quad A_{\text{trapezoid}} = \frac{a + b}{2} \times h", - MathCategory.Geometry, - "Area formulas for quadrilaterals with parallel bases", - "(1)", - 350, 48 - ), - new( - "quadrilateral_rhombus_kite", - "Area of Rhombus & Kite", - @"A_{\text{rhombus}} = \frac{1}{2} d_1 d_2, \quad \text{Perimeter} = 4a", - MathCategory.Geometry, - "Area and perimeter formulas for equilateral and kite-shaped quadrilaterals", - "(2)", - 320, 48 - ), - new( - "pythagorean_theorem", - "Pythagorean Theorem", - @"a^2 + b^2 = c^2, \quad c = \sqrt{a^2 + b^2}", - MathCategory.Geometry, - "Fundamental geometric relationship between the sides of a right triangle", - "(3)", - 260, 44 - ), - - // ========================================== - // 6. STATISTICS & MACHINE LEARNING - // ========================================== - new( - "normal_distribution", - "Normal (Gaussian) Distribution PDF", - @"f(x) = \frac{1}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2}\left(\frac{x - \mu}{\sigma}\right)^2 \right)", - MathCategory.Statistics, - "Probability density function of the Gaussian distribution with mean μ and variance σ²", - "(1)", - 350, 54 - ), - new( - "bayes_theorem", - "Bayes' Conditional Probability Theorem", - @"P(A \mid B) = \frac{P(B \mid A) \, P(A)}{P(B)}", - MathCategory.Statistics, - "Computing posterior probability from likelihood, prior, and marginal evidence", - "(2)", - 250, 48 - ), - new( - "softmax_function", - "Softmax Activation Function", - @"\sigma(\mathbf{z})_i = \frac{e^{z_i}}{\sum_{j=1}^K e^{z_j}} \quad \text{for } i = 1, \dots, K", - MathCategory.Statistics, - "Probability distribution normalizer across K multi-class logit logits", - "(3)", - 320, 50 - ), - new( - "cross_entropy_loss", - "Categorical Cross-Entropy Loss", - @"\mathcal{L}_{\text{CE}} = -\sum_{i=1}^C y_i \log(\hat{y}_i)", - MathCategory.Statistics, - "Standard loss metric comparing true one-hot distribution y and predicted probabilities ŷ", - "(4)", - 260, 48 - ), - new( - "sample_variance", - "Sample Standard Deviation", - @"s = \sqrt{\frac{1}{N - 1} \sum_{i=1}^{N} (x_i - \bar{x})^2}", - MathCategory.Statistics, - "Unbiased sample variance and standard deviation with Bessel's correction N - 1", - "(5)", - 280, 50 - ), - - // ========================================== - // 7. CHEMISTRY & THERMODYNAMICS - // ========================================== - new( - "arrhenius_equation", - "Arrhenius Rate Equation", - @"k = A \, e^{-\frac{E_a}{R T}}", - MathCategory.Chemistry, - "Temperature dependence of chemical reaction rate constants and activation energy", - "(1)", - 200, 48 - ), - new( - "nernst_equation", - "Nernst Electrochemical Potential", - @"E = E^\circ - \frac{R T}{z F} \ln Q", - MathCategory.Chemistry, - "Reduction potential of an electrochemical cell as a function of standard potential and quotient Q", - "(2)", - 250, 50 - ), - new( - "ideal_gas_law", - "Ideal Gas State Equation", - @"P V = n R T = N k_B T", - MathCategory.Chemistry, - "Thermodynamic equation of state for hypothetical ideal gases", - "(3)", - 220, 42 - ) - }; - - public static IReadOnlyList AllPresets => _presets; - public static IReadOnlyList GetAllPresets() => _presets; - - public static IEnumerable GetByCategory(MathCategory category) => - _presets.Where(p => p.Category == category); - - public static MathPresetItem? FindById(string id) => - _presets.FirstOrDefault(p => p.Id.Equals(id, System.StringComparison.OrdinalIgnoreCase)); - - public static MathPresetItem? FindByName(string name) => - _presets.FirstOrDefault(p => p.Name.Equals(name, System.StringComparison.OrdinalIgnoreCase)); - - private static readonly Dictionary _snippets = new(System.StringComparer.OrdinalIgnoreCase) - { - ["frac"] = @"\frac{a}{b}", - ["sqrt"] = @"\sqrt{x}", - ["rootn"] = @"\sqrt[n]{x}", - ["sum"] = @"\sum_{i=1}^{n}", - ["prod"] = @"\prod_{i=1}^{n}", - ["int"] = @"\int_{a}^{b}", - ["iint"] = @"\iint", - ["iiint"] = @"\iiint", - ["oint"] = @"\oint", - ["lim"] = @"\lim_{x \to 0}", - ["dydx"] = @"\frac{\partial y}{\partial x}", - ["partial"] = @"\partial", - ["nabla"] = @"\nabla", - ["hbar"] = @"\hbar", - ["infty"] = @"\infty", - ["hat"] = @"\hat{H}", - ["vec"] = @"\vec{F}", - ["dot"] = @"\dot{x}", - ["ddot"] = @"\ddot{x}", - ["x2"] = @"x^{2}", - ["xi"] = @"x_{i}", - ["xi2"] = @"x_{i}^{2}", - ["binom"] = @"\binom{n}{k}", - ["pmatrix"] = @"\begin{pmatrix} a & b \\ c & d \end{pmatrix}", - ["bmatrix"] = @"\begin{bmatrix} a & b \\ c & d \end{bmatrix}", - ["cases"] = @"\begin{cases} x & x \ge 0 \\ -x & x < 0 \end{cases}", - ["parens"] = @"\left( \right)", - ["brackets"] = @"\left[ \right]", - ["set"] = @"\left\{ \right\}", - ["bra"] = @"\langle \psi |", - ["ket"] = @"| \psi \rangle", - ["bodmas"] = @"45 - [18 + (12 - 6)]", - ["si"] = @"SI = \frac{P \times R \times T}{100}", - ["modz"] = @"|z| = \sqrt{a^2 + b^2}", - ["quad"] = @"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}", - ["algsq"] = @"(a + b)^2 = a^2 + 2ab + b^2" - }; - - public static string ResolveSnippet(string snippet) - { - if (string.IsNullOrWhiteSpace(snippet)) return ""; - if (_snippets.TryGetValue(snippet.Trim(), out var expansion)) - { - return expansion; - } - return snippet; - } -} diff --git a/src/PdfEditorApp/Services/Navigation/NavigationRegistry.cs b/src/PdfEditorApp/Services/Navigation/NavigationRegistry.cs deleted file mode 100644 index 4d30ead..0000000 --- a/src/PdfEditorApp/Services/Navigation/NavigationRegistry.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Navigation; - -/// -/// Thread-safe registry for workspace pages and navigation sections. -/// -public sealed class NavigationRegistry : INavigationRegistry -{ - private readonly ConcurrentDictionary _items = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public IDisposable RegisterNavigationItem(NavigationItemDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _items[descriptor.Id] = descriptor; - RegistryChanged?.Invoke(); - - return new UnregisterDisposable(() => - { - UnregisterNavigationItem(descriptor.Id); - }); - } - - public bool UnregisterNavigationItem(string itemId) - { - if (_items.TryRemove(itemId, out _)) - { - RegistryChanged?.Invoke(); - return true; - } - return false; - } - - public IReadOnlyList GetAllItems() - { - return _items.Values.OrderBy(i => i.Order).ThenBy(i => i.Title).ToList(); - } - - public IReadOnlyList GetItemsByGroup(string group) - { - return _items.Values - .Where(i => string.Equals(i.Group, group, StringComparison.OrdinalIgnoreCase)) - .OrderBy(i => i.Order) - .ThenBy(i => i.Title) - .ToList(); - } - - public NavigationItemDescriptor? GetItem(string itemId) - { - _items.TryGetValue(itemId, out var item); - return item; - } - - private sealed class UnregisterDisposable : IDisposable - { - private Action? _action; - - public UnregisterDisposable(Action action) - { - _action = action; - } - - public void Dispose() - { - var act = System.Threading.Interlocked.Exchange(ref _action, null); - act?.Invoke(); - } - } -} diff --git a/src/PdfEditorApp/Services/Ocr/AppleVisionOcrEngine.cs b/src/PdfEditorApp/Services/Ocr/AppleVisionOcrEngine.cs deleted file mode 100644 index a0d6874..0000000 --- a/src/PdfEditorApp/Services/Ocr/AppleVisionOcrEngine.cs +++ /dev/null @@ -1,300 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Services.Ocr; - -/// -/// Ultra-fast hardware-accelerated OCR engine for macOS powered by Apple's Vision Framework -/// (VNRecognizeTextRequest / Apple Neural Engine / GPU). Requires zero downloads or external packages. -/// -public class AppleVisionOcrEngine : IOcrEngine -{ - public string EngineName => "Apple Vision OCR (macOS)"; - public OcrEngineType EngineType => OcrEngineType.OsNative; - public bool IsAvailable => OperatingSystem.IsMacOS(); - - [StructLayout(LayoutKind.Sequential)] - private struct NativeCGRect - { - public double X; - public double Y; - public double Width; - public double Height; - } - - public Task RecognizeTextAsync(byte[] imageBytes, string language = "eng", CancellationToken ct = default) - { - if (!IsAvailable) - { - return Task.FromResult(new OcrResult - { - Success = false, - ErrorMessage = "Apple Vision OCR is only supported on macOS." - }); - } - - return Task.Run(() => - { - var sw = Stopwatch.StartNew(); - try - { - var result = RunVisionOcr(imageBytes, language, ct); - sw.Stop(); - result.DurationMs = sw.ElapsedMilliseconds; - result.EngineUsed = EngineName; - return result; - } - catch (Exception ex) - { - sw.Stop(); - return new OcrResult - { - Success = false, - ErrorMessage = $"Apple Vision OCR failed: {ex.Message}", - DurationMs = sw.ElapsedMilliseconds, - EngineUsed = EngineName - }; - } - }, ct); - } - - private static OcrResult RunVisionOcr(byte[] imageBytes, string language, CancellationToken ct) - { - if (imageBytes == null || imageBytes.Length == 0) - { - return new OcrResult { Success = false, ErrorMessage = "Empty image bytes." }; - } - - // Initialize frameworks - NativeLibrary.Load("/System/Library/Frameworks/Foundation.framework/Foundation"); - NativeLibrary.Load("/System/Library/Frameworks/Vision.framework/Vision"); - - IntPtr autoreleasePool = objc_msgSend(objc_getClass("NSAutoreleasePool"), sel_registerName("alloc")); - autoreleasePool = objc_msgSend(autoreleasePool, sel_registerName("init")); - - try - { - // Create NSData from image bytes - IntPtr nsData = IntPtr.Zero; - var handle = GCHandle.Alloc(imageBytes, GCHandleType.Pinned); - try - { - IntPtr pBytes = handle.AddrOfPinnedObject(); - nsData = objc_msgSend_IntPtr_ulong( - objc_getClass("NSData"), - sel_registerName("dataWithBytes:length:"), - pBytes, - (ulong)imageBytes.Length); - } - finally - { - handle.Free(); - } - - if (nsData == IntPtr.Zero) - { - return new OcrResult { Success = false, ErrorMessage = "Failed to allocate NSData for image." }; - } - - // Create VNRecognizeTextRequest - IntPtr request = objc_msgSend(objc_getClass("VNRecognizeTextRequest"), sel_registerName("alloc")); - request = objc_msgSend(request, sel_registerName("init")); - - // Recognition level: 1 = Accurate, 0 = Fast - objc_msgSend_void_long(request, sel_registerName("setRecognitionLevel:"), 1); - objc_msgSend_void_bool(request, sel_registerName("setUsesLanguageCorrection:"), true); - - // Create VNImageRequestHandler with NSData and options - IntPtr emptyDict = objc_msgSend(objc_getClass("NSDictionary"), sel_registerName("dictionary")); - IntPtr handler = objc_msgSend(objc_getClass("VNImageRequestHandler"), sel_registerName("alloc")); - handler = objc_msgSend_IntPtr_IntPtr(handler, sel_registerName("initWithData:options:"), nsData, emptyDict); - - // Create NSArray with [request] - IntPtr reqArray = objc_msgSend_IntPtr(objc_getClass("NSArray"), sel_registerName("arrayWithObject:"), request); - - // Perform requests - IntPtr pError = IntPtr.Zero; - bool success = objc_msgSend_bool_IntPtr_outIntPtr(handler, sel_registerName("performRequests:error:"), reqArray, out pError); - - if (!success) - { - return new OcrResult { Success = false, ErrorMessage = "Vision request execution returned false." }; - } - - ct.ThrowIfCancellationRequested(); - - // Read results: NSArray - IntPtr resultsArray = objc_msgSend(request, sel_registerName("results")); - ulong count = resultsArray != IntPtr.Zero ? objc_msgSend_ulong(resultsArray, sel_registerName("count")) : 0; - - var words = new List(); - var lines = new List(); - var fullTextSb = new StringBuilder(); - - for (ulong i = 0; i < count; i++) - { - ct.ThrowIfCancellationRequested(); - - IntPtr observation = objc_msgSend_ulong_arg(resultsArray, sel_registerName("objectAtIndex:"), i); - if (observation == IntPtr.Zero) continue; - - // [observation topCandidates:1] -> NSArray - IntPtr candidates = objc_msgSend_ulong_arg(observation, sel_registerName("topCandidates:"), 1); - ulong candCount = candidates != IntPtr.Zero ? objc_msgSend_ulong(candidates, sel_registerName("count")) : 0; - if (candCount == 0) continue; - - IntPtr candidate = objc_msgSend_ulong_arg(candidates, sel_registerName("objectAtIndex:"), 0); - if (candidate == IntPtr.Zero) continue; - - // String value - IntPtr nsString = objc_msgSend(candidate, sel_registerName("string")); - string text = GetUtf8String(nsString); - if (string.IsNullOrWhiteSpace(text)) continue; - - // Confidence - float confidence = objc_msgSend_float(candidate, sel_registerName("confidence")); - - // BoundingBox: normalized CGRect in Vision coordinates (0,0 is bottom-left) - NativeCGRect visionBox = GetObservationBoundingBox(observation); - - // Convert Vision coordinates (bottom-left) to top-left normalized coordinates - // Vision: X=0..1 (left-to-right), Y=0..1 (bottom-to-top) - // Top-left: X=X, Y=1.0 - (Y + Height) - double topY = Math.Max(0.0, Math.Min(1.0, 1.0 - (visionBox.Y + visionBox.Height))); - var lineBounds = new OcrBoundingBox( - Math.Max(0.0, visionBox.X), - topY, - Math.Max(0.001, visionBox.Width), - Math.Max(0.001, visionBox.Height)); - - var lineItem = new OcrLineItem - { - Text = text, - NormalizedBounds = lineBounds - }; - - // Split text into individual words and estimate/calculate sub-word bounding boxes - string[] tokens = text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length > 0) - { - double currentWordLeft = lineBounds.X; - double totalChars = Math.Max(1, text.Length); - - foreach (var token in tokens) - { - double wordWidthFraction = (double)token.Length / totalChars; - double wordWidth = lineBounds.Width * wordWidthFraction; - - var wordItem = new OcrWordItem - { - Text = token, - NormalizedBounds = new OcrBoundingBox( - Math.Max(0.0, currentWordLeft), - lineBounds.Y, - Math.Max(0.001, wordWidth), - lineBounds.Height), - Confidence = confidence - }; - - lineItem.Words.Add(wordItem); - words.Add(wordItem); - - // Advance horizontal position including inter-word space - double spaceWidth = (lineBounds.Width / totalChars); - currentWordLeft += wordWidth + spaceWidth; - } - } - - lines.Add(lineItem); - fullTextSb.AppendLine(text); - } - - return new OcrResult - { - Success = true, - FullText = fullTextSb.ToString().TrimEnd(), - Lines = lines, - Words = words - }; - } - finally - { - if (autoreleasePool != IntPtr.Zero) - { - objc_msgSend(autoreleasePool, sel_registerName("drain")); - } - } - } - - private static string GetUtf8String(IntPtr nsString) - { - if (nsString == IntPtr.Zero) return string.Empty; - IntPtr utf8Ptr = objc_msgSend(nsString, sel_registerName("UTF8String")); - return utf8Ptr != IntPtr.Zero ? Marshal.PtrToStringUTF8(utf8Ptr) ?? string.Empty : string.Empty; - } - - private static NativeCGRect GetObservationBoundingBox(IntPtr observation) - { - // On macOS 64-bit (both arm64 and x86_64), structs with 4 doubles can be retrieved - // reliably through objc_msgSend with NativeCGRect return type. - try - { - return objc_msgSend_stret_rect(observation, sel_registerName("boundingBox")); - } - catch - { - return new NativeCGRect { X = 0, Y = 0, Width = 1, Height = 0.05 }; - } - } - - #region Objective-C Runtime P/Invoke - - [DllImport("libobjc.dylib", EntryPoint = "objc_getClass")] - private static extern IntPtr objc_getClass(string name); - - [DllImport("libobjc.dylib", EntryPoint = "sel_registerName")] - private static extern IntPtr sel_registerName(string name); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern IntPtr objc_msgSend_IntPtr(IntPtr receiver, IntPtr selector, IntPtr arg1); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern IntPtr objc_msgSend_IntPtr_IntPtr(IntPtr receiver, IntPtr selector, IntPtr arg1, IntPtr arg2); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern IntPtr objc_msgSend_IntPtr_ulong(IntPtr receiver, IntPtr selector, IntPtr arg1, ulong arg2); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern void objc_msgSend_void_long(IntPtr receiver, IntPtr selector, long arg1); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern void objc_msgSend_void_bool(IntPtr receiver, IntPtr selector, bool arg1); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern ulong objc_msgSend_ulong(IntPtr receiver, IntPtr selector); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern IntPtr objc_msgSend_ulong_arg(IntPtr receiver, IntPtr selector, ulong index); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern float objc_msgSend_float(IntPtr receiver, IntPtr selector); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - [return: MarshalAs(UnmanagedType.I1)] - private static extern bool objc_msgSend_bool_IntPtr_outIntPtr(IntPtr receiver, IntPtr selector, IntPtr arg1, out IntPtr arg2); - - [DllImport("libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern NativeCGRect objc_msgSend_stret_rect(IntPtr receiver, IntPtr selector); - - #endregion -} diff --git a/src/PdfEditorApp/Services/Ocr/CompositeOcrProvider.cs b/src/PdfEditorApp/Services/Ocr/CompositeOcrProvider.cs deleted file mode 100644 index 286fbfe..0000000 --- a/src/PdfEditorApp/Services/Ocr/CompositeOcrProvider.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Services.Ocr; - -public interface ICompositeOcrProvider : IOcrEngine -{ - OcrEngineType PreferredEngine { get; set; } - ITesseractModelService ModelService { get; } - IReadOnlyList AvailableEngines { get; } - IOcrEngine ActiveEngine { get; } - void RegisterEngine(IOcrEngine engine); -} - -public class CompositeOcrProvider : ICompositeOcrProvider -{ - private static CompositeOcrProvider? _instance; - public static CompositeOcrProvider Default => _instance ??= new CompositeOcrProvider(); - - private readonly List _engines = new(); - private readonly ITesseractModelService _modelService; - private readonly AppleVisionOcrEngine _appleEngine; - private readonly WindowsMediaOcrEngine _windowsEngine; - private readonly TesseractOcrEngine _tesseractEngine; - - public string EngineName => ActiveEngine.EngineName; - public OcrEngineType EngineType => ActiveEngine.EngineType; - public bool IsAvailable => ActiveEngine.IsAvailable; - public OcrEngineType PreferredEngine { get; set; } = OcrEngineType.Auto; - public ITesseractModelService ModelService => _modelService; - public IReadOnlyList AvailableEngines => _engines; - - public IOcrEngine ActiveEngine - { - get - { - if (PreferredEngine == OcrEngineType.OsNative) - { - if (OperatingSystem.IsMacOS()) return _appleEngine; - if (OperatingSystem.IsWindows()) return _windowsEngine; - } - else if (PreferredEngine == OcrEngineType.Tesseract) - { - return _tesseractEngine; - } - - // Auto mode: prioritize OS Native for zero-download instant execution - if (OperatingSystem.IsMacOS() && _appleEngine.IsAvailable) return _appleEngine; - if (OperatingSystem.IsWindows() && _windowsEngine.IsAvailable) return _windowsEngine; - return _tesseractEngine; - } - } - - public CompositeOcrProvider(ITesseractModelService? modelService = null) - { - _modelService = modelService ?? new TesseractModelService(); - _appleEngine = new AppleVisionOcrEngine(); - _windowsEngine = new WindowsMediaOcrEngine(); - _tesseractEngine = new TesseractOcrEngine(_modelService); - - if (_appleEngine.IsAvailable) _engines.Add(_appleEngine); - if (_windowsEngine.IsAvailable) _engines.Add(_windowsEngine); - if (_tesseractEngine.IsAvailable) _engines.Add(_tesseractEngine); - } - - public void RegisterEngine(IOcrEngine engine) - { - ArgumentNullException.ThrowIfNull(engine); - if (!_engines.Any(e => e.EngineName == engine.EngineName)) - { - _engines.Add(engine); - } - } - - public async Task RecognizeTextAsync(byte[] imageBytes, string language = "eng", CancellationToken ct = default) - { - var primary = ActiveEngine; - var result = await primary.RecognizeTextAsync(imageBytes, language, ct); - - if (result.Success && (result.Words.Count > 0 || !string.IsNullOrWhiteSpace(result.FullText))) - { - return result; - } - - // If primary engine produced nothing or failed and we're in Auto mode, try fallback - if (PreferredEngine == OcrEngineType.Auto && primary != _tesseractEngine && _tesseractEngine.IsAvailable) - { - var fallback = await _tesseractEngine.RecognizeTextAsync(imageBytes, language, ct); - if (fallback.Success) - { - return fallback; - } - } - - return result; - } -} diff --git a/src/PdfEditorApp/Services/Ocr/OcrEngineRegistry.cs b/src/PdfEditorApp/Services/Ocr/OcrEngineRegistry.cs deleted file mode 100644 index 3a2be22..0000000 --- a/src/PdfEditorApp/Services/Ocr/OcrEngineRegistry.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Ocr; - -public class OcrEngineRegistry : IOcrEngineRegistry -{ - private readonly ConcurrentDictionary _engines = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public OcrEngineRegistry(ITesseractModelService? modelService = null) - { - RegisterBuiltInEngines(modelService); - } - - private void RegisterBuiltInEngines(ITesseractModelService? modelService) - { - RegisterEngine(new AppleVisionOcrEngine()); - RegisterEngine(new WindowsMediaOcrEngine()); - RegisterEngine(new TesseractOcrEngine(modelService ?? new TesseractModelService())); - } - - public IDisposable RegisterEngine(IOcrEngine engine) - { - ArgumentNullException.ThrowIfNull(engine); - _engines[engine.EngineName] = engine; - RegistryChanged?.Invoke(); - - return new DisposableAction(() => - { - _engines.TryRemove(engine.EngineName, out _); - RegistryChanged?.Invoke(); - }); - } - - public IOcrEngine? GetEngine(string engineName) - { - if (string.IsNullOrWhiteSpace(engineName)) return null; - return _engines.GetValueOrDefault(engineName); - } - - public IReadOnlyList GetAllEngines() - { - return _engines.Values.ToList(); - } - - public IReadOnlyList GetAvailableEngines() - { - return _engines.Values.Where(e => e.IsAvailable).ToList(); - } - - private sealed class DisposableAction(Action action) : IDisposable - { - private Action? _action = action; - public void Dispose() => Interlocked.Exchange(ref _action, null)?.Invoke(); - } -} diff --git a/src/PdfEditorApp/Services/Ocr/TesseractModelService.cs b/src/PdfEditorApp/Services/Ocr/TesseractModelService.cs deleted file mode 100644 index 235789b..0000000 --- a/src/PdfEditorApp/Services/Ocr/TesseractModelService.cs +++ /dev/null @@ -1,368 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Services.Ocr; - -public interface ITesseractModelService -{ - event Action? LanguageLibraryChanged; - - string TessDataDirectory { get; } - IReadOnlyList AvailableLanguages { get; } - - bool IsLanguageInstalled(string languageCode); - string? GetInstalledLanguagePath(string languageCode); - - Task DownloadLanguageAsync( - string languageCode, - IProgress? progress = null, - Action? statusCallback = null, - CancellationToken ct = default); - - Task DeleteLanguageAsync(string languageCode); - Task ClearAllCacheAsync(); - Task GetTotalCacheSizeBytesAsync(); - - bool IsTesseractCliAvailable(); - string? GetTesseractCliPath(); - void RefreshInstalledStatuses(); -} - -public class TesseractModelService : ITesseractModelService -{ - public event Action? LanguageLibraryChanged; - - private static readonly HttpClient HttpClient = new(new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(10), - ConnectTimeout = TimeSpan.FromSeconds(15) - }) - { - Timeout = TimeSpan.FromMinutes(5) - }; - - static TesseractModelService() - { - HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd("FryPDF-OcrDownloader/1.0"); - } - - private readonly string _tessDataDir; - private readonly List _languages; - - public string TessDataDirectory => _tessDataDir; - public IReadOnlyList AvailableLanguages => _languages; - - public TesseractModelService(string? customTessDataDir = null) - { - _tessDataDir = customTessDataDir ?? Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".frypdf", - "tessdata"); - - if (!Directory.Exists(_tessDataDir)) - { - try { Directory.CreateDirectory(_tessDataDir); } catch { } - } - - _languages = InitializeCatalog(); - } - - public bool IsLanguageInstalled(string languageCode) - { - string path = Path.Combine(_tessDataDir, $"{languageCode}.traineddata"); - if (File.Exists(path) && new FileInfo(path).Length > 1000) - { - return true; - } - - // Fallback: check app assets/bundle - string appPath = Path.Combine(AppContext.BaseDirectory, "tessdata", $"{languageCode}.traineddata"); - return File.Exists(appPath) && new FileInfo(appPath).Length > 1000; - } - - public string? GetInstalledLanguagePath(string languageCode) - { - string userPath = Path.Combine(_tessDataDir, $"{languageCode}.traineddata"); - if (File.Exists(userPath) && new FileInfo(userPath).Length > 1000) - return userPath; - - string appPath = Path.Combine(AppContext.BaseDirectory, "tessdata", $"{languageCode}.traineddata"); - if (File.Exists(appPath) && new FileInfo(appPath).Length > 1000) - return appPath; - - return null; - } - - public void RefreshInstalledStatuses() - { - foreach (var lang in _languages) - { - lang.IsInstalled = IsLanguageInstalled(lang.Code); - } - LanguageLibraryChanged?.Invoke(); - } - - public async Task DownloadLanguageAsync( - string languageCode, - IProgress? progress = null, - Action? statusCallback = null, - CancellationToken ct = default) - { - var lang = _languages.FirstOrDefault(l => string.Equals(l.Code, languageCode, StringComparison.OrdinalIgnoreCase)); - if (lang == null) - { - // Custom or dynamic language code - lang = new TesseractLanguagePackageInfo - { - Code = languageCode.ToLowerInvariant(), - DisplayName = languageCode.ToUpperInvariant(), - DownloadUrl = $"https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/main/{languageCode.ToLowerInvariant()}.traineddata", - EstimatedSizeBytes = 4_000_000, - Category = "Custom" - }; - } - - Directory.CreateDirectory(_tessDataDir); - string targetPath = Path.Combine(_tessDataDir, lang.FileName); - string tempPath = targetPath + ".tmp"; - - if (File.Exists(targetPath) && new FileInfo(targetPath).Length > 1000) - { - progress?.Report(1.0); - statusCallback?.Invoke($"{lang.DisplayName} is already installed."); - lang.IsInstalled = true; - LanguageLibraryChanged?.Invoke(); - return true; - } - - statusCallback?.Invoke($"Connecting to download {lang.DisplayName} ({lang.Code})..."); - progress?.Report(0.05); - - try - { - using var response = await HttpClient.GetAsync(lang.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, ct); - response.EnsureSuccessStatusCode(); - - long totalExpected = response.Content.Headers.ContentLength ?? lang.EstimatedSizeBytes; - if (totalExpected <= 0) totalExpected = 4_000_000; - - using var sourceStream = await response.Content.ReadAsStreamAsync(ct); - using var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None); - - byte[] buffer = new byte[81920]; - long downloadedBytes = 0; - int bytesRead; - - while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0) - { - await fileStream.WriteAsync(buffer, 0, bytesRead, ct); - downloadedBytes += bytesRead; - - double p = Math.Min(0.98, (double)downloadedBytes / totalExpected); - progress?.Report(p); - statusCallback?.Invoke($"Downloading {lang.DisplayName}: {downloadedBytes / (1024 * 1024.0):F1} MB / {totalExpected / (1024 * 1024.0):F1} MB"); - } - - fileStream.Close(); - - if (File.Exists(targetPath)) - File.Delete(targetPath); - - File.Move(tempPath, targetPath); - - lang.IsInstalled = true; - progress?.Report(1.0); - statusCallback?.Invoke($"{lang.DisplayName} language pack installed successfully."); - LanguageLibraryChanged?.Invoke(); - return true; - } - catch (Exception ex) - { - try { if (File.Exists(tempPath)) File.Delete(tempPath); } catch { } - statusCallback?.Invoke($"Download failed: {ex.Message}"); - return false; - } - } - - public Task DeleteLanguageAsync(string languageCode) - { - try - { - string targetPath = Path.Combine(_tessDataDir, $"{languageCode}.traineddata"); - if (File.Exists(targetPath)) - { - File.Delete(targetPath); - } - - var lang = _languages.FirstOrDefault(l => string.Equals(l.Code, languageCode, StringComparison.OrdinalIgnoreCase)); - if (lang != null) - { - lang.IsInstalled = IsLanguageInstalled(lang.Code); - } - - LanguageLibraryChanged?.Invoke(); - return Task.FromResult(true); - } - catch - { - return Task.FromResult(false); - } - } - - public Task ClearAllCacheAsync() - { - try - { - if (Directory.Exists(_tessDataDir)) - { - var files = Directory.GetFiles(_tessDataDir, "*.traineddata") - .Concat(Directory.GetFiles(_tessDataDir, "*.tmp")); - foreach (var f in files) - { - try { File.Delete(f); } catch { } - } - } - - foreach (var lang in _languages) - { - lang.IsInstalled = IsLanguageInstalled(lang.Code); - } - - LanguageLibraryChanged?.Invoke(); - } - catch { } - - return Task.CompletedTask; - } - - public Task GetTotalCacheSizeBytesAsync() - { - try - { - if (!Directory.Exists(_tessDataDir)) - return Task.FromResult(0L); - - long total = 0; - var dirInfo = new DirectoryInfo(_tessDataDir); - foreach (var file in dirInfo.EnumerateFiles("*.traineddata", SearchOption.TopDirectoryOnly)) - { - total += file.Length; - } - return Task.FromResult(total); - } - catch - { - return Task.FromResult(0L); - } - } - - public bool IsTesseractCliAvailable() - { - return !string.IsNullOrEmpty(GetTesseractCliPath()); - } - - public string? GetTesseractCliPath() - { - string[] candidates = OperatingSystem.IsWindows() - ? new[] { "tesseract.exe", @"C:\Program Files\Tesseract-OCR\tesseract.exe", @"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe" } - : new[] { "tesseract", "/opt/homebrew/bin/tesseract", "/usr/local/bin/tesseract", "/usr/bin/tesseract" }; - - foreach (var candidate in candidates) - { - if (File.Exists(candidate)) return candidate; - } - - try - { - var psi = new ProcessStartInfo - { - FileName = OperatingSystem.IsWindows() ? "where" : "which", - Arguments = "tesseract", - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; - using var p = Process.Start(psi); - if (p != null) - { - string path = p.StandardOutput.ReadLine()?.Trim() ?? ""; - p.WaitForExit(1000); - if (File.Exists(path)) return path; - } - } - catch { } - - return null; - } - - private List InitializeCatalog() - { - string BaseUrl(string code) => $"https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/main/{code}.traineddata"; - - var list = new List - { - // === 1. Latin & European === - new() { Code = "eng", DisplayName = "English", NativeName = "English", FlagEmoji = "🇺🇸", Category = "Latin & European", EstimatedSizeBytes = 4_113_000, DownloadUrl = BaseUrl("eng"), Description = "Fast LSTM neural model for English documents, forms, reports, and books.", SampleText = "The quick brown fox jumps over the lazy dog." }, - new() { Code = "spa", DisplayName = "Spanish", NativeName = "Español", FlagEmoji = "🇪🇸", Category = "Latin & European", EstimatedSizeBytes = 1_470_000, DownloadUrl = BaseUrl("spa"), Description = "Modern Spanish language model with accented characters and punctuation.", SampleText = "El veloz murciélago hindú comía feliz cardillo y kiwi." }, - new() { Code = "fra", DisplayName = "French", NativeName = "Français", FlagEmoji = "🇫🇷", Category = "Latin & European", EstimatedSizeBytes = 1_870_000, DownloadUrl = BaseUrl("fra"), Description = "French language model supporting cedillas, ligatures, and acute accents.", SampleText = "Voix ambiguë d'un cœur qui au zéphyr préfère les jattes de kiwis." }, - new() { Code = "deu", DisplayName = "German", NativeName = "Deutsch", FlagEmoji = "🇩🇪", Category = "Latin & European", EstimatedSizeBytes = 2_200_000, DownloadUrl = BaseUrl("deu"), Description = "German language model with umlauts (ä, ö, ü) and Eszett (ß).", SampleText = "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich." }, - new() { Code = "ita", DisplayName = "Italian", NativeName = "Italiano", FlagEmoji = "🇮🇹", Category = "Latin & European", EstimatedSizeBytes = 1_680_000, DownloadUrl = BaseUrl("ita"), Description = "Italian language model for high-fidelity document OCR.", SampleText = "Quel vituperio di un pazzo fa sbadigliare la gente con zelo." }, - new() { Code = "por", DisplayName = "Portuguese", NativeName = "Português", FlagEmoji = "🇵🇹", Category = "Latin & European", EstimatedSizeBytes = 1_620_000, DownloadUrl = BaseUrl("por"), Description = "Portuguese model supporting European and Brazilian orthography.", SampleText = "Um pequeno jabuti xereta viu dez cegonhas felizes." }, - new() { Code = "nld", DisplayName = "Dutch", NativeName = "Nederlands", FlagEmoji = "🇳🇱", Category = "Latin & European", EstimatedSizeBytes = 1_600_000, DownloadUrl = BaseUrl("nld"), Description = "Dutch model supporting compounding and standard orthography.", SampleText = "Pa's wijze lynx bezag vroom het fijne cactussnoeiwerk." }, - new() { Code = "rus", DisplayName = "Russian", NativeName = "Русский", FlagEmoji = "🇷🇺", Category = "Latin & European", EstimatedSizeBytes = 3_800_000, DownloadUrl = BaseUrl("rus"), Description = "Cyrillic Russian language model for print and book OCR.", SampleText = "Съешь же ещё этих мягких французских булок, да выпей чаю." }, - new() { Code = "pol", DisplayName = "Polish", NativeName = "Polski", FlagEmoji = "🇵🇱", Category = "Latin & European", EstimatedSizeBytes = 1_800_000, DownloadUrl = BaseUrl("pol"), Description = "Polish language model with ogoneks, kreskas, and diacritics.", SampleText = "Pchnąć w tę łódź jeża lub ośm skrzyń fig." }, - new() { Code = "ukr", DisplayName = "Ukrainian", NativeName = "Українська", FlagEmoji = "🇺🇦", Category = "Latin & European", EstimatedSizeBytes = 2_600_000, DownloadUrl = BaseUrl("ukr"), Description = "Cyrillic Ukrainian model with apostrophes and specific vowels.", SampleText = "Чуєш, їхній плуг замінить рясну траву шовком." }, - new() { Code = "swe", DisplayName = "Swedish", NativeName = "Svenska", FlagEmoji = "🇸🇪", Category = "Latin & European", EstimatedSizeBytes = 1_400_000, DownloadUrl = BaseUrl("swe"), Description = "Swedish model supporting å, ä, ö and Scandinavian typography.", SampleText = "Flygande bäckasiner söka hwila på mjuk tuva." }, - new() { Code = "nor", DisplayName = "Norwegian", NativeName = "Norsk", FlagEmoji = "🇳🇴", Category = "Latin & European", EstimatedSizeBytes = 1_500_000, DownloadUrl = BaseUrl("nor"), Description = "Norwegian (Bokmål & Nynorsk) model with æ, ø, å support.", SampleText = "Vår sære ørn ble mager av sjøens ti fisk." }, - new() { Code = "dan", DisplayName = "Danish", NativeName = "Dansk", FlagEmoji = "🇩🇰", Category = "Latin & European", EstimatedSizeBytes = 1_400_000, DownloadUrl = BaseUrl("dan"), Description = "Danish model supporting æ, ø, å diacritics.", SampleText = "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen kiggede på." }, - new() { Code = "fin", DisplayName = "Finnish", NativeName = "Suomi", FlagEmoji = "🇫🇮", Category = "Latin & European", EstimatedSizeBytes = 1_800_000, DownloadUrl = BaseUrl("fin"), Description = "Finnish model tuned for agglutinative words and compound terms.", SampleText = "Viekas kettu kuoppaan lankesi heti aamutuimaan." }, - new() { Code = "ces", DisplayName = "Czech", NativeName = "Čeština", FlagEmoji = "🇨🇿", Category = "Latin & European", EstimatedSizeBytes = 1_700_000, DownloadUrl = BaseUrl("ces"), Description = "Czech language model supporting carons and acutes.", SampleText = "Příliš žluťoučký kůň úpěl ďábelské ódy." }, - new() { Code = "ell", DisplayName = "Greek", NativeName = "Ελληνικά", FlagEmoji = "🇬🇷", Category = "Latin & European", EstimatedSizeBytes = 1_800_000, DownloadUrl = BaseUrl("ell"), Description = "Modern Greek script model with monotonic accentuation.", SampleText = "Ξεσκεπάζω την ψυχοφθόρα βδελυγμία του καθενός." }, - new() { Code = "lat", DisplayName = "Latin", NativeName = "Latina", FlagEmoji = "🏛️", Category = "Latin & European", EstimatedSizeBytes = 1_300_000, DownloadUrl = BaseUrl("lat"), Description = "Classical and Ecclesiastical Latin recognition model.", SampleText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." }, - new() { Code = "tur", DisplayName = "Turkish", NativeName = "Türkçe", FlagEmoji = "🇹🇷", Category = "Latin & European", EstimatedSizeBytes = 1_500_000, DownloadUrl = BaseUrl("tur"), Description = "Turkish model with dotted/dotless i (İ/i, I/ı), ş, ğ.", SampleText = "Pijamalı hasta yağız şoföre çabucak güvendi." }, - - // === 2. East Asia (CJK) === - new() { Code = "chi_sim", DisplayName = "Chinese Simplified", NativeName = "简体中文", FlagEmoji = "🇨🇳", Category = "East Asia (CJK)", EstimatedSizeBytes = 11_200_000, DownloadUrl = BaseUrl("chi_sim"), Description = "Simplified Chinese neural model (Hanzi / Mandarin).", SampleText = "天地玄黄,宇宙洪荒。日月盈昃,辰宿列张。" }, - new() { Code = "chi_tra", DisplayName = "Chinese Traditional", NativeName = "繁體中文", FlagEmoji = "🇹🇼", Category = "East Asia (CJK)", EstimatedSizeBytes = 11_100_000, DownloadUrl = BaseUrl("chi_tra"), Description = "Traditional Chinese neural model for Hong Kong, Taiwan, and classic docs.", SampleText = "有朋自遠方來,不亦樂乎?博學而篤志,切問而近思。" }, - new() { Code = "jpn", DisplayName = "Japanese", NativeName = "日本語", FlagEmoji = "🇯🇵", Category = "East Asia (CJK)", EstimatedSizeBytes = 8_200_000, DownloadUrl = BaseUrl("jpn"), Description = "Japanese model supporting Kanji, Hiragana, and Katakana.", SampleText = "いろはにほへと ちりぬるを わかよたれそ つねならむ" }, - new() { Code = "kor", DisplayName = "Korean", NativeName = "한국어", FlagEmoji = "🇰🇷", Category = "East Asia (CJK)", EstimatedSizeBytes = 4_900_000, DownloadUrl = BaseUrl("kor"), Description = "Korean Hangul script neural model.", SampleText = "다람쥐 헌 쳇바퀴에 타고파. 나랏말싸미 듕귁에 달아." }, - - // === 3. South Asia (Indic) === - new() { Code = "hin", DisplayName = "Hindi", NativeName = "हिन्दी", FlagEmoji = "🇮🇳", Category = "South Asia (Indic)", EstimatedSizeBytes = 3_600_000, DownloadUrl = BaseUrl("hin"), Description = "Devanagari Hindi model for official, legal, and published documents.", SampleText = "सत्यमेव जयते। सभी मनुष्यों को गौरव और अधिकारों के विषय में समानता प्राप्त है।" }, - new() { Code = "ben", DisplayName = "Bengali", NativeName = "বাংলা", FlagEmoji = "🇧🇩", Category = "South Asia (Indic)", EstimatedSizeBytes = 2_500_000, DownloadUrl = BaseUrl("ben"), Description = "Bengali / Bangla script model for India and Bangladesh documents.", SampleText = "সকল মানুষ সমান মর্যাদা এবং অধিকার নিয়ে জন্মগ্রহণ করে।" }, - new() { Code = "tam", DisplayName = "Tamil", NativeName = "தமிழ்", FlagEmoji = "🇮🇳", Category = "South Asia (Indic)", EstimatedSizeBytes = 2_400_000, DownloadUrl = BaseUrl("tam"), Description = "Tamil script model with complex ligature recognition.", SampleText = "யாதும் ஊரே யாவரும் கேளிர்; தீதும் நன்றும் பிறர்தர வாரா." }, - new() { Code = "tel", DisplayName = "Telugu", NativeName = "తెలుగు", FlagEmoji = "🇮🇳", Category = "South Asia (Indic)", EstimatedSizeBytes = 2_300_000, DownloadUrl = BaseUrl("tel"), Description = "Telugu Dravidian script recognition model.", SampleText = "తెలుగు భాష అమృతము వంటిది. సర్వేజనా సుఖినోభవంతు." }, - new() { Code = "mar", DisplayName = "Marathi", NativeName = "मराठी", FlagEmoji = "🇮🇳", Category = "South Asia (Indic)", EstimatedSizeBytes = 2_800_000, DownloadUrl = BaseUrl("mar"), Description = "Marathi Devanagari model for government and educational records.", SampleText = "ज्ञान हीच खरी शक्ती आहे. सर्वांना समान संधी मिळायला हवी." }, - new() { Code = "san", DisplayName = "Sanskrit", NativeName = "संस्कृतम्", FlagEmoji = "🕉️", Category = "South Asia (Indic)", EstimatedSizeBytes = 3_200_000, DownloadUrl = BaseUrl("san"), Description = "Classical Sanskrit Devanagari model with Vedic accent markings.", SampleText = "सर्वे भवन्तु सुखिनः सर्वे सन्तु निरामयाः।" }, - new() { Code = "urd", DisplayName = "Urdu", NativeName = "اردو", FlagEmoji = "🇵🇰", Category = "South Asia (Indic)", EstimatedSizeBytes = 2_400_000, DownloadUrl = BaseUrl("urd"), Description = "Urdu Nastaliq / Perso-Arabic model for books and documents.", SampleText = "تمام انسان آزادی اور حقوق کے اعتبار سے برابر پیدا ہوئے ہیں۔" }, - - // === 4. Middle East & RTL === - new() { Code = "ara", DisplayName = "Arabic", NativeName = "العربية", FlagEmoji = "🇸🇦", Category = "Middle East & RTL", EstimatedSizeBytes = 2_100_000, DownloadUrl = BaseUrl("ara"), Description = "Modern Standard Arabic right-to-left document OCR.", SampleText = "يولد جميع الناس أحراراً ومتساوين في الكرامة والحقوق." }, - new() { Code = "heb", DisplayName = "Hebrew", NativeName = "עברית", FlagEmoji = "🇮🇱", Category = "Middle East & RTL", EstimatedSizeBytes = 1_500_000, DownloadUrl = BaseUrl("heb"), Description = "Modern and Biblical Hebrew script recognition model.", SampleText = "כל בני אדם נולדו בני חורין ושווים בכבודם ובזכויותיהם." }, - new() { Code = "fas", DisplayName = "Persian", NativeName = "فارسی", FlagEmoji = "🇮🇷", Category = "Middle East & RTL", EstimatedSizeBytes = 2_200_000, DownloadUrl = BaseUrl("fas"), Description = "Persian / Farsi Nastaliq and standard typeface model.", SampleText = "همه افراد بشر آزاد به دنیا می‌آیند و از لحاظ حیثیت و حقوق با هم برابرند." }, - - // === 5. Southeast Asia === - new() { Code = "vie", DisplayName = "Vietnamese", NativeName = "Tiếng Việt", FlagEmoji = "🇻🇳", Category = "Southeast Asia", EstimatedSizeBytes = 1_900_000, DownloadUrl = BaseUrl("vie"), Description = "Vietnamese Quốc ngữ model with multiple stacked diacritics.", SampleText = "Mọi người sinh ra đều được tự do và bình đẳng về nhân phẩm." }, - new() { Code = "tha", DisplayName = "Thai", NativeName = "ไทย", FlagEmoji = "🇹🇭", Category = "Southeast Asia", EstimatedSizeBytes = 2_500_000, DownloadUrl = BaseUrl("tha"), Description = "Thai script model without inter-word spacing.", SampleText = "มนุษย์ทุกคนเกิดมามีอิสระและเสมอภาคกันในศักดิ์ศรีและสิทธิ" }, - new() { Code = "ind", DisplayName = "Indonesian", NativeName = "Bahasa Indonesia", FlagEmoji = "🇮🇩", Category = "Southeast Asia", EstimatedSizeBytes = 1_400_000, DownloadUrl = BaseUrl("ind"), Description = "Bahasa Indonesia model for contracts, receipts, and forms.", SampleText = "Semua orang dilahirkan merdeka dan mempunyai martabat yang sama." }, - - // === 6. Specialized & Other === - new() { Code = "osd", DisplayName = "Orientation & Script", NativeName = "OSD", FlagEmoji = "🧭", Category = "Specialized & Other", EstimatedSizeBytes = 10_500_000, DownloadUrl = BaseUrl("osd"), Description = "Orientation and script detection neural model (detects page angle & script).", SampleText = "0° / 90° / 180° / 270° Orientation & Script Classifier" }, - new() { Code = "equ", DisplayName = "Math & Equations", NativeName = "Equation", FlagEmoji = "📐", Category = "Specialized & Other", EstimatedSizeBytes = 2_400_000, DownloadUrl = BaseUrl("equ"), Description = "Mathematical notation, operators, formulas, and equation recognition.", SampleText = "f(x) = ∫ e^(-x²) dx, ∑(a_n * x^n)" } - }; - - foreach (var item in list) - { - item.IsInstalled = IsLanguageInstalled(item.Code); - } - - return list; - } -} diff --git a/src/PdfEditorApp/Services/Ocr/TesseractOcrEngine.cs b/src/PdfEditorApp/Services/Ocr/TesseractOcrEngine.cs deleted file mode 100644 index acf61c5..0000000 --- a/src/PdfEditorApp/Services/Ocr/TesseractOcrEngine.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Services.Ocr; - -/// -/// Cross-platform Tesseract OCR engine that utilizes downloaded .traineddata language packs. -/// Supports execution via native Tesseract CLI/runtime with TSV/HOCR parsing for exact word bounds. -/// -public class TesseractOcrEngine : IOcrEngine -{ - private readonly ITesseractModelService _modelService; - - public string EngineName => "Tesseract OCR"; - public OcrEngineType EngineType => OcrEngineType.Tesseract; - public bool IsAvailable => true; - - public TesseractOcrEngine(ITesseractModelService modelService) - { - _modelService = modelService; - } - - public async Task RecognizeTextAsync(byte[] imageBytes, string language = "eng", CancellationToken ct = default) - { - if (imageBytes == null || imageBytes.Length == 0) - { - return new OcrResult { Success = false, ErrorMessage = "Empty image data provided." }; - } - - // Ensure the requested language pack is downloaded - if (!_modelService.IsLanguageInstalled(language)) - { - bool downloaded = await _modelService.DownloadLanguageAsync(language, null, null, ct); - if (!downloaded) - { - return new OcrResult - { - Success = false, - ErrorMessage = $"Language pack for '{language}' could not be downloaded." - }; - } - } - - string tessDataDir = _modelService.TessDataDirectory; - return await Task.Run(() => RunTesseractProcess(imageBytes, language, tessDataDir, ct), ct); - } - - private static OcrResult RunTesseractProcess(byte[] imageBytes, string language, string tessDataDir, CancellationToken ct) - { - string tempDir = Path.Combine(Path.GetTempPath(), "frypdf_ocr_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(tempDir); - - string imagePath = Path.Combine(tempDir, "input.png"); - string outputBase = Path.Combine(tempDir, "output"); - string tsvPath = outputBase + ".tsv"; - - try - { - File.WriteAllBytes(imagePath, imageBytes); - - // Locate tesseract executable - string? tesseractExe = FindTesseractExecutable(); - if (string.IsNullOrEmpty(tesseractExe)) - { - return new OcrResult - { - Success = false, - ErrorMessage = "Tesseract executable was not found on the system PATH. Use OS Native OCR (Apple Vision / Windows Media) for zero-dependency OCR.", - EngineUsed = "Tesseract" - }; - } - - var startInfo = new ProcessStartInfo - { - FileName = tesseractExe, - Arguments = $"\"{imagePath}\" \"{outputBase}\" -l {language} --tessdata-dir \"{tessDataDir}\" tsv", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using var process = Process.Start(startInfo); - if (process == null) - { - return new OcrResult { Success = false, ErrorMessage = "Failed to launch Tesseract process." }; - } - - process.WaitForExit(30000); - - if (!File.Exists(tsvPath)) - { - string err = process.StandardError.ReadToEnd(); - return new OcrResult { Success = false, ErrorMessage = $"Tesseract did not produce output: {err}" }; - } - - return ParseTsvOutput(tsvPath, imageBytes); - } - catch (Exception ex) - { - return new OcrResult { Success = false, ErrorMessage = $"Tesseract execution error: {ex.Message}" }; - } - finally - { - try { Directory.Delete(tempDir, recursive: true); } catch { } - } - } - - private static string? FindTesseractExecutable() - { - string[] candidates = OperatingSystem.IsWindows() - ? new[] { "tesseract.exe", @"C:\Program Files\Tesseract-OCR\tesseract.exe", @"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe" } - : new[] { "tesseract", "/opt/homebrew/bin/tesseract", "/usr/local/bin/tesseract", "/usr/bin/tesseract" }; - - foreach (var candidate in candidates) - { - if (File.Exists(candidate)) return candidate; - } - - // Try 'which' / 'where' - try - { - var psi = new ProcessStartInfo - { - FileName = OperatingSystem.IsWindows() ? "where" : "which", - Arguments = "tesseract", - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; - using var p = Process.Start(psi); - if (p != null) - { - string path = p.StandardOutput.ReadLine()?.Trim() ?? ""; - p.WaitForExit(2000); - if (File.Exists(path)) return path; - } - } - catch { } - - return null; - } - - private static OcrResult ParseTsvOutput(string tsvPath, byte[] imageBytes) - { - var lines = new List(); - var words = new List(); - var fullText = new StringBuilder(); - - // Approximate image dimensions or default 1000x1000 - double imgWidth = 1000; - double imgHeight = 1000; - - string[] tsvLines = File.ReadAllLines(tsvPath); - if (tsvLines.Length <= 1) - { - return new OcrResult { Success = true, FullText = string.Empty, Lines = lines, Words = words }; - } - - // TSV format: level, page_num, block_num, par_num, line_num, word_num, left, top, width, height, conf, text - // Header is line 0 - OcrLineItem? currentLine = null; - int lastLineNum = -1; - - for (int i = 1; i < tsvLines.Length; i++) - { - string line = tsvLines[i]; - string[] cols = line.Split('\t'); - if (cols.Length < 12) continue; - - int level = int.TryParse(cols[0], out var lv) ? lv : 0; - if (level == 1) // Page level - { - if (double.TryParse(cols[8], NumberStyles.Any, CultureInfo.InvariantCulture, out var pw) && pw > 0) - imgWidth = pw; - if (double.TryParse(cols[9], NumberStyles.Any, CultureInfo.InvariantCulture, out var ph) && ph > 0) - imgHeight = ph; - continue; - } - - if (level != 5) continue; // Word level - - string text = cols[11].Trim(); - if (string.IsNullOrEmpty(text)) continue; - - double left = double.TryParse(cols[6], NumberStyles.Any, CultureInfo.InvariantCulture, out var l) ? l : 0; - double top = double.TryParse(cols[7], NumberStyles.Any, CultureInfo.InvariantCulture, out var t) ? t : 0; - double width = double.TryParse(cols[8], NumberStyles.Any, CultureInfo.InvariantCulture, out var w) ? w : 0; - double height = double.TryParse(cols[9], NumberStyles.Any, CultureInfo.InvariantCulture, out var h) ? h : 0; - float conf = float.TryParse(cols[10], NumberStyles.Any, CultureInfo.InvariantCulture, out var c) ? c / 100f : 1.0f; - int lineNum = int.TryParse(cols[4], out var ln) ? ln : 0; - - var normBox = new OcrBoundingBox(left / imgWidth, top / imgHeight, width / imgWidth, height / imgHeight); - var wordItem = new OcrWordItem - { - Text = text, - NormalizedBounds = normBox, - Confidence = conf - }; - - words.Add(wordItem); - - if (currentLine == null || lineNum != lastLineNum) - { - currentLine = new OcrLineItem - { - Text = text, - NormalizedBounds = normBox - }; - currentLine.Words.Add(wordItem); - lines.Add(currentLine); - lastLineNum = lineNum; - } - else - { - currentLine.Text += " " + text; - currentLine.Words.Add(wordItem); - // Union bounding box - double nx = Math.Min(currentLine.NormalizedBounds.X, normBox.X); - double ny = Math.Min(currentLine.NormalizedBounds.Y, normBox.Y); - double nr = Math.Max(currentLine.NormalizedBounds.Right, normBox.Right); - double nb = Math.Max(currentLine.NormalizedBounds.Bottom, normBox.Bottom); - currentLine.NormalizedBounds = new OcrBoundingBox(nx, ny, nr - nx, nb - ny); - } - - fullText.Append(text).Append(' '); - } - - return new OcrResult - { - Success = true, - FullText = fullText.ToString().Trim(), - Lines = lines, - Words = words, - EngineUsed = "Tesseract OCR" - }; - } -} diff --git a/src/PdfEditorApp/Services/Ocr/WindowsMediaOcrEngine.cs b/src/PdfEditorApp/Services/Ocr/WindowsMediaOcrEngine.cs deleted file mode 100644 index 656d4c8..0000000 --- a/src/PdfEditorApp/Services/Ocr/WindowsMediaOcrEngine.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; - -namespace PdfEditorApp.Services.Ocr; - -/// -/// Hardware-accelerated native Windows OCR engine utilizing Windows 10/11 WinRT -/// Windows.Media.Ocr.OcrEngine. Requires zero external downloads on Windows systems. -/// -public class WindowsMediaOcrEngine : IOcrEngine -{ - public string EngineName => "Windows Media OCR"; - public OcrEngineType EngineType => OcrEngineType.OsNative; - public bool IsAvailable => OperatingSystem.IsWindows(); - - public async Task RecognizeTextAsync(byte[] imageBytes, string language = "eng", CancellationToken ct = default) - { - if (!IsAvailable) - { - return new OcrResult - { - Success = false, - ErrorMessage = "Windows Media OCR is only available on Windows 10/11." - }; - } - - return await Task.Run(() => - { - // Windows OCR dynamic invocation or fallback - return new OcrResult - { - Success = false, - ErrorMessage = "Windows Media OCR initialized." - }; - }, ct); - } -} diff --git a/src/PdfEditorApp/Services/Overlays/OverlayRegistry.cs b/src/PdfEditorApp/Services/Overlays/OverlayRegistry.cs deleted file mode 100644 index 1564f4e..0000000 --- a/src/PdfEditorApp/Services/Overlays/OverlayRegistry.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Avalonia.Threading; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Services.Overlays; - -/// -/// Thread-safe registry and lifecycle manager for floating plugins targeting the 'shell.overlay' slot. -/// -public sealed class OverlayRegistry : IOverlayRegistry -{ - private readonly ConcurrentDictionary _descriptors = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _activeInstances = new(StringComparer.OrdinalIgnoreCase); - private readonly IServiceProvider _serviceProvider; - - public event Action? RegistryChanged; - public event Action? ActiveOverlaysChanged; - - public ObservableCollection ActiveOverlays { get; } = new(); - - public OverlayRegistry(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - } - - public IDisposable RegisterOverlay(OverlayDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _descriptors[descriptor.Id] = descriptor; - - if (_activeInstances.TryGetValue(descriptor.Id, out var existingInstance)) - { - try - { - var newContent = descriptor.ViewFactory != null - ? descriptor.ViewFactory(_serviceProvider) - : (descriptor.ViewType != null ? Activator.CreateInstance(descriptor.ViewType) : null); - - RunOnUIThread(() => - { - existingInstance.Content = newContent; - }); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[OverlayRegistry] Error refreshing view for {descriptor.Id}: {ex.Message}"); - } - } - - RegistryChanged?.Invoke(); - - return new UnregisterDisposable(() => - { - UnregisterOverlay(descriptor.Id); - }); - } - - public bool UnregisterOverlay(string overlayId) - { - if (_descriptors.TryRemove(overlayId, out _)) - { - HideOverlay(overlayId); - RegistryChanged?.Invoke(); - return true; - } - return false; - } - - public OverlayDescriptor? GetOverlay(string overlayId) - { - if (_descriptors.TryGetValue(overlayId, out var descriptor)) - { - return descriptor; - } - - if (!overlayId.StartsWith("frypdf.overlay.", StringComparison.OrdinalIgnoreCase)) - { - if (_descriptors.TryGetValue($"frypdf.overlay.{overlayId}", out descriptor)) - { - return descriptor; - } - } - - return null; - } - - public IReadOnlyList GetAllOverlays() - { - return _descriptors.Values.OrderBy(d => d.Title).ToList(); - } - - public void ShowOverlay(string overlayId) - { - var desc = GetOverlay(overlayId); - if (desc == null) return; - - if (_activeInstances.TryGetValue(desc.Id, out var existingInstance)) - { - RunOnUIThread(() => - { - if (existingInstance.Content == null) - { - existingInstance.Content = desc.ViewFactory != null - ? desc.ViewFactory(_serviceProvider) - : (desc.ViewType != null ? Activator.CreateInstance(desc.ViewType) : null); - } - existingInstance.IsVisible = true; - existingInstance.IsMinimized = false; - if (!ActiveOverlays.Contains(existingInstance)) - { - ActiveOverlays.Add(existingInstance); - } - ActiveOverlaysChanged?.Invoke(); - }); - return; - } - - object? content = null; - if (desc.ViewFactory != null) - { - try - { - content = desc.ViewFactory(_serviceProvider); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[OverlayRegistry] Error creating view for {desc.Id}: {ex.Message}"); - } - } - else if (desc.ViewType != null) - { - try - { - content = Activator.CreateInstance(desc.ViewType); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[OverlayRegistry] Error instantiating ViewType {desc.ViewType}: {ex.Message}"); - } - } - - var instance = new OverlayInstanceViewModel(desc, inst => - { - HideOverlay(inst.Id); - }) - { - Content = content, - X = desc.InitialX ?? 0, - Y = desc.InitialY ?? 0, - IsVisible = true, - IsMinimized = false - }; - - if (_activeInstances.TryAdd(desc.Id, instance)) - { - RunOnUIThread(() => - { - if (!ActiveOverlays.Contains(instance)) - { - ActiveOverlays.Add(instance); - } - ActiveOverlaysChanged?.Invoke(); - }); - } - } - - public void HideOverlay(string overlayId) - { - var desc = GetOverlay(overlayId); - var targetId = desc?.Id ?? overlayId; - - if (_activeInstances.TryRemove(targetId, out var instance)) - { - RunOnUIThread(() => - { - instance.IsVisible = false; - ActiveOverlays.Remove(instance); - ActiveOverlaysChanged?.Invoke(); - }); - } - } - - private static void RunOnUIThread(Action action) - { - if (Dispatcher.UIThread.CheckAccess()) - { - action(); - return; - } - - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime) - { - Dispatcher.UIThread.Post(action); - } - else - { - action(); - } - } - - public void ToggleOverlay(string overlayId) - { - if (IsOverlayVisible(overlayId)) - { - HideOverlay(overlayId); - } - else - { - ShowOverlay(overlayId); - } - } - - public bool IsOverlayVisible(string overlayId) - { - var desc = GetOverlay(overlayId); - var targetId = desc?.Id ?? overlayId; - return _activeInstances.TryGetValue(targetId, out var inst) && inst.IsVisible; - } - - private sealed class UnregisterDisposable : IDisposable - { - private Action? _action; - - public UnregisterDisposable(Action action) - { - _action = action; - } - - public void Dispose() - { - var action = System.Threading.Interlocked.Exchange(ref _action, null); - action?.Invoke(); - } - } -} diff --git a/src/PdfEditorApp/Services/PageOrganizerService.cs b/src/PdfEditorApp/Services/PageOrganizerService.cs deleted file mode 100644 index 9655a27..0000000 --- a/src/PdfEditorApp/Services/PageOrganizerService.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public enum PageFilterTarget -{ - All, - EvenPages, - OddPages, - LandscapePages, - PortraitPages -} - -public interface IPageOrganizerService -{ - List SplitEveryNPages(PdfDocumentModel doc, int pagesPerDoc); - List SplitByRanges(PdfDocumentModel doc, string rangeExpression); - PdfDocumentModel ExtractPages(PdfDocumentModel doc, IEnumerable pageIndexes, string? newTitle = null); - PdfDocumentModel MergeDocuments(IEnumerable docs, string mergedTitle); - int BatchRotatePages(PdfDocumentModel doc, PageFilterTarget target, int angleDegrees); - List ParsePageRanges(string rangeExpression, int totalPages); -} - -public class PageOrganizerService : IPageOrganizerService -{ - public List SplitEveryNPages(PdfDocumentModel doc, int pagesPerDoc) - { - if (doc.Pages.Count == 0) return new List(); - int interval = Math.Max(1, pagesPerDoc); - var result = new List(); - - int totalPages = doc.Pages.Count; - int partIndex = 1; - - for (int i = 0; i < totalPages; i += interval) - { - int count = Math.Min(interval, totalPages - i); - var partDoc = new PdfDocumentModel - { - Title = $"{System.IO.Path.GetFileNameWithoutExtension(doc.Title)}_Part{partIndex}.pdf", - Author = doc.Author, - Subject = $"{doc.Subject} (Part {partIndex})", - Keywords = doc.Keywords, - Creator = string.IsNullOrWhiteSpace(doc.Creator) ? "FryPDF" : doc.Creator, - Producer = string.IsNullOrWhiteSpace(doc.Producer) ? "codefrydev.in" : doc.Producer, - CreatedDate = doc.CreatedDate, - SecuritySettings = doc.SecuritySettings - }; - - for (int j = 0; j < count; j++) - { - var page = doc.Pages[i + j]; - var clonedPage = (PdfPageModel)page.Clone(); - clonedPage.PageNumber = j + 1; - partDoc.Pages.Add(clonedPage); - } - - result.Add(partDoc); - partIndex++; - } - - return result; - } - - public List SplitByRanges(PdfDocumentModel doc, string rangeExpression) - { - var result = new List(); - if (doc.Pages.Count == 0 || string.IsNullOrWhiteSpace(rangeExpression)) return result; - - var parts = rangeExpression.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); - int partIndex = 1; - - foreach (var part in parts) - { - var pageIndexes = ParseSingleRange(part.Trim(), doc.Pages.Count); - if (pageIndexes.Count == 0) continue; - - var partDoc = new PdfDocumentModel - { - Title = $"{System.IO.Path.GetFileNameWithoutExtension(doc.Title)}_Range_{part.Trim().Replace(' ', '_')}.pdf", - Author = doc.Author, - Subject = $"{doc.Subject} (Pages {part.Trim()})", - Keywords = doc.Keywords, - Creator = string.IsNullOrWhiteSpace(doc.Creator) ? "FryPDF" : doc.Creator, - Producer = string.IsNullOrWhiteSpace(doc.Producer) ? "codefrydev.in" : doc.Producer, - CreatedDate = doc.CreatedDate - }; - - int pageNum = 1; - foreach (int pIdx in pageIndexes) - { - var page = doc.Pages[pIdx]; - var clonedPage = (PdfPageModel)page.Clone(); - clonedPage.PageNumber = pageNum++; - partDoc.Pages.Add(clonedPage); - } - - result.Add(partDoc); - partIndex++; - } - - return result; - } - - public PdfDocumentModel ExtractPages(PdfDocumentModel doc, IEnumerable pageIndexes, string? newTitle = null) - { - var extractedDoc = new PdfDocumentModel - { - Title = newTitle ?? $"{System.IO.Path.GetFileNameWithoutExtension(doc.Title)}_Extracted.pdf", - Author = doc.Author, - Subject = $"Extracted pages from {doc.Title}", - Keywords = doc.Keywords, - Creator = string.IsNullOrWhiteSpace(doc.Creator) ? "FryPDF" : doc.Creator, - Producer = string.IsNullOrWhiteSpace(doc.Producer) ? "codefrydev.in" : doc.Producer, - CreatedDate = DateTime.Now, - SecuritySettings = doc.SecuritySettings - }; - - int pageNum = 1; - foreach (int pIdx in pageIndexes) - { - if (pIdx >= 0 && pIdx < doc.Pages.Count) - { - var page = doc.Pages[pIdx]; - var clonedPage = (PdfPageModel)page.Clone(); - clonedPage.PageNumber = pageNum++; - extractedDoc.Pages.Add(clonedPage); - } - } - - return extractedDoc; - } - - public PdfDocumentModel MergeDocuments(IEnumerable docs, string mergedTitle) - { - var mergedDoc = new PdfDocumentModel - { - Title = mergedTitle, - Creator = "FryPDF", - Producer = "codefrydev.in", - CreatedDate = DateTime.Now - }; - - int pageNum = 1; - foreach (var doc in docs) - { - if (string.IsNullOrWhiteSpace(mergedDoc.Author) && !string.IsNullOrWhiteSpace(doc.Author)) - { - mergedDoc.Author = doc.Author; - } - - foreach (var page in doc.Pages) - { - var clonedPage = (PdfPageModel)page.Clone(); - clonedPage.PageNumber = pageNum++; - mergedDoc.Pages.Add(clonedPage); - } - } - - return mergedDoc; - } - - public int BatchRotatePages(PdfDocumentModel doc, PageFilterTarget target, int angleDegrees) - { - int count = 0; - for (int i = 0; i < doc.Pages.Count; i++) - { - int pageNum = i + 1; - var page = doc.Pages[i]; - - bool shouldRotate = target switch - { - PageFilterTarget.All => true, - PageFilterTarget.EvenPages => (pageNum % 2 == 0), - PageFilterTarget.OddPages => (pageNum % 2 != 0), - PageFilterTarget.LandscapePages => page.Width > page.Height, - PageFilterTarget.PortraitPages => page.Height >= page.Width, - _ => true - }; - - if (shouldRotate) - { - page.RotationAngle = (page.RotationAngle + angleDegrees + 360) % 360; - count++; - } - } - return count; - } - - public List ParsePageRanges(string rangeExpression, int totalPages) - { - var result = new HashSet(); - if (string.IsNullOrWhiteSpace(rangeExpression) || totalPages <= 0) return result.ToList(); - - var parts = rangeExpression.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var part in parts) - { - foreach (var idx in ParseSingleRange(part.Trim(), totalPages)) - { - result.Add(idx); - } - } - - return result.OrderBy(i => i).ToList(); - } - - private static List ParseSingleRange(string range, int totalPages) - { - var list = new List(); - if (string.IsNullOrWhiteSpace(range)) return list; - - if (range.Contains('-')) - { - var bounds = range.Split('-'); - if (bounds.Length == 2 && - int.TryParse(bounds[0].Trim(), out int start) && - int.TryParse(bounds[1].Trim(), out int end)) - { - int min = Math.Clamp(Math.Min(start, end), 1, totalPages); - int max = Math.Clamp(Math.Max(start, end), 1, totalPages); - for (int i = min; i <= max; i++) - { - list.Add(i - 1); - } - } - } - else if (int.TryParse(range.Trim(), out int single)) - { - if (single >= 1 && single <= totalPages) - { - list.Add(single - 1); - } - } - - return list; - } -} diff --git a/src/PdfEditorApp/Services/Palette/CommandPaletteRegistry.cs b/src/PdfEditorApp/Services/Palette/CommandPaletteRegistry.cs deleted file mode 100644 index 545b317..0000000 --- a/src/PdfEditorApp/Services/Palette/CommandPaletteRegistry.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Palette; - -/// -/// Thread-safe in-memory registry for discovering, searching, and dispatching Command Palette commands. -/// -public class CommandPaletteRegistry : ICommandPaletteRegistry -{ - private readonly ConcurrentDictionary _commands = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public IDisposable RegisterCommand(CommandPaletteDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - ArgumentException.ThrowIfNullOrWhiteSpace(descriptor.Id); - - _commands[descriptor.Id] = descriptor; - RegistryChanged?.Invoke(); - - return new DisposableAction(() => - { - _commands.TryRemove(descriptor.Id, out _); - RegistryChanged?.Invoke(); - }); - } - - public bool UnregisterCommand(string commandId) - { - if (string.IsNullOrWhiteSpace(commandId)) return false; - var removed = _commands.TryRemove(commandId, out _); - if (removed) - { - RegistryChanged?.Invoke(); - } - return removed; - } - - public IReadOnlyList GetAllCommands() - { - return _commands.Values.OrderBy(c => c.Order).ThenBy(c => c.Title).ToList(); - } - - private sealed class DisposableAction : IDisposable - { - private Action? _action; - - public DisposableAction(Action action) - { - _action = action; - } - - public void Dispose() - { - var act = System.Threading.Interlocked.Exchange(ref _action, null); - act?.Invoke(); - } - } -} diff --git a/src/PdfEditorApp/Services/PdfDocumentOperationsService.cs b/src/PdfEditorApp/Services/PdfDocumentOperationsService.cs deleted file mode 100644 index 673f08b..0000000 --- a/src/PdfEditorApp/Services/PdfDocumentOperationsService.cs +++ /dev/null @@ -1,266 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Tools; - -namespace PdfEditorApp.Services; - -public class PdfDocumentOperationsService : IPdfDocumentOperationsService -{ - public IPdfToolRegistry ToolRegistry { get; } - public IPdfPageService PageService { get; } - public IPdfOptimizationService OptimizationService { get; } - public IPdfSecurityService SecurityService { get; } - public IPdfConversionService ConversionService { get; } - public IPdfOcrService OcrService { get; } - public IPdfFormService FormService { get; } - public IAiDocumentService AiService { get; } - public IDocumentTranslationService TranslationService { get; } - public IPdfWorkflowEngine WorkflowEngine { get; } - - public PdfDocumentOperationsService() - : this( - new PdfToolRegistry(), - new PdfPageService(), - new PdfOptimizationService(), - new PdfSecurityService(), - new PdfConversionService(), - new PdfOcrService(), - new PdfFormService(), - new AiDocumentService(), - new DocumentTranslationService(), - new PdfWorkflowEngine()) - { - } - - public PdfDocumentOperationsService( - IPdfToolRegistry toolRegistry, - IPdfPageService pageService, - IPdfOptimizationService optimizationService, - IPdfSecurityService securityService, - IPdfConversionService conversionService, - IPdfOcrService ocrService, - IPdfFormService formService, - IAiDocumentService aiService, - IDocumentTranslationService translationService, - IPdfWorkflowEngine workflowEngine, - PdfEditorApp.Core.Plugins.IFryPluginContext? pluginContext = null) - { - ToolRegistry = toolRegistry; - PageService = pageService; - OptimizationService = optimizationService; - SecurityService = securityService; - ConversionService = conversionService; - OcrService = ocrService; - FormService = formService; - AiService = aiService; - TranslationService = translationService; - WorkflowEngine = workflowEngine; - _pluginContext = pluginContext; - } - - private readonly PdfEditorApp.Core.Plugins.IFryPluginContext? _pluginContext; - - public async Task ExecuteToolAsync(PdfToolId toolId, object options, IProgress? progress = null, CancellationToken ct = default) - { - if (_pluginContext != null) - { - var pipelineContext = new PdfEditorApp.Core.Plugins.Pipelines.PdfToolExecutionPipelineContext(toolId.ToString(), options, progress, ct); - await _pluginContext.ExecuteWaterfallAsync("tool:execute", pipelineContext, async () => - { - pipelineContext.Result = await ExecuteToolCoreAsync(toolId, pipelineContext.Options, progress, ct); - }); - - return pipelineContext.Result as ToolExecutionResult ?? new ToolExecutionResult - { - Success = false, - ErrorMessage = "Tool execution pipeline did not produce a result." - }; - } - - return await ExecuteToolCoreAsync(toolId, options, progress, ct); - } - - private async Task ExecuteToolCoreAsync(PdfToolId toolId, object options, IProgress? progress = null, CancellationToken ct = default) - { - try - { - switch (toolId) - { - case PdfToolId.MergePdf when options is MergeToolOptions mergeOpts: - return await PageService.MergePdfAsync(mergeOpts, progress, ct); - - case PdfToolId.SplitPdf when options is SplitToolOptions splitOpts: - return await PageService.SplitPdfAsync(splitOpts, progress, ct); - - case PdfToolId.RotatePdf when options is RotateToolOptions rotateOpts: - return await PageService.RotatePdfAsync(rotateOpts, progress, ct); - - case PdfToolId.CropPdf when options is CropToolOptions cropOpts: - return await PageService.CropPdfAsync(cropOpts, progress, ct); - - case PdfToolId.OrganizePdf when options is OrganizeToolOptions orgOpts: - return await PageService.OrganizePdfAsync(orgOpts, progress, ct); - - case PdfToolId.PageNumbers when options is PageNumberToolOptions numOpts: - return await PageService.AddPageNumbersAsync(numOpts, progress, ct); - - case PdfToolId.Watermark when options is WatermarkToolOptions wmOpts: - return await PageService.AddWatermarkAsync(wmOpts, progress, ct); - - case PdfToolId.CompressPdf when options is CompressToolOptions compOpts: - return await OptimizationService.CompressPdfAsync(compOpts, progress, ct); - - case PdfToolId.RepairPdf when options is RepairToolOptions repOpts: - return await OptimizationService.RepairPdfAsync(repOpts, progress, ct); - - case PdfToolId.PdfToPdfA when options is PdfAToolOptions pdfAOpts: - return await OptimizationService.ConvertToPdfAAsync(pdfAOpts, progress, ct); - - case PdfToolId.ProtectPdf when options is SecurityToolOptions secOpts: - return await SecurityService.ProtectPdfAsync(secOpts, progress, ct); - - case PdfToolId.UnlockPdf when options is UnlockToolOptions unlOpts: - return await SecurityService.UnlockPdfAsync(unlOpts, progress, ct); - - case PdfToolId.SignPdf when options is SignToolOptions signOpts: - return await SecurityService.SignPdfAsync(signOpts, progress, ct); - - case PdfToolId.RedactPdf when options is RedactionToolOptions redOpts: - return await SecurityService.RedactPdfAsync(redOpts, progress, ct); - - case PdfToolId.PdfToWord when options is WordConversionOptions wordOpts: - return await ConversionService.ConvertPdfToWordAsync(wordOpts, progress, ct); - - case PdfToolId.PdfToExcel when options is ExcelConversionOptions excelOpts: - return await ConversionService.ConvertPdfToExcelAsync(excelOpts, progress, ct); - - case PdfToolId.PdfToPowerPoint when options is PptxConversionOptions pptOpts: - return await ConversionService.ConvertPdfToPowerPointAsync(pptOpts, progress, ct); - - case PdfToolId.WordToPdf when options is OfficeToPdfOptions docxOpts: - return await ConversionService.ConvertWordToPdfAsync(docxOpts, progress, ct); - - case PdfToolId.ExcelToPdf when options is OfficeToPdfOptions xlsxOpts: - return await ConversionService.ConvertExcelToPdfAsync(xlsxOpts, progress, ct); - - case PdfToolId.PowerPointToPdf when options is OfficeToPdfOptions pptxOpts: - return await ConversionService.ConvertPowerPointToPdfAsync(pptxOpts, progress, ct); - - case PdfToolId.PdfToJpg when options is ImageConversionOptions imgOpts: - return await ConversionService.ConvertPdfToImagesAsync(imgOpts, progress, ct); - - case PdfToolId.JpgToPdf when options is ImagesToPdfOptions imgsToPdfOpts: - return await ConversionService.ConvertImagesToPdfAsync(imgsToPdfOpts, progress, ct); - - case PdfToolId.HtmlToPdf when options is HtmlToPdfOptions htmlOpts: - return await ConversionService.ConvertHtmlToPdfAsync(htmlOpts, progress, ct); - - case PdfToolId.PdfToMarkdown when options is MarkdownConversionOptions mdOpts: - return await ConversionService.ConvertPdfToMarkdownAsync(mdOpts, progress, ct); - - case PdfToolId.OcrPdf when options is OcrToolOptions ocrOpts: - return await OcrService.OcrPdfAsync(ocrOpts, progress, ct); - - case PdfToolId.ScanToPdf when options is ScanToolOptions scanOpts: - return await OcrService.ScanToPdfAsync(scanOpts, progress, ct); - - case PdfToolId.PdfForms when options is FormToolOptions formOpts: - return await FormService.ProcessPdfFormsAsync(formOpts, progress, ct); - - case PdfToolId.AiSummarizer when options is AiSummaryOptions aiOpts: - return await AiService.SummarizePdfAsync(aiOpts, progress, ct); - - case PdfToolId.TranslatePdf when options is TranslationOptions transOpts: - return await TranslationService.TranslatePdfAsync(transOpts, progress, ct); - - case PdfToolId.ComparePdf when options is CompareToolOptions compOpts: - { - if (!File.Exists(compOpts.DocumentAPath) || !File.Exists(compOpts.DocumentBPath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "One or both comparison documents do not exist." }; - - var importService = new PdfImportService(); - var compareService = new DocumentCompareService(); - - var docA = await importService.ImportPdfAsync(compOpts.DocumentAPath); - ct.ThrowIfCancellationRequested(); - progress?.Report(40.0); - var docB = await importService.ImportPdfAsync(compOpts.DocumentBPath); - ct.ThrowIfCancellationRequested(); - progress?.Report(70.0); - - var report = await compareService.CompareDocumentsAsync(docA, docB, ct); - - string outDir = Path.GetDirectoryName(compOpts.DocumentBPath) ?? Path.GetTempPath(); - string reportPath = Path.Combine(outDir, $"{Path.GetFileNameWithoutExtension(compOpts.DocumentBPath)}_comparison_report.txt"); - - var sb = new StringBuilder(); - sb.AppendLine("Document Comparison Report"); - sb.AppendLine($"Base document: {report.BaseDocumentTitle} ({report.BasePageCount} pages)"); - sb.AppendLine($"Compared document: {report.ComparedDocumentTitle} ({report.ComparedPageCount} pages)"); - sb.AppendLine($"Generated: {report.ComparisonTimestamp:u}"); - sb.AppendLine(); - sb.AppendLine($"Total differences: {report.TotalDifferencesCount} (Added: {report.AdditionsCount}, Removed: {report.DeletionsCount}, Modified: {report.ModificationsCount})"); - sb.AppendLine(); - foreach (var diff in report.Differences) - { - sb.AppendLine($"[Page {diff.PageNumber}] {diff.DiffType}: {diff.Description}"); - if (!string.IsNullOrEmpty(diff.OldValue) || !string.IsNullOrEmpty(diff.NewValue)) - { - sb.AppendLine($" - Was: {diff.OldValue}"); - sb.AppendLine($" + Now: {diff.NewValue}"); - } - } - - await File.WriteAllTextAsync(reportPath, sb.ToString(), ct); - progress?.Report(100.0); - - return new ToolExecutionResult - { - Success = true, - OutputFilePath = reportPath, - OutputFiles = new List { reportPath }, - Message = report.TotalDifferencesCount == 0 - ? $"No differences found between '{Path.GetFileName(compOpts.DocumentAPath)}' and '{Path.GetFileName(compOpts.DocumentBPath)}'." - : $"Found {report.TotalDifferencesCount} difference(s) between the two documents ({report.AdditionsCount} added, {report.DeletionsCount} removed, {report.ModificationsCount} modified). Report saved to {Path.GetFileName(reportPath)}.", - ExtraData = new Dictionary { ["ComparisonReport"] = report } - }; - } - - case PdfToolId.WorkflowBuilder when options is WorkflowDefinition wfDef: - return await WorkflowEngine.ExecuteWorkflowAsync(wfDef, new string[0], null, ct); - - default: - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"No execution handler mapped for tool '{toolId}' with options '{options?.GetType().Name}'." - }; - } - } - catch (OperationCanceledException) - { - // Let cancellation propagate so PdfToolViewModelBase's dedicated - // OperationCanceledException handler can show a neutral "cancelled" state - // instead of the generic failure path. - throw; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"An error occurred during execution: {ex.Message}" - }; - } - } -} diff --git a/src/PdfEditorApp/Services/PdfExportService.cs b/src/PdfEditorApp/Services/PdfExportService.cs deleted file mode 100644 index 451d0ab..0000000 --- a/src/PdfEditorApp/Services/PdfExportService.cs +++ /dev/null @@ -1,897 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using QRCoder; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Core.Utils; -using PdfEditorApp.Models; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Pipelines; -using PdfEditorApp.Services.MathEngine; -using PdfEditorApp.Services.Typography; - -namespace PdfEditorApp.Services; - -public class PdfExportService : IPdfExportService -{ - private readonly IFryPluginContext? _pluginContext; - - public PdfExportService(IFryPluginContext? pluginContext = null) - { - _pluginContext = pluginContext; - } - - static PdfExportService() - { - RegisterEmbeddedFonts(); - } - - private static void RegisterEmbeddedFonts() - { - try - { - // 1. User on-demand font cache directory - string userFontDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FryPDF", "Fonts"); - if (Directory.Exists(userFontDir)) - { - foreach (var fontFile in Directory.GetFiles(userFontDir, "*.*")) - { - string ext = Path.GetExtension(fontFile).ToLowerInvariant(); - if (ext == ".ttf" || ext == ".otf") - { - try - { - using var stream = File.OpenRead(fontFile); - QuestPDF.Drawing.FontManager.RegisterFont(stream); - } - catch { } - } - } - } - - // 2. Application bundled fonts - string[] searchPaths = - { - Path.Combine(AppContext.BaseDirectory, "Assets", "Fonts"), - Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Assets", "Fonts"), - Path.Combine(Directory.GetCurrentDirectory(), "src", "PdfEditorApp", "Assets", "Fonts"), - Path.Combine(Directory.GetCurrentDirectory(), "Assets", "Fonts") - }; - - foreach (var basePath in searchPaths) - { - if (Directory.Exists(basePath)) - { - var ttfFiles = Directory.GetFiles(basePath, "*.ttf"); - foreach (var ttf in ttfFiles) - { - try - { - using var stream = File.OpenRead(ttf); - QuestPDF.Drawing.FontManager.RegisterFont(stream); - } - catch { } - } - break; - } - } - } - catch { } - } - - public byte[] GeneratePdfBytes(PdfDocumentModel model) - { - return GeneratePdfBytes(model, null); - } - - public byte[] GeneratePdfBytes(PdfDocumentModel model, IProgress? progress) - { - var context = new PdfExportPipelineContext(model, progress); - - if (_pluginContext != null) - { - _pluginContext.ExecuteWaterfallAsync("document:export", context, () => - { - context.ResultPdfBytes = CompileModelToBytes(context.Document, context.Progress); - return Task.CompletedTask; - }).GetAwaiter().GetResult(); - - return context.ResultPdfBytes ?? CompileModelToBytes(model, progress); - } - - return CompileModelToBytes(model, progress); - } - - private static byte[] CompileModelToBytes(PdfDocumentModel model, IProgress? progress) - { - var document = new QuestPdfDocumentWrapper(model, progress); - byte[] rawPdf = document.GeneratePdf(); - - // If scrub metadata is requested, do not embed internal model - if (model.SecuritySettings.ScrubMetadataOnExport) - { - return rawPdf; - } - - // Embed full lossless FryPDF model metadata for 100% perfect roundtrip re-editing - return FryPdfEmbeddingHelper.EmbedModelInPdfBytes(rawPdf, model); - } - - public async Task ExportToBytesAsync(PdfDocumentModel model, IProgress? progress = null, CancellationToken ct = default) - { - ct.ThrowIfCancellationRequested(); - var context = new PdfExportPipelineContext(model, progress, ct); - - if (_pluginContext != null) - { - await _pluginContext.ExecuteWaterfallAsync("document:export", context, () => - { - context.ResultPdfBytes = CompileModelToBytes(context.Document, context.Progress); - return Task.CompletedTask; - }); - - return context.ResultPdfBytes ?? CompileModelToBytes(model, progress); - } - - return await Task.Run(() => CompileModelToBytes(model, progress), ct); - } - - public async Task ExportToFileAsync(PdfDocumentModel model, string filePath, IProgress? progress = null, CancellationToken ct = default) - { - byte[] bytes = await ExportToBytesAsync(model, progress, ct); - - string tempPath = filePath + ".tmp"; - try - { - await File.WriteAllBytesAsync(tempPath, bytes, ct); - - if (File.Exists(filePath)) - { - File.Replace(tempPath, filePath, null); - } - else - { - File.Move(tempPath, filePath); - } - } - catch - { - try { if (File.Exists(tempPath)) File.Delete(tempPath); } catch { } - throw; - } - } -} - -internal class QuestPdfDocumentWrapper : IDocument -{ - private readonly PdfDocumentModel _model; - private readonly IProgress? _progress; - - public QuestPdfDocumentWrapper(PdfDocumentModel model, IProgress? progress = null) - { - _model = model; - _progress = progress; - } - - public DocumentMetadata GetMetadata() - { - if (_model.SecuritySettings.ScrubMetadataOnExport) - { - return new DocumentMetadata - { - Title = "Document", - Author = "Anonymous", - Subject = "", - CreationDate = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), - ModifiedDate = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), - Creator = "FryPDF", - Producer = "codefrydev.in" - }; - } - - return new DocumentMetadata - { - Title = _model.Title, - Author = _model.Author, - Subject = _model.Subject, - Keywords = _model.Keywords, - CreationDate = _model.CreatedDate, - ModifiedDate = _model.ModifiedDate, - Creator = string.IsNullOrWhiteSpace(_model.Creator) ? "FryPDF" : _model.Creator, - Producer = string.IsNullOrWhiteSpace(_model.Producer) ? "codefrydev.in" : _model.Producer - }; - } - - public void Compose(IDocumentContainer container) - { - int totalPages = Math.Max(1, _model.Pages.Count); - int pageIndex = 0; - foreach (var pageModel in _model.Pages) - { - container.Page(page => - { - float pageW = (float)pageModel.Width; - float pageH = (float)pageModel.Height; - - if (pageW <= 0) pageW = 595.28f; // Standard A4 points - if (pageH <= 0) pageH = 841.89f; - - page.Size(new PageSize(pageW, pageH)); - page.Margin(0); - page.PageColor(pageModel.BackgroundColorHex); - page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(10).FontColor("#201F1E")); - - // Absolute Layout Content via Layers - page.Content().Layers(layers => - { - // Primary Base Layer - layers.PrimaryLayer().Element(c => c.Width(pageW).Height(pageH)); - - // Header Zone (if enabled) - if (pageModel.ShowHeaderFooter && (!string.IsNullOrEmpty(pageModel.HeaderLeft) || !string.IsNullOrEmpty(pageModel.HeaderCenter) || !string.IsNullOrEmpty(pageModel.HeaderRight))) - { - layers.Layer().PaddingLeft(36).PaddingTop(18).Width(pageW - 72).Height(24).Row(row => - { - if (!string.IsNullOrEmpty(pageModel.HeaderLeft)) - row.RelativeItem().Text(pageModel.HeaderLeft).FontSize(8).FontColor(Colors.Grey.Medium); - if (!string.IsNullOrEmpty(pageModel.HeaderCenter)) - row.RelativeItem().AlignCenter().Text(pageModel.HeaderCenter).FontSize(8).FontColor(Colors.Grey.Medium); - if (!string.IsNullOrEmpty(pageModel.HeaderRight)) - row.RelativeItem().AlignRight().Text(pageModel.HeaderRight).FontSize(8).FontColor(Colors.Grey.Medium); - }); - } - - // Watermark (if present) - if (pageModel.Watermark != null && !string.IsNullOrWhiteSpace(pageModel.Watermark.Text)) - { - layers.Layer().AlignCenter().AlignMiddle().Rotate((float)pageModel.Watermark.Angle) - .Text(pageModel.Watermark.Text) - .FontSize((float)pageModel.Watermark.FontSize) - .FontColor(pageModel.Watermark.ColorHex) - .Bold(); - } - - // Elements ordered by ZIndex - var sortedElements = pageModel.Elements - .OrderBy(e => e.ZIndex) - .ThenBy(e => e.Y) - .ThenBy(e => e.X) - .ToList(); - - var redactionBoxes = pageModel.Elements.OfType().ToList(); - - foreach (var element in sortedElements) - { - if (_model.SecuritySettings.RemoveCommentsOnExport && element is PdfStickyNoteElement) - { - continue; - } - - float elX = (float)element.X; - float elY = (float)element.Y; - float elW = (float)Math.Max(1, element.Width); - float elH = (float)Math.Max(1, element.Height); - - // Permanent Redaction Sanitization: Strip underlying text/images completely covered by a redaction box - if (element is PdfTextElement or PdfImageElement) - { - bool isFullyCovered = redactionBoxes.Any(r => - r.X <= elX + 1 && - r.Y <= elY + 1 && - (r.X + r.Width) >= (elX + elW - 1) && - (r.Y + r.Height) >= (elY + elH - 1)); - - if (isFullyCovered) - { - continue; - } - } - - layers.Layer().Element(layerContainer => - { - var c = layerContainer - .PaddingLeft(elX) - .PaddingTop(elY) - .Width(elW) - .Height(elH); - - if (element.Rotation != 0) - { - c = c.Rotate((float)element.Rotation); - } - - ComposeElement(c, element); - }); - } - - // Footer Zone (if enabled) - if (pageModel.ShowHeaderFooter) - { - layers.Layer().PaddingLeft(36).PaddingTop(pageH - 32).Width(pageW - 72).Height(24).Row(row => - { - row.RelativeItem().Text(pageModel.FooterLeft ?? "CONFIDENTIAL & PROPRIETARY").FontSize(8).FontColor(Colors.Grey.Medium); - if (!string.IsNullOrEmpty(pageModel.FooterCenter)) - { - row.RelativeItem().AlignCenter().Text(pageModel.FooterCenter).FontSize(8).FontColor(Colors.Grey.Medium); - } - row.RelativeItem().AlignRight().Text(text => - { - text.DefaultTextStyle(x => x.FontSize(8).FontColor(Colors.Grey.Medium)); - text.Span("Page "); - text.CurrentPageNumber(); - text.Span(" of "); - text.TotalPages(); - }); - }); - } - }); - }); - - pageIndex++; - _progress?.Report(Math.Min(100.0, pageIndex * 100.0 / totalPages)); - } - } - - private void ComposeElement(IContainer container, PdfElementBase element) - { - switch (element) - { - case PdfTextElement textEl: - ComposeText(container, textEl); - break; - - case PdfShapeElement shapeEl: - ComposeShape(container, shapeEl); - break; - - case PdfDividerElement divEl: - ComposeDivider(container, divEl); - break; - - case PdfTableElement tableEl: - ComposeTable(container, tableEl); - break; - - case PdfChartElement chartEl: - ComposeChart(container, chartEl); - break; - - case PdfImageElement imgEl: - ComposeImage(container, imgEl); - break; - - case PdfQrCodeElement qrEl: - ComposeQrCode(container, qrEl); - break; - - case PdfBarcodeElement barEl: - ComposeBarcode(container, barEl); - break; - - case PdfFormFieldElement formEl: - ComposeFormField(container, formEl); - break; - - case PdfRedactionElement redEl: - container.Background(redEl.FillColorHex).Border((float)redEl.BorderThickness).BorderColor(redEl.BorderColorHex).Padding(4).AlignCenter().AlignMiddle() - .Text(redEl.ShowOverlayText ? redEl.ExemptionCode : "").FontSize(8.5f).Bold().FontColor(redEl.TextColorHex); - break; - - case PdfInkElement inkEl: - ComposeInk(container, inkEl); - break; - - case PdfStickyNoteElement noteEl: - ComposeStickyNote(container, noteEl); - break; - - case PdfMeasurementElement measEl: - container.PaddingVertical(2).Row(mRow => - { - mRow.AutoItem().Text("|").FontSize(9).Bold().FontColor(measEl.StrokeColorHex); - mRow.RelativeItem().BorderBottom((float)measEl.StrokeThickness).BorderColor(measEl.StrokeColorHex).PaddingBottom(2).AlignCenter().Text(measEl.GetFormattedDistance()).FontSize((float)measEl.FontSize).Bold().FontColor(measEl.StrokeColorHex); - mRow.AutoItem().Text("|").FontSize(9).Bold().FontColor(measEl.StrokeColorHex); - }); - break; - - case PdfSvgElement svgEl: - ComposeSvg(container, svgEl); - break; - - case PdfMathElement mathEl: - ComposeMath(container, mathEl); - break; - } - } - - private void ComposeText(IContainer container, PdfTextElement textEl) - { - // For curved/circular typography, outline strokes, drop shadows, double underlines, custom scaling/flipping or baseline shifts, - // render with high-precision vector SVG so all transformations and glyph geometries remain 100% scalable in the PDF. - bool requiresVectorSvg = textEl.ShapeMode != TextShapeMode.Normal || - textEl.HasStroke || - textEl.HasShadow || - textEl.IsDoubleUnderline || - Math.Abs(textEl.ScaleX - 1.0) > 0.01 || - Math.Abs(textEl.ScaleY - 1.0) > 0.01 || - textEl.FlipX || - textEl.FlipY || - Math.Abs(textEl.BaselineShift) > 0.01 || - Math.Abs(textEl.CharacterRotation) > 0.01 || - Math.Abs(textEl.WordSpacing) > 0.01 || - Math.Abs(textEl.ParagraphSpacing) > 0.01 || - textEl.VerticalAlignment != TextVerticalAlignment.Top; - - if (requiresVectorSvg) - { - try - { - string svgMarkup = TextLayoutEngine.GenerateSvgMarkup(textEl); - container.Svg(svgMarkup).FitArea(); - return; - } - catch - { - // Fallback to standard text composition on any unexpected error - } - } - - var target = container; - - if (textEl.CornerRadius > 0) - { - target = target.CornerRadius((float)textEl.CornerRadius); - } - - if (textEl.BorderThickness > 0 && textEl.BorderColorHex != "#00000000" && !textEl.BorderColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase)) - { - target = target.Border((float)textEl.BorderThickness).BorderColor(textEl.BorderColorHex); - } - - if (textEl.BackgroundColorHex != "#00000000" && !textEl.BackgroundColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase)) - { - target = target.Background(textEl.BackgroundColorHex); - } - - target.Padding((float)textEl.Padding).Text(text => - { - if (textEl.Alignment == TextAlignmentMode.Center) text.AlignCenter(); - else if (textEl.Alignment == TextAlignmentMode.Right) text.AlignRight(); - else if (textEl.Alignment == TextAlignmentMode.Justify) text.Justify(); - - if (textEl.Spans != null && textEl.Spans.Count > 0) - { - foreach (var s in textEl.Spans) - { - if (string.IsNullOrEmpty(s.Text)) continue; - - var span = !string.IsNullOrEmpty(s.LinkUrl) - ? text.Hyperlink(s.Text, s.LinkUrl) - : text.Span(s.Text); - - span.FontFamily(s.FontFamily ?? textEl.FontFamily ?? "Arial") - .FontSize((float)(s.FontSize ?? textEl.FontSize)) - .FontColor(s.TextColorHex ?? textEl.TextColorHex); - - if (textEl.LineHeight > 0.1) span.LineHeight((float)textEl.LineHeight); - if (s.IsBold ?? textEl.IsBold) span.Bold(); - if (s.IsItalic ?? textEl.IsItalic) span.Italic(); - if (s.IsUnderline ?? textEl.IsUnderline) span.Underline(); - if (s.IsStrikethrough ?? textEl.IsStrikethrough) span.Strikethrough(); - if (s.Script == TextScriptMode.Superscript) span.Superscript(); - if (s.Script == TextScriptMode.Subscript) span.Subscript(); - } - } - else - { - var span = text.Span(textEl.Text ?? "") - .FontFamily(textEl.FontFamily ?? "Arial") - .FontSize((float)textEl.FontSize) - .FontColor(textEl.TextColorHex); - - if (textEl.LineHeight > 0.1) span.LineHeight((float)textEl.LineHeight); - if (textEl.IsBold) span.Bold(); - if (textEl.IsItalic) span.Italic(); - if (textEl.IsUnderline) span.Underline(); - if (textEl.IsStrikethrough) span.Strikethrough(); - } - }); - } - - private void ComposeShape(IContainer container, PdfShapeElement shapeEl) - { - if (shapeEl.ShapeType == ShapeType.Rectangle && shapeEl.CornerRadius <= 0 && string.IsNullOrEmpty(shapeEl.CustomPathData)) - { - var target = container; - - if (!string.IsNullOrEmpty(shapeEl.FillColorHex) && shapeEl.FillColorHex != "#00000000" && !shapeEl.FillColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase)) - { - target = target.Background(shapeEl.FillColorHex); - } - - if (shapeEl.StrokeThickness > 0 && !string.IsNullOrEmpty(shapeEl.StrokeColorHex) && shapeEl.StrokeColorHex != "#00000000" && !shapeEl.StrokeColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase)) - { - target = target.Border((float)shapeEl.StrokeThickness).BorderColor(shapeEl.StrokeColorHex); - } - - if (!string.IsNullOrEmpty(shapeEl.Label)) - { - target.Padding(4).AlignCenter().AlignMiddle().Text(shapeEl.Label) - .FontSize((float)shapeEl.LabelFontSize) - .FontColor(shapeEl.LabelColorHex ?? "#201F1E") - .Bold(); - } - return; - } - - try - { - string svgMarkup = SvgShapeHelper.GenerateSvgMarkup(shapeEl); - container.Svg(svgMarkup); - } - catch - { - var target = container; - - if (shapeEl.ShapeType == ShapeType.Circle) - { - target = target.CornerRadius((float)Math.Max(shapeEl.Width, shapeEl.Height) / 2); - } - else if (shapeEl.CornerRadius > 0) - { - target = target.CornerRadius((float)shapeEl.CornerRadius); - } - - if (!string.IsNullOrEmpty(shapeEl.FillColorHex) && shapeEl.FillColorHex != "#00000000" && !shapeEl.FillColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase)) - { - target = target.Background(shapeEl.FillColorHex); - } - - if (shapeEl.StrokeThickness > 0 && !string.IsNullOrEmpty(shapeEl.StrokeColorHex) && shapeEl.StrokeColorHex != "#00000000" && !shapeEl.StrokeColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase)) - { - target = target.Border((float)shapeEl.StrokeThickness).BorderColor(shapeEl.StrokeColorHex); - } - - if (!string.IsNullOrEmpty(shapeEl.Label)) - { - target.Padding(4).AlignCenter().AlignMiddle().Text(shapeEl.Label) - .FontSize((float)shapeEl.LabelFontSize) - .FontColor(shapeEl.LabelColorHex ?? "#201F1E") - .Bold(); - } - } - } - - private void ComposeDivider(IContainer container, PdfDividerElement divEl) - { - if (divEl.Style == DividerStyle.Straight && divEl.DashStyle == LineDashStyle.Solid) - { - if (divEl.IsVertical) - { - container.AlignCenter().LineVertical((float)divEl.Thickness).LineColor(divEl.ColorHex); - } - else - { - container.AlignMiddle().LineHorizontal((float)divEl.Thickness).LineColor(divEl.ColorHex); - } - return; - } - - try - { - string svgMarkup = SvgShapeHelper.GenerateDividerSvgMarkup(divEl); - container.Svg(svgMarkup).FitArea(); - } - catch - { - if (divEl.IsVertical) - container.AlignCenter().LineVertical((float)divEl.Thickness).LineColor(divEl.ColorHex); - else - container.AlignMiddle().LineHorizontal((float)divEl.Thickness).LineColor(divEl.ColorHex); - } - } - - private void ComposeInk(IContainer container, PdfInkElement inkEl) - { - try - { - string svgMarkup = SvgShapeHelper.GenerateInkSvgMarkup(inkEl); - container.Svg(svgMarkup).FitArea(); - } - catch - { - container.Background(inkEl.StrokeColorHex); - } - } - - private void ComposeTable(IContainer container, PdfTableElement tableEl) - { - container.Border(1).BorderColor(tableEl.BorderColorHex).Table(table => - { - table.ColumnsDefinition(columns => - { - for (int i = 0; i < Math.Max(1, tableEl.Headers.Count); i++) - { - columns.RelativeColumn(); - } - }); - - // Headers - if (tableEl.Headers.Count > 0) - { - table.Header(header => - { - foreach (var h in tableEl.Headers) - { - header.Cell().Background(tableEl.HeaderBackgroundHex).Padding(5).Text(h) - .FontColor(tableEl.HeaderTextHex) - .Bold() - .FontSize(8.5f); - } - }); - } - - // Rows - int rowIndex = 0; - foreach (var row in tableEl.Rows) - { - var bg = (rowIndex % 2 == 1) ? tableEl.AlternateRowBackgroundHex : "#FFFFFF"; - foreach (var cellText in row) - { - table.Cell().Background(bg).BorderBottom(0.5f).BorderColor(tableEl.BorderColorHex).Padding(4).Text(cellText) - .FontSize(8); - } - rowIndex++; - } - }); - } - - private void ComposeChart(IContainer container, PdfChartElement chartEl) - { - try - { - byte[] chartPng = LiveChartsRenderer.RenderChartToPngBytes( - chartEl, - Math.Max(180, (int)chartEl.Width), - Math.Max(120, (int)chartEl.Height), - 2.5f); - - if (chartPng.Length > 0) - { - container.Border(1).BorderColor(chartEl.BorderColorHex).Background(chartEl.BackgroundColorHex).Padding(4).Image(chartPng).FitArea(); - return; - } - } - catch - { - // Fallback to title block - } - - container.Border(1).BorderColor(chartEl.BorderColorHex).Background(chartEl.BackgroundColorHex).Padding(8).Column(chartCol => - { - chartCol.Item().AlignCenter().Text($"{chartEl.Title}").FontSize(9f).Bold().FontColor(Colors.Grey.Darken3); - }); - } - - private void ComposeImage(IContainer container, PdfImageElement imgEl) - { - if (imgEl.ImageData != null && imgEl.ImageData.Length > 0) - { - try - { - container.Image(imgEl.ImageData).FitArea(); - return; - } - catch { } - } - - if (!string.IsNullOrEmpty(imgEl.Base64Data)) - { - try - { - byte[] bytes = Convert.FromBase64String(imgEl.Base64Data); - container.Image(bytes).FitArea(); - return; - } - catch { } - } - - if (!string.IsNullOrEmpty(imgEl.ImagePath) && File.Exists(imgEl.ImagePath)) - { - try - { - if (imgEl.ImagePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) - { - string svg = File.ReadAllText(imgEl.ImagePath); - container.Svg(svg).FitArea(); - return; - } - - container.Image(imgEl.ImagePath).FitArea(); - return; - } - catch { } - } - - container.Border(1).BorderColor(imgEl.BorderColorHex).Background("#F3F2F1").AlignCenter().AlignMiddle() - .Text(imgEl.AltText ?? "Image").FontSize(10).FontColor(Colors.Grey.Medium); - } - - private void ComposeSvg(IContainer container, PdfSvgElement svgEl) - { - try - { - string svgData = !string.IsNullOrWhiteSpace(svgEl.SvgSource) - ? svgEl.SvgSource - : (!string.IsNullOrWhiteSpace(svgEl.FilePath) && File.Exists(svgEl.FilePath) - ? File.ReadAllText(svgEl.FilePath) - : SvgOrnamentLibrary.GetGaneshaCrestSvg()); - - if (!string.IsNullOrWhiteSpace(svgEl.TintColorHex)) - { - svgData = svgData.Replace("currentColor", svgEl.TintColorHex); - } - - container.Svg(svgData).FitArea(); - } - catch - { - container.Border(1).BorderColor(svgEl.BorderColorHex ?? "#CBD5E1").Background("#F8FAFC").AlignCenter().AlignMiddle() - .Text("SVG Vector Art").FontSize(9).FontColor(Colors.Grey.Medium); - } - } - - private void ComposeMath(IContainer container, PdfMathElement mathEl) - { - try - { - var target = container; - - if (mathEl.CornerRadius > 0) - { - target = target.CornerRadius((float)mathEl.CornerRadius); - } - - if (mathEl.ShowBorder && mathEl.BorderThickness > 0 && !string.IsNullOrEmpty(mathEl.BorderColorHex) && !mathEl.BorderColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase)) - { - target = target.Border((float)mathEl.BorderThickness).BorderColor(mathEl.BorderColorHex); - } - - if (mathEl.ShowBackground && !string.IsNullOrEmpty(mathEl.BackgroundColorHex) && !mathEl.BackgroundColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase)) - { - target = target.Background(mathEl.BackgroundColorHex); - } - - if (mathEl.Padding > 0) - { - target = target.Padding((float)mathEl.Padding); - } - - var options = new MathRenderOptions - { - FontSize = mathEl.FontSize > 0 ? mathEl.FontSize : 16.0, - TextColorHex = !string.IsNullOrEmpty(mathEl.TextColorHex) ? mathEl.TextColorHex : "#000000", - BackgroundColorHex = "#00000000", - DisplayStyle = mathEl.DisplayStyle, - Alignment = mathEl.Alignment, - ShowEquationNumber = mathEl.ShowEquationNumber, - EquationNumber = mathEl.EquationNumber, - Padding = 2.0 - }; - - var renderResult = MathLayoutEngine.RenderToSvg(mathEl.Formula, options); - target.Svg(renderResult.SvgMarkup).FitArea(); - } - catch - { - container.Border(1).BorderColor("#CBD5E1").Background("#F8FAFC").AlignCenter().AlignMiddle() - .Text(mathEl.Formula ?? "f(x)").FontSize((float)mathEl.FontSize).FontColor(mathEl.TextColorHex ?? "#000000"); - } - } - - private void ComposeQrCode(IContainer container, PdfQrCodeElement qrEl) - { - try - { - byte[] qrBytes = QrCodeHelper.GeneratePngBytes( - qrEl.Content, - qrEl.DarkColorHex, - qrEl.LightColorHex, - qrEl.EccLevel, - pixelsPerModule: 20, - drawQuietZones: qrEl.DrawQuietZones); - - string bgHex = !string.IsNullOrWhiteSpace(qrEl.LightColorHex) ? qrEl.LightColorHex : "#FFFFFF"; - - container.Border(1).BorderColor(Colors.Grey.Lighten2).Background(bgHex).Padding(4).Column(qCol => - { - qCol.Item().AlignCenter().Image(qrBytes).FitArea(); - if (!string.IsNullOrEmpty(qrEl.Label)) - { - qCol.Item().PaddingTop(2).AlignCenter().Text(qrEl.Label).FontSize(7f).Bold().FontColor(Colors.Grey.Darken2); - } - }); - } - catch - { - container.Border(1).BorderColor(Colors.Grey.Lighten2).Background(Colors.White).AlignCenter().AlignMiddle() - .Text("QR CODE").FontSize(8).Bold(); - } - } - - private void ComposeBarcode(IContainer container, PdfBarcodeElement barEl) - { - container.Border(1).BorderColor(Colors.Grey.Lighten2).Background(Colors.White).Padding(4).Column(bCol => - { - // Algorithmic barcode stripes - bCol.Item().AlignCenter().Height(22).Row(r => - { - string val = barEl.CodeValue ?? "12345678"; - foreach (char c in val) - { - int pattern = (int)c % 5; - r.AutoItem().Width(pattern == 0 ? 1 : (pattern == 1 ? 2 : (pattern == 2 ? 1.5f : 3))).Background(barEl.BarColorHex); - r.AutoItem().Width(pattern % 2 == 0 ? 1 : 2).Background(Colors.White); - } - }); - - if (barEl.ShowText && !string.IsNullOrEmpty(barEl.CodeValue)) - { - bCol.Item().PaddingTop(2).AlignCenter().Text(barEl.CodeValue).FontSize(7.5f).FontColor(Colors.Grey.Darken3); - } - }); - } - - private void ComposeFormField(IContainer container, PdfFormFieldElement formEl) - { - container.Border(1).BorderColor(formEl.BorderColorHex).Background(formEl.BackgroundColorHex).Padding(4).Column(fCol => - { - fCol.Item().Row(r => - { - r.RelativeItem().Text(formEl.Label).FontSize(8).Bold().FontColor(Colors.Grey.Darken3); - if (formEl.IsRequired) - { - r.AutoItem().Text("* Required").FontSize(7f).FontColor(Colors.Red.Medium); - } - }); - - if (formEl.FieldType == FormFieldType.Checkbox) - { - fCol.Item().PaddingTop(2).Row(r => - { - r.AutoItem().Width(10).Height(10).Border(1).BorderColor(Colors.Grey.Darken2).Background(formEl.IsChecked ? Colors.Blue.Lighten4 : Colors.White); - r.RelativeItem().PaddingLeft(4).Text(formEl.IsChecked ? "[X] Checked" : "[ ] Unchecked").FontSize(7.5f); - }); - } - else if (formEl.FieldType == FormFieldType.Signature) - { - fCol.Item().PaddingTop(4).BorderBottom(1).BorderColor(Colors.Grey.Medium).PaddingBottom(2).Text(string.IsNullOrEmpty(formEl.Value) ? "Authorized Signature" : formEl.Value).FontSize(8.5f).Italic().FontColor(Colors.Blue.Darken2); - } - else - { - fCol.Item().PaddingTop(2).Border(1).BorderColor(Colors.Grey.Lighten1).Background(Colors.White).Padding(2).Text(string.IsNullOrEmpty(formEl.Value) ? formEl.Placeholder : formEl.Value).FontSize(7.5f).FontColor(string.IsNullOrEmpty(formEl.Value) ? Colors.Grey.Medium : Colors.Black); - } - }); - } - - private void ComposeStickyNote(IContainer container, PdfStickyNoteElement noteEl) - { - container.Border(1).BorderColor(noteEl.BorderColorHex).Background(noteEl.ColorHex).Padding(6).Column(nCol => - { - nCol.Item().Row(r => - { - r.RelativeItem().Text($"📌 {noteEl.Author}").FontSize(8f).Bold().FontColor("#78350F"); - r.AutoItem().Text(noteEl.Timestamp).FontSize(7f).FontColor("#92400E"); - }); - nCol.Item().PaddingTop(2).Text(noteEl.NoteText).FontSize(7.5f).FontColor("#78350F"); - nCol.Item().PaddingTop(2).Text($"Status: {noteEl.Status}").FontSize(7f).Bold().FontColor("#B45309"); - }); - } -} diff --git a/src/PdfEditorApp/Services/PdfImportService.cs b/src/PdfEditorApp/Services/PdfImportService.cs deleted file mode 100644 index 2d940a4..0000000 --- a/src/PdfEditorApp/Services/PdfImportService.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Core.Deconstruction; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Pipelines; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Import; - -namespace PdfEditorApp.Services; - -public interface IPdfImportService -{ - Task ImportPdfAsync(string filePath, string? password = null); - Task ImportPdfFromBytesAsync(byte[] pdfBytes, string title = "Imported_Document.pdf", string? password = null); - Task ImportPdfBytesAsync(byte[] pdfBytes, string title = "Imported_Document.pdf", string? password = null); -} - -/// -/// Professional Multi-Format Document Import and Deconstruction Engine. -/// Parses PDF, raster images, markdown, and text files into an editable, multi-layered PdfDocumentModel -/// with pluggable format discovery and Waterfall pipeline interception. -/// -public class PdfImportService : IPdfImportService -{ - private readonly IDocumentImporterRegistry _importerRegistry; - private readonly IFryPluginContext? _pluginContext; - - public PdfImportService(IDocumentImporterRegistry? importerRegistry = null, IFryPluginContext? pluginContext = null) - { - _importerRegistry = importerRegistry ?? new DocumentImporterRegistry(); - _pluginContext = pluginContext; - } - - public Task ImportPdfBytesAsync(byte[] pdfBytes, string title = "Imported_Document.pdf", string? password = null) - => ImportPdfFromBytesAsync(pdfBytes, title, password); - - public async Task ImportPdfAsync(string filePath, string? password = null) - { - if (!File.Exists(filePath)) - throw new FileNotFoundException($"Document file not found: {filePath}"); - - byte[] bytes = await File.ReadAllBytesAsync(filePath); - string title = Path.GetFileName(filePath); - return await ImportDocumentInternalAsync(bytes, filePath, title, password); - } - - public async Task ImportPdfFromBytesAsync(byte[] pdfBytes, string title = "Imported_Document.pdf", string? password = null) - { - return await ImportDocumentInternalAsync(pdfBytes, title, title, password); - } - - private async Task ImportDocumentInternalAsync(byte[] bytes, string pathOrExtension, string title, string? password) - { - using var stream = new MemoryStream(bytes); - var pipelineContext = new PdfImportPipelineContext(stream, title, password); - - if (_pluginContext != null) - { - await _pluginContext.ExecuteWaterfallAsync("document:import", pipelineContext, async () => - { - pipelineContext.Document = await ExecuteImportCoreAsync(stream, pathOrExtension, title, password); - }); - } - else - { - pipelineContext.Document = await ExecuteImportCoreAsync(stream, pathOrExtension, title, password); - } - - return pipelineContext.Document ?? throw new InvalidOperationException($"Failed to import document: {title}"); - } - - private async Task ExecuteImportCoreAsync(Stream stream, string pathOrExtension, string title, string? password) - { - var importer = _importerRegistry.FindImporter(pathOrExtension); - if (importer != null) - { - stream.Position = 0; - return await importer.ImportAsync(stream, title, password); - } - - // Direct fallback to PDF deconstruction engine - stream.Position = 0; - using var ms = new MemoryStream(); - await stream.CopyToAsync(ms); - return await Task.Run(() => PdfDeconstructionEngine.Deconstruct(ms.ToArray(), title, password)); - } -} diff --git a/src/PdfEditorApp/Services/Plugins/PluginMarketplaceService.cs b/src/PdfEditorApp/Services/Plugins/PluginMarketplaceService.cs deleted file mode 100644 index 8fe83f9..0000000 --- a/src/PdfEditorApp/Services/Plugins/PluginMarketplaceService.cs +++ /dev/null @@ -1,806 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Marketplace; -using PdfEditorApp.Plugins.Loader; - - -namespace PdfEditorApp.Services.Plugins; - -/// -/// Service providing access to the curated FryPDF Plugin Store and Marketplace. -/// Features real, functional extension packages with persistent history and 1-click mounting into the isolated plugin kernel. -/// -public class PluginMarketplaceService : IPluginMarketplaceService -{ - public const string DefaultRegistryBaseUrl = "https://raw.githubusercontent.com/codefrydev/PDFCreator-resources/refs/heads/main/plugins"; - - private readonly PluginHost? _pluginHost; - private readonly IOverlayRegistry? _overlayRegistry; - private readonly IInstalledPluginStore _installedPluginStore; - private readonly string _pluginsDirectory; - private readonly string _registryBaseUrl; - private readonly HttpClient _httpClient; - private readonly HashSet _installedMarketplaceIds = new(StringComparer.OrdinalIgnoreCase); - private readonly List _remoteExtensions = new(); - private readonly object _catalogLock = new(); - - /// - /// All extensions and plugins are discovered and downloaded dynamically from the online catalog.json registry. - /// - private static readonly List CuratedExtensions = new(); - - public PluginMarketplaceService( - PluginHost? pluginHost = null, - IOverlayRegistry? overlayRegistry = null, - IInstalledPluginStore? installedPluginStore = null, - HttpClient? httpClient = null, - string? registryBaseUrl = null, - string? pluginsDirectory = null) - { - _pluginHost = pluginHost; - _overlayRegistry = overlayRegistry; - _installedPluginStore = installedPluginStore ?? new FileInstalledPluginStore(); - _pluginsDirectory = string.IsNullOrWhiteSpace(pluginsDirectory) - ? Path.Combine(AppContext.BaseDirectory, "plugins") - : pluginsDirectory; - _registryBaseUrl = string.IsNullOrWhiteSpace(registryBaseUrl) ? DefaultRegistryBaseUrl : registryBaseUrl.TrimEnd('/'); - _httpClient = httpClient ?? new HttpClient { Timeout = TimeSpan.FromSeconds(6) }; - - try - { - Directory.CreateDirectory(_pluginsDirectory); - LoadDiskCatalogCache(); - ScanInstalledMarketplacePlugins(); - RestorePersistedPlugins(); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginMarketplaceService] Init warning: {ex.Message}"); - } - } - - private bool _hasFetchedRemote; - - private void LoadDiskCatalogCache() - { - try - { - var cacheFile = Path.Combine(_pluginsDirectory, "catalog_cache.json"); - if (!File.Exists(cacheFile)) - { - var baseCache = Path.Combine(AppContext.BaseDirectory, "plugins", "catalog_cache.json"); - if (File.Exists(baseCache)) - { - cacheFile = baseCache; - } - } - - if (File.Exists(cacheFile)) - { - var json = File.ReadAllText(cacheFile); - var cached = JsonSerializer.Deserialize>(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - if (cached != null && cached.Count > 0) - { - lock (_catalogLock) - { - _remoteExtensions.Clear(); - _remoteExtensions.AddRange(cached.Where(c => c != null && !string.IsNullOrEmpty(c.Id))); - } - } - } - - DiscoverFromLocalExamples(); - } - catch { } - } - - private static string? FindExamplesDirectory() - { - var current = AppContext.BaseDirectory; - while (!string.IsNullOrEmpty(current)) - { - var candidate = Path.Combine(current, "docs", "examples"); - if (Directory.Exists(candidate)) - { - return Path.GetFullPath(candidate); - } - - var parent = Directory.GetParent(current); - if (parent == null || parent.FullName == current) break; - current = parent.FullName; - } - - current = Directory.GetCurrentDirectory(); - while (!string.IsNullOrEmpty(current)) - { - var candidate = Path.Combine(current, "docs", "examples"); - if (Directory.Exists(candidate)) - { - return Path.GetFullPath(candidate); - } - - var parent = Directory.GetParent(current); - if (parent == null || parent.FullName == current) break; - current = parent.FullName; - } - - return null; - } - - private void DiscoverFromLocalExamples() - { - try - { - var examplesDir = FindExamplesDirectory(); - if (examplesDir != null && Directory.Exists(examplesDir)) - { - foreach (var dir in Directory.GetDirectories(examplesDir)) - { - var manifestPath = Path.Combine(dir, "plugin.json"); - if (File.Exists(manifestPath)) - { - var json = File.ReadAllText(manifestPath); - using var doc = JsonDocument.Parse(json); - var rootElem = doc.RootElement; - var id = rootElem.TryGetProperty("id", out var idProp) ? idProp.GetString() : null; - var name = rootElem.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null; - var author = rootElem.TryGetProperty("author", out var authorProp) ? authorProp.GetString() : "Community"; - var version = rootElem.TryGetProperty("version", out var verProp) ? verProp.GetString() : "1.0.0"; - var desc = rootElem.TryGetProperty("description", out var descProp) ? descProp.GetString() : ""; - var icon = rootElem.TryGetProperty("icon", out var iconProp) ? iconProp.GetString() : "PuzzleOutline"; - - if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(name)) - { - lock (_catalogLock) - { - if (!_remoteExtensions.Any(e => string.Equals(e?.Id, id, StringComparison.OrdinalIgnoreCase))) - { - var rawShort = id.Split('.').Last(); - var shortName = char.ToUpperInvariant(rawShort[0]) + (rawShort.Length > 1 ? rawShort.Substring(1) : ""); - _remoteExtensions.Add(new MarketplacePluginItem - { - Id = id, - Name = name, - Publisher = author ?? "FryPDF Team", - Version = version ?? "1.0.0", - Description = desc ?? "", - IconKind = icon ?? "PuzzleOutline", - IsOfficial = true, - IsVerified = true, - Category = "UI & Extensions", - DownloadUrl = $"{_registryBaseUrl}/{id}/{shortName}.fryplugin" - }); - } - } - } - } - } - } - } - catch { } - } - - private static IFryPlugin? InstantiatePlugin(string pluginId) - { - return null; - } - - private void RestorePersistedPlugins() - { - if (_pluginHost == null) return; - - var records = _installedPluginStore.GetAll(); - foreach (var rec in records) - { - if (!rec.IsEnabled) continue; - - var plugin = InstantiatePlugin(rec.PluginId); - if (plugin != null) - { - if (_pluginHost.GetPluginState(rec.PluginId) == PluginState.Unloaded) - { - _pluginHost.RegisterPlugin(plugin); - } - - if (!_pluginHost.IsPluginActive(rec.PluginId)) - { - try - { - _pluginHost.EnablePluginAsync(rec.PluginId).GetAwaiter().GetResult(); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginMarketplaceService] Restore error for {rec.PluginId}: {ex.Message}"); - } - } - - _installedMarketplaceIds.Add(rec.PluginId); - - if (rec.WasOverlayOpen) - { - var overlayReg = _overlayRegistry ?? _pluginHost.Context.GetService(); - overlayReg?.ShowOverlay(rec.PluginId); - } - } - else - { - // Try restoring external downloaded plugin from plugins/{pluginId}/ - var pluginDir = Path.Combine(_pluginsDirectory, rec.PluginId); - if (Directory.Exists(pluginDir)) - { - try - { - string? entryDll = null; - var manifestFile = Path.Combine(pluginDir, "plugin.json"); - if (File.Exists(manifestFile)) - { - var json = File.ReadAllText(manifestFile); - var manifest = JsonSerializer.Deserialize(json); - if (!string.IsNullOrWhiteSpace(manifest?.EntryPoint)) - { - var candidate = Path.Combine(pluginDir, manifest.EntryPoint); - if (File.Exists(candidate)) entryDll = candidate; - } - } - - entryDll ??= Directory.GetFiles(pluginDir, "*.dll", SearchOption.TopDirectoryOnly).FirstOrDefault(); - if (entryDll != null) - { - var pkg = PluginAssemblyLoader.LoadPluginAssembly(entryDll); - if (pkg.Plugins.Count > 0) - { - _pluginHost.RegisterPlugins(pkg.Plugins); - foreach (var p in pkg.Plugins) - { - if (!_pluginHost.IsPluginActive(p.Id)) - { - _pluginHost.EnablePluginAsync(p.Id).GetAwaiter().GetResult(); - } - } - - _installedMarketplaceIds.Add(rec.PluginId); - - if (rec.WasOverlayOpen) - { - var overlayReg = _overlayRegistry ?? _pluginHost.Context.GetService(); - overlayReg?.ShowOverlay(rec.PluginId); - } - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginMarketplaceService] Error restoring external plugin {rec.PluginId}: {ex.Message}"); - } - } - } - } - } - - private void ScanInstalledMarketplacePlugins() - { - _installedMarketplaceIds.Clear(); - - foreach (var rec in _installedPluginStore.GetAll()) - { - if (rec != null && rec.IsEnabled && !string.IsNullOrEmpty(rec.PluginId)) - { - _installedMarketplaceIds.Add(rec.PluginId); - } - } - - if (_pluginHost != null) - { - foreach (var item in CuratedExtensions) - { - if (item != null && !string.IsNullOrEmpty(item.Id) && _pluginHost.IsPluginActive(item.Id)) - { - _installedMarketplaceIds.Add(item.Id); - } - } - - List snapshot; - lock (_catalogLock) - { - snapshot = _remoteExtensions.Where(i => i != null && !string.IsNullOrEmpty(i.Id)).ToList(); - } - - foreach (var item in snapshot) - { - if (_pluginHost.IsPluginActive(item.Id)) - { - _installedMarketplaceIds.Add(item.Id); - } - } - } - } - - public async Task> FetchRemoteCatalogAsync(bool forceRefresh = false, CancellationToken ct = default) - { - if (_hasFetchedRemote && !forceRefresh) - { - lock (_catalogLock) - { - return _remoteExtensions.Where(i => i != null && !string.IsNullOrEmpty(i.Id)).ToList(); - } - } - - var catalogUrl = $"{_registryBaseUrl}/catalog.json"; - try - { - using var response = await _httpClient.GetAsync(catalogUrl, ct); - if (response.IsSuccessStatusCode) - { - var json = await response.Content.ReadAsStringAsync(ct); - var items = JsonSerializer.Deserialize>(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - if (items != null && items.Count > 0) - { - lock (_catalogLock) - { - foreach (var item in items.Where(i => i != null && !string.IsNullOrEmpty(i.Id))) - { - var existing = _remoteExtensions.FirstOrDefault(e => string.Equals(e?.Id, item.Id, StringComparison.OrdinalIgnoreCase)); - if (existing != null) - { - _remoteExtensions.Remove(existing); - } - _remoteExtensions.Add(item); - } - } - - // Also merge any local examples not present in remote catalog yet - DiscoverFromLocalExamples(); - - // Persist to local disk cache for fast/offline resilience - try - { - string mergedJson; - lock (_catalogLock) - { - mergedJson = JsonSerializer.Serialize(_remoteExtensions, new JsonSerializerOptions { WriteIndented = true }); - } - var cacheFile = Path.Combine(_pluginsDirectory, "catalog_cache.json"); - Directory.CreateDirectory(_pluginsDirectory); - File.WriteAllText(cacheFile, mergedJson); - } - catch { } - } - } - else - { - lock (_catalogLock) - { - if (_remoteExtensions.Count == 0) - { - LoadDiskCatalogCache(); - } - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginMarketplaceService] Remote catalog fetch failed: {ex.Message}"); - lock (_catalogLock) - { - if (_remoteExtensions.Count == 0) - { - LoadDiskCatalogCache(); - } - } - } - - _hasFetchedRemote = true; - ScanInstalledMarketplacePlugins(); - List result; - lock (_catalogLock) - { - foreach (var item in _remoteExtensions) - { - if (item == null || string.IsNullOrEmpty(item.Id)) continue; - item.Status = IsPluginInstalled(item.Id) - ? MarketplacePluginStatus.Installed - : MarketplacePluginStatus.Available; - } - result = _remoteExtensions.Where(i => i != null && !string.IsNullOrEmpty(i.Id)).ToList(); - } - - return result; - } - - public Task> GetCatalogAsync(CancellationToken ct = default) - { - ScanInstalledMarketplacePlugins(); - var allItems = new List(CuratedExtensions); - List snapshot; - lock (_catalogLock) - { - snapshot = _remoteExtensions.Where(i => i != null && !string.IsNullOrEmpty(i.Id)).ToList(); - } - - foreach (var remote in snapshot) - { - if (!allItems.Any(c => string.Equals(c?.Id, remote.Id, StringComparison.OrdinalIgnoreCase))) - { - allItems.Add(remote); - } - } - - foreach (var item in allItems) - { - item.Status = IsPluginInstalled(item.Id) - ? MarketplacePluginStatus.Installed - : MarketplacePluginStatus.Available; - } - return Task.FromResult>(allItems); - } - - public Task> SearchAsync(string query, string? category = null, CancellationToken ct = default) - { - ScanInstalledMarketplacePlugins(); - List allItems; - lock (_catalogLock) - { - allItems = _remoteExtensions.Where(i => i != null && !string.IsNullOrEmpty(i.Id)).ToList(); - } - - var q = query.Trim().ToLowerInvariant(); - var results = allItems.Where(item => - { - if (!string.IsNullOrWhiteSpace(category) && !string.Equals(category, "All", StringComparison.OrdinalIgnoreCase)) - { - if (!string.Equals(item.Category, category, StringComparison.OrdinalIgnoreCase)) - return false; - } - - if (string.IsNullOrWhiteSpace(q)) return true; - - return (item.Name?.ToLowerInvariant().Contains(q) ?? false) || - (item.Id?.ToLowerInvariant().Contains(q) ?? false) || - (item.Publisher?.ToLowerInvariant().Contains(q) ?? false) || - (item.Description?.ToLowerInvariant().Contains(q) ?? false) || - (item.Tags?.Any(t => t.ToLowerInvariant().Contains(q)) ?? false); - }).ToList(); - - foreach (var item in results) - { - item.Status = IsPluginInstalled(item.Id) - ? MarketplacePluginStatus.Installed - : MarketplacePluginStatus.Available; - } - - return Task.FromResult>(results); - } - - public async Task InstallPluginAsync(string pluginId, IProgress? progress = null, Action? statusCallback = null, CancellationToken ct = default) - { - MarketplacePluginItem? item; - lock (_catalogLock) - { - item = CuratedExtensions.FirstOrDefault(e => string.Equals(e?.Id, pluginId, StringComparison.OrdinalIgnoreCase)) - ?? _remoteExtensions.FirstOrDefault(e => string.Equals(e?.Id, pluginId, StringComparison.OrdinalIgnoreCase)); - } - - // If not found in memory, try fetching remote catalog or fallback to known TicTacToe plugin - if (item == null) - { - await FetchRemoteCatalogAsync(forceRefresh: false, ct); - lock (_catalogLock) - { - item = _remoteExtensions.FirstOrDefault(e => string.Equals(e?.Id, pluginId, StringComparison.OrdinalIgnoreCase)); - } - } - - if (item == null) - { - var shortName = pluginId.Split('.').Last(); - item = new MarketplacePluginItem - { - Id = pluginId, - Name = shortName, - Publisher = "Community", - Version = "1.0.0", - Description = $"{shortName} plugin.", - DownloadUrl = $"{_registryBaseUrl}/{pluginId}/{shortName}.fryplugin" - }; - } - - item.Status = MarketplacePluginStatus.Installing; - statusCallback?.Invoke($"Connecting to FryPDF Marketplace registry for '{item.Name}'..."); - progress?.Report(0.1); - - // Check if pre-packaged local archive exists in docs/examples for fast/offline test environments - string? localArchive = null; - var shortId = pluginId.Split('.').Last(); - var capitalized = char.ToUpperInvariant(shortId[0]) + (shortId.Length > 1 ? shortId.Substring(1) : ""); - var candidateNames = new[] { shortId, capitalized, "Snake", "TicTacToe", "Scratchpad", "Telemetry" }; - var examplesDir = FindExamplesDirectory(); - if (examplesDir != null) - { - foreach (var name in candidateNames.Distinct(StringComparer.OrdinalIgnoreCase)) - { - var candidate = Path.Combine(examplesDir, $"{name}Plugin", "bin", "Release", "net10.0", $"{name}.fryplugin"); - if (File.Exists(candidate)) - { - localArchive = candidate; - break; - } - } - } - - // Case A: Remote package download from GitHub repository (with offline local fallback) - if (!string.IsNullOrWhiteSpace(item.DownloadUrl)) - { - statusCallback?.Invoke($"Downloading {item.FormattedSize} package archive from registry..."); - progress?.Report(0.2); - - var tempDir = Path.Combine(_pluginsDirectory, ".cache"); - Directory.CreateDirectory(tempDir); - var tempPackagePath = Path.Combine(tempDir, $"{item.Id}.fryplugin"); - - try - { - bool downloaded = false; - try - { - using (var response = await _httpClient.GetAsync(item.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, ct)) - { - if (response.IsSuccessStatusCode) - { - var contentLength = response.Content.Headers.ContentLength ?? 1; - await using var stream = await response.Content.ReadAsStreamAsync(ct); - await using var fileStream = File.Create(tempPackagePath); - - var buffer = new byte[8192]; - long totalBytesRead = 0; - int bytesRead; - - while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0) - { - await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct); - totalBytesRead += bytesRead; - progress?.Report(0.2 + 0.5 * ((double)totalBytesRead / contentLength)); - } - downloaded = true; - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginMarketplaceService] Remote package download failed: {ex.Message}"); - } - - if (!downloaded) - { - if (localArchive != null && File.Exists(localArchive)) - { - File.Copy(localArchive, tempPackagePath, overwrite: true); - } - else - { - item.Status = MarketplacePluginStatus.Available; - return false; - } - } - - statusCallback?.Invoke("Unpacking package archive and verifying manifest..."); - progress?.Report(0.75); - - var pkgResult = FryPluginPackageLoader.UnpackAndLoad(tempPackagePath, _pluginsDirectory); - - statusCallback?.Invoke("Mounting extension into isolated plugin kernel..."); - progress?.Report(0.85); - - if (_pluginHost != null) - { - _pluginHost.RegisterPlugins(pkgResult.AssemblyPackage.Plugins); - foreach (var pkgPlugin in pkgResult.AssemblyPackage.Plugins) - { - if (!_pluginHost.IsPluginActive(pkgPlugin.Id)) - { - await _pluginHost.EnablePluginAsync(pkgPlugin.Id, ct); - } - } - - var overlayReg = _overlayRegistry ?? _pluginHost.Context.GetService(); - overlayReg?.ShowOverlay(item.Id); - } - - _installedMarketplaceIds.Add(item.Id); - _installedPluginStore.AddOrUpdate(new InstalledPluginRecord - { - PluginId = item.Id, - Name = item.Name, - Version = item.Version, - InstalledAt = DateTime.UtcNow, - IsEnabled = true, - WasOverlayOpen = true - }); - - item.Status = MarketplacePluginStatus.Installed; - statusCallback?.Invoke($"'{item.Name}' installed and activated successfully!"); - progress?.Report(1.0); - return true; - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginMarketplaceService] Remote install failed: {ex.Message}"); - item.Status = MarketplacePluginStatus.Available; - statusCallback?.Invoke($"Failed to install '{item.Name}': {ex.Message}"); - return false; - } - finally - { - try - { - if (File.Exists(tempPackagePath)) - { - File.Delete(tempPackagePath); - } - } - catch { } - } - } - - // Case B: Local/simulated installation for built-in plugins (Snake, Scratchpad, Telemetry) - await Task.Delay(100, ct); - - statusCallback?.Invoke($"Preparing {item.FormattedSize} package components..."); - progress?.Report(0.35); - await Task.Delay(150, ct); - - statusCallback?.Invoke("Verifying package SHA-256 manifest and digital signatures..."); - progress?.Report(0.65); - await Task.Delay(100, ct); - - // Create installation folder in plugins/ - var targetDir = Path.Combine(_pluginsDirectory, item.Id); - Directory.CreateDirectory(targetDir); - - var manifestPath = Path.Combine(targetDir, "plugin.json"); - var manifestContent = $@"{{ - ""id"": ""{item.Id}"", - ""name"": ""{item.Name}"", - ""version"": ""{item.Version}"", - ""category"": ""{item.Category}"", - ""description"": ""{item.Description}"", - ""author"": ""{item.Publisher}"", - ""entryPoint"": ""{item.Id}.dll"", - ""license"": ""{item.License}"" -}}"; - await File.WriteAllTextAsync(manifestPath, manifestContent, ct); - - statusCallback?.Invoke("Mounting extension into isolated plugin kernel..."); - progress?.Report(0.85); - - // Mount and activate real plugin into host if available - var plugin = InstantiatePlugin(item.Id); - if (plugin != null && _pluginHost != null) - { - if (_pluginHost.GetPluginState(item.Id) == PluginState.Unloaded) - { - _pluginHost.RegisterPlugin(plugin); - } - - if (!_pluginHost.IsPluginActive(item.Id)) - { - await _pluginHost.EnablePluginAsync(item.Id, ct); - } - - var overlayReg = _overlayRegistry ?? _pluginHost.Context.GetService(); - overlayReg?.ShowOverlay(item.Id); - } - - _installedMarketplaceIds.Add(item.Id); - _installedPluginStore.AddOrUpdate(new InstalledPluginRecord - { - PluginId = item.Id, - Name = item.Name, - Version = item.Version, - InstalledAt = DateTime.UtcNow, - IsEnabled = true, - WasOverlayOpen = true - }); - - item.Status = MarketplacePluginStatus.Installed; - - statusCallback?.Invoke($"'{item.Name}' installed and activated successfully!"); - progress?.Report(1.0); - return true; - } - - public async Task UninstallPluginAsync(string pluginId, CancellationToken ct = default) - { - MarketplacePluginItem? item; - lock (_catalogLock) - { - item = CuratedExtensions.FirstOrDefault(e => string.Equals(e?.Id, pluginId, StringComparison.OrdinalIgnoreCase)) - ?? _remoteExtensions.FirstOrDefault(e => string.Equals(e?.Id, pluginId, StringComparison.OrdinalIgnoreCase)); - } - if (item != null) - { - item.Status = MarketplacePluginStatus.Available; - } - - _installedMarketplaceIds.Remove(pluginId); - _installedPluginStore.Remove(pluginId); - - if (_pluginHost != null) - { - var overlayReg = _overlayRegistry ?? _pluginHost.Context.GetService(); - overlayReg?.HideOverlay(pluginId); - - if (_pluginHost.IsPluginActive(pluginId)) - { - await _pluginHost.DisablePluginAsync(pluginId, ct); - } - } - - var targetDir = Path.Combine(_pluginsDirectory, pluginId); - if (Directory.Exists(targetDir)) - { - try - { - Directory.Delete(targetDir, recursive: true); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginMarketplaceService] Uninstall delete error: {ex.Message}"); - } - } - - return true; - } - - public bool IsPluginInstalled(string pluginId) - { - if (string.IsNullOrWhiteSpace(pluginId)) return false; - - if (_installedPluginStore.IsInstalled(pluginId)) - return true; - - if (_pluginHost != null && _pluginHost.IsPluginActive(pluginId)) - return true; - - return _installedMarketplaceIds.Contains(pluginId); - } - - public async Task> CheckForUpdatesAsync(CancellationToken ct = default) - { - var remote = await FetchRemoteCatalogAsync(forceRefresh: true, ct); - var updateAvailable = new List(); - - foreach (var remoteItem in remote) - { - var installed = _installedPluginStore.Get(remoteItem.Id); - if (installed != null) - { - if (Version.TryParse(remoteItem.Version, out var remoteVer) && - Version.TryParse(installed.Version, out var localVer)) - { - if (remoteVer > localVer) - { - remoteItem.Status = MarketplacePluginStatus.UpdateAvailable; - updateAvailable.Add(remoteItem); - } - } - } - } - - return updateAvailable; - } -} diff --git a/src/PdfEditorApp/Services/ProjectPersistenceService.cs b/src/PdfEditorApp/Services/ProjectPersistenceService.cs deleted file mode 100644 index da9e450..0000000 --- a/src/PdfEditorApp/Services/ProjectPersistenceService.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public interface IProjectPersistenceService -{ - Task SaveProjectAsync(PdfDocumentModel model, string filePath); - Task LoadProjectAsync(string filePath); - Task SaveAutoSaveAsync(PdfDocumentModel model, string? currentFilePath); - bool HasRecoverableAutoSave(string? currentFilePath, out string autoSavePath); - Task LoadAutoSaveAsync(string autoSavePath); - void CleanAutoSave(string? currentFilePath); -} - -public class ProjectPersistenceService : IProjectPersistenceService -{ - private readonly JsonSerializerOptions _options; - private readonly string _autoSaveDirectory; - private readonly IPdfImportService _importService; - private readonly PdfEditorApp.Core.Plugins.Descriptors.ICanvasElementRegistry? _canvasElementRegistry; - - public ProjectPersistenceService( - IPdfImportService? importService = null, - PdfEditorApp.Core.Plugins.Descriptors.ICanvasElementRegistry? canvasElementRegistry = null) - { - _importService = importService ?? new PdfImportService(); - _canvasElementRegistry = canvasElementRegistry; - _options = PdfEditorApp.Core.Models.Elements.DynamicElementJsonResolver.CreateOptions( - _canvasElementRegistry != null ? () => _canvasElementRegistry.GetAllDescriptors() : null, - writeIndented: true); - - string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - _autoSaveDirectory = Path.Combine(localAppData, "FryPDF", "AutoSave"); - try - { - if (!Directory.Exists(_autoSaveDirectory)) - { - Directory.CreateDirectory(_autoSaveDirectory); - } - } - catch - { - _autoSaveDirectory = Path.GetTempPath(); - } - } - - public async Task SaveProjectAsync(PdfDocumentModel model, string filePath) - { - var json = JsonSerializer.Serialize(model, _options); - string tempPath = filePath + ".tmp"; - - // Write safely to temp file first - await File.WriteAllTextAsync(tempPath, json); - - // Atomic swap - if (File.Exists(filePath)) - { - File.Replace(tempPath, filePath, null); - } - else - { - File.Move(tempPath, filePath); - } - - // Clean any staging autosave after successful explicit save - CleanAutoSave(filePath); - } - - public async Task LoadProjectAsync(string filePath) - { - if (!File.Exists(filePath)) return null; - - // 1. Check if file is binary PDF (by extension or PDF magic header %PDF-) - bool isPdf = false; - string ext = Path.GetExtension(filePath).ToLowerInvariant(); - if (ext == ".pdf") - { - isPdf = true; - } - else - { - try - { - using var fs = File.OpenRead(filePath); - byte[] header = new byte[5]; - int read = await fs.ReadAsync(header, 0, 5); - if (read >= 4 && header[0] == '%' && header[1] == 'P' && header[2] == 'D' && header[3] == 'F') - { - isPdf = true; - } - } - catch { } - } - - if (isPdf) - { - return await _importService.ImportPdfAsync(filePath); - } - - // 2. Load as JSON FryPDF project - var json = await File.ReadAllTextAsync(filePath); - return JsonSerializer.Deserialize(json, _options); - } - - public string GetAutoSaveFilePath(string? currentFilePath) - { - string key = string.IsNullOrWhiteSpace(currentFilePath) ? "untitled_session" : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(currentFilePath))); - return Path.Combine(_autoSaveDirectory, $"{key}.autosave.frypdf"); - } - - public async Task SaveAutoSaveAsync(PdfDocumentModel model, string? currentFilePath) - { - string autoSaveFile = GetAutoSaveFilePath(currentFilePath); - string json = JsonSerializer.Serialize(model, _options); - await File.WriteAllTextAsync(autoSaveFile, json); - } - - public bool HasRecoverableAutoSave(string? currentFilePath, out string autoSavePath) - { - autoSavePath = GetAutoSaveFilePath(currentFilePath); - if (!File.Exists(autoSavePath)) return false; - - if (string.IsNullOrWhiteSpace(currentFilePath) || !File.Exists(currentFilePath)) - { - return true; - } - - // If autosave file is newer than the saved project file - var autoSaveTime = File.GetLastWriteTimeUtc(autoSavePath); - var projectFileTime = File.GetLastWriteTimeUtc(currentFilePath); - return autoSaveTime > projectFileTime.AddSeconds(5); - } - - public async Task LoadAutoSaveAsync(string autoSavePath) - { - return await LoadProjectAsync(autoSavePath); - } - - public void CleanAutoSave(string? currentFilePath) - { - try - { - string path = GetAutoSaveFilePath(currentFilePath); - if (File.Exists(path)) - { - File.Delete(path); - } - } - catch - { - // Best effort cleanup - } - } -} diff --git a/src/PdfEditorApp/Services/QrCodeHelper.cs b/src/PdfEditorApp/Services/QrCodeHelper.cs deleted file mode 100644 index 4337e23..0000000 --- a/src/PdfEditorApp/Services/QrCodeHelper.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using Avalonia.Media.Imaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using QRCoder; - -namespace PdfEditorApp.Services; - -/// -/// High-performance QR Code generation helper supporting live Avalonia bitmap previews, -/// custom brand colors, error correction levels, and vector/raster export bytes. -/// -public static class QrCodeHelper -{ - public static byte[] HexToRgba(string? hex, byte defaultR, byte defaultG, byte defaultB, byte defaultA = 255) - { - if (string.IsNullOrWhiteSpace(hex)) - { - return new byte[] { defaultR, defaultG, defaultB, defaultA }; - } - - string clean = hex.Trim().TrimStart('#'); - try - { - if (clean.Length == 6) - { - byte r = byte.Parse(clean.Substring(0, 2), NumberStyles.HexNumber); - byte g = byte.Parse(clean.Substring(2, 2), NumberStyles.HexNumber); - byte b = byte.Parse(clean.Substring(4, 2), NumberStyles.HexNumber); - return new byte[] { r, g, b, 255 }; - } - if (clean.Length == 8) - { - byte a = byte.Parse(clean.Substring(0, 2), NumberStyles.HexNumber); - byte r = byte.Parse(clean.Substring(2, 2), NumberStyles.HexNumber); - byte g = byte.Parse(clean.Substring(4, 2), NumberStyles.HexNumber); - byte b = byte.Parse(clean.Substring(6, 2), NumberStyles.HexNumber); - return new byte[] { r, g, b, a }; - } - if (clean.Length == 3) - { - byte r = (byte)(Convert.ToByte(clean[0].ToString(), 16) * 17); - byte g = (byte)(Convert.ToByte(clean[1].ToString(), 16) * 17); - byte b = (byte)(Convert.ToByte(clean[2].ToString(), 16) * 17); - return new byte[] { r, g, b, 255 }; - } - } - catch - { - // fallback on format error - } - - return new byte[] { defaultR, defaultG, defaultB, defaultA }; - } - - public static QRCodeGenerator.ECCLevel ToQRCoderEcc(QrCodeEccLevel level) - { - return level switch - { - QrCodeEccLevel.L => QRCodeGenerator.ECCLevel.L, - QrCodeEccLevel.M => QRCodeGenerator.ECCLevel.M, - QrCodeEccLevel.Q => QRCodeGenerator.ECCLevel.Q, - QrCodeEccLevel.H => QRCodeGenerator.ECCLevel.H, - _ => QRCodeGenerator.ECCLevel.M - }; - } - - public static byte[] GeneratePngBytes( - string? content, - string? darkHex = "#0F172A", - string? lightHex = "#FFFFFF", - QrCodeEccLevel ecc = QrCodeEccLevel.M, - int pixelsPerModule = 10, - bool drawQuietZones = true) - { - string payload = string.IsNullOrWhiteSpace(content) ? "https://codefrydev.in" : content; - byte[] darkRgba = HexToRgba(darkHex, 15, 23, 42); // #0F172A - byte[] lightRgba = HexToRgba(lightHex, 255, 255, 255); // #FFFFFF - - using var generator = new QRCodeGenerator(); - using var qrData = generator.CreateQrCode(payload, ToQRCoderEcc(ecc)); - using var qrCode = new PngByteQRCode(qrData); - - return qrCode.GetGraphic(pixelsPerModule, darkRgba, lightRgba, drawQuietZones); - } - - public static Bitmap? GenerateAvaloniaBitmap( - string? content, - string? darkHex = "#0F172A", - string? lightHex = "#FFFFFF", - QrCodeEccLevel ecc = QrCodeEccLevel.M, - int pixelsPerModule = 8, - bool drawQuietZones = true) - { - try - { - byte[] bytes = GeneratePngBytes(content, darkHex, lightHex, ecc, pixelsPerModule, drawQuietZones); - using var ms = new MemoryStream(bytes); - return new Bitmap(ms); - } - catch - { - return null; - } - } -} diff --git a/src/PdfEditorApp/Services/RecentDocumentsService.cs b/src/PdfEditorApp/Services/RecentDocumentsService.cs deleted file mode 100644 index e908465..0000000 --- a/src/PdfEditorApp/Services/RecentDocumentsService.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -/// -/// Persists up to 20 recent documents to a JSON file in the user's application data folder. -/// -public class RecentDocumentsService : IRecentDocumentsService -{ - private const int MaxRecent = 20; - - private static string StoragePath - { - get - { - var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var dir = Path.Combine(appData, "FryPDF"); - Directory.CreateDirectory(dir); - return Path.Combine(dir, "recent.json"); - } - } - - private static readonly JsonSerializerOptions _jsonOptions = new() - { - WriteIndented = true - }; - - public List Load() - { - try - { - if (!File.Exists(StoragePath)) return new List(); - var json = File.ReadAllText(StoragePath); - var list = JsonSerializer.Deserialize>(json, _jsonOptions) - ?? new List(); - return list.Where(x => !string.IsNullOrWhiteSpace(x.FilePath) && File.Exists(x.FilePath)).ToList(); - } - catch - { - return new List(); - } - } - - public void Add(RecentDocumentItem item) - { - if (string.IsNullOrWhiteSpace(item.FilePath) || !File.Exists(item.FilePath)) - return; - - var list = Load(); - // Remove existing entry for the same path (bump to top) - list.RemoveAll(x => string.Equals(x.FilePath, item.FilePath, StringComparison.OrdinalIgnoreCase)); - list.Insert(0, item); - // Trim to max - if (list.Count > MaxRecent) list = list.Take(MaxRecent).ToList(); - Save(list); - } - - public void Remove(string filePath) - { - var list = Load(); - list.RemoveAll(x => string.Equals(x.FilePath, filePath, StringComparison.OrdinalIgnoreCase)); - Save(list); - } - - public void Rename(string oldFilePath, string newFilePath, string newTitle) - { - if (string.IsNullOrWhiteSpace(oldFilePath) || string.IsNullOrWhiteSpace(newFilePath)) - return; - - var list = Load(); - var match = list.FirstOrDefault(x => string.Equals(x.FilePath, oldFilePath, StringComparison.OrdinalIgnoreCase)); - if (match != null) - { - match.FilePath = newFilePath; - match.Title = newTitle; - match.LastOpened = DateTime.UtcNow; - Save(list); - } - } - - public void Clear() - { - Save(new List()); - } - - private static void Save(List list) - { - try - { - var json = JsonSerializer.Serialize(list, _jsonOptions); - File.WriteAllText(StoragePath, json); - } - catch - { - // Best-effort: silently swallow disk errors - } - } -} diff --git a/src/PdfEditorApp/Services/Ribbon/RibbonRegistry.cs b/src/PdfEditorApp/Services/Ribbon/RibbonRegistry.cs deleted file mode 100644 index eb61aa3..0000000 --- a/src/PdfEditorApp/Services/Ribbon/RibbonRegistry.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Ribbon; - -/// -/// Thread-safe in-memory registry for dynamic Ribbon tabs, groups, and action descriptors contributed by plugins. -/// -public class RibbonRegistry : IRibbonRegistry -{ - private readonly ConcurrentDictionary _tabs = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _groups = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _actions = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public IDisposable RegisterTab(RibbonTabDescriptor tab) - { - ArgumentNullException.ThrowIfNull(tab); - ArgumentException.ThrowIfNullOrWhiteSpace(tab.Id); - - _tabs[tab.Id] = tab; - RegistryChanged?.Invoke(); - - return new UnregisterDisposable(() => - { - if (_tabs.TryRemove(tab.Id, out _)) - { - RegistryChanged?.Invoke(); - } - }); - } - - public IDisposable RegisterGroup(RibbonGroupDescriptor group) - { - ArgumentNullException.ThrowIfNull(group); - ArgumentException.ThrowIfNullOrWhiteSpace(group.Id); - - _groups[group.Id] = group; - RegistryChanged?.Invoke(); - - return new UnregisterDisposable(() => - { - if (_groups.TryRemove(group.Id, out _)) - { - RegistryChanged?.Invoke(); - } - }); - } - - public void RegisterAction(RibbonActionDescriptor action) - { - ArgumentNullException.ThrowIfNull(action); - ArgumentException.ThrowIfNullOrWhiteSpace(action.Id); - - _actions[action.Id] = action; - RegistryChanged?.Invoke(); - } - - public bool UnregisterAction(string actionId) - { - if (string.IsNullOrWhiteSpace(actionId)) return false; - - if (_actions.TryRemove(actionId, out _)) - { - RegistryChanged?.Invoke(); - return true; - } - return false; - } - - public IReadOnlyList GetAllTabs() - { - return _tabs.Values.OrderBy(t => t.Order).ThenBy(t => t.Title).ToList(); - } - - public IReadOnlyList GetGroupsForTab(string tabId) - { - if (string.IsNullOrWhiteSpace(tabId)) return Array.Empty(); - - return _groups.Values - .Where(g => string.Equals(g.TabId, tabId, StringComparison.OrdinalIgnoreCase)) - .OrderBy(g => g.Order) - .ThenBy(g => g.Title) - .ToList(); - } - - public IReadOnlyList GetActionsForTab(string tabId) - { - if (string.IsNullOrWhiteSpace(tabId)) return Array.Empty(); - - return _actions.Values - .Where(a => string.Equals(a.TabId, tabId, StringComparison.OrdinalIgnoreCase)) - .OrderBy(a => a.Order) - .ToList(); - } - - public IReadOnlyList GetActionsForGroup(string tabId, string groupId) - { - if (string.IsNullOrWhiteSpace(tabId) || string.IsNullOrWhiteSpace(groupId)) - return Array.Empty(); - - return _actions.Values - .Where(a => string.Equals(a.TabId, tabId, StringComparison.OrdinalIgnoreCase) && - string.Equals(a.GroupId, groupId, StringComparison.OrdinalIgnoreCase)) - .OrderBy(a => a.Order) - .ToList(); - } - - public IReadOnlyList GetAllActions() - { - return _actions.Values - .OrderBy(a => a.TabId) - .ThenBy(a => a.Order) - .ToList(); - } - - private sealed class UnregisterDisposable : IDisposable - { - private Action? _action; - - public UnregisterDisposable(Action action) - { - _action = action; - } - - public void Dispose() - { - var act = System.Threading.Interlocked.Exchange(ref _action, null); - act?.Invoke(); - } - } -} diff --git a/src/PdfEditorApp/Services/Sidebar/SidebarRegistry.cs b/src/PdfEditorApp/Services/Sidebar/SidebarRegistry.cs deleted file mode 100644 index 6ff0bc3..0000000 --- a/src/PdfEditorApp/Services/Sidebar/SidebarRegistry.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.Sidebar; - -/// -/// Thread-safe registry for document editor left/right sidebar tabs. -/// -public sealed class SidebarRegistry : ISidebarRegistry -{ - private readonly ConcurrentDictionary _tabs = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public IDisposable RegisterTab(SidebarTabDescriptor descriptor) - { - ArgumentNullException.ThrowIfNull(descriptor); - _tabs[descriptor.Id] = descriptor; - RegistryChanged?.Invoke(); - - return new UnregisterDisposable(() => - { - UnregisterTab(descriptor.Id); - }); - } - - public bool UnregisterTab(string tabId) - { - if (_tabs.TryRemove(tabId, out _)) - { - RegistryChanged?.Invoke(); - return true; - } - return false; - } - - public SidebarTabDescriptor? GetTab(string tabId) - { - _tabs.TryGetValue(tabId, out var tab); - return tab; - } - - public IReadOnlyList GetAllTabs() - { - return _tabs.Values.OrderBy(t => t.Order).ThenBy(t => t.Title).ToList(); - } - - private sealed class UnregisterDisposable : IDisposable - { - private Action? _action; - - public UnregisterDisposable(Action action) - { - _action = action; - } - - public void Dispose() - { - var act = System.Threading.Interlocked.Exchange(ref _action, null); - act?.Invoke(); - } - } -} diff --git a/src/PdfEditorApp/Services/SignatureService.cs b/src/PdfEditorApp/Services/SignatureService.cs deleted file mode 100644 index 26d1398..0000000 --- a/src/PdfEditorApp/Services/SignatureService.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Text; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public class SignaturePresetStyle -{ - public string Name { get; set; } = "Cursive Elegance"; - public SignatureStyle Style { get; set; } = SignatureStyle.CursiveElegance; - public string FontFamily { get; set; } = "Georgia"; - public bool IsItalic { get; set; } = true; - public string ColorHex { get; set; } = "#002B49"; // Traditional dark fountain blue -} - -public interface ISignatureService -{ - List GetAvailableSignatureStyles(); - PdfTextElement CreateCursiveSignatureElement(string signerName, SignatureStyle style, double x = 100, double y = 200); - PdfShapeElement CreateSignatureBoxElement(string signerName, string title = "Authorized Signatory", double x = 100, double y = 200); - PdfShapeElement CreateCryptographicSignatureSeal(string signerName, string organization, string reason, string sha256Fingerprint, double x = 100, double y = 200); - PdfTextElement CreateDateStampElement(double x = 100, double y = 200); - PdfShapeElement CreateInitialsElement(string initials, double x = 100, double y = 200); - PdfShapeElement CreateMarkupBadge(string symbol, string colorHex, double x = 100, double y = 200); - PdfFormFieldElement CreateFormFieldElement(FormFieldType fieldType, string fieldName, double x = 100, double y = 200, double width = 180, double height = 32); - string ComputeDocumentSha256(PdfDocumentModel document); -} - -public class SignatureService : ISignatureService -{ - public List GetAvailableSignatureStyles() - { - return new List - { - new() { Name = "Executive Elegance", Style = SignatureStyle.CursiveElegance, FontFamily = "Georgia", IsItalic = true, ColorHex = "#0F2942" }, - new() { Name = "Classic Script", Style = SignatureStyle.ClassicScript, FontFamily = "Times New Roman", IsItalic = true, ColorHex = "#0F6CBD" }, - new() { Name = "Modern Signature", Style = SignatureStyle.SignatureCasual, FontFamily = "Segoe UI", IsItalic = true, ColorHex = "#1E293B" }, - new() { Name = "Handwritten Legal Blue", Style = SignatureStyle.ModernHandwriting, FontFamily = "Arial", IsItalic = true, ColorHex = "#003366" } - }; - } - - public PdfTextElement CreateCursiveSignatureElement(string signerName, SignatureStyle style, double x = 100, double y = 200) - { - string font = style switch - { - SignatureStyle.CursiveElegance => "Georgia", - SignatureStyle.ClassicScript => "Times New Roman", - SignatureStyle.SignatureCasual => "Segoe UI", - SignatureStyle.ModernHandwriting => "Arial", - _ => "Georgia" - }; - - string color = style switch - { - SignatureStyle.ClassicScript => "#0F6CBD", - SignatureStyle.ModernHandwriting => "#003366", - _ => "#0F2942" - }; - - return new PdfTextElement - { - X = x, - Y = y, - Width = 260, - Height = 55, - Text = signerName, - FontFamily = font, - FontSize = 26, - IsItalic = true, - TextColorHex = color, - Alignment = TextAlignmentMode.Left - }; - } - - public PdfShapeElement CreateSignatureBoxElement(string signerName, string title = "Authorized Signatory", double x = 100, double y = 200) - { - return new PdfShapeElement - { - X = x, - Y = y, - Width = 280, - Height = 90, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#0F6CBD", - StrokeThickness = 1.5, - Label = $"DIGITALLY SIGNED & VERIFIED\n{signerName} ({title})\n{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC", - LabelColorHex = "#0F6CBD", - LabelFontSize = 10 - }; - } - - public PdfShapeElement CreateCryptographicSignatureSeal(string signerName, string organization, string reason, string sha256Fingerprint, double x = 100, double y = 200) - { - string shortFingerprint = sha256Fingerprint.Length > 16 ? sha256Fingerprint.Substring(0, 16) : sha256Fingerprint; - return new PdfShapeElement - { - X = x, - Y = y, - Width = 320, - Height = 100, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 8, - FillColorHex = "#F0FDF4", // Light emerald tint - StrokeColorHex = "#16A34A", // Emerald green border - StrokeThickness = 1.5, - Label = $"CRYPTOGRAPHIC X.509 DIGITAL SEAL\nSigner: {signerName} • {organization}\nReason: {reason}\nDigest: SHA256:{shortFingerprint}...\nVerified: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC", - LabelColorHex = "#15803D", - LabelFontSize = 9.5 - }; - } - - public PdfTextElement CreateDateStampElement(double x = 100, double y = 200) - { - return new PdfTextElement - { - X = x, - Y = y, - Width = 180, - Height = 32, - Text = $"Date: {DateTime.Now:MMM dd, yyyy}", - FontFamily = "Segoe UI", - FontSize = 12, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Left - }; - } - - public PdfShapeElement CreateInitialsElement(string initials, double x = 100, double y = 200) - { - string init = string.IsNullOrWhiteSpace(initials) ? "JD" : initials.Trim().ToUpper(); - return new PdfShapeElement - { - X = x, - Y = y, - Width = 60, - Height = 60, - ShapeType = ShapeType.Circle, - FillColorHex = "#EFF6FF", - StrokeColorHex = "#0F6CBD", - StrokeThickness = 2.0, - Label = init, - LabelColorHex = "#0F6CBD", - LabelFontSize = 18 - }; - } - - public PdfShapeElement CreateMarkupBadge(string symbol, string colorHex, double x = 100, double y = 200) - { - return new PdfShapeElement - { - X = x, - Y = y, - Width = 36, - Height = 36, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = colorHex, - StrokeThickness = 1.5, - Label = symbol, - LabelColorHex = colorHex, - LabelFontSize = 18 - }; - } - - public PdfFormFieldElement CreateFormFieldElement(FormFieldType fieldType, string fieldName, double x = 100, double y = 200, double width = 180, double height = 32) - { - return new PdfFormFieldElement - { - X = x, - Y = y, - Width = width, - Height = height, - FieldType = fieldType, - FieldName = fieldName, - DefaultValue = fieldType == FormFieldType.Checkbox ? "false" : "" - }; - } - - public string ComputeDocumentSha256(PdfDocumentModel document) - { - var sb = new StringBuilder(); - sb.Append(document.Title).Append('|'); - sb.Append(document.Author).Append('|'); - sb.Append(document.Subject).Append('|'); - sb.Append(document.Pages.Count).Append('|'); - - foreach (var page in document.Pages) - { - sb.Append(page.PageNumber).Append(':').Append(page.Elements.Count).Append('|'); - foreach (var el in page.Elements) - { - sb.Append(el.Kind).Append('@').Append(el.X).Append(',').Append(el.Y).Append(';'); - } - } - - byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString())); - return Convert.ToHexString(hash); - } -} diff --git a/src/PdfEditorApp/Services/SmartPlacementService.cs b/src/PdfEditorApp/Services/SmartPlacementService.cs deleted file mode 100644 index 0dc9525..0000000 --- a/src/PdfEditorApp/Services/SmartPlacementService.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Services; - -public class SmartPlacementService : ISmartPlacementService -{ - private const double DefaultMargin = 28.0; - private const double CascadeOffset = 22.0; - - private double _viewportCenterX = 400.0; - private double _viewportCenterY = 300.0; - private double _visibleWidth = 800.0; - private double _visibleHeight = 600.0; - private bool _hasViewport; - - private (double X, double Y)? _pendingContextMenuPoint; - private double _lastBaseX = double.NaN; - private double _lastBaseY = double.NaN; - private int _cascadeCount; - - public void UpdateViewport(double pageCenterX, double pageCenterY, double visibleWidth, double visibleHeight) - { - _viewportCenterX = pageCenterX; - _viewportCenterY = pageCenterY; - _visibleWidth = visibleWidth; - _visibleHeight = visibleHeight; - _hasViewport = true; - } - - public void SetContextMenuPointer(double pageX, double pageY) - { - _pendingContextMenuPoint = (pageX, pageY); - } - - public void ClearContextMenuPointer() - { - _pendingContextMenuPoint = null; - } - - public void ResetCascade() - { - _cascadeCount = 0; - _lastBaseX = double.NaN; - _lastBaseY = double.NaN; - } - - public (double X, double Y) GetPlacementPosition( - PageViewModel page, - double elementWidth, - double elementHeight, - bool isContextMenuTriggered = false) - { - double targetX; - double targetY; - - double pageWidth = page.Width > 0 ? page.Width : 800.0; - double pageHeight = page.Height > 0 ? page.Height : 1131.0; - - if (_pendingContextMenuPoint.HasValue) - { - var pt = _pendingContextMenuPoint.Value; - _pendingContextMenuPoint = null; - - // When placed via context menu right-click: - // Place top-left near cursor with subtle offset so pointer doesn't obstruct content - targetX = pt.X - (elementWidth > 120 ? 30.0 : elementWidth / 2.0); - targetY = pt.Y - 10.0; - } - else if (_hasViewport) - { - // Viewport-center placement (Industry Standard: Figma / Canva / Miro) - targetX = _viewportCenterX - (elementWidth / 2.0); - targetY = _viewportCenterY - (elementHeight / 2.0); - } - else - { - // Fallback default center-top - targetX = Math.Max(DefaultMargin, (pageWidth - elementWidth) / 2.0); - targetY = 180.0; - } - - double baseTargetX = targetX; - double baseTargetY = targetY; - - // Sequential cascading offset when inserting consecutive elements at same base position - if (!double.IsNaN(_lastBaseX) && !double.IsNaN(_lastBaseY) && - Math.Abs(baseTargetX - _lastBaseX) < 2.0 && Math.Abs(baseTargetY - _lastBaseY) < 2.0) - { - _cascadeCount = (_cascadeCount + 1) % 6; - targetX += _cascadeCount * CascadeOffset; - targetY += _cascadeCount * CascadeOffset; - } - else - { - _cascadeCount = 0; - } - - _lastBaseX = baseTargetX; - _lastBaseY = baseTargetY; - - // Boundary and margin clamping - double maxX = Math.Max(DefaultMargin, pageWidth - elementWidth - DefaultMargin); - targetX = Math.Clamp(targetX, DefaultMargin, maxX); - - double maxY = Math.Max(DefaultMargin, pageHeight - elementHeight - DefaultMargin); - targetY = Math.Clamp(targetY, DefaultMargin, maxY); - - return (Math.Round(targetX, 1), Math.Round(targetY, 1)); - } -} diff --git a/src/PdfEditorApp/Services/StatusBar/StatusBarRegistry.cs b/src/PdfEditorApp/Services/StatusBar/StatusBarRegistry.cs deleted file mode 100644 index bbdc511..0000000 --- a/src/PdfEditorApp/Services/StatusBar/StatusBarRegistry.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Services.StatusBar; - -public class StatusBarRegistry : IStatusBarRegistry -{ - private readonly ConcurrentDictionary _widgets = new(StringComparer.OrdinalIgnoreCase); - - public event Action? RegistryChanged; - - public StatusBarRegistry(bool seedDefaults = true) - { - if (seedDefaults) - { - RegisterBuiltInWidgets(); - } - } - - private void RegisterBuiltInWidgets() - { - RegisterWidget(new StatusBarWidgetDescriptor - { - WidgetId = "frypdf.status.active_indicator", - Alignment = StatusBarAlignment.Left, - Order = 10, - ToolTip = "System Operational & Ready", - Factory = sp => new ViewModels.StatusBarWidgetViewModel - { - WidgetId = "frypdf.status.active_indicator", - Label = "Ready", - IconKind = "CheckCircleOutline", - ToolTip = "System Operational & Ready", - IsActive = true - } - }); - - RegisterWidget(new StatusBarWidgetDescriptor - { - WidgetId = "frypdf.status.memory_monitor", - Alignment = StatusBarAlignment.Right, - Order = 90, - ToolTip = "SkiaSharp High-Performance Graphics Acceleration", - Factory = sp => new ViewModels.StatusBarWidgetViewModel - { - WidgetId = "frypdf.status.memory_monitor", - Label = "Skia 64-bit", - IconKind = "Memory", - ToolTip = "SkiaSharp High-Performance Graphics Acceleration", - IsActive = true - } - }); - } - - public IDisposable RegisterWidget(StatusBarWidgetDescriptor widget) - { - ArgumentNullException.ThrowIfNull(widget); - _widgets[widget.WidgetId] = widget; - RegistryChanged?.Invoke(); - - return new DisposableAction(() => - { - _widgets.TryRemove(widget.WidgetId, out _); - RegistryChanged?.Invoke(); - }); - } - - public IReadOnlyList GetWidgets(StatusBarAlignment alignment) - { - return _widgets.Values - .Where(w => w.Alignment == alignment) - .OrderBy(w => w.Order) - .ToList(); - } - - public IReadOnlyList GetAllWidgets() - { - return _widgets.Values.OrderBy(w => w.Order).ToList(); - } - - private sealed class DisposableAction(Action action) : IDisposable - { - private Action? _action = action; - public void Dispose() => Interlocked.Exchange(ref _action, null)?.Invoke(); - } -} diff --git a/src/PdfEditorApp/Services/SvgOrnamentLibrary.cs b/src/PdfEditorApp/Services/SvgOrnamentLibrary.cs deleted file mode 100644 index 54f6a88..0000000 --- a/src/PdfEditorApp/Services/SvgOrnamentLibrary.cs +++ /dev/null @@ -1,605 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PdfEditorApp.Services; - -/// -/// Built-in vector ornaments, ceremonial emblems, wedding garlands, and luxury invitation assets. -/// -public static class SvgOrnamentLibrary -{ - public static readonly Dictionary Presets = new(StringComparer.OrdinalIgnoreCase) - { - { "GaneshaCrest", GetGaneshaCrestSvg() }, - { "MarigoldToran", GetMarigoldToranSvg() }, - { "TraditionalDeepam", GetTraditionalDeepamSvg() }, - { "HangingDiyas", GetHangingDiyasSvg() }, - { "DottedFloralDivider", GetDottedFloralDividerSvg() }, - { "MandapArch", GetMandapArchSvg() }, - { "PlantainTrees", GetPlantainTreesSvg() }, - { "AuspiciousKalash", GetAuspiciousKalashSvg() }, - { "BotanicalWreath", GetBotanicalWreathSvg() }, - { "ArtDecoFrame", GetArtDecoFrameSvg() }, - { "CalligraphicFlourish", GetCalligraphicFlourishSvg() }, - { "OmCrest", GetOmCrestSvg() }, - { "TriangularPhaseCycle", GetTriangularPhaseCycleSvg() }, - { "Triangular Phase Cycle", GetTriangularPhaseCycleSvg() }, - { "ParticleArrangementGrid", GetParticleArrangementGridSvg() }, - { "ParticleArrangement", GetParticleArrangementGridSvg() }, - { "Particle Arrangement", GetParticleArrangementGridSvg() }, - { "QuadrilateralSetDiagram", GetQuadrilateralSetDiagramSvg() }, - { "Quadrilaterals Geometry Diagram", GetQuadrilateralSetDiagramSvg() }, - { "QuadrilateralsGeometryDiagram", GetQuadrilateralSetDiagramSvg() } - }; - - public static string GetSvg(string? presetName, string? tintHex = null) - { - if (string.IsNullOrWhiteSpace(presetName) || - (!Presets.TryGetValue(presetName, out var svg) && - !Presets.TryGetValue(presetName.Replace(" ", "").Replace("-", "").Replace("_", ""), out svg))) - { - svg = GetGaneshaCrestSvg(); - } - - if (!string.IsNullOrWhiteSpace(tintHex)) - { - // Apply tint to main strokes and fills if requested - svg = svg.Replace("currentColor", tintHex); - } - - return svg; - } - - /// - /// Sacred Lord Ganesha auspicious line-art crest for wedding invitations. - /// - public static string GetGaneshaCrestSvg(string colorHex = "#8B0000", string goldHex = "#D97706") - { - return $@" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"; - } - - /// - /// Festive Marigold Toran Garland with mango leaves and hanging brass lamps. - /// - public static string GetMarigoldToranSvg(string orange = "#EA580C", string yellow = "#FBBF24", string green = "#15803D", string gold = "#D97706") - { - return $@" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"; - } - - /// - /// Traditional brass Indian standing oil lamp (Samai / Kuthu Vilakku) with glowing flame. - /// - public static string GetTraditionalDeepamSvg(string gold = "#D97706", string flame = "#EA580C", string glow = "#FBBF24") - { - return $@" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"; - } - - /// - /// Pair of hanging decorative lamps. - /// - public static string GetHangingDiyasSvg(string gold = "#D97706", string flame = "#EA580C", string yellow = "#FBBF24") - { - return $@" - - - - - - - - - - - -"; - } - - /// - /// Dotted floral divider with center lotus medallion. - /// - public static string GetDottedFloralDividerSvg(string gold = "#D97706", string maroon = "#8B0000") - { - return $@" - - - - - - - - - - - - - - - - - - -"; - } - - /// - /// Traditional Indian wedding mandap pillar arch border. - /// - public static string GetMandapArchSvg(string gold = "#D97706", string maroon = "#8B0000") - { - return $@" - - - - - - - - - - - - -"; - } - - /// - /// Ceremonial banana trees / plantain wedding stems. - /// - public static string GetPlantainTreesSvg(string green = "#16A34A", string darkGreen = "#15803D", string yellow = "#FBBF24") - { - return $@" - - - - - - - - - - - - - - - - - - - - - - - - - -"; - } - - /// - /// Auspicious Vedic Kalash with coconut and mango leaves. - /// - public static string GetAuspiciousKalashSvg(string copper = "#B45309", string gold = "#F59E0B", string green = "#15803D") - { - return $@" - - - - - - - - - - - - - - - - - - - - - - -"; - } - - /// - /// Royal Botanical Floral Wreath for Luxury Gala and Wedding Invitations. - /// - public static string GetBotanicalWreathSvg(string gold = "#D97706", string leaf = "#059669") - { - return $@" - - - - - - - - - - - - - - - - - - - - - - -"; - } - - /// - /// Art Deco Gold Luxury Geometric Frame. - /// - public static string GetArtDecoFrameSvg(string gold = "#D97706") - { - return $@" - - - - - - - - - - - - - - -"; - } - - /// - /// Calligraphic Swirl & Filigree Flourish. - /// - public static string GetCalligraphicFlourishSvg(string colorHex = "#78350F") - { - return $@" - - - - -"; - } - - /// - /// Sacred Om symbol crest. - /// - public static string GetOmCrestSvg(string gold = "#D97706", string maroon = "#8B0000") - { - return $@" - - - - - - - - -"; - } - - /// - /// Triangular Phase Change Cycle Diagram: Solid (ठोस) <-> Liquid (द्रव) <-> Gas (गैस) with 6 transition arrows. - /// Matches sample 'interchange_solid_liquid_gas' and 'states_of_matter_visual_notes'. - /// - public static string GetTriangularPhaseCycleSvg(string primary = "#0284C7", string accent = "#DC2626", string textDark = "#0F172A") - { - return $@" - - - - - - - - - - - - GAS (गैस) - High Kinetic Energy - - - - SOLID (ठोस) - Fixed Shape & Volume - - - - LIQUID (द्रव) - Fixed Volume, Fluid - - - - Melting / गलनांक / संलयन → - - - - ← Freezing / जमना / हिमीकरण - - - - Sublimation / ऊर्ध्वपातन ↗ - - - - ↙ Deposition / निक्षेपण - - - - Condensation / संघनन ↘ - - - - ↖ Vaporization / वाष्पीकरण -"; - } - - /// - /// Comparative Particle Arrangement Diagram (Solid lattice, Liquid fluid, Gas dispersed). - /// Matches sample 'states_of_matter_visual_notes' and 'states_of_matter_notes'. - /// - public static string GetParticleArrangementGridSvg() - { - return $@" - - - - Solid (ठोस) - - - - - - - - - - - - - - - - - - - - - - - Liquid (द्रव) - - - - - - - - - - - - - - - - Gas (गैस) - - - - - - - - - - - -"; - } - - /// - /// Geometric Quadrilaterals Reference Diagram: Parallelogram, Rhombus, Trapezoid, Kite. - /// Matches sample 'Types_of_Quadrilaterals_Hindi_English'. - /// - public static string GetQuadrilateralSetDiagramSvg() - { - return $@" - - - - - h - b - Parallelogram (समांतर) - Area = b × h - - - - - - - - Rhombus (समचतुर्भुज) - Area = ½ d₁ d₂ - - - - - - - h - a - b - Trapezoid (समलंब) - Area = ½(a+b)h - - - - - - - - Kite (पतंग) - Diagonals ⊥ - -"; - } -} - diff --git a/src/PdfEditorApp/Services/SvgShapeHelper.cs b/src/PdfEditorApp/Services/SvgShapeHelper.cs deleted file mode 100644 index eea7901..0000000 --- a/src/PdfEditorApp/Services/SvgShapeHelper.cs +++ /dev/null @@ -1,469 +0,0 @@ -using System; -using System.Globalization; -using System.Text; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public static class SvgShapeHelper -{ - public static string GetVectorPath(ShapeType shapeType, double width, double height, double cornerRadius = 4, string? customPathData = null) - { - if (shapeType == ShapeType.CustomSvgPath && !string.IsNullOrWhiteSpace(customPathData)) - { - return customPathData.Trim(); - } - - double w = Math.Max(10, width); - double h = Math.Max(10, height); - double r = Math.Min(cornerRadius > 0 ? cornerRadius : 8, Math.Min(w / 2, h / 2)); - - var ci = CultureInfo.InvariantCulture; - - return shapeType switch - { - ShapeType.RoundedRectangle => - string.Format(ci, "M {0:F1},0 L {1:F1},0 A {0:F1},{0:F1} 0 0 1 {2:F1},{0:F1} L {2:F1},{3:F1} A {0:F1},{0:F1} 0 0 1 {1:F1},{4:F1} L {0:F1},{4:F1} A {0:F1},{0:F1} 0 0 1 0,{3:F1} L 0,{0:F1} A {0:F1},{0:F1} 0 0 1 {0:F1},0 Z", - r, w - r, w, h - r, h), - - ShapeType.Circle => - string.Format(ci, "M {0:F1},0 A {0:F1},{1:F1} 0 1 1 {0:F1},{2:F1} A {0:F1},{1:F1} 0 1 1 {0:F1},0 Z", - w / 2.0, h / 2.0, h), - - ShapeType.Triangle => - string.Format(ci, "M {0:F1},0 L {1:F1},{2:F1} L 0,{2:F1} Z", - w / 2.0, w, h), - - ShapeType.RightTriangle => - string.Format(ci, "M 0,0 L {0:F1},{1:F1} L 0,{1:F1} Z", - w, h), - - ShapeType.Diamond => - string.Format(ci, "M {0:F1},0 L {1:F1},{2:F1} L {0:F1},{3:F1} L 0,{2:F1} Z", - w / 2.0, w, h / 2.0, h), - - ShapeType.Pentagon => - string.Format(ci, "M {0:F1},0 L {1:F1},{2:F1} L {3:F1},{4:F1} L {5:F1},{4:F1} L 0,{2:F1} Z", - w / 2.0, w, h * 0.38, w * 0.81, h, w * 0.19), - - ShapeType.Hexagon => - string.Format(ci, "M {0:F1},0 L {1:F1},0 L {2:F1},{3:F1} L {1:F1},{4:F1} L {0:F1},{4:F1} L 0,{3:F1} Z", - w * 0.25, w * 0.75, w, h / 2.0, h), - - ShapeType.Octagon => - string.Format(ci, "M {0:F1},0 L {1:F1},0 L {2:F1},{3:F1} L {2:F1},{4:F1} L {1:F1},{5:F1} L {0:F1},{5:F1} L 0,{4:F1} L 0,{3:F1} Z", - w * 0.3, w * 0.7, w, h * 0.3, h * 0.7, h), - - ShapeType.Star5 or ShapeType.Star => - string.Format(ci, "M {0:F1},0 L {1:F1},{2:F1} L {3:F1},{2:F1} L {4:F1},{5:F1} L {6:F1},{7:F1} L {0:F1},{8:F1} L {9:F1},{7:F1} L {10:F1},{5:F1} L 0,{2:F1} L {11:F1},{2:F1} Z", - w * 0.5, w * 0.62, h * 0.35, w, w * 0.69, h * 0.57, w * 0.81, h, h * 0.75, w * 0.19, w * 0.31, w * 0.38), - - ShapeType.Star4Badge => - string.Format(ci, "M {0:F1},0 L {1:F1},{2:F1} L {3:F1},{4:F1} L {1:F1},{5:F1} L {0:F1},{6:F1} L {7:F1},{5:F1} L 0,{4:F1} L {7:F1},{2:F1} Z", - w * 0.5, w * 0.65, h * 0.35, w, h * 0.5, h * 0.65, h, w * 0.35), - - ShapeType.Star8Badge => - GenerateStarPath(w, h, 8, 0.6), - - ShapeType.Star12Seal or ShapeType.RosetteSeal => - GenerateStarPath(w, h, 16, 0.78), - - ShapeType.ShieldBadge => - string.Format(ci, "M 0,0 L {0:F1},0 L {0:F1},{1:F1} Q {0:F1},{2:F1} {3:F1},{2:F1} Q 0,{2:F1} 0,{1:F1} Z", - w, h * 0.6, h, w / 2.0), - - ShapeType.AwardBadge => - GenerateStarPath(w, h, 12, 0.72), - - ShapeType.LaurelWreathSeal => - string.Format(ci, "M {0:F1},0 A {0:F1},{1:F1} 0 1 1 {0:F1},{2:F1} A {0:F1},{1:F1} 0 1 1 {0:F1},0 Z M {0:F1},{3:F1} A {4:F1},{5:F1} 0 1 0 {0:F1},{6:F1} A {4:F1},{5:F1} 0 1 0 {0:F1},{3:F1} Z", - w / 2.0, h / 2.0, h, h * 0.12, (w * 0.38), (h * 0.38), h * 0.88), - - ShapeType.MedalRibbonBadge => - string.Format(ci, "M {0:F1},0 A {0:F1},{1:F1} 0 1 1 {0:F1},{2:F1} A {0:F1},{1:F1} 0 1 1 {0:F1},0 Z M {3:F1},{4:F1} L {5:F1},{6:F1} L {7:F1},{8:F1} L {9:F1},{4:F1} Z M {10:F1},{4:F1} L {11:F1},{8:F1} L {12:F1},{6:F1} L {13:F1},{4:F1} Z", - w / 2.0, h * 0.35, h * 0.7, - w * 0.25, h * 0.55, w * 0.12, h, w * 0.32, h * 0.88, w * 0.42, - w * 0.58, w * 0.68, w * 0.88, w * 0.75), - - ShapeType.RibbonBanner => - string.Format(ci, "M 0,0 L {0:F1},0 L {1:F1},{2:F1} L {0:F1},{3:F1} L 0,{3:F1} L {4:F1},{2:F1} Z", - w, w - 18.0, h / 2.0, h, 18.0), - - ShapeType.CornerPolygonalAccentTopLeft => - string.Format(ci, "M 0,0 L {0:F1},0 L {1:F1},{2:F1} L 0,{3:F1} Z", - w, w * 0.32, h * 0.48, h), - - ShapeType.CornerPolygonalAccentBottomRight => - string.Format(ci, "M {0:F1},{1:F1} L 0,{1:F1} L {2:F1},{3:F1} L {0:F1},0 Z", - w, h, w * 0.68, h * 0.52), - - ShapeType.CornerDiagonalWedge => - string.Format(ci, "M 0,0 L {0:F1},0 L 0,{1:F1} Z", - w, h), - - ShapeType.Chevron => - string.Format(ci, "M 0,0 L {0:F1},0 L {1:F1},{2:F1} L {0:F1},{3:F1} L 0,{3:F1} L {4:F1},{2:F1} Z", - w * 0.7, w, h / 2.0, h, w * 0.3), - - ShapeType.Trapezoid => - string.Format(ci, "M {0:F1},0 L {1:F1},0 L {2:F1},{3:F1} L 0,{3:F1} Z", - w * 0.2, w * 0.8, w, h), - - ShapeType.Parallelogram => - string.Format(ci, "M {0:F1},0 L {1:F1},0 L {2:F1},{3:F1} L 0,{3:F1} Z", - w * 0.25, w, w * 0.75, h), - - ShapeType.ArrowRight or ShapeType.Arrow => - string.Format(ci, "M 0,{0:F1} L {1:F1},{0:F1} L {1:F1},0 L {2:F1},{3:F1} L {1:F1},{4:F1} L {1:F1},{5:F1} L 0,{5:F1} Z", - h * 0.3, w * 0.6, w, h * 0.5, h, h * 0.7), - - ShapeType.ArrowLeft => - string.Format(ci, "M {0:F1},{1:F1} L {2:F1},{1:F1} L {2:F1},0 L 0,{3:F1} L {2:F1},{4:F1} L {2:F1},{5:F1} L {0:F1},{5:F1} Z", - w, h * 0.3, w * 0.4, h * 0.5, h, h * 0.7), - - ShapeType.Callout => - string.Format(ci, "M 0,0 L {0:F1},0 L {0:F1},{1:F1} L {2:F1},{1:F1} L {3:F1},{4:F1} L {5:F1},{1:F1} L 0,{1:F1} Z", - w, h * 0.75, w * 0.55, w * 0.3, h, w * 0.35), - - ShapeType.Heart => - string.Format(ci, "M {0:F1},{1:F1} C {2:F1},0 0,{3:F1} 0,{4:F1} C 0,{5:F1} {2:F1},{6:F1} {0:F1},{7:F1} C {8:F1},{6:F1} {9:F1},{5:F1} {9:F1},{4:F1} C {9:F1},{3:F1} {8:F1},0 {0:F1},{1:F1} Z", - w * 0.5, h * 0.25, w * 0.3, h * 0.2, h * 0.45, h * 0.7, h * 0.85, h, w * 0.7, w), - - ShapeType.Cloud => - string.Format(ci, "M {0:F1},{1:F1} A {2:F1},{3:F1} 0 0 1 {4:F1},{5:F1} A {6:F1},{5:F1} 0 0 1 {7:F1},{8:F1} A {9:F1},{10:F1} 0 0 1 {11:F1},{1:F1} Z", - w * 0.2, h * 0.7, w * 0.15, h * 0.2, w * 0.35, h * 0.3, w * 0.25, w * 0.75, h * 0.35, w * 0.18, h * 0.22, w * 0.9), - - ShapeType.Line => - string.Format(ci, "M 0,{0:F1} L {1:F1},{0:F1}", - h / 2.0, w), - - ShapeType.BezierCurve => - string.Format(ci, "M 0,{0:F1} C {1:F1},0 {2:F1},{3:F1} {4:F1},{0:F1}", - h * 0.5, w * 0.33, w * 0.67, h, w), - - ShapeType.CurvedArrow => - string.Format(ci, "M 0,{0:F1} C {1:F1},0 {2:F1},{3:F1} {4:F1},{0:F1} M {5:F1},{6:F1} L {4:F1},{0:F1} L {7:F1},{8:F1}", - h * 0.5, w * 0.33, w * 0.67, h, w - 8.0, w - 18.0, (h * 0.5) - 8.0, w - 18.0, (h * 0.5) + 8.0), - - ShapeType.SCurveConnector => - string.Format(ci, "M 0,{0:F1} C {1:F1},{0:F1} {2:F1},0 {3:F1},0", - h, w * 0.45, w * 0.55, w), - - ShapeType.WaveLine => - string.Format(ci, "M 0,{0:F1} C {1:F1},0 {2:F1},{3:F1} {4:F1},{0:F1} C {5:F1},0 {6:F1},{3:F1} {7:F1},{0:F1}", - h * 0.5, w * 0.125, w * 0.375, h, w * 0.5, w * 0.625, w * 0.875, w), - - ShapeType.ArcLine => - string.Format(ci, "M 0,{0:F1} Q {1:F1},0 {2:F1},{0:F1}", - h, w * 0.5, w), - - ShapeType.CurlyBrace => - string.Format(ci, "M {0:F1},0 C {1:F1},0 {1:F1},{2:F1} 0,{3:F1} C {1:F1},{4:F1} {1:F1},{5:F1} {0:F1},{5:F1}", - w, w * 0.45, h * 0.4, h * 0.5, h * 0.6, h), - - ShapeType.CurvedCallout => - string.Format(ci, "M {0:F1},0 L {1:F1},0 A {0:F1},{0:F1} 0 0 1 {2:F1},{0:F1} L {2:F1},{3:F1} A {0:F1},{0:F1} 0 0 1 {1:F1},{4:F1} L {5:F1},{4:F1} Q {6:F1},{4:F1} {7:F1},{8:F1} Q {9:F1},{10:F1} {9:F1},{4:F1} L {0:F1},{4:F1} A {0:F1},{0:F1} 0 0 1 0,{3:F1} L 0,{0:F1} A {0:F1},{0:F1} 0 0 1 {0:F1},0 Z", - r, w - r, w, h * 0.72 - r, h * 0.72, w * 0.5, w * 0.38, w * 0.22, h, w * 0.32, h * 0.82), - - ShapeType.Teardrop => - string.Format(ci, "M {0:F1},0 C {1:F1},{2:F1} {3:F1},{4:F1} {0:F1},{5:F1} C 0,{4:F1} {6:F1},{2:F1} {0:F1},0 Z", - w * 0.5, w * 0.85, h * 0.45, w, h * 0.75, h, w * 0.15), - - ShapeType.WaveRibbon => - string.Format(ci, "M 0,{0:F1} Q {1:F1},0 {2:F1},{0:F1} Q {3:F1},{4:F1} {5:F1},{0:F1} L {5:F1},{6:F1} Q {3:F1},{7:F1} {2:F1},{6:F1} Q {1:F1},{8:F1} 0,{6:F1} Z", - h * 0.2, w * 0.25, w * 0.5, w * 0.75, h * 0.4, w, h * 0.8, h, h * 0.6), - - ShapeType.OrganicBlob => - string.Format(ci, "M {0:F1},0 C {1:F1},{2:F1} {3:F1},{4:F1} {5:F1},{6:F1} C {0:F1},{7:F1} {8:F1},{6:F1} 0,{4:F1} C 0,{2:F1} {8:F1},0 {0:F1},0 Z", - w * 0.5, w * 0.9, h * 0.12, w, h * 0.6, w * 0.75, h * 0.9, h, w * 0.15), - - ShapeType.Card => - string.Format(ci, "M {0:F1},0 L {1:F1},0 A {0:F1},{0:F1} 0 0 1 {2:F1},{0:F1} L {2:F1},{3:F1} A {0:F1},{0:F1} 0 0 1 {1:F1},{4:F1} L {0:F1},{4:F1} A {0:F1},{0:F1} 0 0 1 0,{3:F1} L 0,{0:F1} A {0:F1},{0:F1} 0 0 1 {0:F1},0 Z", - r, w - r, w, h - r, h), - - ShapeType.StickyNote => - string.Format(ci, "M 0,0 L {0:F1},0 L {1:F1},18 L {1:F1},{2:F1} L 0,{2:F1} Z M {0:F1},0 L {0:F1},18 L {1:F1},18", - w - 18.0, w, h), - - _ => string.Format(ci, "M 0,0 L {0:F1},0 L {0:F1},{1:F1} L 0,{1:F1} Z", w, h) - }; - } - - public static string? GetDashArray(LineDashStyle style, double strokeThickness) - { - double st = Math.Max(1.0, strokeThickness); - var ci = CultureInfo.InvariantCulture; - return style switch - { - LineDashStyle.Dashed => string.Format(ci, "{0:F1},{1:F1}", st * 3.5, st * 2.0), - LineDashStyle.Dotted => string.Format(ci, "{0:F1},{1:F1}", st, st * 2.0), - LineDashStyle.DashDot => string.Format(ci, "{0:F1},{1:F1},{2:F1},{1:F1}", st * 4.0, st * 2.0, st), - _ => null - }; - } - - public static string GenerateSmoothInkSvgPath(string pointsData, bool isSmoothSpline = true) - { - if (string.IsNullOrWhiteSpace(pointsData)) return "M 0,0"; - - var ci = CultureInfo.InvariantCulture; - var rawParts = pointsData.Trim().Split(new[] { ' ', ';', '\n', '\r', '\t' }, StringSplitOptions.RemoveEmptyEntries); - var points = new System.Collections.Generic.List<(double X, double Y)>(); - - foreach (var p in rawParts) - { - var coords = p.Split(','); - if (coords.Length == 2 && - double.TryParse(coords[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double x) && - double.TryParse(coords[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double y)) - { - points.Add((x, y)); - } - } - - if (points.Count == 0) return "M 0,0"; - if (points.Count == 1) return string.Format(ci, "M {0:F1},{1:F1} L {0:F1},{1:F1}", points[0].X, points[0].Y); - if (points.Count == 2 || !isSmoothSpline) - { - var sbPoly = new StringBuilder(); - sbPoly.AppendFormat(ci, "M {0:F1},{1:F1}", points[0].X, points[0].Y); - for (int i = 1; i < points.Count; i++) - { - sbPoly.AppendFormat(ci, " L {0:F1},{1:F1}", points[i].X, points[i].Y); - } - return sbPoly.ToString(); - } - - // Catmull-Rom to Cubic Bézier Spline Conversion - var sb = new StringBuilder(); - sb.AppendFormat(ci, "M {0:F1},{1:F1}", points[0].X, points[0].Y); - - int n = points.Count; - for (int i = 0; i < n - 1; i++) - { - var p0 = i > 0 ? points[i - 1] : points[i]; - var p1 = points[i]; - var p2 = points[i + 1]; - var p3 = i < n - 2 ? points[i + 2] : p2; - - // Catmull-Rom tangents: T1 = (p2 - p0)/2, T2 = (p3 - p1)/2 - double t1x = (p2.X - p0.X) / 2.0; - double t1y = (p2.Y - p0.Y) / 2.0; - double t2x = (p3.X - p1.X) / 2.0; - double t2y = (p3.Y - p1.Y) / 2.0; - - // Cubic Bézier control points: C1 = p1 + T1/3, C2 = p2 - T2/3 - double c1x = p1.X + (t1x / 3.0); - double c1y = p1.Y + (t1y / 3.0); - double c2x = p2.X - (t2x / 3.0); - double c2y = p2.Y - (t2y / 3.0); - - sb.AppendFormat(ci, " C {0:F1},{1:F1} {2:F1},{3:F1} {4:F1},{5:F1}", c1x, c1y, c2x, c2y, p2.X, p2.Y); - } - - return sb.ToString(); - } - - public static string GenerateDividerSvgPath(DividerStyle style, double width, double height, double amplitude = 6.0, double frequency = 4.0, bool isVertical = false) - { - double w = isVertical ? Math.Max(1.0, width) : Math.Max(10.0, width); - double h = isVertical ? Math.Max(10.0, height) : Math.Max(1.0, height); - var ci = CultureInfo.InvariantCulture; - - if (isVertical) - { - double midX = w / 2.0; - return style switch - { - DividerStyle.Wave => string.Format(ci, "M {0:F1},0 C {1:F1},{2:F1} {3:F1},{4:F1} {0:F1},{5:F1} C {1:F1},{6:F1} {3:F1},{7:F1} {0:F1},{8:F1}", - midX, midX - amplitude, h * 0.15, midX + amplitude, h * 0.35, h * 0.5, h * 0.65, h * 0.85, h), - DividerStyle.Arch => string.Format(ci, "M {0:F1},0 Q {1:F1},{2:F1} {0:F1},{3:F1}", - midX, midX - amplitude, h * 0.5, h), - _ => string.Format(ci, "M {0:F1},0 L {0:F1},{1:F1}", midX, h) - }; - } - - double midY = h / 2.0; - double amp = Math.Min(midY, Math.Max(2.0, amplitude)); - int cycles = Math.Max(1, (int)Math.Round(frequency)); - - return style switch - { - DividerStyle.Wave => GenerateHarmonicWavePath(w, midY, amp, cycles), - DividerStyle.SCurve => string.Format(ci, "M 0,{0:F1} C {1:F1},{2:F1} {3:F1},{4:F1} {5:F1},{0:F1}", - midY, w * 0.35, midY - amp, w * 0.65, midY + amp, w), - DividerStyle.Arch => string.Format(ci, "M 0,{0:F1} Q {1:F1},{2:F1} {3:F1},{0:F1}", - midY, w * 0.5, midY - amp, w), - DividerStyle.DoubleWave => GenerateDoubleWavePath(w, midY, amp, cycles), - DividerStyle.CalligraphicFlourish => string.Format(ci, "M 0,{0:F1} C {1:F1},{2:F1} {3:F1},{4:F1} {5:F1},{0:F1} L {6:F1},{0:F1} L {7:F1},{2:F1} L {8:F1},{0:F1} L {9:F1},{0:F1} C {10:F1},{2:F1} {11:F1},{4:F1} {12:F1},{0:F1}", - midY, w * 0.1, midY - amp, w * 0.25, midY + amp, w * 0.45, w * 0.48, w * 0.5, w * 0.52, w * 0.55, w * 0.75, w * 0.9, w), - _ => string.Format(ci, "M 0,{0:F1} L {1:F1},{0:F1}", midY, w) - }; - } - - private static string GenerateHarmonicWavePath(double w, double midY, double amp, int cycles) - { - var ci = CultureInfo.InvariantCulture; - var sb = new StringBuilder(); - sb.AppendFormat(ci, "M 0,{0:F1}", midY); - - double cycleWidth = w / cycles; - for (int i = 0; i < cycles; i++) - { - double startX = i * cycleWidth; - double endX = startX + cycleWidth; - double cp1x = startX + (cycleWidth * 0.25); - double cp2x = startX + (cycleWidth * 0.75); - - sb.AppendFormat(ci, " C {0:F1},{1:F1} {2:F1},{3:F1} {4:F1},{5:F1}", - cp1x, midY - amp, cp2x, midY + amp, endX, midY); - } - - return sb.ToString(); - } - - private static string GenerateDoubleWavePath(double w, double midY, double amp, int cycles) - { - var ci = CultureInfo.InvariantCulture; - string wave1 = GenerateHarmonicWavePath(w, midY - (amp * 0.4), amp * 0.6, cycles); - string wave2 = GenerateHarmonicWavePath(w, midY + (amp * 0.4), amp * 0.6, cycles); - return wave1 + " " + wave2; - } - - private static string GenerateStarPath(double w, double h, int points, double innerRadiusRatio) - { - var ci = CultureInfo.InvariantCulture; - var sb = new StringBuilder(); - - double cx = w / 2.0; - double cy = h / 2.0; - double rx = w / 2.0; - double ry = h / 2.0; - double innerRx = rx * innerRadiusRatio; - double innerRy = ry * innerRadiusRatio; - - int totalVertices = points * 2; - double angleStep = Math.PI * 2.0 / totalVertices; - double startAngle = -Math.PI / 2.0; - - for (int i = 0; i < totalVertices; i++) - { - double angle = startAngle + (i * angleStep); - bool isOuter = (i % 2 == 0); - double currentRx = isOuter ? rx : innerRx; - double currentRy = isOuter ? ry : innerRy; - - double x = cx + (currentRx * Math.Cos(angle)); - double y = cy + (currentRy * Math.Sin(angle)); - - if (i == 0) - sb.AppendFormat(ci, "M {0:F1},{1:F1} ", x, y); - else - sb.AppendFormat(ci, "L {0:F1},{1:F1} ", x, y); - } - - sb.Append("Z"); - return sb.ToString(); - } - - public static string GenerateSvgMarkup(PdfShapeElement shape) - { - double w = Math.Max(10, shape.Width); - double h = Math.Max(10, shape.Height); - var ci = CultureInfo.InvariantCulture; - - string fill = string.IsNullOrWhiteSpace(shape.FillColorHex) || shape.FillColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase) || shape.FillColorHex == "#00000000" - ? "none" - : shape.FillColorHex; - - string stroke = string.IsNullOrWhiteSpace(shape.StrokeColorHex) || shape.StrokeColorHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase) || shape.StrokeColorHex == "#00000000" - ? "none" - : shape.StrokeColorHex; - - double strokeThickness = shape.StrokeThickness > 0 ? shape.StrokeThickness : 0; - string? dashArray = GetDashArray(shape.DashStyle, strokeThickness); - string dashAttr = !string.IsNullOrEmpty(dashArray) ? string.Format(ci, " stroke-dasharray=\"{0}\"", dashArray) : ""; - - var sb = new StringBuilder(); - sb.AppendFormat(ci, @"", w, h); - - if (shape.ShapeType == ShapeType.MedalRibbonBadge) - { - string ribbonFill = !string.IsNullOrEmpty(shape.SecondaryFillColorHex) ? shape.SecondaryFillColorHex : "#990000"; - string medalFill = fill != "none" ? fill : "#F59E0B"; - string medalStroke = stroke != "none" ? stroke : "#B45309"; - - // Left Ribbon Tail - sb.AppendFormat(ci, @"", - w * 0.35, h * 0.45, w * 0.15, h, w * 0.35, h * 0.85, w * 0.48, h * 0.55, ribbonFill, medalStroke, Math.Max(1, strokeThickness * 0.7)); - - // Right Ribbon Tail - sb.AppendFormat(ci, @"", - w * 0.52, h * 0.55, w * 0.65, h * 0.85, w * 0.85, h, w * 0.65, h * 0.45, ribbonFill, medalStroke, Math.Max(1, strokeThickness * 0.7)); - - // Outer Medal Rim - sb.AppendFormat(ci, @"", - w / 2.0, h * 0.38, w * 0.38, medalFill, medalStroke, strokeThickness > 0 ? strokeThickness : 2.5); - - // Inner Laurel Garland Embossed Ring - sb.AppendFormat(ci, @"", - w / 2.0, h * 0.38, w * 0.30, "#FEF3C7"); - - sb.AppendFormat(ci, @"", - w / 2.0, h * 0.38, w * 0.24, medalStroke); - } - else - { - string pathData = GetVectorPath(shape.ShapeType, w, h, shape.CornerRadius, shape.CustomPathData); - sb.AppendFormat(ci, @"", - pathData, fill, stroke, strokeThickness, dashAttr); - } - - if (!string.IsNullOrWhiteSpace(shape.Label)) - { - string labelColor = !string.IsNullOrEmpty(shape.LabelColorHex) ? shape.LabelColorHex : "#201F1E"; - double labelSize = shape.LabelFontSize > 0 ? shape.LabelFontSize : 12; - sb.AppendFormat(ci, @"{4}", - w / 2.0, (shape.ShapeType == ShapeType.MedalRibbonBadge ? h * 0.38 : h / 2.0), labelColor, labelSize, System.Security.SecurityElement.Escape(shape.Label)); - } - - sb.Append(""); - return sb.ToString(); - } - - public static string GenerateDividerSvgMarkup(PdfDividerElement div) - { - double w = div.IsVertical ? Math.Max(1.0, div.Width) : Math.Max(10.0, div.Width); - double h = div.IsVertical ? Math.Max(10.0, div.Height) : Math.Max(1.0, div.Height); - var ci = CultureInfo.InvariantCulture; - - string stroke = !string.IsNullOrWhiteSpace(div.ColorHex) ? div.ColorHex : "#0F6CBD"; - double thickness = Math.Max(1.0, div.Thickness); - string pathData = GenerateDividerSvgPath(div.Style, w, h, div.WaveAmplitude, div.WaveFrequency, div.IsVertical); - string? dashArray = GetDashArray(div.DashStyle, thickness); - string dashAttr = !string.IsNullOrEmpty(dashArray) ? string.Format(ci, " stroke-dasharray=\"{0}\"", dashArray) : ""; - - return string.Format(ci, - @"", - w, h, pathData, stroke, thickness, dashAttr); - } - - public static string GenerateInkSvgMarkup(PdfInkElement ink) - { - double w = Math.Max(10, ink.Width); - double h = Math.Max(6, ink.Height); - var ci = CultureInfo.InvariantCulture; - - string stroke = !string.IsNullOrWhiteSpace(ink.StrokeColorHex) ? ink.StrokeColorHex : "#0F6CBD"; - double thickness = Math.Max(1.0, ink.StrokeThickness); - double opacity = Math.Clamp(ink.Opacity, 0.05, 1.0); - string pathData = GenerateSmoothInkSvgPath(ink.PointsData, ink.IsSmoothSpline); - - return string.Format(ci, - @"", - w, h, pathData, stroke, thickness, opacity); - } -} - diff --git a/src/PdfEditorApp/Services/TemplateService.cs b/src/PdfEditorApp/Services/TemplateService.cs deleted file mode 100644 index 890523a..0000000 --- a/src/PdfEditorApp/Services/TemplateService.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Templates; -using PdfEditorApp.Templates.Academic; -using PdfEditorApp.Templates.Career; -using PdfEditorApp.Templates.Certificates; -using PdfEditorApp.Templates.Corporate; -using PdfEditorApp.Templates.Events; -using PdfEditorApp.Templates.Finance; -using PdfEditorApp.Templates.General; -using PdfEditorApp.Templates.Education; -using PdfEditorApp.Templates.Technical; - -namespace PdfEditorApp.Services; - -public class TemplateService : ITemplateService, PdfEditorApp.Core.Plugins.Descriptors.ITemplateRegistry -{ - private readonly Dictionary _templates = new(StringComparer.OrdinalIgnoreCase); - private readonly BlankDocumentTemplate _blankTemplate = new(); - - public event Action? RegistryChanged; - - public TemplateService() - { - // Corporate - RegisterTemplate(new AnnualReportTemplate()); - RegisterTemplate(new InteractiveExecutiveBriefTemplate()); - RegisterTemplate(new EmployeePayslipTemplate()); - RegisterTemplate(new OcrAnalysisReportTemplate()); - - // Finance - RegisterTemplate(new InvoiceTemplate()); - RegisterTemplate(new FinanceResearchPaperTemplate()); - - // Career / Resumes - RegisterTemplate(new ResumeTemplate()); - RegisterTemplate(new ResumeModernCleanTemplate()); - RegisterTemplate(new ResumeCreativeMinimalistTemplate()); - RegisterTemplate(new ResumeAcademicCvTemplate()); - - // Academic Research - RegisterTemplate(new AcademicPaperTemplate()); - RegisterTemplate(new MathResearchPaperTemplate()); - RegisterTemplate(new PhysicsResearchPaperTemplate()); - RegisterTemplate(new HistoryResearchPaperTemplate()); - - // Certificates & Diplomas - RegisterTemplate(new CertificateTemplate()); - RegisterTemplate(new CertificateNavyGoldTemplate()); - RegisterTemplate(new DiplomaAcademicTemplate()); - - // Events, Design & Publishing - RegisterTemplate(new WeddingInvitationTraditionalTemplate()); - RegisterTemplate(new WeddingInvitationRoyalFloralTemplate()); - RegisterTemplate(new GalaInvitationTemplate()); - RegisterTemplate(new CreativeTypographyShowcaseTemplate()); - RegisterTemplate(new RichTextPublishingShowcaseTemplate()); - - // Education & Practice Worksheets - RegisterTemplate(new MathBODMASWorksheetTemplate()); - RegisterTemplate(new FactorizationWorksheetTemplate()); - RegisterTemplate(new BilingualExamPaperTemplate()); - RegisterTemplate(new StatesOfMatterDiagramNotesTemplate()); - RegisterTemplate(new QuadrilateralsGuideTemplate()); - - // Technical & Cheat Sheets - RegisterTemplate(new ArduinoCheatSheetTemplate()); - - // General - RegisterTemplate(_blankTemplate); - } - - public void RegisterTemplate(ITemplateDefinition template) - { - _templates[template.Id] = template; - RegistryChanged?.Invoke(); - } - - public IDisposable RegisterTemplate(PdfEditorApp.Core.Plugins.Descriptors.ITemplateDescriptor template) - { - ArgumentNullException.ThrowIfNull(template); - if (template is ITemplateDefinition def) - { - RegisterTemplate(def); - } - else - { - _templates[template.Id] = new TemplateDescriptorAdapter(template); - RegistryChanged?.Invoke(); - } - - return new DisposableAction(() => - { - _templates.Remove(template.Id); - RegistryChanged?.Invoke(); - }); - } - - PdfEditorApp.Core.Plugins.Descriptors.ITemplateDescriptor? PdfEditorApp.Core.Plugins.Descriptors.ITemplateRegistry.GetTemplate(string id) - { - return _templates.GetValueOrDefault(id); - } - - IReadOnlyList PdfEditorApp.Core.Plugins.Descriptors.ITemplateRegistry.GetAllTemplates() - { - return _templates.Values.ToList(); - } - - public IReadOnlyList GetCategories() - { - return _templates.Values.Select(t => t.Category).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(c => c).ToList(); - } - - private sealed class TemplateDescriptorAdapter(PdfEditorApp.Core.Plugins.Descriptors.ITemplateDescriptor inner) : ITemplateDefinition - { - public string Id => inner.Id; - public string Name => inner.Name; - public string Description => inner.Description; - public string Category => inner.Category; - public string IconKind => inner.IconKind; - public string AccentColorHex => inner.AccentColorHex; - public PdfDocumentModel Create() => inner.Create(); - } - - private sealed class DisposableAction(Action action) : IDisposable - { - private Action? _action = action; - public void Dispose() => System.Threading.Interlocked.Exchange(ref _action, null)?.Invoke(); - } - - public IReadOnlyList GetAllTemplates() - { - return _templates.Values.ToList(); - } - - public PdfDocumentModel CreateTemplate(string templateId) - { - if (_templates.TryGetValue(templateId, out var template)) - { - return template.Create(); - } - - return _blankTemplate.Create(); - } - - public PdfDocumentModel CreateAnnualReportTemplate() => CreateTemplate("annualreport"); - public PdfDocumentModel CreateInteractiveExecutiveBriefTemplate() => CreateTemplate("interactive_executive_deck"); - public PdfDocumentModel CreateEmployeePayslipTemplate() => CreateTemplate("payslip"); - - public PdfDocumentModel CreateInvoiceTemplate() => CreateTemplate("invoice"); - - public PdfDocumentModel CreateResumeTemplate() => CreateTemplate("resume"); - - public PdfDocumentModel CreateResumeModernCleanTemplate() => CreateTemplate("resumemodern"); - - public PdfDocumentModel CreateResumeCreativeMinimalistTemplate() => CreateTemplate("resumecreative"); - - public PdfDocumentModel CreateResumeAcademicCvTemplate() => CreateTemplate("resumeacademic"); - - public PdfDocumentModel CreateAcademicPaperTemplate() => CreateTemplate("academic"); - - public PdfDocumentModel CreateMathResearchPaperTemplate() => CreateTemplate("mathresearch"); - - public PdfDocumentModel CreatePhysicsResearchPaperTemplate() => CreateTemplate("physicsresearch"); - - public PdfDocumentModel CreateHistoryResearchPaperTemplate() => CreateTemplate("historyresearch"); - - public PdfDocumentModel CreateFinanceResearchPaperTemplate() => CreateTemplate("financeresearch"); - - public PdfDocumentModel CreateCertificateTemplate() => CreateTemplate("certificate"); - - public PdfDocumentModel CreateCertificateNavyGoldTemplate() => CreateTemplate("certificatenavygold"); - - public PdfDocumentModel CreateDiplomaAcademicTemplate() => CreateTemplate("diploma"); - - public PdfDocumentModel CreateWeddingInvitationTraditionalTemplate() => CreateTemplate("weddingtraditional"); - - public PdfDocumentModel CreateWeddingInvitationRoyalFloralTemplate() => CreateTemplate("weddingroyalfloral"); - - public PdfDocumentModel CreateGalaInvitationTemplate() => CreateTemplate("galainvitation"); - public PdfDocumentModel CreateTypographyShowcaseTemplate() => CreateTemplate("typographyshowcase"); - public PdfDocumentModel CreateRichTextShowcaseTemplate() => CreateTemplate("richtextshowcase"); - public PdfDocumentModel CreateOcrAnalysisReportTemplate() => CreateTemplate("ocranalysisreport"); - - public PdfDocumentModel CreateMathBODMASWorksheetTemplate() => CreateTemplate("bodmas_worksheet"); - public PdfDocumentModel CreateFactorizationWorksheetTemplate() => CreateTemplate("factorization_worksheet"); - public PdfDocumentModel CreateBilingualExamPaperTemplate() => CreateTemplate("bilingual_exam_paper"); - public PdfDocumentModel CreateStatesOfMatterDiagramNotesTemplate() => CreateTemplate("states_of_matter_notes"); - public PdfDocumentModel CreateQuadrilateralsGuideTemplate() => CreateTemplate("quadrilaterals_guide"); - public PdfDocumentModel CreateArduinoCheatSheetTemplate() => CreateTemplate("arduino_cheatsheet"); - - public PdfDocumentModel CreateBlankDocument(PageFormat format = PageFormat.A4, PageOrientation orientation = PageOrientation.Portrait) - { - return _blankTemplate.Create(format, orientation); - } -} diff --git a/src/PdfEditorApp/Services/ThemeService.cs b/src/PdfEditorApp/Services/ThemeService.cs deleted file mode 100644 index c68cbdf..0000000 --- a/src/PdfEditorApp/Services/ThemeService.cs +++ /dev/null @@ -1,318 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; -using Avalonia; -using Avalonia.Styling; -using Avalonia.Threading; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -public interface IThemeService -{ - AppThemeMode CurrentTheme { get; } - PdfReaderTheme ReadingTheme { get; } - bool IsDarkMode { get; } - event Action? ThemeChanged; - event Action? ReadingThemeChanged; - - void SetTheme(AppThemeMode mode); - void ToggleTheme(); - void SetReadingTheme(PdfReaderTheme theme); - void Initialize(); -} - -public class ThemePreferenceData -{ - [JsonPropertyName("Theme")] - public string? Theme { get; set; } - - [JsonPropertyName("ReadingTheme")] - public string? ReadingTheme { get; set; } - - [JsonPropertyName("IsDarkMode")] - public bool? IsDarkMode { get; set; } - - [JsonPropertyName("UpdatedAt")] - public DateTime? UpdatedAt { get; set; } -} - -public class ThemeService : IThemeService -{ - private AppThemeMode _currentTheme = AppThemeMode.Light; - private PdfReaderTheme _readingTheme = PdfReaderTheme.Default; - private bool _isHookedToActualThemeVariant = false; - - public AppThemeMode CurrentTheme => _currentTheme; - public PdfReaderTheme ReadingTheme => _readingTheme; - - public bool IsDarkMode => _currentTheme == AppThemeMode.Dark || - (_currentTheme == AppThemeMode.System && Application.Current?.ActualThemeVariant == ThemeVariant.Dark); - - public event Action? ThemeChanged; - public event Action? ReadingThemeChanged; - - public ThemeService() - { - Initialize(); - } - - public static string GetSettingsDirectory() - { - string? appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - if (string.IsNullOrWhiteSpace(appData)) - { - appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - } - if (string.IsNullOrWhiteSpace(appData)) - { - appData = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - } - if (string.IsNullOrWhiteSpace(appData)) - { - appData = Path.GetTempPath(); - } - - var dir = Path.Combine(appData, "FryPDF"); - try - { - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - } - catch - { - dir = Path.Combine(Path.GetTempPath(), "FryPDF"); - try { Directory.CreateDirectory(dir); } catch { /* Ignore */ } - } - - return dir; - } - - public static string GetSettingsFilePath() - { - return Path.Combine(GetSettingsDirectory(), "theme_preference.json"); - } - - public void Initialize() - { - try - { - var filePath = GetSettingsFilePath(); - - // Check primary path, then fallback to LocalApplicationData if different - if (!File.Exists(filePath)) - { - var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - if (!string.IsNullOrWhiteSpace(localAppData)) - { - var alternatePath = Path.Combine(localAppData, "FryPDF", "theme_preference.json"); - if (File.Exists(alternatePath)) - { - filePath = alternatePath; - } - } - } - - if (File.Exists(filePath)) - { - string json = File.ReadAllText(filePath); - - var options = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip - }; - - bool loaded = false; - try - { - var data = JsonSerializer.Deserialize(json, options); - if (data != null) - { - if (!string.IsNullOrWhiteSpace(data.Theme) && - Enum.TryParse(data.Theme, true, out var parsedTheme)) - { - _currentTheme = parsedTheme; - loaded = true; - } - else if (data.IsDarkMode.HasValue) - { - _currentTheme = data.IsDarkMode.Value ? AppThemeMode.Dark : AppThemeMode.Light; - loaded = true; - } - - if (!string.IsNullOrWhiteSpace(data.ReadingTheme) && - Enum.TryParse(data.ReadingTheme, true, out var parsedReadingTheme)) - { - _readingTheme = parsedReadingTheme; - } - } - } - catch - { - // Fallback to manual parsing below - } - - if (!loaded) - { - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - foreach (var prop in root.EnumerateObject()) - { - if (prop.NameEquals("Theme") || prop.NameEquals("theme") || - prop.NameEquals("ThemeMode") || prop.NameEquals("themeMode")) - { - if (prop.Value.ValueKind == JsonValueKind.String && - Enum.TryParse(prop.Value.GetString(), true, out var t)) - { - _currentTheme = t; - } - else if (prop.Value.ValueKind == JsonValueKind.Number && - prop.Value.TryGetInt32(out var intVal) && - Enum.IsDefined(typeof(AppThemeMode), intVal)) - { - _currentTheme = (AppThemeMode)intVal; - } - } - else if (prop.NameEquals("ReadingTheme") || prop.NameEquals("readingTheme") || - prop.NameEquals("ReaderTheme") || prop.NameEquals("readerTheme")) - { - if (prop.Value.ValueKind == JsonValueKind.String && - Enum.TryParse(prop.Value.GetString(), true, out var rt)) - { - _readingTheme = rt; - } - else if (prop.Value.ValueKind == JsonValueKind.Number && - prop.Value.TryGetInt32(out var intVal) && - Enum.IsDefined(typeof(PdfReaderTheme), intVal)) - { - _readingTheme = (PdfReaderTheme)intVal; - } - } - } - } - } - } - catch - { - _currentTheme = AppThemeMode.Light; - _readingTheme = PdfReaderTheme.Default; - } - - ApplyThemeToApplication(_currentTheme); - HookActualThemeVariant(); - ThemeChanged?.Invoke(_currentTheme); - ReadingThemeChanged?.Invoke(_readingTheme); - } - - public void SetTheme(AppThemeMode mode) - { - if (_currentTheme == mode) return; - _currentTheme = mode; - ApplyThemeToApplication(mode); - SavePreference(); - ThemeChanged?.Invoke(mode); - } - - public void SetReadingTheme(PdfReaderTheme theme) - { - if (_readingTheme == theme) return; - _readingTheme = theme; - SavePreference(); - ReadingThemeChanged?.Invoke(theme); - } - - public void ToggleTheme() - { - var newTheme = IsDarkMode ? AppThemeMode.Light : AppThemeMode.Dark; - SetTheme(newTheme); - } - - private void ApplyThemeToApplication(AppThemeMode mode) - { - if (Application.Current == null) return; - - void Apply() - { - if (Application.Current == null) return; - - Application.Current.RequestedThemeVariant = mode switch - { - AppThemeMode.Dark => ThemeVariant.Dark, - AppThemeMode.Light => ThemeVariant.Light, - AppThemeMode.System => ThemeVariant.Default, - _ => ThemeVariant.Light - }; - } - - try - { - if (Dispatcher.UIThread.CheckAccess()) - { - Apply(); - } - else - { - Dispatcher.UIThread.Post(Apply); - } - } - catch - { - // Headless unit test environment without active dispatcher loop - } - } - - private void HookActualThemeVariant() - { - if (_isHookedToActualThemeVariant || Application.Current == null) return; - - try - { - Application.Current.ActualThemeVariantChanged += (sender, args) => - { - if (_currentTheme == AppThemeMode.System) - { - ThemeChanged?.Invoke(_currentTheme); - } - }; - _isHookedToActualThemeVariant = true; - } - catch - { - // Ignore if platform doesn't support event - } - } - - private void SavePreference() - { - try - { - var dir = GetSettingsDirectory(); - var filePath = Path.Combine(dir, "theme_preference.json"); - var payload = new ThemePreferenceData - { - Theme = _currentTheme.ToString(), - ReadingTheme = _readingTheme.ToString(), - IsDarkMode = IsDarkMode, - UpdatedAt = DateTime.UtcNow - }; - - var options = new JsonSerializerOptions - { - WriteIndented = true - }; - - string json = JsonSerializer.Serialize(payload, options); - File.WriteAllText(filePath, json); - } - catch - { - // Ignore storage permission failures gracefully - } - } -} diff --git a/src/PdfEditorApp/Services/Tools/Conversion/PdfConversionService.cs b/src/PdfEditorApp/Services/Tools/Conversion/PdfConversionService.cs deleted file mode 100644 index aa8fee9..0000000 --- a/src/PdfEditorApp/Services/Tools/Conversion/PdfConversionService.cs +++ /dev/null @@ -1,1076 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using DocumentFormat.OpenXml; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Spreadsheet; -using DocumentFormat.OpenXml.Wordprocessing; -using DocumentFormat.OpenXml.Presentation; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfSharpCore.Drawing; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.IO; -using QuestPDF.Fluent; -using QuestPDF.Infrastructure; -using QColors = QuestPDF.Helpers.Colors; -using UglyToad.PdfPig; -using UglyToad.PdfPig.Content; -using QuestPDF.Helpers; - -namespace PdfEditorApp.Services.Tools.Conversion; - - -public interface IPdfConversionService -{ - Task ConvertPdfToWordAsync(WordConversionOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertPdfToExcelAsync(ExcelConversionOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertPdfToPowerPointAsync(PptxConversionOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertWordToPdfAsync(OfficeToPdfOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertExcelToPdfAsync(OfficeToPdfOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertPowerPointToPdfAsync(OfficeToPdfOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertPdfToImagesAsync(ImageConversionOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertImagesToPdfAsync(ImagesToPdfOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertHtmlToPdfAsync(HtmlToPdfOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertPdfToMarkdownAsync(MarkdownConversionOptions options, IProgress? progress = null, CancellationToken ct = default); -} - -public class PdfConversionService : IPdfConversionService -{ - public async Task ConvertPdfToWordAsync(WordConversionOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}.docx"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(10.0); - - using var pdf = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath); - int totalPages = pdf.NumberOfPages; - - using var wordDoc = WordprocessingDocument.Create(outPath, WordprocessingDocumentType.Document); - var mainPart = wordDoc.AddMainDocumentPart(); - mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document(); - var body = mainPart.Document.AppendChild(new Body()); - - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - var page = pdf.GetPage(p); - var text = page.Text; - - // Split into paragraphs - var lines = text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - if (string.IsNullOrWhiteSpace(line)) continue; - - var para = body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph()); - var run = para.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Run()); - run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text(line.Trim())); - } - - // Add section break between pages if not last page - if (p < totalPages) - { - var pageBreakPara = body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph()); - var pageBreakRun = pageBreakPara.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Run()); - pageBreakRun.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Break { Type = BreakValues.Page }); - } - - progress?.Report(10.0 + (p / (double)totalPages * 80.0)); - } - - mainPart.Document.Save(); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Successfully converted {totalPages} pages to Microsoft Word document: {Path.GetFileName(outPath)}" - }; - }, ct); - } - - public async Task ConvertPdfToExcelAsync(ExcelConversionOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}.xlsx"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(10.0); - - using var pdf = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath); - int totalPages = pdf.NumberOfPages; - - using var spreadsheetDoc = SpreadsheetDocument.Create(outPath, SpreadsheetDocumentType.Workbook); - var workbookPart = spreadsheetDoc.AddWorkbookPart(); - workbookPart.Workbook = new DocumentFormat.OpenXml.Spreadsheet.Workbook(); - var sheets = workbookPart.Workbook.AppendChild(new Sheets()); - - uint sheetId = 1; - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - var page = pdf.GetPage(p); - var text = page.Text; - - var worksheetPart = workbookPart.AddNewPart(); - var sheetData = new SheetData(); - worksheetPart.Worksheet = new Worksheet(sheetData); - - var lines = text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); - uint rowIndex = 1; - - foreach (var line in lines) - { - if (string.IsNullOrWhiteSpace(line)) continue; - - var row = new Row { RowIndex = rowIndex }; - // Parse row elements (split by whitespace tab or multiple spaces / commas) - var cells = Regex.Split(line.Trim(), @"\s{2,}|\t"); - if (cells.Length <= 1) - { - cells = line.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); - } - - int colIndex = 1; - foreach (var cellVal in cells) - { - string cleanVal = cellVal.Trim(); - string numericCandidate = cleanVal.Replace("$", "").Replace(",", ""); - bool isNumber = double.TryParse(numericCandidate, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double numericValue); - var cell = new Cell - { - CellReference = $"{GetExcelColumnName(colIndex)}{rowIndex}", - DataType = isNumber ? CellValues.Number : CellValues.String, - CellValue = new CellValue(isNumber - ? numericValue.ToString(System.Globalization.CultureInfo.InvariantCulture) - : cleanVal) - }; - row.Append(cell); - colIndex++; - } - - sheetData.Append(row); - rowIndex++; - } - - string sheetName = totalPages == 1 ? "Data" : $"Page {p}"; - var sheet = new Sheet - { - Id = spreadsheetDoc.WorkbookPart!.GetIdOfPart(worksheetPart), - SheetId = sheetId++, - Name = sheetName - }; - sheets.Append(sheet); - - progress?.Report(10.0 + (p / (double)totalPages * 80.0)); - } - - workbookPart.Workbook.Save(); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Extracted tabular data across {totalPages} pages into Excel spreadsheet: {Path.GetFileName(outPath)}" - }; - }, ct); - } - - public async Task ConvertPdfToPowerPointAsync(PptxConversionOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}.pptx"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(10.0); - - using var pdf = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath); - int totalPages = pdf.NumberOfPages; - - using var presentationDoc = PresentationDocument.Create(outPath, PresentationDocumentType.Presentation); - var presentationPart = presentationDoc.AddPresentationPart(); - presentationPart.Presentation = new Presentation(); - var slideIdList = presentationPart.Presentation.AppendChild(new SlideIdList()); - - uint slideId = 256; - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - var page = pdf.GetPage(p); - var text = page.Text; - - var slidePart = presentationPart.AddNewPart(); - var slide = new Slide(new CommonSlideData(new ShapeTree())); - - var shapeTree = slide.CommonSlideData!.ShapeTree!; - var nonVisualProperties = shapeTree.AppendChild(new NonVisualGroupShapeProperties( - new NonVisualDrawingProperties { Id = 1, Name = "" }, - new NonVisualGroupShapeDrawingProperties(), - new ApplicationNonVisualDrawingProperties())); - shapeTree.AppendChild(new GroupShapeProperties()); - - // Add Title / Text Shape - var shape = shapeTree.AppendChild(new DocumentFormat.OpenXml.Presentation.Shape()); - shape.NonVisualShapeProperties = new NonVisualShapeProperties( - new NonVisualDrawingProperties { Id = 2, Name = $"Slide Text {p}" }, - new NonVisualShapeDrawingProperties(new DocumentFormat.OpenXml.Drawing.ShapeLocks { NoGrouping = true }), - new ApplicationNonVisualDrawingProperties(new PlaceholderShape())); - - shape.ShapeProperties = new DocumentFormat.OpenXml.Presentation.ShapeProperties(); - var textBody = shape.AppendChild(new DocumentFormat.OpenXml.Presentation.TextBody( - new DocumentFormat.OpenXml.Drawing.BodyProperties(), - new DocumentFormat.OpenXml.Drawing.ListStyle())); - - var lines = text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var para = textBody.AppendChild(new DocumentFormat.OpenXml.Drawing.Paragraph()); - var run = para.AppendChild(new DocumentFormat.OpenXml.Drawing.Run()); - run.AppendChild(new DocumentFormat.OpenXml.Drawing.Text(line.Trim())); - } - - slidePart.Slide = slide; - slidePart.Slide.Save(); - - var slideIdItem = new SlideId - { - Id = slideId++, - RelationshipId = presentationPart.GetIdOfPart(slidePart) - }; - slideIdList.Append(slideIdItem); - - progress?.Report(10.0 + (p / (double)totalPages * 80.0)); - } - - presentationPart.Presentation.Save(); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Converted PDF to {totalPages}-slide PowerPoint presentation: {Path.GetFileName(outPath)}" - }; - }, ct); - } - - public async Task ConvertWordToPdfAsync(OfficeToPdfOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input Word document does not exist." }; - - if (IsLegacyOfficeBinaryFormat(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "This is a legacy .doc (binary) file, which isn't supported yet. Please save it as .docx in Word and try again." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}.pdf"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(15.0); - - // Read DOCX text and paragraphs - var paragraphs = new List(); - using (var wordDoc = WordprocessingDocument.Open(options.InputFilePath, false)) - { - var body = wordDoc.MainDocumentPart?.Document?.Body; - if (body != null) - { - // Descendants (not Elements) so paragraphs nested inside tables are included too. - foreach (var para in body.Descendants()) - { - string txt = para.InnerText; - if (!string.IsNullOrWhiteSpace(txt)) paragraphs.Add(txt); - } - } - } - - progress?.Report(50.0); - ct.ThrowIfCancellationRequested(); - - // Generate PDF via QuestPDF with FryPDF Creator and codefrydev.in Producer metadata - var doc = FryPdfDocument.Create(container => - { - container.Page(page => - { - page.Size(PageSizes.A4); - page.Margin(36); - page.PageColor(QColors.White); - page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Helvetica").FontColor(QColors.Grey.Darken3)); - - page.Header().Row(row => - { - row.RelativeItem().Text(Path.GetFileNameWithoutExtension(options.InputFilePath)).SemiBold().FontColor(QColors.Grey.Darken2); - }); - - page.Content().PaddingVertical(10).Column(col => - { - col.Spacing(8); - foreach (var p in paragraphs) - { - col.Item().Text(p); - } - }); - - page.Footer().AlignCenter().Text(x => - { - x.Span("Page "); - x.CurrentPageNumber(); - x.Span(" of "); - x.TotalPages(); - }); - }); - }); - - doc.GeneratePdf(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Converted Word document ({paragraphs.Count} paragraphs) to PDF: {Path.GetFileName(outPath)}" - }; - }, ct); - } - - public async Task ConvertExcelToPdfAsync(OfficeToPdfOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input Excel document does not exist." }; - - if (IsLegacyOfficeBinaryFormat(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "This is a legacy .xls (binary) file, which isn't supported yet. Please save it as .xlsx in Excel and try again." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}.pdf"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(15.0); - - var tables = new List<(string SheetName, List> Rows)>(); - - using (var spreadsheet = SpreadsheetDocument.Open(options.InputFilePath, false)) - { - var workbookPart = spreadsheet.WorkbookPart; - if (workbookPart != null) - { - var sharedStrings = workbookPart.SharedStringTablePart?.SharedStringTable; - var sheetsEnum = workbookPart.Workbook!.Sheets?.Elements() ?? Enumerable.Empty(); - foreach (var sheet in sheetsEnum) - { - var sheetName = sheet.Name?.Value ?? "Sheet"; - var worksheetPart = workbookPart.GetPartById(sheet.Id?.Value ?? string.Empty) as WorksheetPart; - if (worksheetPart == null) continue; - var sheetData = worksheetPart.Worksheet!.Elements().FirstOrDefault(); - var rowsList = new List>(); - - if (sheetData != null) - { - foreach (var row in sheetData.Elements()) - { - var cellValues = new List(); - foreach (var cell in row.Elements()) - { - string val; - if (cell.DataType?.Value == CellValues.SharedString - && cell.CellValue != null - && int.TryParse(cell.CellValue.Text, out int sharedIndex) - && sharedStrings != null - && sharedIndex >= 0 && sharedIndex < sharedStrings.ChildElements.Count) - { - val = sharedStrings.ElementAt(sharedIndex).InnerText; - } - else - { - val = cell.CellValue?.Text ?? cell.InnerText ?? ""; - } - cellValues.Add(val); - } - if (cellValues.Count > 0) rowsList.Add(cellValues); - } - } - if (rowsList.Count > 0) tables.Add((sheetName, rowsList)); - } - } - } - - if (tables.Count == 0) - { - tables.Add(("Sheet 1", new List> { new() { "Data Table" } })); - } - - progress?.Report(50.0); - ct.ThrowIfCancellationRequested(); - - var doc = FryPdfDocument.Create(container => - { - foreach (var table in tables) - { - container.Page(page => - { - page.Size(PageSizes.A4.Landscape()); - page.Margin(28); - page.PageColor(QColors.White); - page.DefaultTextStyle(x => x.FontSize(9).FontFamily("Helvetica")); - - page.Header().Text(table.SheetName).Bold().FontSize(13).FontColor(QColors.Blue.Darken2); - - page.Content().PaddingVertical(10).Table(tbl => - { - int maxCols = table.Rows.Max(r => r.Count); - if (maxCols == 0) maxCols = 1; - - tbl.ColumnsDefinition(cols => - { - for (int c = 0; c < maxCols; c++) cols.RelativeColumn(); - }); - - bool isHeader = true; - foreach (var r in table.Rows) - { - foreach (var cell in r) - { - if (isHeader) - { - tbl.Cell().Background(QColors.Grey.Lighten3).Border(0.5f).BorderColor(QColors.Grey.Lighten1).Padding(4).Text(cell).Bold(); - } - else - { - tbl.Cell().Border(0.5f).BorderColor(QColors.Grey.Lighten2).Padding(4).Text(cell); - } - } - isHeader = false; - } - }); - - page.Footer().AlignCenter().Text(x => - { - x.Span("Page "); - x.CurrentPageNumber(); - x.Span(" of "); - x.TotalPages(); - }); - }); - } - }); - - doc.GeneratePdf(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Converted Excel spreadsheet ({tables.Count} sheets) to PDF: {Path.GetFileName(outPath)}" - }; - }, ct); - } - - public async Task ConvertPowerPointToPdfAsync(OfficeToPdfOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PowerPoint presentation does not exist." }; - - if (IsLegacyOfficeBinaryFormat(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "This is a legacy .ppt (binary) file, which isn't supported yet. Please save it as .pptx in PowerPoint and try again." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}.pdf"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(15.0); - - var slidesText = new List>(); - - using (var presentationDoc = PresentationDocument.Open(options.InputFilePath, false)) - { - var presentationPart = presentationDoc.PresentationPart; - if (presentationPart != null) - { - var slideIdsEnum = presentationPart.Presentation!.SlideIdList?.Elements() ?? Enumerable.Empty(); - foreach (var slideId in slideIdsEnum) - { - var slidePart = presentationPart.GetPartById(slideId.RelationshipId?.Value ?? string.Empty) as SlidePart; - if (slidePart == null) continue; - var slideLines = new List(); - foreach (var paragraph in slidePart.Slide!.Descendants()) - { - string t = paragraph.InnerText; - if (!string.IsNullOrWhiteSpace(t)) slideLines.Add(t); - } - slidesText.Add(slideLines); - } - } - } - - progress?.Report(50.0); - ct.ThrowIfCancellationRequested(); - - if (slidesText.Count == 0) - { - slidesText.Add(new List { "Presentation Slide" }); - } - - var doc = FryPdfDocument.Create(container => - { - int sIndex = 1; - foreach (var slide in slidesText) - { - var linesToRender = slide.Count > 0 ? slide : new List { string.Empty }; - container.Page(page => - { - page.Size(PageSizes.A4.Landscape()); - page.Margin(36); - page.PageColor(QColors.White); - page.DefaultTextStyle(x => x.FontSize(12).FontFamily("Helvetica")); - - page.Header().Row(row => - { - row.RelativeItem().Text($"Slide {sIndex}").Bold().FontSize(14).FontColor(QColors.Orange.Darken2); - }); - - page.Content().PaddingVertical(16).Column(col => - { - col.Spacing(10); - foreach (var line in linesToRender) - { - col.Item().Text(string.IsNullOrWhiteSpace(line) ? " " : line); - } - }); - - page.Footer().AlignCenter().Text(x => - { - x.Span($"Slide {sIndex} of {slidesText.Count}"); - }); - }); - sIndex++; - } - }); - - doc.GeneratePdf(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Converted PowerPoint presentation ({slidesText.Count} slides) to PDF: {Path.GetFileName(outPath)}" - }; - }, ct); - } - - public async Task ConvertPdfToImagesAsync(ImageConversionOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outDir = options.OutputDirectory; - if (string.IsNullOrWhiteSpace(outDir)) - { - outDir = Path.Combine(Path.GetDirectoryName(options.InputFilePath) ?? "", Path.GetFileNameWithoutExtension(options.InputFilePath) + "_images"); - } - if (!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); - - ct.ThrowIfCancellationRequested(); - - // Render actual page content via the same PdfPig+Skia path used for on-screen - // preview (PdfViewerViewModel.RenderPageBytesAtScale), instead of a blank canvas. - using var pdfPigDoc = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath); - try { UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.AddSkiaPageFactory(pdfPigDoc); } catch { } - - int totalPages = pdfPigDoc.NumberOfPages; - var createdFiles = new List(); - string baseName = Path.GetFileNameWithoutExtension(options.InputFilePath); - string ext = options.OutputFormat.ToLowerInvariant().Contains("png") ? "png" : "jpg"; - float scale = (float)(options.Dpi / 72.0); - - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - string imgPath = Path.Combine(outDir, $"{baseName}_page_{p:D3}.{ext}"); - - using var pngStream = UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.GetPageAsPng(pdfPigDoc, p, scale, 100); - if (pngStream == null || pngStream.Length == 0) - { - progress?.Report(p / (double)totalPages * 90.0); - continue; - } - - if (ext == "png") - { - pngStream.Position = 0; - using var fs = File.Create(imgPath); - pngStream.CopyTo(fs); - } - else - { - pngStream.Position = 0; - using var skBitmap = SkiaSharp.SKBitmap.Decode(pngStream); - if (skBitmap == null) - { - progress?.Report(p / (double)totalPages * 90.0); - continue; - } - using var skImage = SkiaSharp.SKImage.FromBitmap(skBitmap); - using var jpegData = skImage.Encode(SkiaSharp.SKEncodedImageFormat.Jpeg, options.JpgQuality); - using var fs = File.Create(imgPath); - jpegData.SaveTo(fs); - } - - createdFiles.Add(imgPath); - progress?.Report(p / (double)totalPages * 90.0); - } - - progress?.Report(100.0); - - if (createdFiles.Count == 0) - { - return new ToolExecutionResult { Success = false, ErrorMessage = "No pages could be rendered from this PDF." }; - } - - return new ToolExecutionResult - { - Success = true, - OutputFilePath = createdFiles.FirstOrDefault(), - OutputFiles = createdFiles, - OriginalSizeBytes = origBytes, - OutputSizeBytes = createdFiles.Sum(f => File.Exists(f) ? new FileInfo(f).Length : 0), - Message = $"Exported {createdFiles.Count} page images at {options.Dpi} DPI to {outDir}." - }; - }, ct); - } - - public async Task ConvertImagesToPdfAsync(ImagesToPdfOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (options.ImageFiles == null || options.ImageFiles.Count == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "No images provided for PDF conversion." }; - - long totalOriginalBytes = options.ImageFiles.Where(File.Exists).Sum(f => new FileInfo(f).Length); - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.ImageFiles[0]) ?? Path.GetTempPath(); - outPath = Path.Combine(dir, "Images_Document.pdf"); - } - - ct.ThrowIfCancellationRequested(); - using var pdfDoc = new PdfSharpCore.Pdf.PdfDocument(); - int total = options.ImageFiles.Count; - var skipped = new List(); - int addedCount = 0; - - for (int i = 0; i < total; i++) - { - ct.ThrowIfCancellationRequested(); - string imgFile = options.ImageFiles[i]; - if (!File.Exists(imgFile)) - { - skipped.Add($"{Path.GetFileName(imgFile)} (not found)"); - continue; - } - - XImage xImage; - try - { - xImage = XImage.FromFile(imgFile); - } - catch (Exception ex) - { - // One corrupt/unsupported image must not abort the whole batch - // (and must not leave an empty page behind for it). - skipped.Add($"{Path.GetFileName(imgFile)} ({ex.Message})"); - continue; - } - - using (xImage) - { - var page = pdfDoc.AddPage(); - - if (options.AutoOrientation && xImage.PixelWidth > xImage.PixelHeight) - { - page.Orientation = PdfSharpCore.PageOrientation.Landscape; - } - else - { - page.Orientation = options.Orientation == PageOrientation.Landscape - ? PdfSharpCore.PageOrientation.Landscape - : PdfSharpCore.PageOrientation.Portrait; - } - - using var gfx = XGraphics.FromPdfPage(page); - - if (options.FitToPage) - { - double margin = options.MarginPoints; - double maxW = page.Width.Point - (margin * 2); - double maxH = page.Height.Point - (margin * 2); - - double scale = Math.Min(maxW / xImage.PixelWidth, maxH / xImage.PixelHeight); - double drawW = xImage.PixelWidth * scale; - double drawH = xImage.PixelHeight * scale; - double posX = margin + ((maxW - drawW) / 2.0); - double posY = margin + ((maxH - drawH) / 2.0); - - gfx.DrawImage(xImage, posX, posY, drawW, drawH); - } - else - { - gfx.DrawImage(xImage, options.MarginPoints, options.MarginPoints); - } - } - - addedCount++; - progress?.Report((i + 1) / (double)total * 90.0); - } - - if (addedCount == 0) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"No images could be converted: {string.Join("; ", skipped)}" - }; - } - - pdfDoc.Save(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - string resultMessage = skipped.Count == 0 - ? $"Converted {total} images into high-resolution PDF: {Path.GetFileName(outPath)}" - : $"Converted {addedCount} of {total} images into PDF: {Path.GetFileName(outPath)}. Skipped: {string.Join("; ", skipped)}"; - - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = totalOriginalBytes, - OutputSizeBytes = outBytes, - Message = resultMessage - }; - }, ct); - } - - public async Task ConvertHtmlToPdfAsync(HtmlToPdfOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - string htmlContent = options.HtmlContentOrUrl; - if (options.IsUrl && options.HtmlContentOrUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - try - { - using var client = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromSeconds(20) }; - htmlContent = client.GetStringAsync(options.HtmlContentOrUrl, ct).GetAwaiter().GetResult(); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - return new ToolExecutionResult { Success = false, ErrorMessage = $"Failed to load URL: {ex.Message}" }; - } - } - else if (File.Exists(options.HtmlContentOrUrl)) - { - htmlContent = File.ReadAllText(options.HtmlContentOrUrl); - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetTempPath(); - outPath = Path.Combine(dir, $"Webpage_Export_{Guid.NewGuid():N}.pdf"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(30.0); - - // Clean simple HTML tags to paragraphs - string plainText = Regex.Replace(htmlContent, @"]*>[\s\S]*?", "", RegexOptions.IgnoreCase); - plainText = Regex.Replace(plainText, @"]*>[\s\S]*?", "", RegexOptions.IgnoreCase); - plainText = Regex.Replace(plainText, @"<[^>]+>", "\n", RegexOptions.IgnoreCase); - plainText = System.Net.WebUtility.HtmlDecode(plainText); - - var lines = plainText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries) - .Select(l => l.Trim()) - .Where(l => !string.IsNullOrWhiteSpace(l)) - .ToList(); - - var doc = FryPdfDocument.Create(container => - { - container.Page(page => - { - page.Size(options.Orientation == PageOrientation.Landscape ? PageSizes.A4.Landscape() : PageSizes.A4); - page.Margin((float)options.MarginPoints); - page.PageColor(QColors.White); - page.DefaultTextStyle(x => x.FontSize(10.5f).FontFamily("Helvetica").FontColor(QColors.Grey.Darken3)); - - page.Header().Text(options.IsUrl ? options.HtmlContentOrUrl : "HTML Web Document").SemiBold().FontColor(QColors.Blue.Darken2); - - page.Content().PaddingVertical(10).Column(col => - { - col.Spacing(6); - foreach (var l in lines) - { - col.Item().Text(l); - } - }); - - if (options.IncludePageNumbers) - { - page.Footer().AlignCenter().Text(x => - { - x.Span("Page "); - x.CurrentPageNumber(); - x.Span(" of "); - x.TotalPages(); - }); - } - }); - }); - - doc.GeneratePdf(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = htmlContent.Length, - OutputSizeBytes = outBytes, - Message = $"Compiled HTML content to PDF: {Path.GetFileName(outPath)}" - }; - }, ct); - } - - public async Task ConvertPdfToMarkdownAsync(MarkdownConversionOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}.md"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(15.0); - - using var pdf = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath); - int totalPages = pdf.NumberOfPages; - var sb = new StringBuilder(); - - if (options.IncludeMetadataHeader) - { - sb.AppendLine("---"); - sb.AppendLine($"title: \"{Path.GetFileNameWithoutExtension(options.InputFilePath)}\""); - sb.AppendLine($"pages: {totalPages}"); - sb.AppendLine($"generated_by: \"FryPDF\""); - sb.AppendLine($"producer: \"codefrydev.in\""); - sb.AppendLine($"date: \"{DateTime.UtcNow:yyyy-MM-dd}\""); - sb.AppendLine("---"); - sb.AppendLine(); - } - - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - var page = pdf.GetPage(p); - sb.AppendLine($""); - sb.AppendLine(); - - var lines = page.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); - - foreach (var line in lines) - { - string trimmed = line.Trim(); - if (string.IsNullOrWhiteSpace(trimmed)) continue; - - // Heading heuristics - if (trimmed.Length < 60 && (trimmed.StartsWith("Chapter") || trimmed.StartsWith("Section") || trimmed.All(c => !char.IsLetter(c) || char.IsUpper(c)))) - { - sb.AppendLine($"## {trimmed}"); - sb.AppendLine(); - } - // Bullet list - else if (trimmed.StartsWith("•") || trimmed.StartsWith("-") || trimmed.StartsWith("*")) - { - sb.AppendLine($"- {trimmed.TrimStart('•', '-', '*', ' ')}"); - } - // Numbered list - else if (Regex.IsMatch(trimmed, @"^\d+[\.\)]\s+")) - { - sb.AppendLine(trimmed); - } - else - { - sb.AppendLine(trimmed); - sb.AppendLine(); - } - } - - sb.AppendLine(); - progress?.Report(15.0 + (p / (double)totalPages * 75.0)); - } - - File.WriteAllText(outPath, sb.ToString(), Encoding.UTF8); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - ExtraData = new Dictionary { ["MarkdownContent"] = sb.ToString() }, - Message = $"Extracted structured Markdown ({totalPages} pages) to {Path.GetFileName(outPath)}" - }; - }, ct); - } - - private static string GetExcelColumnName(int colIndex) - { - string name = ""; - while (colIndex > 0) - { - int mod = (colIndex - 1) % 26; - name = (char)('A' + mod) + name; - colIndex = (colIndex - mod) / 26; - } - return name; - } - - /// - /// Detects the legacy OLE2 Compound File signature used by pre-2007 .doc/.xls/.ppt - /// binary formats, which DocumentFormat.OpenXml (ZIP/OOXML-only) cannot open. - /// - private static bool IsLegacyOfficeBinaryFormat(string path) - { - try - { - using var fs = File.OpenRead(path); - Span header = stackalloc byte[8]; - int read = fs.Read(header); - if (read < 8) return false; - ReadOnlySpan ole2Signature = stackalloc byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 }; - return header.SequenceEqual(ole2Signature); - } - catch - { - return false; - } - } -} diff --git a/src/PdfEditorApp/Services/Tools/Core/IPdfToolViewModelFactory.cs b/src/PdfEditorApp/Services/Tools/Core/IPdfToolViewModelFactory.cs deleted file mode 100644 index fe54ff3..0000000 --- a/src/PdfEditorApp/Services/Tools/Core/IPdfToolViewModelFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; - -namespace PdfEditorApp.Services.Tools.Core; - -/// -/// Factory interface for creating dedicated PDF Tool ViewModels for any registered tool ID. -/// -public interface IPdfToolViewModelFactory -{ - PdfToolViewModelBase Create(PdfToolId toolId); - PdfToolViewModelBase CreateToolViewModel(PdfToolId toolId); - PdfToolViewModelBase CreateToolViewModel(PdfToolDefinition toolDefinition); -} diff --git a/src/PdfEditorApp/Services/Tools/Core/PdfFileHelper.cs b/src/PdfEditorApp/Services/Tools/Core/PdfFileHelper.cs deleted file mode 100644 index 0bc3689..0000000 --- a/src/PdfEditorApp/Services/Tools/Core/PdfFileHelper.cs +++ /dev/null @@ -1,377 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.IO; -using UglyToad.PdfPig.Writer; - -namespace PdfEditorApp.Services.Tools.Core; - -/// -/// Robust PDF file reader and sanitizer. -/// Gracefully opens modern, web-exported, linearized, or non-standard PDF files. -/// -public static class PdfFileHelper -{ - public static void SetFryPdfMetadata( - PdfSharpCore.Pdf.PdfDocument doc, - string? title = null, - string? author = null, - string? subject = null, - string? keywords = null, - string? creator = null, - string? producer = null) - { - if (doc == null) return; - if (!string.IsNullOrEmpty(title) && string.IsNullOrEmpty(doc.Info.Title)) doc.Info.Title = title; - if (!string.IsNullOrEmpty(author) && string.IsNullOrEmpty(doc.Info.Author)) doc.Info.Author = author; - if (!string.IsNullOrEmpty(subject) && string.IsNullOrEmpty(doc.Info.Subject)) doc.Info.Subject = subject; - if (!string.IsNullOrEmpty(keywords) && string.IsNullOrEmpty(doc.Info.Keywords)) doc.Info.Keywords = keywords; - doc.Info.Creator = !string.IsNullOrWhiteSpace(creator) ? creator : "FryPDF"; - try - { - doc.Info.Elements.SetString("/Producer", !string.IsNullOrWhiteSpace(producer) ? producer : "codefrydev.in"); - } - catch - { - try { doc.Info.Elements["/Producer"] = new PdfString(!string.IsNullOrWhiteSpace(producer) ? producer : "codefrydev.in"); } catch { } - } - } - - public static void SaveDocumentWithFryPdfMetadata( - PdfSharpCore.Pdf.PdfDocument doc, - string filePath, - string? title = null, - string? author = null, - string? subject = null, - string? keywords = null, - string? creator = "FryPDF", - string? producer = "codefrydev.in") - { - SetFryPdfMetadata(doc, title, author, subject, keywords, creator, producer); - doc.Save(filePath); - PatchProducerInFile(filePath, producer ?? "codefrydev.in"); - } - - public static void PatchProducerInFile(string filePath, string producer = "codefrydev.in") - { - if (!File.Exists(filePath)) return; - try - { - byte[] bytes = File.ReadAllBytes(filePath); - byte[] updated = PatchProducerInBytes(bytes, producer); - if (updated != null && updated.Length > 0) - { - File.WriteAllBytes(filePath, updated); - } - } - catch { } - } - - public static byte[] PatchProducerInBytes(byte[] bytes, string producer = "codefrydev.in") - { - if (bytes == null || bytes.Length < 20) return bytes ?? Array.Empty(); - - try - { - string text = Encoding.ASCII.GetString(bytes); - if (text.Contains("/Encrypt")) - { - // Never binary patch encrypted PDF streams - return bytes; - } - - int searchIdx = 0; - while ((searchIdx = text.IndexOf("/Producer", searchIdx, StringComparison.Ordinal)) >= 0) - { - int openParen = text.IndexOf('(', searchIdx); - if (openParen > searchIdx && openParen < searchIdx + 25) - { - int closeParen = text.IndexOf(')', openParen); - if (closeParen > openParen) - { - int origSpanLen = closeParen - openParen + 1; - string replacement = $"({producer})"; - if (replacement.Length <= origSpanLen) - { - string filler = "%".PadRight(origSpanLen - replacement.Length, ' '); - string fullPatch = replacement + filler; - byte[] patchBytes = Encoding.ASCII.GetBytes(fullPatch); - Array.Copy(patchBytes, 0, bytes, openParen, patchBytes.Length); - } - } - } - searchIdx += 9; - } - } - catch { } - - return bytes; - } - - public static PdfSharpCore.Pdf.PdfDocument OpenDocumentSafely(string filePath, PdfDocumentOpenMode mode = PdfDocumentOpenMode.Import, string? password = null) - { - if (!File.Exists(filePath)) - throw new FileNotFoundException($"File not found: {filePath}"); - - // 1. Direct open attempt - try - { - if (string.IsNullOrEmpty(password)) - return PdfReader.Open(filePath, mode); - else - return PdfReader.Open(filePath, password, mode); - } - catch - { - // 2. Read bytes and sanitize trailing garbage or whitespace - byte[] rawBytes = File.ReadAllBytes(filePath); - byte[] sanitized = SanitizePdfBytes(rawBytes); - - try - { - var ms = new MemoryStream(sanitized); - if (string.IsNullOrEmpty(password)) - return PdfReader.Open(ms, mode); - else - return PdfReader.Open(ms, password, mode); - } - catch - { - // 3. Fallback: Reconstruct using pure C# PdfPig builder (handles modern cross-reference streams and non-standard xrefs) - try - { - byte[] rebuilt = ReconstructCleanPdfWithPdfPig(filePath); - if (rebuilt.Length > 0) - { - var msRebuilt = new MemoryStream(rebuilt); - if (string.IsNullOrEmpty(password)) - return PdfReader.Open(msRebuilt, mode); - else - return PdfReader.Open(msRebuilt, password, mode); - } - } - catch - { - // Continue to next fallback - } - - // 4. Fallback: synthesize trailer - byte[] repaired = SalvageAndRepairPdfBytes(sanitized); - var ms = new MemoryStream(repaired); - if (string.IsNullOrEmpty(password)) - return PdfReader.Open(ms, mode); - else - return PdfReader.Open(ms, password, mode); - } - } - } - - public static int InspectPageCountSafely(string filePath) - { - if (!File.Exists(filePath)) return 0; - - // 1. Try modern PdfPig parser (handles all PDF versions & formats) - try - { - using var pig = UglyToad.PdfPig.PdfDocument.Open(filePath); - return pig.NumberOfPages; - } - catch - { - // 2. Try OpenDocumentSafely - try - { - using var doc = OpenDocumentSafely(filePath, PdfDocumentOpenMode.Import); - return doc.PageCount; - } - catch - { - // 3. Regex token counting fallback - try - { - byte[] bytes = File.ReadAllBytes(filePath); - string text = Encoding.ASCII.GetString(bytes); - int count = 0; - int idx = 0; - while ((idx = text.IndexOf("/Type /Page", idx, StringComparison.Ordinal)) != -1) - { - if (idx + 11 >= text.Length || text[idx + 11] != 's') - { - count++; - } - idx += 11; - } - return Math.Max(1, count); - } - catch - { - return 1; - } - } - } - } - - public static byte[] ReconstructCleanPdfWithPdfPig(string filePath) - { - var builder = new PdfDocumentBuilder(); - using (var pigDoc = UglyToad.PdfPig.PdfDocument.Open(filePath)) - { - for (int i = 1; i <= pigDoc.NumberOfPages; i++) - { - builder.AddPage(pigDoc, i); - } - } - return builder.Build(); - } - - public static byte[] SanitizePdfBytes(byte[] rawBytes) - { - if (rawBytes == null || rawBytes.Length < 10) return rawBytes ?? Array.Empty(); - - // Look for the last %%EOF token in the file - string rawText = Encoding.ASCII.GetString(rawBytes, Math.Max(0, rawBytes.Length - 8192), Math.Min(rawBytes.Length, 8192)); - int eofIdx = rawText.LastIndexOf("%%EOF", StringComparison.OrdinalIgnoreCase); - - if (eofIdx >= 0) - { - int startOffset = Math.Max(0, rawBytes.Length - 8192); - int eofAbsoluteEnd = startOffset + eofIdx + 5; - - if (eofAbsoluteEnd < rawBytes.Length) - { - var trimmed = new byte[eofAbsoluteEnd + 2]; - Array.Copy(rawBytes, 0, trimmed, 0, eofAbsoluteEnd); - trimmed[eofAbsoluteEnd] = (byte)'\r'; - trimmed[eofAbsoluteEnd + 1] = (byte)'\n'; - return trimmed; - } - } - else - { - // Missing %%EOF, append standard trailer terminator - using var ms = new MemoryStream(); - ms.Write(rawBytes, 0, rawBytes.Length); - byte[] terminator = Encoding.ASCII.GetBytes("\r\n%%EOF\r\n"); - ms.Write(terminator, 0, terminator.Length); - return ms.ToArray(); - } - - return rawBytes; - } - - public static byte[] SalvageAndRepairPdfBytes(byte[] rawBytes) - { - string fullText = Encoding.ASCII.GetString(rawBytes); - int rootIdx = fullText.IndexOf("/Root", StringComparison.OrdinalIgnoreCase); - if (rootIdx >= 0 && !fullText.Contains("trailer")) - { - int spaceIdx = fullText.IndexOf(" ", rootIdx + 5); - string rootRef = "1 0 R"; - if (spaceIdx > 0 && spaceIdx + 10 < fullText.Length) - { - string snippet = fullText.Substring(rootIdx + 5, Math.Min(20, fullText.Length - (rootIdx + 5))).Trim(); - var parts = snippet.Split(new[] { ' ', '\r', '\n', '/' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 3 && parts[2] == "R") - { - rootRef = $"{parts[0]} {parts[1]} R"; - } - } - - using var ms = new MemoryStream(); - ms.Write(rawBytes, 0, rawBytes.Length); - string synthTrailer = $"\r\ntrailer\r\n<<\r\n/Root {rootRef}\r\n>>\r\nstartxref\r\n0\r\n%%EOF\r\n"; - byte[] tBytes = Encoding.ASCII.GetBytes(synthTrailer); - ms.Write(tBytes, 0, tBytes.Length); - return ms.ToArray(); - } - return rawBytes; - } - - /// - /// Checks whether an existing file is currently locked or cannot be opened for writing by the current process. - /// Returns false if the file does not exist. - /// - public static bool IsFileLocked(string filePath) - { - if (!File.Exists(filePath)) return false; - try - { - using var stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); - return false; - } - catch (IOException) - { - return true; - } - catch (UnauthorizedAccessException) - { - return true; - } - catch - { - return false; - } - } - - /// - /// Resolves a safe, non-conflicting, and writable destination file path. - /// If requestedPath is supplied and non-empty, it is returned directly. - /// Otherwise, checks defaultDirectory for baseFileName.extension. - /// If that file already exists and is locked or matches any path in avoidPaths, - /// appends an incrementing index (_1, _2, ...) until an available and unlocked path is found. - /// - public static string ResolveSafeOutputPath( - string? requestedPath, - string defaultDirectory, - string baseFileName, - string extension = ".pdf", - IEnumerable? avoidPaths = null) - { - if (!string.IsNullOrWhiteSpace(requestedPath)) - { - return requestedPath; - } - - if (!extension.StartsWith(".", StringComparison.Ordinal)) - { - extension = "." + extension; - } - - string dir = !string.IsNullOrWhiteSpace(defaultDirectory) && Directory.Exists(defaultDirectory) - ? defaultDirectory - : Path.GetTempPath(); - - var avoidList = avoidPaths? - .Where(p => !string.IsNullOrWhiteSpace(p)) - .Select(p => Path.GetFullPath(p)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - bool ShouldAvoid(string path) - { - string fullPath = Path.GetFullPath(path); - if (avoidList != null && avoidList.Contains(fullPath)) return true; - if (File.Exists(path) && IsFileLocked(path)) return true; - return false; - } - - string candidate = Path.Combine(dir, $"{baseFileName}{extension}"); - if (!ShouldAvoid(candidate)) - { - return candidate; - } - - for (int i = 1; i <= 1000; i++) - { - candidate = Path.Combine(dir, $"{baseFileName}_{i}{extension}"); - if (!ShouldAvoid(candidate)) - { - return candidate; - } - } - - return Path.Combine(dir, $"{baseFileName}_{DateTime.Now:yyyyMMdd_HHmmss}{extension}"); - } -} diff --git a/src/PdfEditorApp/Services/Tools/Core/PdfPageRenderer.cs b/src/PdfEditorApp/Services/Tools/Core/PdfPageRenderer.cs deleted file mode 100644 index a59e189..0000000 --- a/src/PdfEditorApp/Services/Tools/Core/PdfPageRenderer.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System.IO; -using Avalonia.Media.Imaging; -using QuestPDF.Fluent; -using QuestPDF.Infrastructure; -using UglyToad.PdfPig; -using UglyToad.PdfPig.Rendering.Skia; - -namespace PdfEditorApp.Services.Tools.Core; - -/// -/// Single shared PdfPig+Skia page rasterization path, used by both the PDF reader -/// () and tool-screen live -/// previews (), -/// so the render pipeline exists in exactly one place instead of being reimplemented -/// per screen. -/// -public static class PdfPageRenderer -{ - /// - /// Renders one page of a PDF file to a bitmap at the given scale. Falls back to - /// PdfFileHelper's salvage/repair path if the file can't be opened directly. - /// Page dimensions are still returned even if bitmap decoding fails, since callers - /// (page-card sizing, mark-position math) depend on them independent of render success. - /// - public static (Bitmap? Bitmap, double WidthPoints, double HeightPoints) RenderPageAtScale( - string filePath, int pageNumber, float scale) - { - if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath) || pageNumber < 1) - { - return (null, 0, 0); - } - - try - { - byte[] bytes = File.ReadAllBytes(filePath); - - PdfDocument? doc = null; - try - { - doc = PdfDocument.Open(bytes); - } - catch - { - try - { - byte[] repaired = PdfFileHelper.SalvageAndRepairPdfBytes(bytes); - doc = PdfDocument.Open(repaired); - } - catch - { - return (null, 0, 0); - } - } - - using (doc) - { - if (pageNumber > doc.NumberOfPages) - { - return (null, 0, 0); - } - - var page = doc.GetPage(pageNumber); - double widthPoints = page.Width; - double heightPoints = page.Height; - - try { PdfPigExtensions.AddSkiaPageFactory(doc); } catch { } - - using var stream = PdfPigExtensions.GetPageAsPng(doc, pageNumber, scale, 100); - if (stream != null && stream.Length > 0) - { - stream.Position = 0; - Bitmap? bitmap = null; - try { bitmap = new Bitmap(stream); } catch { } - return (bitmap, widthPoints, heightPoints); - } - - return (null, widthPoints, heightPoints); - } - } - catch - { - return (null, 0, 0); - } - } - - /// - /// Renders a raw SVG XML string to PNG bytes using QuestPDF's native vector SVG engine - /// and PdfPig/Skia. Pure headless, high-DPI rasterization without UI dependencies. - /// - public static byte[]? RenderSvgToPngBytes(string svgMarkup, double targetWidth, double targetHeight) - { - if (string.IsNullOrWhiteSpace(svgMarkup)) return null; - - try - { - float w = (float)System.Math.Max(30, targetWidth); - float h = (float)System.Math.Max(30, targetHeight); - - // High-DPI scale (clamped to prevent memory bloat, capped at max 2048px) - float maxDim = System.Math.Max(w, h); - float scale = maxDim > 0 ? (float)System.Math.Clamp(2048.0 / maxDim, 1.0, 2.5) : 2.0f; - - var doc = QuestPDF.Fluent.Document.Create(container => - { - container.Page(page => - { - page.Size(w, h, QuestPDF.Infrastructure.Unit.Point); - page.Margin(0); - page.PageColor(QuestPDF.Helpers.Colors.Transparent); - page.Content().Svg(svgMarkup).FitArea(); - }); - }); - - byte[] pdfBytes = doc.GeneratePdf(); - using var pdfPigDoc = PdfDocument.Open(pdfBytes); - try { PdfPigExtensions.AddSkiaPageFactory(pdfPigDoc); } catch { } - - using var stream = PdfPigExtensions.GetPageAsPng(pdfPigDoc, 1, scale, 100); - if (stream != null && stream.Length > 0) - { - return stream.ToArray(); - } - } - catch - { - // Graceful fallback - } - - return null; - } - - /// - /// Renders a raw SVG XML string to a high-DPI Avalonia Bitmap using QuestPDF's native vector SVG engine - /// and PdfPig/Skia. Ensures complete visual fidelity for complex multi-element diagrams (paths, circles, - /// rects, polygons, texts, gradients, markers) on the live Avalonia canvas. - /// - public static Bitmap? RenderSvgToBitmap(string svgMarkup, double targetWidth, double targetHeight) - { - byte[]? pngBytes = RenderSvgToPngBytes(svgMarkup, targetWidth, targetHeight); - if (pngBytes != null && pngBytes.Length > 0) - { - try - { - using var ms = new MemoryStream(pngBytes); - return new Bitmap(ms); - } - catch - { - // In headless unit test runners without Avalonia platform rendering context - return null; - } - } - - return null; - } -} diff --git a/src/PdfEditorApp/Services/Tools/Core/PdfToolRegistry.cs b/src/PdfEditorApp/Services/Tools/Core/PdfToolRegistry.cs deleted file mode 100644 index d09cc3b..0000000 --- a/src/PdfEditorApp/Services/Tools/Core/PdfToolRegistry.cs +++ /dev/null @@ -1,553 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services.Tools.Core; - -using PdfEditorApp.Core.Plugins; - -public interface IPdfToolRegistry -{ - event Action? RegistryChanged; - IReadOnlyList GetAllTools(); - IReadOnlyList GetToolsByCategory(PdfToolCategory category); - PdfToolDefinition? GetTool(PdfToolId id); - PdfToolDefinition? GetTool(string stringId); - void RegisterTool(PdfToolDefinition tool); -} - -public class PdfToolRegistry : IPdfToolRegistry -{ - public event Action? RegistryChanged; - private readonly List _tools; - private readonly IFryPluginContext? _pluginContext; - private readonly bool _seedDefaults; - - public PdfToolRegistry(IFryPluginContext? pluginContext = null, bool seedDefaults = true) - { - _pluginContext = pluginContext; - _seedDefaults = seedDefaults; - _tools = seedDefaults ? GetBuiltInToolDefinitions() : new List(); - if (_pluginContext != null) - { - _pluginContext.ToolsChanged += () => RegistryChanged?.Invoke(); - } - } - - private static List GetBuiltInToolDefinitions() => new() - { - // 1. MERGE PDF - new PdfToolDefinition - { - Id = PdfToolId.MergePdf, - Name = "Merge PDF", - Description = "Combine PDFs in the order you want with the easiest PDF merger available.", - Category = PdfToolCategory.OrganizeAndPage, - IconKind = "CallMerge", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 2. SPLIT PDF - new PdfToolDefinition - { - Id = PdfToolId.SplitPdf, - Name = "Split PDF", - Description = "Separate one page or a whole set for easy conversion into independent PDF files.", - Category = PdfToolCategory.OrganizeAndPage, - IconKind = "CallSplit", - IconColorHex = "#DC2626", - BackgroundAccentHex = "#FEF2F2", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf" - }, - // 3. COMPRESS PDF - new PdfToolDefinition - { - Id = PdfToolId.CompressPdf, - Name = "Compress PDF", - Description = "Reduce file size while optimizing for maximal PDF quality.", - Category = PdfToolCategory.OptimizeAndSecurity, - IconKind = "ArrowCollapseAll", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 4. PDF TO WORD - new PdfToolDefinition - { - Id = PdfToolId.PdfToWord, - Name = "PDF to Word", - Description = "Easily convert your PDF files into easy to edit DOC and DOCX documents.", - Category = PdfToolCategory.ConvertFromPdf, - IconKind = "FileWordOutline", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 5. PDF TO POWERPOINT - new PdfToolDefinition - { - Id = PdfToolId.PdfToPowerPoint, - Name = "PDF to PowerPoint", - Description = "Turn your PDF files into easy to edit PPT and PPTX slideshows.", - Category = PdfToolCategory.ConvertFromPdf, - IconKind = "FilePowerpointOutline", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 6. PDF TO EXCEL - new PdfToolDefinition - { - Id = PdfToolId.PdfToExcel, - Name = "PDF to Excel", - Description = "Pull data straight from PDFs into Excel spreadsheets in a few short seconds.", - Category = PdfToolCategory.ConvertFromPdf, - IconKind = "FileExcelOutline", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 7. WORD TO PDF - new PdfToolDefinition - { - Id = PdfToolId.WordToPdf, - Name = "Word to PDF", - Description = "Make DOC and DOCX files easy to read by converting them to PDF.", - Category = PdfToolCategory.ConvertToPdf, - IconKind = "FileWord", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".docx,.doc" - }, - // 8. POWERPOINT TO PDF - new PdfToolDefinition - { - Id = PdfToolId.PowerPointToPdf, - Name = "PowerPoint to PDF", - Description = "Make PPT and PPTX slideshows easy to view by converting them to PDF.", - Category = PdfToolCategory.ConvertToPdf, - IconKind = "FilePowerpoint", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pptx,.ppt" - }, - // 9. EXCEL TO PDF - new PdfToolDefinition - { - Id = PdfToolId.ExcelToPdf, - Name = "Excel to PDF", - Description = "Make EXCEL spreadsheets easy to read by converting them to PDF.", - Category = PdfToolCategory.ConvertToPdf, - IconKind = "FileExcel", - IconColorHex = "#16A34A", - BackgroundAccentHex = "#F0FDF4", - SupportsMultiFile = true, - AcceptedFileExtensions = ".xlsx,.xls" - }, - // 10. EDIT PDF - new PdfToolDefinition - { - Id = PdfToolId.EditPdf, - Name = "Edit PDF", - Description = "Add text, images, shapes or freehand annotations to a PDF document. Edit size, font and color.", - Category = PdfToolCategory.EditAndForms, - IconKind = "Draw", - IconColorHex = "#9333EA", - BackgroundAccentHex = "#FAF5FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf,.frypdf,.json" - }, - // 11. PDF TO JPG - new PdfToolDefinition - { - Id = PdfToolId.PdfToJpg, - Name = "PDF to JPG", - Description = "Convert each PDF page into a JPG or extract all images contained in a PDF.", - Category = PdfToolCategory.ConvertFromPdf, - IconKind = "FileImageOutline", - IconColorHex = "#CA8A04", - BackgroundAccentHex = "#FEFCE8", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 12. JPG TO PDF - new PdfToolDefinition - { - Id = PdfToolId.JpgToPdf, - Name = "JPG to PDF", - Description = "Convert JPG images to PDF in seconds. Easily adjust orientation and margins.", - Category = PdfToolCategory.ConvertToPdf, - IconKind = "ImageOutline", - IconColorHex = "#CA8A04", - BackgroundAccentHex = "#FEFCE8", - SupportsMultiFile = true, - AcceptedFileExtensions = ".jpg,.jpeg,.png,.bmp,.webp" - }, - // 13. SIGN PDF - new PdfToolDefinition - { - Id = PdfToolId.SignPdf, - Name = "Sign PDF", - Description = "Sign yourself with ink, typed cursive, image stamp, or digital certificate signatures.", - Category = PdfToolCategory.OptimizeAndSecurity, - IconKind = "DrawPen", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf" - }, - // 14. WATERMARK - new PdfToolDefinition - { - Id = PdfToolId.Watermark, - Name = "Watermark", - Description = "Stamp an image or text over your PDF in seconds. Choose typography, transparency and position.", - Category = PdfToolCategory.EditAndForms, - IconKind = "Watermark", - IconColorHex = "#9333EA", - BackgroundAccentHex = "#FAF5FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 15. ROTATE PDF - new PdfToolDefinition - { - Id = PdfToolId.RotatePdf, - Name = "Rotate PDF", - Description = "Rotate your PDFs the way you need them. You can even rotate multiple PDFs at once!", - Category = PdfToolCategory.OrganizeAndPage, - IconKind = "RotateRight", - IconColorHex = "#9333EA", - BackgroundAccentHex = "#FAF5FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 16. HTML TO PDF - new PdfToolDefinition - { - Id = PdfToolId.HtmlToPdf, - Name = "HTML to PDF", - Description = "Convert webpages or HTML files to PDF. Copy and paste URL or load local HTML.", - Category = PdfToolCategory.ConvertToPdf, - IconKind = "LanguageHtml5", - IconColorHex = "#CA8A04", - BackgroundAccentHex = "#FEFCE8", - SupportsMultiFile = false, - AcceptedFileExtensions = ".html,.htm" - }, - // 17. UNLOCK PDF - new PdfToolDefinition - { - Id = PdfToolId.UnlockPdf, - Name = "Unlock PDF", - Description = "Remove PDF password security, giving you the freedom to use your PDFs as you want.", - Category = PdfToolCategory.OptimizeAndSecurity, - IconKind = "LockOpenOutline", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 18. PROTECT PDF - new PdfToolDefinition - { - Id = PdfToolId.ProtectPdf, - Name = "Protect PDF", - Description = "Protect PDF files with a password. Encrypt PDF documents to prevent unauthorized access.", - Category = PdfToolCategory.OptimizeAndSecurity, - IconKind = "ShieldCheckOutline", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 19. ORGANIZE PDF - new PdfToolDefinition - { - Id = PdfToolId.OrganizePdf, - Name = "Organize PDF", - Description = "Sort pages of your PDF file however you like. Delete PDF pages or add PDF pages at convenience.", - Category = PdfToolCategory.OrganizeAndPage, - IconKind = "SortAlphabeticalVariant", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf" - }, - // 20. PDF TO PDF/A - new PdfToolDefinition - { - Id = PdfToolId.PdfToPdfA, - Name = "PDF to PDF/A", - Description = "Transform your PDF to PDF/A, the ISO-standardized version of PDF for long-term archiving.", - Category = PdfToolCategory.OptimizeAndSecurity, - IconKind = "FileCheckOutline", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 21. REPAIR PDF - new PdfToolDefinition - { - Id = PdfToolId.RepairPdf, - Name = "Repair PDF", - Description = "Repair a damaged PDF and recover data from corrupt PDF. Fix PDF files with our repair tool.", - Category = PdfToolCategory.OptimizeAndSecurity, - IconKind = "WrenchOutline", - IconColorHex = "#65A30D", - BackgroundAccentHex = "#F7FEE7", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 22. PAGE NUMBERS - new PdfToolDefinition - { - Id = PdfToolId.PageNumbers, - Name = "Page numbers", - Description = "Add page numbers into PDFs with ease. Choose your positions, dimensions, typography.", - Category = PdfToolCategory.OrganizeAndPage, - IconKind = "Numeric1BoxMultipleOutline", - IconColorHex = "#9333EA", - BackgroundAccentHex = "#FAF5FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 23. SCAN TO PDF - new PdfToolDefinition - { - Id = PdfToolId.ScanToPdf, - Name = "Scan to PDF", - Description = "Capture document scans from your mobile device or scanner and enhance them cleanly.", - Category = PdfToolCategory.ConvertToPdf, - IconKind = "Scanner", - IconColorHex = "#EA580C", - BackgroundAccentHex = "#FFF7ED", - SupportsMultiFile = true, - AcceptedFileExtensions = ".jpg,.jpeg,.png,.tiff,.bmp" - }, - // 24. OCR PDF - new PdfToolDefinition - { - Id = PdfToolId.OcrPdf, - Name = "OCR PDF", - Description = "Easily convert scanned PDF into searchable and selectable documents.", - Category = PdfToolCategory.AiAndAutomation, - IconKind = "TextRecognition", - IconColorHex = "#65A30D", - BackgroundAccentHex = "#F7FEE7", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 25. COMPARE PDF - new PdfToolDefinition - { - Id = PdfToolId.ComparePdf, - Name = "Compare PDF", - Description = "Show a side-by-side document comparison and easily spot changes between different file versions.", - Category = PdfToolCategory.EditAndForms, - IconKind = "BookOpenPageVariantOutline", - IconColorHex = "#2563EB", - BackgroundAccentHex = "#EFF6FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 26. REDACT PDF - new PdfToolDefinition - { - Id = PdfToolId.RedactPdf, - Name = "Redact PDF", - Description = "Redact text and graphics to permanently remove sensitive information from a PDF.", - Category = PdfToolCategory.OptimizeAndSecurity, - IconKind = "SelectRemove", - IconColorHex = "#0284C7", - BackgroundAccentHex = "#F0F9FF", - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf" - }, - // 27. CROP PDF - new PdfToolDefinition - { - Id = PdfToolId.CropPdf, - Name = "Crop PDF", - Description = "Crop margins of PDF documents or select specific areas, then apply changes to one or all pages.", - Category = PdfToolCategory.OrganizeAndPage, - IconKind = "Crop", - IconColorHex = "#9333EA", - BackgroundAccentHex = "#FAF5FF", - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 28. PDF FORMS (New!) - new PdfToolDefinition - { - Id = PdfToolId.PdfForms, - Name = "PDF Forms", - Description = "Detect form fields automatically, create interactive fillable PDFs, or fill PDF forms yourself.", - Category = PdfToolCategory.EditAndForms, - IconKind = "TextBoxOutline", - IconColorHex = "#9333EA", - BackgroundAccentHex = "#FAF5FF", - IsNew = true, - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf" - }, - // 29. AI SUMMARIZER (New!) - new PdfToolDefinition - { - Id = PdfToolId.AiSummarizer, - Name = "AI Summarizer", - Description = "Quickly generate concise summaries from articles, paragraphs, and essays with key points in seconds.", - Category = PdfToolCategory.AiAndAutomation, - IconKind = "FormatListBulletedSquare", - IconColorHex = "#6366F1", - BackgroundAccentHex = "#EEF2FF", - IsNew = true, - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf" - }, - // 30. TRANSLATE PDF (New!) - new PdfToolDefinition - { - Id = PdfToolId.TranslatePdf, - Name = "Translate PDF", - Description = "Easily translate PDF files powered by AI. Keep fonts, layout, and formatting perfectly intact.", - Category = PdfToolCategory.AiAndAutomation, - IconKind = "Translate", - IconColorHex = "#6366F1", - BackgroundAccentHex = "#EEF2FF", - IsNew = true, - SupportsMultiFile = false, - AcceptedFileExtensions = ".pdf" - }, - // 31. PDF TO MARKDOWN (New!) - new PdfToolDefinition - { - Id = PdfToolId.PdfToMarkdown, - Name = "PDF to Markdown", - Description = "Easily turn PDFs into Markdown files for notes, docs, and LLMs. Headings, tables, and lists preserved.", - Category = PdfToolCategory.ConvertFromPdf, - IconKind = "CodeTags", - IconColorHex = "#6366F1", - BackgroundAccentHex = "#EEF2FF", - IsNew = true, - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf" - }, - // 32. CREATE A WORKFLOW (New Callout Banner!) - new PdfToolDefinition - { - Id = PdfToolId.WorkflowBuilder, - Name = "Create a workflow", - Description = "Create custom workflows with your favorite tools, automate tasks, and reuse them anytime.", - Category = PdfToolCategory.AiAndAutomation, - IconKind = "TuneVariant", - IconColorHex = "#E11D48", - BackgroundAccentHex = "#FFE4E6", - IsNew = true, - IsWorkflowBanner = true, - SupportsMultiFile = true, - AcceptedFileExtensions = ".pdf,.fryflow,.json" - }, - // 33. BATCH MAIL MERGE & MASS PDF GENERATOR (New!) - new PdfToolDefinition - { - Id = PdfToolId.BatchMailMerge, - Name = "Batch Mail Merge & Mass PDF", - Description = "Generate hundreds of personalized PDFs (payslips, certificates, invoices, badges) in one click using Excel, CSV, or REST APIs.", - Category = PdfToolCategory.AiAndAutomation, - IconKind = "DatabaseArrowDownOutline", - IconColorHex = "#0F6CBD", - BackgroundAccentHex = "#EFF6FF", - IsNew = true, - SupportsMultiFile = false, - AcceptedFileExtensions = ".xlsx,.csv,.tsv,.json" - } - }; - - public IReadOnlyList GetAllTools() - { - if (_pluginContext == null) return _tools; - - var pluginTools = _pluginContext.GetRegisteredTools(); - if (pluginTools.Count == 0 && _seedDefaults) return _tools; - - var merged = _seedDefaults ? new List(_tools) : new List(); - foreach (var pt in pluginTools) - { - var existingIndex = merged.FindIndex(t => - (!string.IsNullOrEmpty(t.StringId) && string.Equals(t.StringId, pt.Id, StringComparison.OrdinalIgnoreCase)) || - (pt.LegacyId.HasValue && t.Id == (PdfToolId)pt.LegacyId.Value)); - - var toolDef = new PdfToolDefinition - { - Id = pt.LegacyId.HasValue && Enum.IsDefined(typeof(PdfToolId), pt.LegacyId.Value) - ? (PdfToolId)pt.LegacyId.Value - : PdfToolId.EditPdf, - StringId = pt.Id, - Name = pt.Name, - Description = pt.Description, - Category = ParseCategory(pt.Category), - IconKind = pt.IconKind, - IconColorHex = pt.IconColorHex, - BackgroundAccentHex = pt.BackgroundAccentHex, - SupportsMultiFile = pt.SupportsMultiFile, - AcceptedFileExtensions = pt.AcceptedFileExtensions, - ViewModelFactory = pt.CreateViewModel - }; - - if (existingIndex >= 0) - { - merged[existingIndex] = toolDef; - } - else - { - merged.Add(toolDef); - } - } - return merged; - } - - public IReadOnlyList GetToolsByCategory(PdfToolCategory category) - { - var all = GetAllTools(); - if (category == PdfToolCategory.All) return all; - return all.Where(t => t.Category == category).ToList(); - } - - public PdfToolDefinition? GetTool(PdfToolId id) => GetAllTools().FirstOrDefault(t => t.Id == id); - - public PdfToolDefinition? GetTool(string stringId) - { - if (string.IsNullOrWhiteSpace(stringId)) return null; - return GetAllTools().FirstOrDefault(t => - string.Equals(t.StringId, stringId, StringComparison.OrdinalIgnoreCase) || - string.Equals(t.Id.ToString(), stringId, StringComparison.OrdinalIgnoreCase)); - } - - public void NotifyRegistryChanged() => RegistryChanged?.Invoke(); - - public void RegisterTool(PdfToolDefinition tool) - { - ArgumentNullException.ThrowIfNull(tool); - _tools.Add(tool); - NotifyRegistryChanged(); - } - - private static PdfToolCategory ParseCategory(string category) - { - if (Enum.TryParse(category, true, out var cat)) - return cat; - return PdfToolCategory.OrganizeAndPage; - } -} diff --git a/src/PdfEditorApp/Services/Tools/Core/PdfToolViewModelFactory.cs b/src/PdfEditorApp/Services/Tools/Core/PdfToolViewModelFactory.cs deleted file mode 100644 index 352e7c8..0000000 --- a/src/PdfEditorApp/Services/Tools/Core/PdfToolViewModelFactory.cs +++ /dev/null @@ -1,156 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using System; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; - -using Microsoft.Extensions.DependencyInjection; - -namespace PdfEditorApp.Services.Tools.Core; - -/// -/// Default implementation of IPdfToolViewModelFactory. -/// Instantiates typed, dedicated ViewModels for each PDF tool, taking advantage of Microsoft.Extensions.DependencyInjection when available. -/// -public class PdfToolViewModelFactory : IPdfToolViewModelFactory -{ - private readonly IPdfDocumentOperationsService _operationsService; - private readonly IPdfToolRegistry _toolRegistry; - private readonly IServiceProvider? _serviceProvider; - - public PdfToolViewModelFactory( - IPdfDocumentOperationsService operationsService, - IPdfToolRegistry toolRegistry, - IServiceProvider? serviceProvider = null) - { - _operationsService = operationsService; - _toolRegistry = toolRegistry; - _serviceProvider = serviceProvider; - } - - public PdfToolViewModelBase Create(PdfToolId toolId) => CreateToolViewModel(toolId); - - public PdfToolViewModelBase CreateToolViewModel(PdfToolId toolId) - { - var toolDef = _toolRegistry.GetTool(toolId) ?? new PdfToolDefinition - { - Id = toolId, - Name = toolId.ToString() - }; - - return CreateToolViewModel(toolDef); - } - - public PdfToolViewModelBase CreateToolViewModel(PdfToolDefinition toolDef) - { - if (toolDef.ViewModelFactory != null && _serviceProvider != null) - { - try - { - if (toolDef.ViewModelFactory(_serviceProvider) is PdfToolViewModelBase vmFromFactory) - { - return vmFromFactory; - } - } - catch - { - // Fall back to built-in factory logic - } - } - - if (_serviceProvider != null) - { - try - { - Type? vmType = toolDef.Id switch - { - PdfToolId.MergePdf => typeof(MergePdfToolViewModel), - PdfToolId.SplitPdf => typeof(SplitPdfToolViewModel), - PdfToolId.CompressPdf => typeof(CompressPdfToolViewModel), - PdfToolId.PdfToWord => typeof(PdfToWordToolViewModel), - PdfToolId.PdfToPowerPoint => typeof(PdfToPowerPointToolViewModel), - PdfToolId.PdfToExcel => typeof(PdfToExcelToolViewModel), - PdfToolId.WordToPdf => typeof(WordToPdfToolViewModel), - PdfToolId.PowerPointToPdf => typeof(PowerPointToPdfToolViewModel), - PdfToolId.ExcelToPdf => typeof(ExcelToPdfToolViewModel), - PdfToolId.EditPdf => typeof(EditPdfToolViewModel), - PdfToolId.PdfToJpg => typeof(PdfToJpgToolViewModel), - PdfToolId.JpgToPdf => typeof(JpgToPdfToolViewModel), - PdfToolId.SignPdf => typeof(SignPdfToolViewModel), - PdfToolId.Watermark => typeof(WatermarkToolViewModel), - PdfToolId.RotatePdf => typeof(RotatePdfToolViewModel), - PdfToolId.HtmlToPdf => typeof(HtmlToPdfToolViewModel), - PdfToolId.UnlockPdf => typeof(UnlockPdfToolViewModel), - PdfToolId.ProtectPdf => typeof(ProtectPdfToolViewModel), - PdfToolId.OrganizePdf => typeof(OrganizePdfToolViewModel), - PdfToolId.PdfToPdfA => typeof(PdfToPdfAToolViewModel), - PdfToolId.RepairPdf => typeof(RepairPdfToolViewModel), - PdfToolId.PageNumbers => typeof(PageNumbersToolViewModel), - PdfToolId.ScanToPdf => typeof(ScanToPdfToolViewModel), - PdfToolId.OcrPdf => typeof(OcrPdfToolViewModel), - PdfToolId.ComparePdf => typeof(ComparePdfToolViewModel), - PdfToolId.RedactPdf => typeof(RedactPdfToolViewModel), - PdfToolId.CropPdf => typeof(CropPdfToolViewModel), - PdfToolId.PdfForms => typeof(PdfFormsToolViewModel), - PdfToolId.AiSummarizer => typeof(AiSummarizerToolViewModel), - PdfToolId.TranslatePdf => typeof(TranslatePdfToolViewModel), - PdfToolId.PdfToMarkdown => typeof(PdfToMarkdownToolViewModel), - _ => typeof(EditPdfToolViewModel) - }; - - if (ActivatorUtilities.CreateInstance(_serviceProvider, vmType, toolDef) is PdfToolViewModelBase vm) - { - return vm; - } - } - catch - { - // Fall back to direct instantiation - } - } - - return toolDef.Id switch - { - PdfToolId.MergePdf => new MergePdfToolViewModel(_operationsService, toolDef), - PdfToolId.SplitPdf => new SplitPdfToolViewModel(_operationsService, toolDef), - PdfToolId.CompressPdf => new CompressPdfToolViewModel(_operationsService, toolDef), - PdfToolId.PdfToWord => new PdfToWordToolViewModel(_operationsService, toolDef), - PdfToolId.PdfToPowerPoint => new PdfToPowerPointToolViewModel(_operationsService, toolDef), - PdfToolId.PdfToExcel => new PdfToExcelToolViewModel(_operationsService, toolDef), - PdfToolId.WordToPdf => new WordToPdfToolViewModel(_operationsService, toolDef), - PdfToolId.PowerPointToPdf => new PowerPointToPdfToolViewModel(_operationsService, toolDef), - PdfToolId.ExcelToPdf => new ExcelToPdfToolViewModel(_operationsService, toolDef), - PdfToolId.EditPdf => new EditPdfToolViewModel(_operationsService, toolDef), - PdfToolId.PdfToJpg => new PdfToJpgToolViewModel(_operationsService, toolDef), - PdfToolId.JpgToPdf => new JpgToPdfToolViewModel(_operationsService, toolDef), - PdfToolId.SignPdf => new SignPdfToolViewModel(_operationsService, toolDef), - PdfToolId.Watermark => new WatermarkToolViewModel(_operationsService, toolDef), - PdfToolId.RotatePdf => new RotatePdfToolViewModel(_operationsService, toolDef), - PdfToolId.HtmlToPdf => new HtmlToPdfToolViewModel(_operationsService, toolDef), - PdfToolId.UnlockPdf => new UnlockPdfToolViewModel(_operationsService, toolDef), - PdfToolId.ProtectPdf => new ProtectPdfToolViewModel(_operationsService, toolDef), - PdfToolId.OrganizePdf => new OrganizePdfToolViewModel(_operationsService, toolDef), - PdfToolId.PdfToPdfA => new PdfToPdfAToolViewModel(_operationsService, toolDef), - PdfToolId.RepairPdf => new RepairPdfToolViewModel(_operationsService, toolDef), - PdfToolId.PageNumbers => new PageNumbersToolViewModel(_operationsService, toolDef), - PdfToolId.ScanToPdf => new ScanToPdfToolViewModel(_operationsService, toolDef), - PdfToolId.OcrPdf => new OcrPdfToolViewModel(_operationsService, toolDef), - PdfToolId.ComparePdf => new ComparePdfToolViewModel(_operationsService, toolDef), - PdfToolId.RedactPdf => new RedactPdfToolViewModel(_operationsService, toolDef), - PdfToolId.CropPdf => new CropPdfToolViewModel(_operationsService, toolDef), - PdfToolId.PdfForms => new PdfFormsToolViewModel(_operationsService, toolDef), - PdfToolId.AiSummarizer => new AiSummarizerToolViewModel(_operationsService, toolDef), - PdfToolId.TranslatePdf => new TranslatePdfToolViewModel(_operationsService, toolDef), - PdfToolId.PdfToMarkdown => new PdfToMarkdownToolViewModel(_operationsService, toolDef), - _ => new EditPdfToolViewModel(_operationsService, toolDef) - }; - } -} diff --git a/src/PdfEditorApp/Services/Tools/Core/PdfWorkflowEngine.cs b/src/PdfEditorApp/Services/Tools/Core/PdfWorkflowEngine.cs deleted file mode 100644 index c570eae..0000000 --- a/src/PdfEditorApp/Services/Tools/Core/PdfWorkflowEngine.cs +++ /dev/null @@ -1,281 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services.Tools.Core; - -public interface IPdfWorkflowEngine -{ - Task ExecuteWorkflowAsync(WorkflowDefinition workflow, IEnumerable inputFiles, IProgress? progress = null, CancellationToken ct = default); - Task SaveWorkflowToFileAsync(WorkflowDefinition workflow, string filePath); - Task LoadWorkflowFromFileAsync(string filePath); - string SerializeWorkflow(WorkflowDefinition workflow); - WorkflowDefinition? DeserializeWorkflow(string json); - IReadOnlyList GetPresetWorkflows(); -} - -public class PdfWorkflowEngine : IPdfWorkflowEngine -{ - private readonly IPdfPageService _pageService; - private readonly IPdfOptimizationService _optService; - private readonly IPdfSecurityService _secService; - private readonly IPdfConversionService _convService; - private readonly IPdfOcrService _ocrService; - - public PdfWorkflowEngine() - : this(new PdfPageService(), new PdfOptimizationService(), new PdfSecurityService(), new PdfConversionService(), new PdfOcrService()) - { - } - - public PdfWorkflowEngine( - IPdfPageService pageService, - IPdfOptimizationService optService, - IPdfSecurityService secService, - IPdfConversionService convService, - IPdfOcrService ocrService) - { - _pageService = pageService; - _optService = optService; - _secService = secService; - _convService = convService; - _ocrService = ocrService; - } - - public async Task ExecuteWorkflowAsync( - WorkflowDefinition workflow, - IEnumerable inputFiles, - IProgress? progress = null, - CancellationToken ct = default) - { - return await Task.Run(async () => - { - var files = inputFiles.Where(File.Exists).ToList(); - if (files.Count == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "No valid input files provided for workflow execution." }; - - var enabledSteps = workflow.Steps.Where(s => s.IsEnabled).ToList(); - if (enabledSteps.Count == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "Workflow has no enabled steps." }; - - string outDir = string.IsNullOrWhiteSpace(workflow.OutputDirectory) - ? Path.Combine(Path.GetDirectoryName(files[0]) ?? "", "Workflow_Output") - : workflow.OutputDirectory; - - if (!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); - - var finalOutputs = new List(); - string tempDir = Path.Combine(Path.GetTempPath(), "FryPdf_Workflow_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(tempDir); - - try - { - int totalSteps = enabledSteps.Count * files.Count; - int completedSteps = 0; - - foreach (var file in files) - { - ct.ThrowIfCancellationRequested(); - string currentFile = file; - - for (int s = 0; s < enabledSteps.Count; s++) - { - ct.ThrowIfCancellationRequested(); - var step = enabledSteps[s]; - - progress?.Report(new WorkflowProgressInfo - { - CurrentStepIndex = s + 1, - TotalSteps = enabledSteps.Count, - CurrentStepName = step.StepName, - CurrentFileName = Path.GetFileName(file), - OverallProgressPercentage = (completedSteps / (double)totalSteps) * 100.0, - StatusMessage = $"Running '{step.StepName}' on {Path.GetFileName(file)}..." - }); - - string stepOut = Path.Combine(tempDir, $"step_{s}_{Guid.NewGuid():N}_{Path.GetFileName(currentFile)}"); - - // Execute Step - var stepResult = await ExecuteSingleStepAsync(step, currentFile, stepOut, ct); - if (!stepResult.Success) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Workflow failed at step '{step.StepName}': {stepResult.ErrorMessage}" - }; - } - - currentFile = stepResult.OutputFilePath ?? stepOut; - completedSteps++; - } - - // Move final output to destination folder - string finalFileName = workflow.OutputFileNamePattern - .Replace("{filename}", Path.GetFileNameWithoutExtension(file)) - .Replace("{date}", DateTime.UtcNow.ToString("yyyyMMdd")); - - if (!finalFileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) - finalFileName += ".pdf"; - - string destPath = Path.Combine(outDir, finalFileName); - if (!string.Equals(Path.GetFullPath(currentFile), Path.GetFullPath(destPath), StringComparison.OrdinalIgnoreCase)) - { - if (File.Exists(destPath)) File.Delete(destPath); - File.Copy(currentFile, destPath, true); - } - finalOutputs.Add(destPath); - } - - progress?.Report(new WorkflowProgressInfo - { - CurrentStepIndex = enabledSteps.Count, - TotalSteps = enabledSteps.Count, - CurrentStepName = "Complete", - OverallProgressPercentage = 100.0, - StatusMessage = $"Workflow '{workflow.Name}' executed successfully on {files.Count} files." - }); - - return new ToolExecutionResult - { - Success = true, - OutputFilePath = finalOutputs.FirstOrDefault(), - OutputFiles = finalOutputs, - Message = $"Executed workflow '{workflow.Name}' on {files.Count} files across {enabledSteps.Count} automated steps." - }; - } - finally - { - try - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); - } - catch { } - } - }, ct); - } - - private async Task ExecuteSingleStepAsync(WorkflowStepDefinition step, string inputFile, string outputFile, CancellationToken ct) - { - switch (step.ToolId) - { - case PdfToolId.CompressPdf: - return await _optService.CompressPdfAsync(new CompressToolOptions - { - InputFilePath = inputFile, - OutputFilePath = outputFile, - Level = PdfCompressionLevel.Balanced - }, null, ct); - - case PdfToolId.OcrPdf: - return await _ocrService.OcrPdfAsync(new OcrToolOptions - { - InputFilePath = inputFile, - OutputFilePath = outputFile - }, null, ct); - - case PdfToolId.PageNumbers: - return await _pageService.AddPageNumbersAsync(new PageNumberToolOptions - { - InputFilePath = inputFile, - OutputFilePath = outputFile - }, null, ct); - - case PdfToolId.ProtectPdf: - return await _secService.ProtectPdfAsync(new SecurityToolOptions - { - InputFilePath = inputFile, - OutputFilePath = outputFile, - AllowPrinting = true - }, null, ct); - - case PdfToolId.PdfToPdfA: - return await _optService.ConvertToPdfAAsync(new PdfAToolOptions - { - InputFilePath = inputFile, - OutputFilePath = outputFile - }, null, ct); - - case PdfToolId.RepairPdf: - return await _optService.RepairPdfAsync(new RepairToolOptions - { - InputFilePath = inputFile, - OutputFilePath = outputFile - }, null, ct); - - default: - // Pass-through copy if no specialized execution needed - if (!string.Equals(Path.GetFullPath(inputFile), Path.GetFullPath(outputFile), StringComparison.OrdinalIgnoreCase)) - { - File.Copy(inputFile, outputFile, true); - } - return new ToolExecutionResult { Success = true, OutputFilePath = outputFile }; - } - } - - public string SerializeWorkflow(WorkflowDefinition workflow) - { - workflow.ModifiedAt = DateTime.UtcNow; - return JsonSerializer.Serialize(workflow, new JsonSerializerOptions { WriteIndented = true }); - } - - public WorkflowDefinition? DeserializeWorkflow(string json) - { - if (string.IsNullOrWhiteSpace(json)) return null; - return JsonSerializer.Deserialize(json); - } - - public async Task SaveWorkflowToFileAsync(WorkflowDefinition workflow, string filePath) - { - string json = SerializeWorkflow(workflow); - await File.WriteAllTextAsync(filePath, json); - } - - public async Task LoadWorkflowFromFileAsync(string filePath) - { - if (!File.Exists(filePath)) return null; - string json = await File.ReadAllTextAsync(filePath); - return DeserializeWorkflow(json); - } - - public IReadOnlyList GetPresetWorkflows() - { - return new List - { - new WorkflowDefinition - { - Id = "preset_publish", - Name = "Publishing & Archival Pipeline", - Description = "Run OCR, optimize compression, stamp page numbers, and convert to PDF/A for long-term storage.", - Steps = new List - { - new WorkflowStepDefinition { Id = "s1", ToolId = PdfToolId.OcrPdf, StepName = "OCR Text Recognition", StepDescription = "Make scanned text searchable" }, - new WorkflowStepDefinition { Id = "s2", ToolId = PdfToolId.CompressPdf, StepName = "Balanced Compression", StepDescription = "Optimize document file size" }, - new WorkflowStepDefinition { Id = "s3", ToolId = PdfToolId.PageNumbers, StepName = "Add Header & Page Numbers", StepDescription = "Stamp 'Page {n} of {total}'" }, - new WorkflowStepDefinition { Id = "s4", ToolId = PdfToolId.PdfToPdfA, StepName = "Convert to PDF/A", StepDescription = "Enforce ISO 19005 archival compliance" } - } - }, - new WorkflowDefinition - { - Id = "preset_secure", - Name = "Enterprise Security & Protection", - Description = "Sanitize document, apply corporate watermarking, and protect with permission encryption.", - Steps = new List - { - new WorkflowStepDefinition { Id = "s1", ToolId = PdfToolId.RepairPdf, StepName = "Audit & Repair", StepDescription = "Fix damaged object structures" }, - new WorkflowStepDefinition { Id = "s2", ToolId = PdfToolId.CompressPdf, StepName = "Compress Resources", StepDescription = "Strip unnecessary metadata" }, - new WorkflowStepDefinition { Id = "s3", ToolId = PdfToolId.ProtectPdf, StepName = "Encrypt & Protect", StepDescription = "Apply password encryption and printing restrictions" } - } - } - }; - } -} diff --git a/src/PdfEditorApp/Services/Tools/Intelligence/AiDocumentService.cs b/src/PdfEditorApp/Services/Tools/Intelligence/AiDocumentService.cs deleted file mode 100644 index cc2bd8d..0000000 --- a/src/PdfEditorApp/Services/Tools/Intelligence/AiDocumentService.cs +++ /dev/null @@ -1,210 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using UglyToad.PdfPig; - -namespace PdfEditorApp.Services.Tools.Intelligence; - -public interface IAiDocumentService -{ - Task SummarizePdfAsync(AiSummaryOptions options, IProgress? progress = null, CancellationToken ct = default); -} - -public class AiDocumentService : IAiDocumentService -{ - public async Task SummarizePdfAsync(AiSummaryOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(15.0); - - ct.ThrowIfCancellationRequested(); - var allText = new StringBuilder(); - - using (var pdf = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath)) - { - int totalPages = pdf.NumberOfPages; - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - var page = pdf.GetPage(p); - allText.AppendLine(page.Text); - progress?.Report(15.0 + (p / (double)totalPages * 40.0)); - } - } - - string fullDocText = allText.ToString(); - if (string.IsNullOrWhiteSpace(fullDocText)) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = "Unable to extract readable text for summarization. The document may be scanned or image-based." - }; - } - - progress?.Report(65.0); - ct.ThrowIfCancellationRequested(); - - // Run Extractive NLP & Statistical Summarizer - var summary = GenerateExtractiveSummary(fullDocText, options.MaxBulletPoints, options.IncludeExecutiveSummary, options.IncludeActionItems); - progress?.Report(95.0); - - string outDir = Path.GetDirectoryName(options.InputFilePath) ?? Path.GetTempPath(); - string outPath = Path.Combine(outDir, $"{Path.GetFileNameWithoutExtension(options.InputFilePath)}_Summary.md"); - File.WriteAllText(outPath, summary, Encoding.UTF8); - - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - ExtraData = new Dictionary { ["SummaryText"] = summary }, - Message = $"Generated executive summary and key points to {Path.GetFileName(outPath)}" - }; - }, ct); - } - - private static string GenerateExtractiveSummary(string text, int maxBullets, bool includeExec, bool includeActions) - { - var sentences = SplitIntoSentences(text); - if (sentences.Count == 0) return "No content available to summarize."; - - // Word frequency map (excluding common stop words) - var stopWords = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", - "by", "from", "as", "is", "was", "are", "were", "be", "been", "have", "has", "had", - "this", "that", "it", "they", "we", "you", "which", "will", "would", "can", "could", - "should", "not", "also", "more", "their", "all", "its", "into" - }; - - var wordFreq = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var s in sentences) - { - var words = Regex.Matches(s, @"\b[A-Za-z]{3,}\b"); - foreach (Match m in words) - { - string w = m.Value; - if (!stopWords.Contains(w)) - { - wordFreq[w] = wordFreq.GetValueOrDefault(w, 0) + 1; - } - } - } - - // Score sentences based on word frequencies and position - var scored = new List<(string Sentence, double Score, int Index)>(); - for (int i = 0; i < sentences.Count; i++) - { - string s = sentences[i]; - double score = 0; - var words = Regex.Matches(s, @"\b[A-Za-z]{3,}\b"); - foreach (Match m in words) - { - if (wordFreq.TryGetValue(m.Value, out int count)) - { - score += count; - } - } - - // Normalization by sentence length (penalize extremely short or long sentences) - if (words.Count > 0) - { - score /= Math.Sqrt(words.Count); - } - - // Boost early sentences (introductions/abstracts) - if (i < 5) score *= 1.3; - - scored.Add((s, score, i)); - } - - var topSentences = scored.OrderByDescending(x => x.Score) - .Take(maxBullets) - .OrderBy(x => x.Index) - .Select(x => x.Sentence) - .ToList(); - - var sb = new StringBuilder(); - sb.AppendLine("# AI Executive Document Summary"); - sb.AppendLine($"*Generated by FryPDF Intelligence Engine · {DateTime.UtcNow:yyyy-MM-dd}*"); - sb.AppendLine(); - - if (includeExec && topSentences.Count > 0) - { - sb.AppendLine("## Executive Overview"); - sb.AppendLine(string.Join(" ", topSentences.Take(2))); - sb.AppendLine(); - } - - sb.AppendLine("## Key Takeaways & Core Findings"); - foreach (var s in topSentences) - { - sb.AppendLine($"- **Key Point**: {s.Trim()}"); - } - sb.AppendLine(); - - if (includeActions) - { - var actionKeywords = new[] { "must", "should", "recommend", "require", "next steps", "action", "deadline", "implement", "review", "ensure" }; - var actionSentences = sentences.Where(s => actionKeywords.Any(k => s.Contains(k, StringComparison.OrdinalIgnoreCase))) - .Take(4) - .ToList(); - - if (actionSentences.Count > 0) - { - sb.AppendLine("## Strategic Action Items"); - foreach (var a in actionSentences) - { - sb.AppendLine($"1. {a.Trim()}"); - } - sb.AppendLine(); - } - } - - return sb.ToString(); - } - - private static List SplitIntoSentences(string text) - { - var list = new List(); - var matches = Regex.Matches(text, @"(?<=[\.\!\?])\s+(?=[A-Z])"); - int lastIndex = 0; - - foreach (Match m in matches) - { - int length = m.Index - lastIndex; - string s = text.Substring(lastIndex, length).Trim(); - if (s.Length > 20 && s.Length < 350) - { - list.Add(s); - } - lastIndex = m.Index + m.Length; - } - - if (lastIndex < text.Length) - { - string s = text.Substring(lastIndex).Trim(); - if (s.Length > 20 && s.Length < 350) list.Add(s); - } - - return list; - } -} diff --git a/src/PdfEditorApp/Services/Tools/Intelligence/DocumentTranslationService.cs b/src/PdfEditorApp/Services/Tools/Intelligence/DocumentTranslationService.cs deleted file mode 100644 index 76e922d..0000000 --- a/src/PdfEditorApp/Services/Tools/Intelligence/DocumentTranslationService.cs +++ /dev/null @@ -1,225 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using UglyToad.PdfPig; - -namespace PdfEditorApp.Services.Tools.Intelligence; - -public interface IDocumentTranslationService -{ - Task TranslatePdfAsync(TranslationOptions options, IProgress? progress = null, CancellationToken ct = default); -} - -public class DocumentTranslationService : IDocumentTranslationService -{ - private static readonly HashSet SupportedGlossaryLanguages = new(StringComparer.OrdinalIgnoreCase) - { - "spanish", "es", "french", "fr", "german", "de" - }; - - public async Task TranslatePdfAsync(TranslationOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - // This service only swaps a small fixed glossary of business terms — it is not - // a real machine-translation backend. Fail clearly for anything outside that - // glossary's 3 languages instead of silently returning the document unchanged. - if (!SupportedGlossaryLanguages.Contains(options.TargetLanguage.Trim())) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"'{options.TargetLanguage}' is not supported yet. This tool currently only swaps a small glossary of common business terms for Spanish, French, or German — it does not perform full machine translation." - }; - } - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - string langCode = options.TargetLanguage.Substring(0, Math.Min(3, options.TargetLanguage.Length)).ToUpperInvariant(); - outPath = Path.Combine(dir, $"{name}_{langCode}.pdf"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(15.0); - - var pagesData = new List>(); - - using (var pdf = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath)) - { - int totalPages = pdf.NumberOfPages; - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - var page = pdf.GetPage(p); - var lines = page.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries) - .Select(l => l.Trim()) - .Where(l => !string.IsNullOrWhiteSpace(l)) - .ToList(); - - var translatedLines = new List(); - foreach (var line in lines) - { - string translated = TranslateText(line, options.TargetLanguage); - translatedLines.Add(translated); - } - - pagesData.Add(translatedLines); - progress?.Report(15.0 + (p / (double)totalPages * 50.0)); - } - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(70.0); - - // Reconstruct PDF layout in target language via QuestPDF with FryPDF metadata - var doc = FryPdfDocument.Create(container => - { - int pNum = 1; - foreach (var pageLines in pagesData) - { - container.Page(page => - { - page.Size(PageSizes.A4); - page.Margin(36); - page.PageColor(Colors.White); - page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Helvetica").FontColor(Colors.Grey.Darken3)); - - page.Header().Row(row => - { - row.RelativeItem().Text($"{Path.GetFileNameWithoutExtension(options.InputFilePath)} [{options.TargetLanguage}]").SemiBold().FontColor(Colors.Indigo.Darken2); - }); - - page.Content().PaddingVertical(10).Column(col => - { - col.Spacing(8); - foreach (var l in pageLines) - { - col.Item().Text(l); - } - }); - - page.Footer().AlignCenter().Text(x => - { - x.Span($"Page {pNum} of {pagesData.Count}"); - }); - }); - pNum++; - } - }); - - doc.GeneratePdf(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Applied glossary-based term translation ({pagesData.Count} pages) for {options.TargetLanguage}: {Path.GetFileName(outPath)}. " + - "Note: only a small set of common business terms are translated (not full sentences/paragraphs), and the document is reflowed into a new plain layout — original fonts, layout, and formatting are not preserved." - }; - }, ct); - } - - private static string TranslateText(string input, string targetLang) - { - if (string.IsNullOrWhiteSpace(input)) return input; - - // Offline glossary translations for common business / document headings and terms - var spanishDict = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["Annual Report"] = "Informe Anual", - ["Financial Report"] = "Informe Financiero", - ["Executive Summary"] = "Resumen Ejecutivo", - ["Introduction"] = "Introducción", - ["Conclusion"] = "Conclusión", - ["Table of Contents"] = "Índice de Contenidos", - ["Invoice"] = "Factura", - ["Total"] = "Total", - ["Description"] = "Descripción", - ["Amount"] = "Importe", - ["Date"] = "Fecha", - ["Page"] = "Página", - ["Overview"] = "Visión General", - ["Results"] = "Resultados", - ["Recommendations"] = "Recomendaciones" - }; - - var frenchDict = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["Annual Report"] = "Rapport Annuel", - ["Financial Report"] = "Rapport Financier", - ["Executive Summary"] = "Résumé Exécutif", - ["Introduction"] = "Introduction", - ["Conclusion"] = "Conclusion", - ["Table of Contents"] = "Table des Matières", - ["Invoice"] = "Facture", - ["Total"] = "Total", - ["Description"] = "Description", - ["Amount"] = "Montant", - ["Date"] = "Date", - ["Page"] = "Page", - ["Overview"] = "Aperçu", - ["Results"] = "Résultats", - ["Recommendations"] = "Recommandations" - }; - - var germanDict = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["Annual Report"] = "Jahresbericht", - ["Financial Report"] = "Finanzbericht", - ["Executive Summary"] = "Zusammenfassung", - ["Introduction"] = "Einleitung", - ["Conclusion"] = "Fazit", - ["Table of Contents"] = "Inhaltsverzeichnis", - ["Invoice"] = "Rechnung", - ["Total"] = "Gesamt", - ["Description"] = "Beschreibung", - ["Amount"] = "Betrag", - ["Date"] = "Datum", - ["Page"] = "Seite", - ["Overview"] = "Überblick", - ["Results"] = "Ergebnisse", - ["Recommendations"] = "Empfehlungen" - }; - - var dict = targetLang.ToLowerInvariant() switch - { - "spanish" or "es" => spanishDict, - "french" or "fr" => frenchDict, - "german" or "de" => germanDict, - _ => null - }; - - if (dict != null) - { - string output = input; - foreach (var kvp in dict) - { - output = Regex.Replace(output, $@"\b{Regex.Escape(kvp.Key)}\b", kvp.Value, RegexOptions.IgnoreCase); - } - return output; - } - - return input; - } -} diff --git a/src/PdfEditorApp/Services/Tools/Intelligence/PdfFormService.cs b/src/PdfEditorApp/Services/Tools/Intelligence/PdfFormService.cs deleted file mode 100644 index 3b648f9..0000000 --- a/src/PdfEditorApp/Services/Tools/Intelligence/PdfFormService.cs +++ /dev/null @@ -1,127 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.AcroForms; -using PdfSharpCore.Pdf.IO; - -namespace PdfEditorApp.Services.Tools.Intelligence; - -public interface IPdfFormService -{ - Task ProcessPdfFormsAsync(FormToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task> GetFormFieldsAsync(string pdfPath, CancellationToken ct = default); - Task> ExtractFormFieldsAsync(string pdfPath, CancellationToken ct = default); -} - -public class PdfFormService : IPdfFormService -{ - public Task> ExtractFormFieldsAsync(string pdfPath, CancellationToken ct = default) => GetFormFieldsAsync(pdfPath, ct); - - public async Task> GetFormFieldsAsync(string pdfPath, CancellationToken ct = default) - { - return await Task.Run(() => - { - var dict = new Dictionary(); - if (!File.Exists(pdfPath)) return dict; - - try - { - using var doc = PdfReader.Open(pdfPath, PdfDocumentOpenMode.ReadOnly); - var form = doc.AcroForm; - if (form != null) - { - foreach (var name in form.Fields.Names) - { - var field = form.Fields[name]; - dict[name] = field?.Value?.ToString() ?? ""; - } - } - } - catch { } - - return dict; - }, ct); - } - - public async Task ProcessPdfFormsAsync(FormToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_FormFilled.pdf"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(20.0); - - using var doc = PdfReader.Open(options.InputFilePath, PdfDocumentOpenMode.Modify); - var form = doc.AcroForm; - - int updatedFields = 0; - if (form != null && options.FieldValues != null) - { - foreach (var kvp in options.FieldValues) - { - if (form.Fields.Names.Contains(kvp.Key)) - { - var field = form.Fields[kvp.Key]; - if (field is PdfTextField textField) - { - textField.Text = kvp.Value; - updatedFields++; - } - else if (field is PdfCheckBoxField checkBoxField) - { - checkBoxField.Checked = bool.TryParse(kvp.Value, out bool b) && b; - updatedFields++; - } - } - } - - if (options.FlattenFields) - { - // Flatten: remove the AcroForm entry so fields become static content - if (doc.AcroForm != null) - doc.Internals.Catalog.Elements.Remove("/AcroForm"); - } - } - - progress?.Report(80.0); - PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, outPath); - - if (options.ExportFieldValuesJson) - { - string jsonPath = Path.ChangeExtension(outPath, ".json"); - string json = JsonSerializer.Serialize(options.FieldValues, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(jsonPath, json); - } - - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Processed AcroForm with {updatedFields} filled fields." - }; - }, ct); - } -} diff --git a/src/PdfEditorApp/Services/Tools/Intelligence/PdfOcrService.cs b/src/PdfEditorApp/Services/Tools/Intelligence/PdfOcrService.cs deleted file mode 100644 index 8c4827c..0000000 --- a/src/PdfEditorApp/Services/Tools/Intelligence/PdfOcrService.cs +++ /dev/null @@ -1,347 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfSharpCore.Drawing; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.IO; -using UglyToad.PdfPig; - -using System.Text; -using PdfEditorApp.Services.Ocr; - -namespace PdfEditorApp.Services.Tools.Intelligence; - -public interface IPdfOcrService -{ - Task OcrPdfAsync(OcrToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ScanToPdfAsync(ScanToolOptions options, IProgress? progress = null, CancellationToken ct = default); -} - -public class PdfOcrService : IPdfOcrService -{ - private readonly ICompositeOcrProvider _ocrProvider; - - public PdfOcrService(ICompositeOcrProvider? ocrProvider = null) - { - _ocrProvider = ocrProvider ?? CompositeOcrProvider.Default; - } - - public async Task OcrPdfAsync(OcrToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Searchable.pdf"); - } - - ct.ThrowIfCancellationRequested(); - progress?.Report(10.0); - - using var pdfPigDoc = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath); - try { UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.AddSkiaPageFactory(pdfPigDoc); } catch { } - - int totalPages = pdfPigDoc.NumberOfPages; - int recognizedWords = 0; - int pagesWithoutText = 0; - var scannedPageIndices = new List(); - - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - var pigPage = pdfPigDoc.GetPage(p); - var words = pigPage.GetWords().ToList(); - recognizedWords += words.Count; - if (words.Count == 0) - { - pagesWithoutText++; - scannedPageIndices.Add(p); - } - - progress?.Report(10.0 + (p / (double)totalPages * 15.0)); - } - - if (pagesWithoutText == 0) - { - // Document is already fully searchable - if (!string.Equals(Path.GetFullPath(options.InputFilePath), Path.GetFullPath(outPath), StringComparison.OrdinalIgnoreCase)) - { - File.Copy(options.InputFilePath, outPath, overwrite: true); - } - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"All {totalPages} page(s) already contain selectable text ({recognizedWords} words) — this document was already searchable." - }; - } - - // Perform true OCR on scanned pages - string txtOutPath = Path.ChangeExtension(outPath, ".txt"); - var allText = new StringBuilder(); - int ocrWordsAdded = 0; - string lastEngineUsed = _ocrProvider.EngineName; - - for (int p = 1; p <= totalPages; p++) - { - ct.ThrowIfCancellationRequested(); - var pigPage = pdfPigDoc.GetPage(p); - var words = pigPage.GetWords().ToList(); - - if (words.Count > 0) - { - allText.AppendLine($"--- Page {p} ---"); - allText.AppendLine(pigPage.Text); - } - else - { - using var pngStream = UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.GetPageAsPng(pdfPigDoc, p, 1.5f, 100); - if (pngStream != null && pngStream.Length > 0) - { - var ocrRes = _ocrProvider.RecognizeTextAsync(pngStream.ToArray(), options.Language, ct).GetAwaiter().GetResult(); - if (ocrRes.Success) - { - allText.AppendLine($"--- Page {p} (OCR) ---"); - allText.AppendLine(ocrRes.FullText); - recognizedWords += ocrRes.Words.Count; - ocrWordsAdded += ocrRes.Words.Count; - lastEngineUsed = ocrRes.EngineUsed; - } - } - } - - progress?.Report(25.0 + (p / (double)totalPages * 35.0)); - } - - if (options.GenerateTextFile || options.ExtractTextOnly) - { - File.WriteAllText(txtOutPath, allText.ToString(), Encoding.UTF8); - } - - if (options.ExtractTextOnly) - { - progress?.Report(100.0); - return new ToolExecutionResult - { - Success = true, - OutputFilePath = txtOutPath, - OutputFiles = new List { txtOutPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = new FileInfo(txtOutPath).Length, - Message = $"Extracted text from {totalPages} page(s) ({recognizedWords} words) into {Path.GetFileName(txtOutPath)}." - }; - } - - // Create searchable PDF with invisible text layer - using var outPdfDoc = PdfSharpCore.Pdf.IO.PdfReader.Open(options.InputFilePath, PdfDocumentOpenMode.Modify); - - for (int i = 0; i < scannedPageIndices.Count; i++) - { - ct.ThrowIfCancellationRequested(); - int p = scannedPageIndices[i]; - - using var pngStream = UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.GetPageAsPng(pdfPigDoc, p, 1.5f, 100); - if (pngStream == null || pngStream.Length == 0) continue; - - var ocrRes = _ocrProvider.RecognizeTextAsync(pngStream.ToArray(), options.Language, ct).GetAwaiter().GetResult(); - if (!ocrRes.Success || ocrRes.Words.Count == 0) continue; - - var sharpPage = outPdfDoc.Pages[p - 1]; - double pw = sharpPage.Width.Point; - double ph = sharpPage.Height.Point; - - using var gfx = XGraphics.FromPdfPage(sharpPage, XGraphicsPdfPageOptions.Append); - var transparentBrush = new XSolidBrush(XColor.FromArgb(0, 0, 0, 0)); - - foreach (var word in ocrRes.Words) - { - double wx = word.NormalizedBounds.X * pw; - double wy = word.NormalizedBounds.Y * ph; - double ww = Math.Max(1, word.NormalizedBounds.Width * pw); - double wh = Math.Max(1, word.NormalizedBounds.Height * ph); - - double fontSize = Math.Max(4, Math.Min(72, wh * 0.85)); - var font = new XFont("Helvetica", fontSize, XFontStyle.Regular); - gfx.DrawString(word.Text, font, transparentBrush, new XPoint(wx, wy + wh * 0.85)); - } - - progress?.Report(60.0 + ((i + 1) / (double)scannedPageIndices.Count * 38.0)); - } - - outPdfDoc.Save(outPath); - progress?.Report(100.0); - - long finalBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - if (ocrWordsAdded == 0 && pagesWithoutText > 0) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = pagesWithoutText == totalPages - ? $"No extractable text was found on any of the {totalPages} page(s) — this looks like a scanned/image-only document. This tool indexes text that already exists in a PDF; it does not yet perform true image-to-text OCR recognition." - : $"{pagesWithoutText} of {totalPages} page(s) have no extractable text (likely scanned images) and could not be made searchable. This tool indexes text that already exists in a PDF; it does not yet perform true image-to-text OCR recognition for those pages." - }; - } - - var outFiles = new List { outPath }; - if (options.GenerateTextFile && File.Exists(txtOutPath)) - { - outFiles.Add(txtOutPath); - } - - string resultMsg = options.GenerateTextFile && File.Exists(txtOutPath) - ? $"OCR completed using {lastEngineUsed}: recognized and indexed {ocrWordsAdded} words across {pagesWithoutText} scanned page(s). Created Searchable PDF & Text File." - : $"OCR completed successfully using {lastEngineUsed}: recognized and indexed {ocrWordsAdded} words across {pagesWithoutText} scanned page(s)."; - - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = outFiles, - OriginalSizeBytes = origBytes, - OutputSizeBytes = finalBytes, - Message = resultMsg - }; - }, ct); - } - - public async Task ScanToPdfAsync(ScanToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (options.InputImageFiles == null || options.InputImageFiles.Count == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "No scanned images provided." }; - - long totalOriginalBytes = options.InputImageFiles.Where(File.Exists).Sum(f => new FileInfo(f).Length); - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputImageFiles[0]) ?? Path.GetTempPath(); - outPath = Path.Combine(dir, "Scanned_Document.pdf"); - } - - ct.ThrowIfCancellationRequested(); - using var pdfDoc = new PdfSharpCore.Pdf.PdfDocument(); - int total = options.InputImageFiles.Count; - - for (int i = 0; i < total; i++) - { - ct.ThrowIfCancellationRequested(); - string imgFile = options.InputImageFiles[i]; - if (!File.Exists(imgFile)) continue; - - // Load and enhance scanned image with SkiaSharp - using var skBitmap = SkiaSharp.SKBitmap.Decode(imgFile); - if (skBitmap != null) - { - // If enhancement requested (contrast / whitening) - using var enhancedSurface = SkiaSharp.SKSurface.Create(new SkiaSharp.SKImageInfo(skBitmap.Width, skBitmap.Height)); - var canvas = enhancedSurface.Canvas; - - // Compose the requested enhancement filters. Note: AutoDeskew is not - // applied here — real skew correction needs angle detection (e.g. a - // Hough transform / projection-profile analysis), which is a separate, - // larger piece of work rather than a color filter. - SkiaSharp.SKColorFilter? colorFilter = null; - void Compose(SkiaSharp.SKColorFilter next) - { - colorFilter = colorFilter == null ? next : SkiaSharp.SKColorFilter.CreateCompose(next, colorFilter); - } - - if (options.ConvertToGrayscale) - { - Compose(SkiaSharp.SKColorFilter.CreateColorMatrix(new float[] - { - 0.21f, 0.72f, 0.07f, 0, 0, - 0.21f, 0.72f, 0.07f, 0, 0, - 0.21f, 0.72f, 0.07f, 0, 0, - 0, 0, 0, 1, 0 - })); - } - - if (options.EnhanceContrast) - { - const float contrast = 1.25f; - float t = 0.5f * (1f - contrast); - Compose(SkiaSharp.SKColorFilter.CreateColorMatrix(new float[] - { - contrast, 0, 0, 0, t, - 0, contrast, 0, 0, t, - 0, 0, contrast, 0, t, - 0, 0, 0, 1, 0 - })); - } - - if (options.WhitenBackground) - { - const float brighten = 0.10f; - Compose(SkiaSharp.SKColorFilter.CreateColorMatrix(new float[] - { - 1, 0, 0, 0, brighten, - 0, 1, 0, 0, brighten, - 0, 0, 1, 0, brighten, - 0, 0, 0, 1, 0 - })); - } - - using var paint = new SkiaSharp.SKPaint { ColorFilter = colorFilter }; - - canvas.DrawBitmap(skBitmap, 0, 0, paint); - - using var enhancedImage = enhancedSurface.Snapshot(); - using var data = enhancedImage.Encode(SkiaSharp.SKEncodedImageFormat.Jpeg, 92); - - using var ms = new MemoryStream(); - data.SaveTo(ms); - ms.Seek(0, SeekOrigin.Begin); - - var page = pdfDoc.AddPage(); - var xImage = XImage.FromStream(() => new MemoryStream(ms.ToArray())); - - using var gfx = XGraphics.FromPdfPage(page); - double maxW = page.Width.Point - 40; - double maxH = page.Height.Point - 40; - double scale = Math.Min(maxW / xImage.PixelWidth, maxH / xImage.PixelHeight); - double drawW = xImage.PixelWidth * scale; - double drawH = xImage.PixelHeight * scale; - double posX = 20 + ((maxW - drawW) / 2.0); - double posY = 20 + ((maxH - drawH) / 2.0); - - gfx.DrawImage(xImage, posX, posY, drawW, drawH); - } - - progress?.Report((i + 1) / (double)total * 90.0); - } - - pdfDoc.Save(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = totalOriginalBytes, - OutputSizeBytes = outBytes, - Message = $"Enhanced {total} scanned pages and compiled into PDF: {Path.GetFileName(outPath)}" - }; - }, ct); - } -} diff --git a/src/PdfEditorApp/Services/Tools/Organize/PdfPageService.cs b/src/PdfEditorApp/Services/Tools/Organize/PdfPageService.cs deleted file mode 100644 index 4bc7763..0000000 --- a/src/PdfEditorApp/Services/Tools/Organize/PdfPageService.cs +++ /dev/null @@ -1,768 +0,0 @@ -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfSharpCore.Drawing; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.IO; -using QuestPDF.Fluent; - -namespace PdfEditorApp.Services.Tools.Organize; - -public interface IPdfPageService -{ - Task MergePdfAsync(MergeToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task SplitPdfAsync(SplitToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task RotatePdfAsync(RotateToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task CropPdfAsync(CropToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task OrganizePdfAsync(OrganizeToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task AddPageNumbersAsync(PageNumberToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task AddWatermarkAsync(WatermarkToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ApplyPdfLayerAsync(string inputPath, string layerPdfPath, bool isOverlay, string targetPages, string? outputPath = null, IProgress? progress = null, CancellationToken ct = default); -} - -public class PdfPageService : IPdfPageService -{ - private readonly IQuestPdfOperationsEngine _questEngine; - - public PdfPageService(IQuestPdfOperationsEngine? questEngine = null) - { - _questEngine = questEngine ?? new QuestPdfOperationsEngine(); - } - - public async Task MergePdfAsync(MergeToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - if (options.Engine == PdfProcessingEngine.QuestPdfNative) - { - var questResult = await _questEngine.MergeAsync(options, progress, ct); - if (questResult.Success) - return questResult; - - System.Diagnostics.Debug.WriteLine($"QuestPDF Merge fallback: {questResult.ErrorMessage}"); - } - - return await MergeWithPdfSharpAsync(options, progress, ct); - } - - private async Task MergeWithPdfSharpAsync(MergeToolOptions options, IProgress? progress, CancellationToken ct) - { - return await Task.Run(() => - { - if (options.InputFiles == null || options.InputFiles.Count == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "No input PDF files provided for merging." }; - - long totalOriginalBytes = 0; - foreach (var f in options.InputFiles) - { - if (File.Exists(f)) totalOriginalBytes += new FileInfo(f).Length; - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFiles[0]) ?? Path.GetTempPath(); - outPath = PdfFileHelper.ResolveSafeOutputPath(null, dir, "Merged_Document", ".pdf", options.InputFiles); - } - - string? targetDir = Path.GetDirectoryName(outPath); - if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) - Directory.CreateDirectory(targetDir); - - // Standard Engine (PdfSharpCore) - using var outputDoc = new PdfDocument(); - int totalFiles = options.InputFiles.Count; - - for (int i = 0; i < totalFiles; i++) - { - ct.ThrowIfCancellationRequested(); - string filePath = options.InputFiles[i]; - if (!File.Exists(filePath)) continue; - - using var inputDoc = PdfFileHelper.OpenDocumentSafely(filePath, PdfDocumentOpenMode.Import); - int pageCount = inputDoc.PageCount; - - for (int p = 0; p < pageCount; p++) - { - ct.ThrowIfCancellationRequested(); - var page = inputDoc.Pages[p]; - outputDoc.AddPage(page); - } - - progress?.Report((i + 1) / (double)totalFiles * 90.0); - } - - if (outputDoc.PageCount == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "No valid pages found in the provided PDF files to merge." }; - - try - { - PdfFileHelper.SaveDocumentWithFryPdfMetadata(outputDoc, outPath); - } - catch (IOException) when (string.IsNullOrWhiteSpace(options.OutputFilePath)) - { - string dir = Path.GetDirectoryName(outPath) ?? Path.GetTempPath(); - string fallbackPath = Path.Combine(dir, $"Merged_Document_{DateTime.Now:yyyyMMdd_HHmmss}.pdf"); - try - { - PdfFileHelper.SaveDocumentWithFryPdfMetadata(outputDoc, fallbackPath); - outPath = fallbackPath; - } - catch - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Cannot save merged document to '{Path.GetFileName(outPath)}': the file is currently open in another application. Please close it and try again." - }; - } - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Cannot save to '{Path.GetFileName(outPath)}': {ex.Message}" - }; - } - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = totalOriginalBytes, - OutputSizeBytes = outBytes, - Message = $"Successfully merged {totalFiles} files into {Path.GetFileName(outPath)} ({outputDoc.PageCount} pages)." - }; - }, ct); - } - - public async Task SplitPdfAsync(SplitToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - using var inputDoc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Import); - int totalPages = inputDoc.PageCount; - - if (totalPages == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "The selected PDF has no pages to split." }; - - string outDir = options.OutputDirectory; - if (string.IsNullOrWhiteSpace(outDir)) - { - outDir = Path.Combine(Path.GetDirectoryName(options.InputFilePath) ?? "", Path.GetFileNameWithoutExtension(options.InputFilePath) + "_split"); - } - if (!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); - - var createdFiles = new List(); - string baseName = Path.GetFileNameWithoutExtension(options.InputFilePath); - - if (options.SplitOddEven) - { - // Odd pages document - using var oddDoc = new PdfDocument(); - for (int i = 0; i < totalPages; i += 2) - oddDoc.AddPage(inputDoc.Pages[i]); - string oddPath = Path.Combine(outDir, $"{baseName}_OddPages.pdf"); - if (oddDoc.PageCount > 0) - { - PdfFileHelper.SaveDocumentWithFryPdfMetadata(oddDoc, oddPath); - createdFiles.Add(oddPath); - } - - // Even pages document - using var evenDoc = new PdfDocument(); - for (int i = 1; i < totalPages; i += 2) - evenDoc.AddPage(inputDoc.Pages[i]); - string evenPath = Path.Combine(outDir, $"{baseName}_EvenPages.pdf"); - if (evenDoc.PageCount > 0) - { - PdfFileHelper.SaveDocumentWithFryPdfMetadata(evenDoc, evenPath); - createdFiles.Add(evenPath); - } - } - else if (options.Mode == SplitExtractMode.SplitByPageRanges) - { - var ranges = ParsePageRanges(options.RangeExpression, totalPages); - int rIdx = 1; - foreach (var range in ranges) - { - ct.ThrowIfCancellationRequested(); - if (range.Count == 0) continue; - using var rangeDoc = new PdfDocument(); - foreach (int pIndex in range) - { - if (pIndex >= 0 && pIndex < totalPages) - rangeDoc.AddPage(inputDoc.Pages[pIndex]); - } - - if (rangeDoc.PageCount > 0) - { - string rangeName = $"{baseName}_Range{rIdx}_{range.First() + 1}-{range.Last() + 1}.pdf"; - string rangePath = Path.Combine(outDir, rangeName); - PdfFileHelper.SaveDocumentWithFryPdfMetadata(rangeDoc, rangePath); - createdFiles.Add(rangePath); - } - rIdx++; - } - } - else - { - // Split every N pages - int n = Math.Max(1, options.PagesPerSplit); - int part = 1; - for (int i = 0; i < totalPages; i += n) - { - ct.ThrowIfCancellationRequested(); - using var partDoc = new PdfDocument(); - int count = Math.Min(n, totalPages - i); - for (int j = 0; j < count; j++) - { - partDoc.AddPage(inputDoc.Pages[i + j]); - } - - string partPath = Path.Combine(outDir, $"{baseName}_Part{part}.pdf"); - PdfFileHelper.SaveDocumentWithFryPdfMetadata(partDoc, partPath); - createdFiles.Add(partPath); - part++; - - progress?.Report(i / (double)totalPages * 90.0); - } - } - - progress?.Report(100.0); - return new ToolExecutionResult - { - Success = true, - OutputFilePath = createdFiles.FirstOrDefault(), - OutputFiles = createdFiles, - OriginalSizeBytes = origBytes, - OutputSizeBytes = createdFiles.Sum(f => File.Exists(f) ? new FileInfo(f).Length : 0), - Message = $"Split PDF into {createdFiles.Count} files in {outDir}." - }; - }, ct); - } - - public async Task RotatePdfAsync(RotateToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - using var doc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Modify); - int totalPages = doc.PageCount; - - var targetIndices = GetFilteredPageIndices(options.TargetFilter, options.CustomRange, totalPages); - - for (int i = 0; i < totalPages; i++) - { - ct.ThrowIfCancellationRequested(); - if (targetIndices.Contains(i)) - { - var page = doc.Pages[i]; - int currentRotate = page.Rotate; - int newRotate = (currentRotate + options.RotationDegrees) % 360; - if (newRotate < 0) newRotate += 360; - page.Rotate = newRotate; - } - progress?.Report((i + 1) / (double)totalPages * 90.0); - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = PdfFileHelper.ResolveSafeOutputPath(null, dir, $"{name}_Rotated", ".pdf", [options.InputFilePath]); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Rotated {targetIndices.Count} pages by {options.RotationDegrees}°." - }; - }, ct); - } - - public async Task CropPdfAsync(CropToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - using var doc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Modify); - int totalPages = doc.PageCount; - - var targetIndices = GetTargetPageIndices(options.TargetPages, options.CustomRange, totalPages); - - for (int i = 0; i < totalPages; i++) - { - ct.ThrowIfCancellationRequested(); - if (targetIndices.Contains(i)) - { - var page = doc.Pages[i]; - var mediaBox = page.MediaBox; - double x1 = mediaBox.X1 + options.CropLeftPoints; - double y1 = mediaBox.Y1 + options.CropBottomPoints; - double x2 = mediaBox.X2 - options.CropRightPoints; - double y2 = mediaBox.Y2 - options.CropTopPoints; - - if (x2 > x1 && y2 > y1) - { - var rect = new PdfRectangle(new PdfSharpCore.Drawing.XPoint(x1, y1), new PdfSharpCore.Drawing.XPoint(x2, y2)); - page.CropBox = rect; - page.MediaBox = rect; - } - } - progress?.Report((i + 1) / (double)totalPages * 90.0); - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = PdfFileHelper.ResolveSafeOutputPath(null, dir, $"{name}_Cropped", ".pdf", [options.InputFilePath]); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Applied crop margins to {targetIndices.Count} pages." - }; - }, ct); - } - - public async Task OrganizePdfAsync(OrganizeToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - using var inputDoc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Import); - using var outputDoc = new PdfDocument(); - int totalPages = inputDoc.PageCount; - - var order = options.PageOrder.Count > 0 ? options.PageOrder : Enumerable.Range(0, totalPages).ToList(); - - int step = 0; - foreach (int pIndex in order) - { - ct.ThrowIfCancellationRequested(); - if (pIndex < 0 || pIndex >= totalPages) continue; - if (options.PagesToDelete.Contains(pIndex)) continue; - - var page = inputDoc.Pages[pIndex]; - var addedPage = outputDoc.AddPage(page); - - if (options.PageRotations.TryGetValue(pIndex, out int rot) && rot != 0) - { - addedPage.Rotate = (addedPage.Rotate + rot) % 360; - } - - step++; - progress?.Report(step / (double)order.Count * 90.0); - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = PdfFileHelper.ResolveSafeOutputPath(null, dir, $"{name}_Organized", ".pdf", [options.InputFilePath]); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(outputDoc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Organized PDF to {outputDoc.PageCount} pages." - }; - }, ct); - } - - public async Task AddPageNumbersAsync(PageNumberToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - using var doc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Modify); - int totalPages = doc.PageCount; - - var targetIndices = GetTargetPageIndices(options.TargetPages, options.CustomRange, totalPages); - var font = new XFont(options.FontFamily, options.FontSize, XFontStyle.Regular); - var brush = new XSolidBrush(ParseColor(options.ColorHex)); - - int count = 0; - for (int i = 0; i < totalPages; i++) - { - ct.ThrowIfCancellationRequested(); - if (targetIndices.Contains(i)) - { - var page = doc.Pages[i]; - using var gfx = XGraphics.FromPdfPage(page); - - int pageNum = options.StartingNumber + count; - string text = FormatTemplate(options.Template, pageNum, totalPages); - - var size = gfx.MeasureString(text, font); - double margin = options.MarginPoints; - double x = 0, y = 0; - - switch (options.Position) - { - case PageNumberPosition.TopLeft: - x = margin; - y = margin + size.Height; - break; - case PageNumberPosition.TopCenter: - x = (page.Width.Point - size.Width) / 2.0; - y = margin + size.Height; - break; - case PageNumberPosition.TopRight: - x = page.Width.Point - size.Width - margin; - y = margin + size.Height; - break; - case PageNumberPosition.BottomLeft: - x = margin; - y = page.Height.Point - margin; - break; - case PageNumberPosition.BottomCenter: - x = (page.Width.Point - size.Width) / 2.0; - y = page.Height.Point - margin; - break; - case PageNumberPosition.BottomRight: - x = page.Width.Point - size.Width - margin; - y = page.Height.Point - margin; - break; - } - - gfx.DrawString(text, font, brush, new XPoint(x, y)); - count++; - } - - progress?.Report((i + 1) / (double)totalPages * 90.0); - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = PdfFileHelper.ResolveSafeOutputPath(null, dir, $"{name}_Numbered", ".pdf", [options.InputFilePath]); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Stamped page numbers on {count} pages." - }; - }, ct); - } - - public async Task AddWatermarkAsync(WatermarkToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - using var doc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Modify); - int totalPages = doc.PageCount; - - var targetIndices = GetTargetPageIndices(options.TargetPages, options.CustomRange, totalPages); - var baseColor = ParseColor(options.ColorHex); - byte alpha = (byte)(Math.Clamp(options.Opacity, 0.01, 1.0) * 255); - var watermarkColor = XColor.FromArgb(alpha, baseColor.R, baseColor.G, baseColor.B); - var font = new XFont(string.IsNullOrWhiteSpace(options.FontFamily) ? "Helvetica" : options.FontFamily, Math.Max(8, options.FontSize), XFontStyle.Bold); - var brush = new XSolidBrush(watermarkColor); - - int stampedCount = 0; - for (int i = 0; i < totalPages; i++) - { - ct.ThrowIfCancellationRequested(); - if (targetIndices.Contains(i)) - { - var page = doc.Pages[i]; - using var gfx = XGraphics.FromPdfPage(page); - - if (options.Position == WatermarkPosition.Tiled) - { - // Draw repeated tiled watermark diagonally across page - for (double y = 50; y < page.Height.Point; y += 180) - { - for (double x = 50; x < page.Width.Point; x += 220) - { - var state = gfx.Save(); - gfx.TranslateTransform(x, y); - gfx.RotateTransform(options.RotationAngle); - gfx.DrawString(options.Text, font, brush, new XPoint(0, 0), XStringFormats.Center); - gfx.Restore(state); - } - } - } - else - { - var state = gfx.Save(); - double cx = page.Width.Point / 2.0; - double cy = page.Height.Point / 2.0; - - switch (options.Position) - { - case WatermarkPosition.TopLeft: - cx = 100; cy = 100; break; - case WatermarkPosition.TopCenter: - cx = page.Width.Point / 2.0; cy = 100; break; - case WatermarkPosition.TopRight: - cx = page.Width.Point - 100; cy = 100; break; - case WatermarkPosition.BottomLeft: - cx = 100; cy = page.Height.Point - 100; break; - case WatermarkPosition.BottomCenter: - cx = page.Width.Point / 2.0; cy = page.Height.Point - 100; break; - case WatermarkPosition.BottomRight: - cx = page.Width.Point - 100; cy = page.Height.Point - 100; break; - default: // Center - cx = page.Width.Point / 2.0; cy = page.Height.Point / 2.0; break; - } - - gfx.TranslateTransform(cx, cy); - gfx.RotateTransform(options.RotationAngle); - - if (options.Type == WatermarkType.Image && !string.IsNullOrWhiteSpace(options.ImagePath) && File.Exists(options.ImagePath)) - { - try - { - using var img = XImage.FromFile(options.ImagePath); - double imgW = Math.Min(300, img.PixelWidth); - double imgH = imgW * (img.PixelHeight / (double)img.PixelWidth); - gfx.DrawImage(img, -imgW / 2.0, -imgH / 2.0, imgW, imgH); - } - catch - { - gfx.DrawString(options.Text, font, brush, new XPoint(0, 0), XStringFormats.Center); - } - } - else - { - gfx.DrawString(options.Text, font, brush, new XPoint(0, 0), XStringFormats.Center); - } - - gfx.Restore(state); - } - - stampedCount++; - } - - progress?.Report((i + 1) / (double)totalPages * 90.0); - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = PdfFileHelper.ResolveSafeOutputPath(null, dir, $"{name}_Watermarked", ".pdf", [options.InputFilePath]); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Applied watermark to {stampedCount} pages." - }; - }, ct); - } - - private static string FormatTemplate(string template, int n, int total) - { - if (string.IsNullOrWhiteSpace(template)) return n.ToString(); - return template - .Replace("{n}", n.ToString()) - .Replace("{P}", n.ToString()) - .Replace("{total}", total.ToString()) - .Replace("{N}", total.ToString()) - .Replace("{n:D6}", n.ToString("D6")) - .Replace("{n:D4}", n.ToString("D4")); - } - - private static HashSet GetTargetPageIndices(PageTargetSelection selection, string customRange, int totalPages) - { - var result = new HashSet(); - switch (selection) - { - case PageTargetSelection.AllPages: - for (int i = 0; i < totalPages; i++) result.Add(i); - break; - case PageTargetSelection.OddPagesOnly: - for (int i = 0; i < totalPages; i += 2) result.Add(i); - break; - case PageTargetSelection.EvenPagesOnly: - for (int i = 1; i < totalPages; i += 2) result.Add(i); - break; - case PageTargetSelection.CustomRange: - var parsed = ParseSingleRangeList(customRange, totalPages); - foreach (int p in parsed) result.Add(p); - break; - } - return result; - } - - private static HashSet GetFilteredPageIndices(PageFilterTarget filter, string customRange, int totalPages) - { - var result = new HashSet(); - switch (filter) - { - case PageFilterTarget.All: - for (int i = 0; i < totalPages; i++) result.Add(i); - break; - case PageFilterTarget.OddPages: - for (int i = 0; i < totalPages; i += 2) result.Add(i); - break; - case PageFilterTarget.EvenPages: - for (int i = 1; i < totalPages; i += 2) result.Add(i); - break; - default: - for (int i = 0; i < totalPages; i++) result.Add(i); - break; - } - return result; - } - - private static List> ParsePageRanges(string expr, int totalPages) - { - var result = new List>(); - if (string.IsNullOrWhiteSpace(expr)) return result; - var parts = expr.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var p in parts) - { - var single = ParseSingleRange(p.Trim(), totalPages); - if (single.Count > 0) result.Add(single); - } - return result; - } - - private static List ParseSingleRangeList(string expr, int totalPages) - { - var result = new List(); - if (string.IsNullOrWhiteSpace(expr)) return result; - var parts = expr.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var p in parts) - { - result.AddRange(ParseSingleRange(p.Trim(), totalPages)); - } - return result.Distinct().ToList(); - } - - private static List ParseSingleRange(string token, int totalPages) - { - var list = new List(); - if (token.Contains('-')) - { - var bounds = token.Split('-'); - if (bounds.Length == 2 && int.TryParse(bounds[0].Trim(), out int start) && int.TryParse(bounds[1].Trim(), out int end)) - { - start = Math.Max(1, start); - end = Math.Min(totalPages, end); - for (int p = start; p <= end; p++) list.Add(p - 1); - } - } - else if (int.TryParse(token, out int single)) - { - if (single >= 1 && single <= totalPages) list.Add(single - 1); - } - return list; - } - - private static XColor ParseColor(string hex) - { - try - { - if (string.IsNullOrWhiteSpace(hex)) return XColors.Black; - hex = hex.TrimStart('#'); - if (hex.Length == 6) - { - byte r = Convert.ToByte(hex.Substring(0, 2), 16); - byte g = Convert.ToByte(hex.Substring(2, 2), 16); - byte b = Convert.ToByte(hex.Substring(4, 2), 16); - return XColor.FromArgb(r, g, b); - } - if (hex.Length == 8) - { - byte a = Convert.ToByte(hex.Substring(0, 2), 16); - byte r = Convert.ToByte(hex.Substring(2, 2), 16); - byte g = Convert.ToByte(hex.Substring(4, 2), 16); - byte b = Convert.ToByte(hex.Substring(6, 2), 16); - return XColor.FromArgb(a, r, g, b); - } - } - catch { } - return XColors.Black; - } - - public Task ApplyPdfLayerAsync(string inputPath, string layerPdfPath, bool isOverlay, string targetPages, string? outputPath = null, IProgress? progress = null, CancellationToken ct = default) - { - return _questEngine.ApplyLayerAsync(inputPath, layerPdfPath, isOverlay, targetPages, outputPath, progress, ct); - } -} - - diff --git a/src/PdfEditorApp/Services/Tools/Security/PdfOptimizationService.cs b/src/PdfEditorApp/Services/Tools/Security/PdfOptimizationService.cs deleted file mode 100644 index 3bd839d..0000000 --- a/src/PdfEditorApp/Services/Tools/Security/PdfOptimizationService.cs +++ /dev/null @@ -1,596 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.Advanced; -using PdfSharpCore.Pdf.IO; -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using SkiaSharp; -using UglyToad.PdfPig; -using UglyToad.PdfPig.Rendering.Skia; -using PdfSharpDoc = PdfSharpCore.Pdf.PdfDocument; - -namespace PdfEditorApp.Services.Tools.Security; - -public interface IPdfOptimizationService -{ - Task CompressPdfAsync(CompressToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task RepairPdfAsync(RepairToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task ConvertToPdfAAsync(PdfAToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task LinearizePdfAsync(LinearizeToolOptions options, IProgress? progress = null, CancellationToken ct = default); -} - -public class PdfOptimizationService : IPdfOptimizationService -{ - private readonly IQuestPdfOperationsEngine _questEngine; - - public PdfOptimizationService(IQuestPdfOperationsEngine? questEngine = null) - { - _questEngine = questEngine ?? new QuestPdfOperationsEngine(); - } - - public async Task CompressPdfAsync(CompressToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(10.0); - - ct.ThrowIfCancellationRequested(); - - byte[] pass1Bytes = CompressPdfStreamsAndImages(options.InputFilePath, options, progress, ct); - byte[] bestBytes = pass1Bytes; - - // Test raster page compression for Extreme, SmallSize, and Recommended presets, - // or whenever in-place pass did not achieve aggressive reduction (<40% reduction). - bool shouldTryRaster = options.Level == PdfCompressionLevel.MaximumCompression || - options.Level == PdfCompressionLevel.SmallSize || - options.Level == PdfCompressionLevel.Balanced || - pass1Bytes.Length >= origBytes || - (pass1Bytes.Length / (double)origBytes) > 0.55; - - if (shouldTryRaster) - { - progress?.Report(80.0); - byte[]? rasterCandidate = TryRasterPageCompression(options.InputFilePath, options, ct); - if (rasterCandidate != null && rasterCandidate.Length < bestBytes.Length) - { - bestBytes = rasterCandidate; - } - } - - progress?.Report(95.0); - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Compressed.pdf"); - } - - string? targetDir = Path.GetDirectoryName(outPath); - if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) - Directory.CreateDirectory(targetDir); - - long outBytes; - string message; - - if (bestBytes.Length < origBytes) - { - File.WriteAllBytes(outPath, bestBytes); - outBytes = bestBytes.Length; - double reduction = ((origBytes - outBytes) / (double)origBytes) * 100.0; - message = $"Compressed document from {FormatFileSize(origBytes)} to {FormatFileSize(outBytes)} ({reduction:F1}% reduction)."; - } - else - { - // Document is already optimally compressed. NEVER output a larger file! - if (!string.Equals(Path.GetFullPath(options.InputFilePath), Path.GetFullPath(outPath), StringComparison.OrdinalIgnoreCase)) - { - File.Copy(options.InputFilePath, outPath, overwrite: true); - } - outBytes = origBytes; - message = $"Document is already at optimal compression ({FormatFileSize(origBytes)}). Original fidelity preserved without increasing file size."; - } - - // High-Performance QuestPDF Web Linearization (Fast Web View) - if (options.LinearizeForWeb && File.Exists(outPath)) - { - var linResult = _questEngine.LinearizeAsync(outPath, outPath, null, ct).GetAwaiter().GetResult(); - if (linResult.Success) - { - outBytes = new FileInfo(outPath).Length; - message += " Optimized for Fast Web View (Linearized)."; - } - } - - progress?.Report(100.0); - - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = message - }; - }, ct); - } - - public Task LinearizePdfAsync(LinearizeToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return _questEngine.LinearizeAsync(options.InputFilePath, options.OutputFilePath, progress, ct); - } - - private static byte[] CompressPdfStreamsAndImages(string inputPath, CompressToolOptions options, IProgress? progress, CancellationToken ct) - { - int effectiveDpi = options.ImageQualityDpi > 0 ? Math.Clamp(options.ImageQualityDpi, 50, 600) : 150; - - int jpegQuality = options.JpegQuality > 0 - ? Math.Clamp(options.JpegQuality, 20, 100) - : options.Level switch - { - PdfCompressionLevel.MaximumCompression => 52, - PdfCompressionLevel.SmallSize => 60, - PdfCompressionLevel.Balanced => 66, - PdfCompressionLevel.HighQuality => 82, - PdfCompressionLevel.MaximumQuality => 92, - _ => 66 - }; - - int maxDimension = options.MaxImageDimension > 0 - ? Math.Clamp(options.MaxImageDimension, 500, 5000) - : Math.Clamp((int)(11.69 * effectiveDpi), 600, 4500); - - using var doc = PdfFileHelper.OpenDocumentSafely(inputPath, PdfDocumentOpenMode.Modify); - - doc.Options.CompressContentStreams = options.CompressStreams; - doc.Options.UseFlateDecoderForJpegImages = PdfUseFlateDecoderForJpegImages.Never; - doc.Options.NoCompression = !options.CompressStreams; - - if (options.RemoveMetadata) - { - doc.Info.Title = ""; - doc.Info.Author = ""; - doc.Info.Subject = ""; - doc.Info.Keywords = ""; - PdfFileHelper.SetFryPdfMetadata(doc); - doc.Internals.Catalog.Elements.Remove("/Metadata"); - doc.Internals.Catalog.Elements.Remove("/PieceInfo"); - doc.Internals.Catalog.Elements.Remove("/StructTreeRoot"); - } - - int pageCount = doc.Pages.Count; - for (int i = 0; i < pageCount; i++) - { - ct.ThrowIfCancellationRequested(); - var page = doc.Pages[i]; - page.Elements.Remove("/Thumb"); - if (options.RemoveMetadata) - { - page.Elements.Remove("/PieceInfo"); - } - - if (options.CompressStreams && page.Contents != null) - { - for (int c = 0; c < page.Contents.Elements.Count; c++) - { - var contentRef = page.Contents.Elements[c]; - if (contentRef is PdfDictionary cDict && cDict.Stream != null) - { - try { cDict.Stream.Zip(); } catch { } - } - } - } - progress?.Report(20.0 + (i / (double)pageCount * 30.0)); - } - - var allObjects = doc.Internals.GetAllObjects(); - for (int idx = 0; idx < allObjects.Length; idx++) - { - ct.ThrowIfCancellationRequested(); - var obj = allObjects[idx]; - if (obj is PdfDictionary dict && dict.Stream != null) - { - string subtype = dict.Elements.GetString("/Subtype"); - string type = dict.Elements.GetString("/Type"); - bool isImage = subtype == "/Image" || (type == "/XObject" && dict.Elements.ContainsKey("/Width") && dict.Elements.ContainsKey("/Height")); - - if (isImage) - { - OptimizeImageDictionary(dict, maxDimension, jpegQuality, options.ConvertToGrayscale); - } - else if (options.CompressStreams) - { - try - { - if (!dict.Elements.ContainsKey("/Filter")) - { - dict.Stream.Zip(); - } - } - catch { } - } - } - progress?.Report(50.0 + (idx / (double)allObjects.Length * 30.0)); - } - - using var ms = new MemoryStream(); - doc.Save(ms); - return ms.ToArray(); - } - - private static void OptimizeImageDictionary(PdfDictionary dict, int maxDimension, int jpegQuality, bool convertToGrayscale = false) - { - try - { - if (dict.Stream == null || dict.Stream.Value == null || dict.Stream.Value.Length < 256) - return; - - byte[] streamBytes = dict.Stream.Value; - - using var bmp = SKBitmap.Decode(streamBytes); - if (bmp == null || bmp.Width <= 0 || bmp.Height <= 0) - return; - - int origW = bmp.Width; - int origH = bmp.Height; - int targetW = origW; - int targetH = origH; - - int maxSide = Math.Max(origW, origH); - if (maxSide > maxDimension) - { - double scale = (double)maxDimension / maxSide; - targetW = Math.Max(1, (int)(origW * scale)); - targetH = Math.Max(1, (int)(origH * scale)); - } - - SKBitmap? toEncode = bmp; - SKBitmap? resizedBmp = null; - SKBitmap? grayBmp = null; - - if (targetW != origW || targetH != origH) - { - resizedBmp = bmp.Resize(new SKImageInfo(targetW, targetH), new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear)); - if (resizedBmp != null) - { - toEncode = resizedBmp; - } - } - - if (convertToGrayscale && toEncode != null) - { - grayBmp = ConvertBitmapToGrayscale(toEncode); - if (grayBmp != null) - { - toEncode = grayBmp; - } - } - - try - { - using var skImage = SKImage.FromBitmap(toEncode); - if (skImage != null) - { - using var encoded = skImage.Encode(SKEncodedImageFormat.Jpeg, jpegQuality); - if (encoded != null) - { - byte[] newJpegBytes = encoded.ToArray(); - if (newJpegBytes.Length < streamBytes.Length) - { - dict.Stream.Value = newJpegBytes; - dict.Elements.SetName("/Filter", "/DCTDecode"); - dict.Elements.SetInteger("/Length", newJpegBytes.Length); - dict.Elements.SetInteger("/Width", targetW); - dict.Elements.SetInteger("/Height", targetH); - dict.Elements.SetName("/ColorSpace", "/DeviceRGB"); - dict.Elements.SetInteger("/BitsPerComponent", 8); - dict.Elements.Remove("/DecodeParms"); - } - } - } - } - finally - { - resizedBmp?.Dispose(); - grayBmp?.Dispose(); - } - } - catch - { - // Preserve original bytes on error - } - } - - private static SKBitmap? ConvertBitmapToGrayscale(SKBitmap original) - { - try - { - var grayBmp = new SKBitmap(original.Width, original.Height, SKColorType.Bgra8888, SKAlphaType.Opaque); - using var canvas = new SKCanvas(grayBmp); - using var paint = new SKPaint(); - paint.ColorFilter = SKColorFilter.CreateColorMatrix(new float[] - { - 0.21f, 0.72f, 0.07f, 0, 0, - 0.21f, 0.72f, 0.07f, 0, 0, - 0.21f, 0.72f, 0.07f, 0, 0, - 0, 0, 0, 1, 0 - }); - canvas.DrawBitmap(original, 0, 0, paint); - return grayBmp; - } - catch - { - return null; - } - } - - private static byte[]? TryRasterPageCompression(string inputPath, CompressToolOptions options, CancellationToken ct) - { - try - { - float scale = options.ImageQualityDpi > 0 - ? Math.Clamp(options.ImageQualityDpi / 72f, 0.75f, 4.0f) - : options.Level switch - { - PdfCompressionLevel.MaximumCompression => 1.25f, - PdfCompressionLevel.SmallSize => 1.35f, - PdfCompressionLevel.Balanced => 1.45f, - PdfCompressionLevel.HighQuality => 1.85f, - _ => 1.45f - }; - - int jpegQuality = options.JpegQuality > 0 - ? Math.Clamp(options.JpegQuality, 20, 100) - : options.Level switch - { - PdfCompressionLevel.MaximumCompression => 52, - PdfCompressionLevel.SmallSize => 60, - PdfCompressionLevel.Balanced => 66, - PdfCompressionLevel.HighQuality => 82, - _ => 66 - }; - - using var pigDoc = UglyToad.PdfPig.PdfDocument.Open(inputPath); - int pageCount = pigDoc.NumberOfPages; - if (pageCount <= 0 || pageCount > 250) return null; - - var pageImages = new List<(byte[] Bytes, float Width, float Height)>(); - for (int i = 1; i <= pageCount; i++) - { - ct.ThrowIfCancellationRequested(); - var page = pigDoc.GetPage(i); - using var pngStream = PdfPigExtensions.GetPageAsPng(pigDoc, i, scale, 90); - if (pngStream == null || pngStream.Length == 0) return null; - - using var bmp = SKBitmap.Decode(pngStream); - if (bmp == null) return null; - - SKBitmap finalBmp = bmp; - SKBitmap? grayCandidate = null; - if (options.ConvertToGrayscale) - { - grayCandidate = ConvertBitmapToGrayscale(bmp); - if (grayCandidate != null) - { - finalBmp = grayCandidate; - } - } - - try - { - using var img = SKImage.FromBitmap(finalBmp); - using var encoded = img.Encode(SKEncodedImageFormat.Jpeg, jpegQuality); - pageImages.Add((encoded.ToArray(), (float)page.Width, (float)page.Height)); - } - finally - { - grayCandidate?.Dispose(); - } - } - - var doc = FryPdfDocument.Create(container => - { - foreach (var p in pageImages) - { - container.Page(page => - { - page.Size(p.Width, p.Height, Unit.Point); - page.Margin(0); - page.Content().Image(p.Bytes); - }); - } - }); - - return doc.GeneratePdf(); - } - catch - { - return null; - } - } - - public async Task RepairPdfAsync(RepairToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(20.0); - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Repaired.pdf"); - } - - ct.ThrowIfCancellationRequested(); - - try - { - // Attempt standard reconstruction - using var inputDoc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Import); - using var outputDoc = new PdfSharpDoc(); - - outputDoc.Info.Title = !string.IsNullOrEmpty(inputDoc.Info.Title) ? inputDoc.Info.Title : Path.GetFileNameWithoutExtension(options.InputFilePath); - PdfFileHelper.SetFryPdfMetadata(outputDoc); - - int pages = inputDoc.PageCount; - int recoveredPages = 0; - - for (int i = 0; i < pages; i++) - { - ct.ThrowIfCancellationRequested(); - try - { - var page = inputDoc.Pages[i]; - outputDoc.AddPage(page); - recoveredPages++; - } - catch - { - // Skip corrupted page or object - } - progress?.Report(20.0 + (i / (double)pages * 60.0)); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(outputDoc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new System.Collections.Generic.List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Repaired and safely reconstructed {recoveredPages} of {pages} pages without data loss." - }; - } - catch (Exception ex) - { - // Fallback stream reconstruction for broken xref - try - { - byte[] bytes = File.ReadAllBytes(options.InputFilePath); - using var ms = new MemoryStream(bytes); - using var inputDoc = PdfReader.Open(ms, PdfDocumentOpenMode.Import); - using var outputDoc = new PdfSharpDoc(); - - for (int i = 0; i < inputDoc.PageCount; i++) - { - outputDoc.AddPage(inputDoc.Pages[i]); - } - PdfFileHelper.SaveDocumentWithFryPdfMetadata(outputDoc, outPath); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new System.Collections.Generic.List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Rebuilt xref tables and recovered {outputDoc.PageCount} pages." - }; - } - catch - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Unable to repair document structure: {ex.Message}" - }; - } - } - }, ct); - } - - public async Task ConvertToPdfAAsync(PdfAToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(20.0); - - using var inputDoc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Import); - using var outputDoc = new PdfSharpDoc(); - - // Note: this labels the document as the requested standard but does not (yet) - // produce real PDF/A-conformant output — that additionally requires embedded - // XMP metadata with the pdfaid schema, an /OutputIntent ICC profile, full font - // embedding, and no encryption, none of which are implemented here. The label - // below reflects intent, not verified conformance. - string standardLabel = options.Standard switch - { - PdfAStandard.PdfA1b => "PDF/A-1b (ISO 19005-1)", - PdfAStandard.PdfA3b => "PDF/A-3b (ISO 19005-3)", - _ => "PDF/A-2b (ISO 19005-2)" - }; - - outputDoc.Info.Title = inputDoc.Info.Title; - outputDoc.Info.Author = inputDoc.Info.Author; - outputDoc.Info.Subject = $"Prepared for archiving (targeting {standardLabel})"; - PdfFileHelper.SetFryPdfMetadata(outputDoc); - outputDoc.Options.CompressContentStreams = true; - - int pageCount = inputDoc.PageCount; - for (int i = 0; i < pageCount; i++) - { - ct.ThrowIfCancellationRequested(); - outputDoc.AddPage(inputDoc.Pages[i]); - progress?.Report(20.0 + (i / (double)pageCount * 60.0)); - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_PDFA.pdf"); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(outputDoc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new System.Collections.Generic.List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Prepared document targeting {standardLabel}. Note: this does not yet produce verified PDF/A-conformant output (no embedded XMP/pdfaid metadata, ICC output intent, or font-embedding/encryption checks) — do not rely on this for formal archival compliance." - }; - }, ct); - } - - private static string FormatFileSize(long bytes) - { - if (bytes < 1024) return $"{bytes} B"; - if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; - return $"{bytes / (1024.0 * 1024.0):F2} MB"; - } -} diff --git a/src/PdfEditorApp/Services/Tools/Security/PdfSecurityService.cs b/src/PdfEditorApp/Services/Tools/Security/PdfSecurityService.cs deleted file mode 100644 index 09f5def..0000000 --- a/src/PdfEditorApp/Services/Tools/Security/PdfSecurityService.cs +++ /dev/null @@ -1,562 +0,0 @@ -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfSharpCore.Drawing; -using PdfSharpCore.Pdf; -using PdfSharpCore.Pdf.IO; -using PdfSharpCore.Pdf.Security; -using QuestPDF.Fluent; - -namespace PdfEditorApp.Services.Tools.Security; - -public interface IPdfSecurityService -{ - Task ProtectPdfAsync(SecurityToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task UnlockPdfAsync(UnlockToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task SignPdfAsync(SignToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task RedactPdfAsync(RedactionToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task AddWatermarkAsync(WatermarkToolOptions options, IProgress? progress = null, CancellationToken ct = default); - - /// - /// Read-only: finds regions matching without writing anything. - /// Lets a preview UI show matches for review before committing to RedactPdfAsync. - /// - Task> FindRedactionMatchesAsync(string filePath, string pattern, bool caseSensitive, CancellationToken ct = default); -} - -public class PdfSecurityService : IPdfSecurityService -{ - private readonly IPdfPageService _pageService; - private readonly IQuestPdfOperationsEngine _questEngine; - - public PdfSecurityService(IQuestPdfOperationsEngine? questEngine = null, IPdfPageService? pageService = null) - { - _questEngine = questEngine ?? new QuestPdfOperationsEngine(); - _pageService = pageService ?? new PdfPageService(_questEngine); - } - - public Task AddWatermarkAsync(WatermarkToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return _pageService.AddWatermarkAsync(options, progress, ct); - } - - public async Task ProtectPdfAsync(SecurityToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - if (options.Engine == PdfProcessingEngine.QuestPdfNative) - { - var questResult = await _questEngine.EncryptAsync(options, progress, ct); - if (questResult.Success) - return questResult; - - System.Diagnostics.Debug.WriteLine($"QuestPDF encryption fallback: {questResult.ErrorMessage}"); - } - - return await ProtectWithPdfSharpAsync(options, progress, ct); - } - - private async Task ProtectWithPdfSharpAsync(SecurityToolOptions options, IProgress? progress, CancellationToken ct) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(20.0); - ct.ThrowIfCancellationRequested(); - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Protected.pdf"); - } - - string? targetDir = Path.GetDirectoryName(outPath); - if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) - Directory.CreateDirectory(targetDir); - - using var doc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Modify); - var security = doc.SecuritySettings; - - string userPwd = options.UserPassword ?? string.Empty; - string ownerPwd = options.OwnerPassword ?? string.Empty; - string? generatedOwnerPassword = null; - - if (!string.IsNullOrEmpty(userPwd)) - security.UserPassword = userPwd; - - if (!string.IsNullOrEmpty(ownerPwd)) - { - security.OwnerPassword = ownerPwd; - } - else if (!string.IsNullOrEmpty(userPwd)) - { - generatedOwnerPassword = GenerateRandomPassword(); - security.OwnerPassword = generatedOwnerPassword; - } - - security.PermitPrint = options.AllowPrinting; - security.PermitModifyDocument = options.AllowModifying; - security.PermitExtractContent = options.AllowCopying; - security.PermitAnnotations = options.AllowAnnotating; - security.PermitFormsFill = options.AllowFormFilling; - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, outPath); - progress?.Report(100.0); - - long stdOutBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = stdOutBytes, - Message = generatedOwnerPassword != null - ? $"Document successfully encrypted and secured with requested permission constraints. No owner password was provided, so a random one was generated (you'll need it later to change permissions): {generatedOwnerPassword}" - : "Document successfully encrypted and secured with requested permission constraints." - }; - }, ct); - } - - private static string GenerateRandomPassword(int length = 20) - { - const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*"; - var bytes = RandomNumberGenerator.GetBytes(length); - var sb = new StringBuilder(length); - foreach (var b in bytes) - { - sb.Append(chars[b % chars.Length]); - } - return sb.ToString(); - } - - public async Task UnlockPdfAsync(UnlockToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - if (options.Engine == PdfProcessingEngine.QuestPdfNative) - { - var questResult = await _questEngine.DecryptAsync(options, progress, ct); - if (questResult.Success) - return questResult; - - System.Diagnostics.Debug.WriteLine($"QuestPDF unlock fallback: {questResult.ErrorMessage}"); - } - - return await UnlockWithPdfSharpAsync(options, progress, ct); - } - - private async Task UnlockWithPdfSharpAsync(UnlockToolOptions options, IProgress? progress, CancellationToken ct) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(20.0); - ct.ThrowIfCancellationRequested(); - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Unlocked.pdf"); - } - - string? targetDir = Path.GetDirectoryName(outPath); - if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) - Directory.CreateDirectory(targetDir); - - try - { - using var inputDoc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Import, options.Password ?? ""); - using var outputDoc = new PdfDocument(); - - PdfFileHelper.SetFryPdfMetadata(outputDoc, inputDoc.Info.Title, inputDoc.Info.Author); - - int pageCount = inputDoc.PageCount; - for (int i = 0; i < pageCount; i++) - { - ct.ThrowIfCancellationRequested(); - outputDoc.AddPage(inputDoc.Pages[i]); - progress?.Report(20.0 + (i / (double)pageCount * 70.0)); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(outputDoc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = "Security restrictions and passwords successfully removed. Output PDF is fully unlocked." - }; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Failed to unlock PDF with provided credentials: {ex.Message}" - }; - } - }, ct); - } - - public async Task SignPdfAsync(SignToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(20.0); - - ct.ThrowIfCancellationRequested(); - using var doc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Modify); - - int targetPageIndex = Math.Max(0, Math.Min(doc.PageCount - 1, options.TargetPageNumber - 1)); - var page = doc.Pages[targetPageIndex]; - - using var gfx = XGraphics.FromPdfPage(page); - - // Draw visual signature badge - double x = options.X; - double y = options.Y; - double w = options.Width; - double h = options.Height; - - // Background badge border - var rectBrush = new XSolidBrush(XColor.FromArgb(245, 247, 250)); - var borderPen = new XPen(XColor.FromArgb(15, 108, 189), 1.5); - gfx.DrawRoundedRectangle(borderPen, rectBrush, x, y, w, h, 6, 6); - - // Draw cursive signature text or imported signature image - if (!string.IsNullOrWhiteSpace(options.SignatureImageDataUri) && options.SignatureImageDataUri.StartsWith("data:image")) - { - try - { - int commaIdx = options.SignatureImageDataUri.IndexOf(','); - string b64 = commaIdx >= 0 ? options.SignatureImageDataUri.Substring(commaIdx + 1) : options.SignatureImageDataUri; - byte[] imgBytes = Convert.FromBase64String(b64); - using var ms = new MemoryStream(imgBytes); - var xImg = XImage.FromStream(() => new MemoryStream(imgBytes)); - gfx.DrawImage(xImg, x + 8, y + 6, w - 16, h - 26); - } - catch { } - } - else - { - string sigFont = options.Style switch - { - SignatureStyle.CursiveElegance => "Times New Roman", - SignatureStyle.ClassicScript => "Georgia", - SignatureStyle.ModernHandwriting => "Courier New", - _ => "Helvetica" - }; - - var cursiveFont = new XFont(sigFont, 18, XFontStyle.Italic | XFontStyle.Bold); - var cursiveBrush = new XSolidBrush(XColor.FromArgb(15, 108, 189)); - gfx.DrawString(options.SignerName, cursiveFont, cursiveBrush, new XPoint(x + 10, y + 24)); - } - - // Draw cryptographic / verification badge metadata - var metaFont = new XFont("Helvetica", 8, XFontStyle.Regular); - var metaBrush = new XSolidBrush(XColor.FromArgb(100, 116, 139)); - string dateStr = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss 'UTC'"); - string reasonStr = !string.IsNullOrWhiteSpace(options.Reason) ? $" • {options.Reason}" : ""; - gfx.DrawString($"Digitally Verified: {options.SignerName}{reasonStr}", metaFont, metaBrush, new XPoint(x + 10, y + h - 14)); - gfx.DrawString($"Timestamp: {dateStr}", metaFont, metaBrush, new XPoint(x + 10, y + h - 4)); - - // If a certificate is provided, read it to label the visual badge with the - // signer identity it names. Note: this does NOT apply a cryptographic PDF - // signature (no /ByteRange or signature dictionary is embedded) — it's a - // visual badge only, so the document is not tamper-evident. Say so honestly - // rather than claiming "Digitally Signed", which would be a false integrity - // guarantee for anyone relying on it. - if (!string.IsNullOrEmpty(options.CertificatePath) && File.Exists(options.CertificatePath)) - { - try - { - var cert = System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadPkcs12FromFile(options.CertificatePath, options.CertificatePassword); - doc.Info.Subject = $"Visual signature badge referencing certificate '{cert.SubjectName.Name}' at {dateStr} — not a cryptographic PDF signature"; - } - catch { } - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Signed.pdf"); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(doc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new System.Collections.Generic.List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Placed electronic signature for '{options.SignerName}' on page {options.TargetPageNumber}." - }; - }, ct); - } - - public async Task RedactPdfAsync(RedactionToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(async () => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(10.0); - - ct.ThrowIfCancellationRequested(); - - // Start from any regions supplied directly (e.g. a UI that already resolved - // matches or manual marks for the user to review), then add matches for - // SearchPatternToRedact if one was also given. - var regions = new List(options.Regions); - string? pattern = options.SearchPatternToRedact?.Trim(); - if (!string.IsNullOrEmpty(pattern)) - { - var matched = await FindRedactionMatchesAsync(options.InputFilePath, pattern, options.CaseSensitive, ct); - regions.AddRange(matched); - } - - progress?.Report(30.0); - - if (regions.Count == 0) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = string.IsNullOrEmpty(pattern) - ? "No redaction regions were specified." - : $"No matches for \"{pattern}\" were found in this document — nothing was redacted." - }; - } - - var regionsByPage = regions - .Where(r => r.PageIndex >= 0) - .GroupBy(r => r.PageIndex) - .ToDictionary(g => g.Key, g => g.ToList()); - - using var inputDoc = PdfFileHelper.OpenDocumentSafely(options.InputFilePath, PdfDocumentOpenMode.Import); - using var outputDoc = new PdfDocument(); - - // For genuine (not just visual) sanitization, redacted pages are rendered to a - // raster image with the black boxes baked in, then that image becomes the - // page's ENTIRE content — the original text/vector data for that page is never - // copied into the output at all, so it can't be recovered by anyone who removes - // the black box or runs text extraction. Pages with no matches are left as - // normal, fully-selectable vector pages. - UglyToad.PdfPig.PdfDocument? pigForRender = null; - if (options.PermanentScrubText) - { - pigForRender = UglyToad.PdfPig.PdfDocument.Open(options.InputFilePath); - try { UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.AddSkiaPageFactory(pigForRender); } catch { } - } - - try - { - int totalRedactions = 0; - int totalPages = inputDoc.PageCount; - for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) - { - ct.ThrowIfCancellationRequested(); - bool hasRegions = regionsByPage.TryGetValue(pageIndex, out var pageRegions) && pageRegions!.Count > 0; - bool flattened = false; - - if (hasRegions && options.PermanentScrubText && pigForRender != null) - { - const float scale = 2.0f; // ~144 DPI: legible while keeping file size reasonable - using var pngStream = UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.GetPageAsPng(pigForRender, pageIndex + 1, scale, 100); - if (pngStream != null && pngStream.Length > 0) - { - pngStream.Position = 0; - using var bitmap = SkiaSharp.SKBitmap.Decode(pngStream); - if (bitmap != null) - { - using (var canvas = new SkiaSharp.SKCanvas(bitmap)) - using (var blackPaint = new SkiaSharp.SKPaint { Color = SkiaSharp.SKColors.Black, Style = SkiaSharp.SKPaintStyle.Fill }) - { - foreach (var r in pageRegions!) - { - canvas.DrawRect((float)(r.X * scale), (float)(r.Y * scale), (float)(r.Width * scale), (float)(r.Height * scale), blackPaint); - totalRedactions++; - } - } - - using var flattenedImage = SkiaSharp.SKImage.FromBitmap(bitmap); - using var flattenedData = flattenedImage.Encode(SkiaSharp.SKEncodedImageFormat.Png, 100); - var flattenedBytes = flattenedData.ToArray(); - - var srcPage = inputDoc.Pages[pageIndex]; - var newPage = outputDoc.AddPage(); - newPage.Width = srcPage.Width; - newPage.Height = srcPage.Height; - using var gfx = XGraphics.FromPdfPage(newPage); - using var xImage = XImage.FromStream(() => new MemoryStream(flattenedBytes)); - gfx.DrawImage(xImage, 0, 0, newPage.Width.Point, newPage.Height.Point); - flattened = true; - } - } - } - - if (!flattened) - { - var importedPage = outputDoc.AddPage(inputDoc.Pages[pageIndex]); - if (hasRegions) - { - using var gfx = XGraphics.FromPdfPage(importedPage); - var fillBrush = new XSolidBrush(XColors.Black); - foreach (var r in pageRegions!) - { - gfx.DrawRectangle(fillBrush, r.X, r.Y, r.Width, r.Height); - if (!string.IsNullOrWhiteSpace(r.Reason) && r.Width > 40 && r.Height > 12) - { - var reasonFont = new XFont("Helvetica", Math.Min(9, r.Height * 0.5), XFontStyle.Bold); - var reasonBrush = new XSolidBrush(XColors.White); - gfx.DrawString($"[{r.Reason}]", reasonFont, reasonBrush, new XPoint(r.X + 4, r.Y + (r.Height * 0.7))); - } - totalRedactions++; - } - } - } - - progress?.Report(30.0 + (pageIndex / (double)totalPages * 60.0)); - } - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Redacted.pdf"); - } - - PdfFileHelper.SaveDocumentWithFryPdfMetadata(outputDoc, outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = options.PermanentScrubText - ? $"Permanently redacted {totalRedactions} region(s) — affected pages were flattened to images so the underlying text cannot be recovered." - : $"Redacted {totalRedactions} region(s) with a visual overlay only — underlying text is still present in the PDF and can be recovered by anyone who removes the overlay. Enable \"Permanent Deep Content Sanitization\" for irrecoverable redaction." - }; - } - finally - { - pigForRender?.Dispose(); - } - }, ct); - } - - public async Task> FindRedactionMatchesAsync(string filePath, string pattern, bool caseSensitive, CancellationToken ct = default) - { - return await Task.Run(() => - { - var regions = new List(); - string? trimmed = pattern?.Trim(); - if (string.IsNullOrEmpty(trimmed) || !File.Exists(filePath)) return regions; - - var comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - string[] patternWords = trimmed.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - - void AddMatchRegions(int pageIndex, double pageHeight, IEnumerable matchedWords) - { - var lineGroups = matchedWords - .GroupBy(w => Math.Round(w.BoundingBox.Bottom / 4.0) * 4.0) - .OrderByDescending(g => g.Key); - - foreach (var line in lineGroups) - { - double left = line.Min(w => w.BoundingBox.Left); - double right = line.Max(w => w.BoundingBox.Right); - double top = line.Max(w => w.BoundingBox.Top); - double bottom = line.Min(w => w.BoundingBox.Bottom); - - regions.Add(new RedactionRegion - { - PageIndex = pageIndex, - X = left, - Y = Math.Max(0, pageHeight - top), - Width = Math.Max(1, right - left), - Height = Math.Max(1, top - bottom), - Reason = "Pattern match" - }); - } - } - - using var pigDoc = UglyToad.PdfPig.PdfDocument.Open(filePath); - for (int p = 1; p <= pigDoc.NumberOfPages; p++) - { - ct.ThrowIfCancellationRequested(); - var pigPage = pigDoc.GetPage(p); - double pageHeight = pigPage.Height; - var words = pigPage.GetWords().ToList(); - - if (patternWords.Length > 1) - { - for (int i = 0; i + patternWords.Length <= words.Count; i++) - { - bool match = true; - for (int k = 0; k < patternWords.Length; k++) - { - if (!string.Equals(words[i + k].Text, patternWords[k], comparison)) { match = false; break; } - } - if (match) - { - var matchedSubList = words.GetRange(i, patternWords.Length); - AddMatchRegions(p - 1, pageHeight, matchedSubList); - } - } - } - else if (patternWords.Length == 1) - { - foreach (var w in words) - { - if (w.Text.IndexOf(patternWords[0], comparison) >= 0) - { - AddMatchRegions(p - 1, pageHeight, new[] { w }); - } - } - } - } - - return regions; - }, ct); - } -} diff --git a/src/PdfEditorApp/Services/Tools/Security/QuestPdfOperationsEngine.cs b/src/PdfEditorApp/Services/Tools/Security/QuestPdfOperationsEngine.cs deleted file mode 100644 index f715fcd..0000000 --- a/src/PdfEditorApp/Services/Tools/Security/QuestPdfOperationsEngine.cs +++ /dev/null @@ -1,559 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Tools.Core; -using QuestPDF.Fluent; - -namespace PdfEditorApp.Services.Tools.Security; - -/// -/// High-performance, extensible QuestPDF native operations engine. -/// Encapsulates native Skia/C++ document operations: -/// multi-document merging with range syntax, AES-256 encryption, decryption, -/// Fast Web View linearization, vector PDF overlays/underlays, attachments, and XMP metadata. -/// -public interface IQuestPdfOperationsEngine -{ - Task MergeAsync(MergeToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task EncryptAsync(SecurityToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task DecryptAsync(UnlockToolOptions options, IProgress? progress = null, CancellationToken ct = default); - Task LinearizeAsync(string inputPath, string outputPath, IProgress? progress = null, CancellationToken ct = default); - Task ApplyLayerAsync(string inputPath, string layerPdfPath, bool isOverlay, string targetPages, string? outputPath = null, IProgress? progress = null, CancellationToken ct = default); - Task AddAttachmentAsync(string inputPath, string attachmentFilePath, string? description = null, string? mimeType = null, DocumentOperation.DocumentAttachmentRelationship relationship = DocumentOperation.DocumentAttachmentRelationship.Unspecified, string? outputPath = null, IProgress? progress = null, CancellationToken ct = default); - Task ExtendMetadataAsync(string inputPath, string customXmpXml, string? outputPath = null, IProgress? progress = null, CancellationToken ct = default); -} - -public class QuestPdfOperationsEngine : IQuestPdfOperationsEngine -{ - public async Task MergeAsync(MergeToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (options.InputFiles == null || options.InputFiles.Count == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "No input PDF files provided for merging." }; - - var validFiles = options.InputFiles.Where(File.Exists).ToList(); - if (validFiles.Count == 0) - return new ToolExecutionResult { Success = false, ErrorMessage = "None of the specified input PDF files exist." }; - - long totalOriginalBytes = validFiles.Sum(f => new FileInfo(f).Length); - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(validFiles[0]) ?? Path.GetTempPath(); - outPath = PdfFileHelper.ResolveSafeOutputPath(null, dir, "Merged_Document", ".pdf", validFiles); - } - - EnsureDirectoryExists(outPath); - progress?.Report(10.0); - ct.ThrowIfCancellationRequested(); - - try - { - string firstFile = validFiles[0]; - var op = DocumentOperation.LoadFile(firstFile); - - if (options.FilePageRanges != null && - options.FilePageRanges.TryGetValue(firstFile, out var firstRange) && - !string.IsNullOrWhiteSpace(firstRange)) - { - op = op.TakePages(firstRange); - } - - for (int i = 1; i < validFiles.Count; i++) - { - ct.ThrowIfCancellationRequested(); - string f = validFiles[i]; - string? range = null; - options.FilePageRanges?.TryGetValue(f, out range); - - op = string.IsNullOrWhiteSpace(range) - ? op.MergeFile(f) - : op.MergeFile(f, range); - - progress?.Report(10.0 + ((i + 1) / (double)validFiles.Count * 80.0)); - } - - op.Save(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = totalOriginalBytes, - OutputSizeBytes = outBytes, - Message = $"Successfully merged {validFiles.Count} files into {Path.GetFileName(outPath)} via QuestPDF Native Engine." - }; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"QuestPDF document merge failed: {ex.Message}" - }; - } - }, ct); - } - - public async Task EncryptAsync(SecurityToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(20.0); - ct.ThrowIfCancellationRequested(); - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Protected.pdf"); - } - - EnsureDirectoryExists(outPath); - - string userPwd = options.UserPassword ?? string.Empty; - string ownerPwd = options.OwnerPassword ?? string.Empty; - string? generatedOwnerPassword = null; - if (string.IsNullOrEmpty(ownerPwd) && !string.IsNullOrEmpty(userPwd)) - { - generatedOwnerPassword = GenerateRandomPassword(); - ownerPwd = generatedOwnerPassword; - } - - try - { - var op = DocumentOperation.LoadFile(options.InputFilePath); - switch (options.EncryptionLevel) - { - case PdfEncryptionLevel.Aes256Bit: - op = op.Encrypt(new DocumentOperation.Encryption256Bit - { - UserPassword = userPwd, - OwnerPassword = ownerPwd, - AllowPrinting = options.AllowPrinting, - AllowModification = options.AllowModifying, - AllowContentExtraction = options.AllowCopying, - AllowAnnotation = options.AllowAnnotating, - AllowFillingForms = options.AllowFormFilling, - AllowAssembly = options.AllowAssembly, - EncryptMetadata = options.EncryptMetadata - }); - break; - - case PdfEncryptionLevel.Aes128Bit: - op = op.Encrypt(new DocumentOperation.Encryption128Bit - { - UserPassword = userPwd, - OwnerPassword = ownerPwd, - AllowPrinting = options.AllowPrinting, - AllowModification = options.AllowModifying, - AllowContentExtraction = options.AllowCopying, - AllowAnnotation = options.AllowAnnotating, - AllowFillingForms = options.AllowFormFilling, - AllowAssembly = options.AllowAssembly, - EncryptMetadata = options.EncryptMetadata - }); - break; - - case PdfEncryptionLevel.Rc440Bit: - op = op.Encrypt(new DocumentOperation.Encryption40Bit - { - UserPassword = userPwd, - OwnerPassword = ownerPwd, - AllowPrinting = options.AllowPrinting, - AllowModification = options.AllowModifying, - AllowContentExtraction = options.AllowCopying, - AllowAnnotation = options.AllowAnnotating - }); - break; - } - - progress?.Report(85.0); - op.Save(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - string levelText = options.EncryptionLevel switch - { - PdfEncryptionLevel.Aes256Bit => "256-bit AES (QuestPDF Native)", - PdfEncryptionLevel.Aes128Bit => "128-bit AES", - _ => "40-bit Legacy" - }; - - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = generatedOwnerPassword != null - ? $"Document successfully encrypted with {levelText} and secured with requested permission constraints. Generated owner password: {generatedOwnerPassword}" - : $"Document successfully encrypted with {levelText} and secured with requested permission constraints." - }; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"QuestPDF encryption failed: {ex.Message}" - }; - } - }, ct); - } - - public async Task DecryptAsync(UnlockToolOptions options, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(options.InputFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(options.InputFilePath).Length; - progress?.Report(20.0); - ct.ThrowIfCancellationRequested(); - - string outPath = options.OutputFilePath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(options.InputFilePath) ?? ""; - string name = Path.GetFileNameWithoutExtension(options.InputFilePath); - outPath = Path.Combine(dir, $"{name}_Unlocked.pdf"); - } - - EnsureDirectoryExists(outPath); - - try - { - DocumentOperation - .LoadFile(options.InputFilePath, options.Password ?? string.Empty) - .Decrypt() - .RemoveRestrictions() - .Save(outPath); - - progress?.Report(100.0); - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = "Security restrictions and passwords successfully removed via QuestPDF Native Engine. Output PDF is fully unlocked." - }; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Failed to unlock PDF via QuestPDF: {ex.Message}" - }; - } - }, ct); - } - - public async Task LinearizeAsync(string inputPath, string outputPath, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(inputPath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - - long origBytes = new FileInfo(inputPath).Length; - progress?.Report(20.0); - ct.ThrowIfCancellationRequested(); - - string outPath = outputPath; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(inputPath) ?? ""; - string name = Path.GetFileNameWithoutExtension(inputPath); - outPath = Path.Combine(dir, $"{name}_Linearized.pdf"); - } - - EnsureDirectoryExists(outPath); - - try - { - bool inPlace = string.Equals(Path.GetFullPath(inputPath), Path.GetFullPath(outPath), StringComparison.OrdinalIgnoreCase); - string saveTarget = outPath; - string? tempFile = null; - - if (inPlace) - { - string dir = Path.GetDirectoryName(outPath) ?? Path.GetTempPath(); - tempFile = Path.Combine(dir, $"quest_lin_temp_{Guid.NewGuid():N}.pdf"); - saveTarget = tempFile; - } - - DocumentOperation.LoadFile(inputPath).Linearize().Save(saveTarget); - - if (inPlace && tempFile != null && File.Exists(tempFile)) - { - File.Move(tempFile, outPath, overwrite: true); - } - - progress?.Report(100.0); - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Document successfully linearized for Fast Web View ({FormatFileSize(outBytes)}). Viewers can stream and display Page 1 immediately." - }; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"QuestPDF linearization failed: {ex.Message}" - }; - } - }, ct); - } - - public async Task ApplyLayerAsync(string inputPath, string layerPdfPath, bool isOverlay, string targetPages, string? outputPath = null, IProgress? progress = null, CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(inputPath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - if (!File.Exists(layerPdfPath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Layer/Letterhead PDF file does not exist." }; - - long origBytes = new FileInfo(inputPath).Length; - progress?.Report(20.0); - ct.ThrowIfCancellationRequested(); - - string outPath = outputPath ?? string.Empty; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(inputPath) ?? ""; - string name = Path.GetFileNameWithoutExtension(inputPath); - string suffix = isOverlay ? "Overlay" : "Underlay"; - outPath = Path.Combine(dir, $"{name}_{suffix}.pdf"); - } - - EnsureDirectoryExists(outPath); - - try - { - var layerConfig = new DocumentOperation.LayerConfiguration - { - FilePath = layerPdfPath, - TargetPages = string.IsNullOrWhiteSpace(targetPages) ? "1-z" : targetPages, - SourcePages = "1", - RepeatSourcePages = "1" - }; - - var op = DocumentOperation.LoadFile(inputPath); - op = isOverlay ? op.OverlayFile(layerConfig) : op.UnderlayFile(layerConfig); - - progress?.Report(80.0); - op.Save(outPath); - progress?.Report(100.0); - - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - string layerType = isOverlay ? "Overlay" : "Letterhead / Underlay"; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Successfully applied {layerType} PDF to '{Path.GetFileName(outPath)}' via QuestPDF Native Engine." - }; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Failed to apply PDF layer: {ex.Message}" - }; - } - }, ct); - } - - public async Task AddAttachmentAsync( - string inputPath, - string attachmentFilePath, - string? description = null, - string? mimeType = null, - DocumentOperation.DocumentAttachmentRelationship relationship = DocumentOperation.DocumentAttachmentRelationship.Unspecified, - string? outputPath = null, - IProgress? progress = null, - CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(inputPath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - if (!File.Exists(attachmentFilePath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Attachment file does not exist." }; - - long origBytes = new FileInfo(inputPath).Length; - progress?.Report(20.0); - ct.ThrowIfCancellationRequested(); - - string outPath = outputPath ?? string.Empty; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(inputPath) ?? ""; - string name = Path.GetFileNameWithoutExtension(inputPath); - outPath = Path.Combine(dir, $"{name}_Attached.pdf"); - } - - EnsureDirectoryExists(outPath); - - try - { - var attach = new DocumentOperation.DocumentAttachment - { - FilePath = attachmentFilePath, - AttachmentName = Path.GetFileName(attachmentFilePath), - Description = description ?? "Embedded supporting document", - MimeType = mimeType ?? "application/octet-stream", - Relationship = relationship, - CreationDate = DateTime.UtcNow, - ModificationDate = DateTime.UtcNow, - Replace = true - }; - - DocumentOperation - .LoadFile(inputPath) - .AddAttachment(attach) - .Save(outPath); - - progress?.Report(100.0); - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = $"Successfully embedded attachment '{Path.GetFileName(attachmentFilePath)}' ({relationship}) via QuestPDF Native Engine." - }; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Failed to embed attachment: {ex.Message}" - }; - } - }, ct); - } - - public async Task ExtendMetadataAsync( - string inputPath, - string customXmpXml, - string? outputPath = null, - IProgress? progress = null, - CancellationToken ct = default) - { - return await Task.Run(() => - { - if (!File.Exists(inputPath)) - return new ToolExecutionResult { Success = false, ErrorMessage = "Input PDF file does not exist." }; - if (string.IsNullOrWhiteSpace(customXmpXml)) - return new ToolExecutionResult { Success = false, ErrorMessage = "No custom XMP metadata XML provided." }; - - long origBytes = new FileInfo(inputPath).Length; - progress?.Report(20.0); - ct.ThrowIfCancellationRequested(); - - string outPath = outputPath ?? string.Empty; - if (string.IsNullOrWhiteSpace(outPath)) - { - string dir = Path.GetDirectoryName(inputPath) ?? ""; - string name = Path.GetFileNameWithoutExtension(inputPath); - outPath = Path.Combine(dir, $"{name}_ExtendedXmp.pdf"); - } - - EnsureDirectoryExists(outPath); - - try - { - DocumentOperation - .LoadFile(inputPath) - .ExtendMetadata(customXmpXml) - .Save(outPath); - - progress?.Report(100.0); - long outBytes = File.Exists(outPath) ? new FileInfo(outPath).Length : 0; - return new ToolExecutionResult - { - Success = true, - OutputFilePath = outPath, - OutputFiles = new List { outPath }, - OriginalSizeBytes = origBytes, - OutputSizeBytes = outBytes, - Message = "Successfully extended document XMP metadata via QuestPDF Native Engine." - }; - } - catch (Exception ex) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"Failed to extend metadata: {ex.Message}" - }; - } - }, ct); - } - - private static void EnsureDirectoryExists(string filePath) - { - string? dir = Path.GetDirectoryName(filePath); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - } - - private static string GenerateRandomPassword(int length = 20) - { - const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*"; - var bytes = RandomNumberGenerator.GetBytes(length); - var sb = new StringBuilder(length); - foreach (var b in bytes) - { - sb.Append(chars[b % chars.Length]); - } - return sb.ToString(); - } - - private static string FormatFileSize(long bytes) - { - if (bytes < 1024) return $"{bytes} B"; - if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; - return $"{bytes / (1024.0 * 1024.0):F2} MB"; - } -} diff --git a/src/PdfEditorApp/Services/Typography/RichTextHelper.cs b/src/PdfEditorApp/Services/Typography/RichTextHelper.cs deleted file mode 100644 index 6bea7dc..0000000 --- a/src/PdfEditorApp/Services/Typography/RichTextHelper.cs +++ /dev/null @@ -1,732 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; -using System.Text.RegularExpressions; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Services.Typography; - -/// -/// High-performance parser and serializer for rich text formatting. -/// Translates between inline Markdown/HTML tags, plain text, and structured runs. -/// -public static class RichTextHelper -{ - private static readonly Regex ColorTagRegex = new(@"[#\w\d]+)>(?.*?)", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); - private static readonly Regex UnderlineTagRegex = new(@"(?.*?)", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); - private static readonly Regex LinkTagRegex = new(@"\[(?", EscapeXml(span.Text)); - } - sb.Append(""); - } - else - { - string escText = EscapeXml(line.Text); - sb.AppendFormat(Ci, "{0}", escText); - } - - // Double underline vector line if requested - if (el.IsDoubleUnderline) - { - double uY1 = line.BaselineY + 2; - double uY2 = line.BaselineY + 5; - sb.AppendFormat(Ci, "", line.X, uY1, line.X + line.Width, fontColor); - sb.AppendFormat(Ci, "", line.X, uY2, line.X + line.Width, fontColor); - } - } - } - else if (el.ShapeMode == TextShapeMode.BezierCurve) - { - var bezierLayout = CalculateBezierGlyphs( - el.Text, - el.FontFamily, - el.FontSize, - el.IsBold, - el.IsItalic, - el.Width, - el.Height, - new Point(el.BezierP0X, el.BezierP0Y), - new Point(el.BezierP1X, el.BezierP1Y), - new Point(el.BezierP2X, el.BezierP2Y), - new Point(el.BezierP3X, el.BezierP3Y), - el.CurveInvert, - el.CharacterSpacing, - el.BaselineShift - ); - - foreach (var g in bezierLayout.Glyphs) - { - string escChar = EscapeXml(g.Text); - sb.AppendFormat(Ci, "{0}", escChar); - } - } - else // Curved or Circular Mode - { - var curvedLayout = CalculateCurvedGlyphs( - el.Text, - el.FontFamily, - el.FontSize, - el.IsBold, - el.IsItalic, - el.Width, - el.Height, - el.CurveRadius, - el.CurveArcAngle, - el.CurveStartAngle, - el.CurveClockwise, - el.CurveInvert, - el.CharacterSpacing, - el.CircularPlacement, - el.ShapeMode, - el.BaselineShift - ); - - foreach (var g in curvedLayout.Glyphs) - { - string escChar = EscapeXml(g.Text); - sb.AppendFormat(Ci, "{0}", escChar); - } - } - - sb.Append(""); - sb.Append(""); - - return sb.ToString(); - } - - private static string EscapeXml(string unescaped) - { - if (string.IsNullOrEmpty(unescaped)) return ""; - return unescaped - .Replace("&", "&") - .Replace("<", "<") - .Replace(">", ">") - .Replace("\"", """) - .Replace("'", "'"); - } -} diff --git a/src/PdfEditorApp/Services/UiSettingsService.cs b/src/PdfEditorApp/Services/UiSettingsService.cs deleted file mode 100644 index 0ab2da1..0000000 --- a/src/PdfEditorApp/Services/UiSettingsService.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Services; - -/// -/// Default implementation of with JSON persistence. -/// -public class UiSettingsService : IUiSettingsService -{ - private readonly string _settingsFilePath; - private UiSettingsModel _settings; - private readonly object _lock = new(); - - private static readonly JsonSerializerOptions JsonOptions = new() - { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip, - Converters = { new JsonStringEnumConverter() } - }; - - public UiSettingsModel Settings - { - get - { - lock (_lock) - { - return _settings; - } - } - } - - public event Action? SettingsChanged; - - public UiSettingsService(string? customSettingsFilePath = null) - { - _settingsFilePath = customSettingsFilePath ?? GetDefaultSettingsFilePath(); - _settings = LoadSettings(); - } - - public static string GetDefaultSettingsFilePath() - { - return Path.Combine(ThemeService.GetSettingsDirectory(), "ui_settings.json"); - } - - private UiSettingsModel LoadSettings() - { - try - { - if (File.Exists(_settingsFilePath)) - { - var json = File.ReadAllText(_settingsFilePath); - var loaded = JsonSerializer.Deserialize(json, JsonOptions); - if (loaded != null) - { - return loaded; - } - } - } - catch - { - // Gracefully fall back to defaults if file is corrupt or unreadable - } - - return new UiSettingsModel(); - } - - public void Save() - { - lock (_lock) - { - try - { - _settings.UpdatedAt = DateTime.UtcNow; - var dir = Path.GetDirectoryName(_settingsFilePath); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - var json = JsonSerializer.Serialize(_settings, JsonOptions); - File.WriteAllText(_settingsFilePath, json); - } - catch - { - // Never crash the UI on disk write failure - } - } - - SettingsChanged?.Invoke(Settings); - } - - public void UpdateSettings(Action updateAction) - { - ArgumentNullException.ThrowIfNull(updateAction); - - lock (_lock) - { - updateAction(_settings); - } - - Save(); - } - - public void ResetToDefaults() - { - lock (_lock) - { - _settings = new UiSettingsModel(); - } - - Save(); - } -} diff --git a/src/PdfEditorApp/Services/UndoRedoService.cs b/src/PdfEditorApp/Services/UndoRedoService.cs deleted file mode 100644 index 63b35a2..0000000 --- a/src/PdfEditorApp/Services/UndoRedoService.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace PdfEditorApp.Services; - -public interface IUndoRedoService -{ - bool CanUndo { get; } - bool CanRedo { get; } - string? NextUndoDescription { get; } - string? NextRedoDescription { get; } - event EventHandler? StateChanged; - - void RecordAction(string description, Action undoAction, Action redoAction, Action? onDiscarded = null); - string? Undo(); - string? Redo(); - void Clear(); -} - -public class UndoRedoAction -{ - public string Description { get; set; } = ""; - public Action UndoAction { get; set; } = () => { }; - public Action RedoAction { get; set; } = () => { }; - - /// - /// Optional cleanup fired only when this action is discarded while it represents a - /// deleted (currently off-canvas) element — capacity eviction from the undo list, or a - /// full on document teardown. Never fired for the - /// automatic redo-stack clear on a new action, since a discarded redo-side delete means - /// the element was already restored and is currently visible. - /// - public Action? OnDiscarded { get; set; } -} - -public class UndoRedoService : IUndoRedoService -{ - private readonly LinkedList _undoList = new(); - private readonly Stack _redoStack = new(); - private const int MaxHistorySize = 100; - - public bool CanUndo => _undoList.Count > 0; - public bool CanRedo => _redoStack.Count > 0; - - public string? NextUndoDescription => _undoList.Last?.Value.Description; - public string? NextRedoDescription => _redoStack.Count > 0 ? _redoStack.Peek().Description : null; - - public event EventHandler? StateChanged; - - public void RecordAction(string description, Action undoAction, Action redoAction, Action? onDiscarded = null) - { - if (_undoList.Count >= MaxHistorySize) - { - DiscardAction(_undoList.First!.Value); - _undoList.RemoveFirst(); - } - - _undoList.AddLast(new UndoRedoAction - { - Description = description, - UndoAction = undoAction, - RedoAction = redoAction, - OnDiscarded = onDiscarded - }); - - _redoStack.Clear(); - StateChanged?.Invoke(this, EventArgs.Empty); - } - - private static void DiscardAction(UndoRedoAction action) - { - try - { - action.OnDiscarded?.Invoke(); - } - catch (Exception ex) - { - Debug.WriteLine($"[UndoRedoService] Error during OnDiscarded '{action.Description}': {ex}"); - } - } - - public string? Undo() - { - if (_undoList.Count == 0) return null; - - var node = _undoList.Last!; - _undoList.RemoveLast(); - var action = node.Value; - - try - { - action.UndoAction(); - } - catch (Exception ex) - { - Debug.WriteLine($"[UndoRedoService] Error during Undo '{action.Description}': {ex}"); - } - - _redoStack.Push(action); - StateChanged?.Invoke(this, EventArgs.Empty); - return action.Description; - } - - public string? Redo() - { - if (_redoStack.Count == 0) return null; - - var action = _redoStack.Pop(); - try - { - action.RedoAction(); - } - catch (Exception ex) - { - Debug.WriteLine($"[UndoRedoService] Error during Redo '{action.Description}': {ex}"); - } - - _undoList.AddLast(action); - StateChanged?.Invoke(this, EventArgs.Empty); - return action.Description; - } - - public void Clear() - { - // Document teardown: anything still holding a deleted element's resources (e.g. a - // decoded chart/image bitmap) is about to become unreachable anyway, since the whole - // document is being replaced — release it now instead of waiting on the GC. The redo - // stack is intentionally left alone: any delete sitting there was already undone, so - // the element it refers to is currently live elsewhere on the page. - foreach (var action in _undoList) - { - DiscardAction(action); - } - - _undoList.Clear(); - _redoStack.Clear(); - StateChanged?.Invoke(this, EventArgs.Empty); - } -} diff --git a/src/PdfEditorApp/Styles/FluentOfficeStyles.axaml b/src/PdfEditorApp/Styles/FluentOfficeStyles.axaml deleted file mode 100644 index ef48735..0000000 --- a/src/PdfEditorApp/Styles/FluentOfficeStyles.axaml +++ /dev/null @@ -1,670 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml b/src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml deleted file mode 100644 index 2a46642..0000000 --- a/src/PdfEditorApp/Styles/Material3ExpressiveStyles.axaml +++ /dev/null @@ -1,851 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml b/src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml deleted file mode 100644 index b493d67..0000000 --- a/src/PdfEditorApp/Styles/Material3ExpressiveTokens.axaml +++ /dev/null @@ -1,406 +0,0 @@ - - - - - - - - 0 - 4 - 8 - 12 - 16 - 28 - 9999 - - - 12,12,0,0 - 0,0,12,12 - 9999,0,0,9999 - 0,9999,9999,0 - - - 10 - 0 - 0 - 26 - 26 - 22 - 22 - 22 - 22 - 11 - 11 - - - avares://PdfEditorApp/Assets/Fonts#Roboto, avares://PdfEditorApp/Assets/Fonts#Inter, Segoe UI, -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif - avares://PdfEditorApp/Assets/Fonts#Roboto, Segoe UI, sans-serif - avares://PdfEditorApp/Assets/Fonts#FiraCode, avares://PdfEditorApp/Assets/Fonts#RobotoMono, Consolas, monospace - - 57 - 45 - 36 - 32 - 28 - 24 - 22 - 16 - 14 - 16 - 14 - 12 - 14 - 12 - 11 - - - 0 0 0 transparent - 0 1 3 #14000000, 0 1 2 #1E000000 - 0 2 6 #16000000, 0 4 8 #1C000000 - 0 4 12 #1A000000, 0 8 20 #20000000 - 0 6 18 #1E000000, 0 12 28 #22000000 - 0 8 24 #22000000, 0 16 36 #26000000 - - 0 8 24 #28000000, 0 24 64 #48000000 - - - - - - - - - - #1E5EEB - #FFFFFF - #E0EAFF - #001A42 - - - #4A607A - #FFFFFF - #D7E3F8 - #081D33 - - - #E05D38 - #FFFFFF - #FFDBD0 - #380D03 - - - #BA1A1A - #FFFFFF - #FFDAD6 - #410002 - - #198754 - #FFFFFF - #D1E7DD - #0A3622 - - #D97706 - #FFFFFF - #FEF3C7 - #78350F - - - #F8FAFD - #D9E2EC - #FFFFFF - #FFFFFF - #F1F5F9 - #E9EEF6 - #E1E8F2 - #D7E1EE - - - #191C20 - #44474E - #74777F - #C4C7D0 - #2E3135 - #F0F1F5 - #A8C7FA - #52000000 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #F8FAFD - #FFFFFF - #FFFFFF - #F1F5F9 - #E2E8F0 - #EDF2F7 - #EBF2FD - #D7E3F8 - #E0EAFF - #191C20 - #5B6471 - #8E97A6 - #1E5EEB - #1548B8 - #E0EAFF - #A8C7FA - #BA1A1A - #FFDAD6 - #FFB4AB - #198754 - #D1E7DD - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #A8C7FA - #062E6F - #0F387D - #D3E3FD - - - #BDC7DC - #273142 - #343E4E - #D9E3F8 - - - #FFB4A1 - #581E10 - #6C2C1E - #FFDBD0 - - - #FFB4AB - #690005 - #93000A - #FFDAD6 - - #75D59A - #00391A - #0F5229 - #A8F5C1 - - #FBBF24 - #451A03 - #78350F - #FEF3C7 - - - #101317 - #0D1013 - #343940 - #0B0E11 - #161A1F - #1C2128 - #252C36 - #2F3743 - - - #E2E2E6 - #9BA1AD - #8E919A - #3D4450 - #E2E2E6 - #191C20 - #1E5EEB - #80000000 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #101317 - #161A1F - #1C2128 - #0B0E11 - #2F3743 - #202630 - #252C36 - #343E4E - #182338 - #F8FAFC - #9BA1AD - #647082 - #A8C7FA - #7AA9F8 - #0F387D - #4D8DF7 - #FFB4AB - #4E0003 - #93000A - #75D59A - #0F5229 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Templates/Academic/AcademicPaperTemplate.cs b/src/PdfEditorApp/Templates/Academic/AcademicPaperTemplate.cs deleted file mode 100644 index 6070a97..0000000 --- a/src/PdfEditorApp/Templates/Academic/AcademicPaperTemplate.cs +++ /dev/null @@ -1,707 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Academic; - -public class AcademicPaperTemplate : ITemplateDefinition -{ - public string Id => "academic"; - public string Name => "Academic Research Paper"; - public string Description => "Full 2-page peer-reviewed research paper with mathematical equations, benchmark data table, author bios, and citations"; - public string Category => "Academic"; - public string IconKind => "BookOpenPageVariantOutline"; - public string AccentColorHex => "#D97706"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Research_Paper_Vector_Graphics_2026.pdf", - Author = "Dr. John Doe, Jane Doe, and Alex Doe", - Subject = "High-Performance Cross-Platform Desktop Vector Rendering" - }; - - // ========================================================================= - // PAGE 1: Title, Authors, Abstract, Introduction & Mathematical Theory - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "IEEE TRANSACTIONS ON SOFTWARE ENGINEERING • ISSN 0098-5589 • DOI: 10.1109/TSE.2026.892014", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Header Metadata Strip - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "IEEE TRANSACTIONS ON SOFTWARE ENGINEERING, VOL. 52, NO. 4, AUGUST 2026", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // Main Paper Title - new PdfTextElement - { - X = 55, - Y = 58, - Width = 690, - Height = 48, - Text = "High-Performance Cross-Platform Vector Graphics Architecture in Modern .NET Environments", - FontSize = 17, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center - }, - - // Authors & Institutional Affiliation - new PdfTextElement - { - X = 55, - Y = 108, - Width = 690, - Height = 34, - Text = "John Doe, Ph.D.¹, Jane Doe, M.Sc.², Alex Doe, Ph.D.¹\n¹Institute for Advanced Computing, CodeFryDev Institute • ²Department of Computer Science, CodeFryDev University", - FontSize = 9.5, - FontFamily = "Times New Roman", - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Center - }, - - // Abstract & Keywords Callout Card - new PdfShapeElement - { - X = 75, - Y = 145, - Width = 650, - Height = 104, - CornerRadius = 4, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 90, - Y = 150, - Width = 620, - Height = 68, - Text = "Abstract— We present a unified, hardware-accelerated rendering pipeline for interactive desktop publishing software running on Avalonia and SkiaSharp within .NET 9. By decoupling scene graph state from rendering primitives and implementing a zero-allocation spatial indexing tree, we achieve a 4.2× reduction in garbage collection stalls and consistent 60 FPS multi-touch canvas manipulation across Windows, macOS, and Linux.", - FontSize = 9, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Justify - }, - new PdfTextElement - { - X = 90, - Y = 222, - Width = 620, - Height = 22, - Text = "Index Terms— Vector graphics, SkiaSharp, Avalonia UI, cross-platform publishing, GPU shaders, garbage collection.", - FontSize = 8.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - - // Column Split Divider - new PdfDividerElement - { - X = 55, - Y = 254, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 262, - Width = 330, - Height = 22, - Text = "I. INTRODUCTION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 286, - Width = 330, - Height = 135, - Text = "Desktop publishing workflows impose rigorous demands on color space management, sub-pixel glyph rasterization, and interactive transform matrices [1]. Historically, applications relied on platform-specific APIs such as Direct2D on Windows and CoreGraphics on macOS, leading to divergent rendering artifacts and doubled maintenance overhead. Our framework leverages Skia-backed hardware surfaces unified under a reactive MVVM state model.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 425, - Width = 330, - Height = 22, - Text = "II. MATHEMATICAL FORMULATION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 449, - Width = 330, - Height = 70, - Text = "Let Ω ⊂ ℝ² define the bounded document viewport. For each vector primitive P_i with boundary curve ∂P_i, the continuous antialiased rasterization intensity I(p) at coordinate p = (x, y) is governed by the signed distance field filter:", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Formal Equation Box 1 - new PdfShapeElement - { - X = 55, - Y = 522, - Width = 330, - Height = 36, - CornerRadius = 3, - FillColorHex = "#F1F5F9", - StrokeColorHex = "#CBD5E1", - StrokeThickness = 0.5 - }, - new PdfTextElement - { - X = 65, - Y = 528, - Width = 310, - Height = 24, - Text = "I(p) = clamp( 1/2 - dist(p, ∂P_i) / w_pixel, 0, 1 ) (1)", - FontSize = 9.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center - }, - - new PdfTextElement - { - X = 55, - Y = 562, - Width = 330, - Height = 120, - Text = "Equation (1) enables constant-time sub-pixel coverage evaluation per fragment without requiring expensive multi-sample supersampling (MSAA) buffers [2]. The spatial complexity of the scene bounding hierarchy scales logarithmically with depth: O(log N) for hit testing across N = 10⁴ interactive vector shapes.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 686, - Width = 330, - Height = 22, - Text = "III. SCENE GRAPH & ZERO-COPY BUFFERING", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 710, - Width = 330, - Height = 145, - Text = "Managed runtime garbage collection cycles introduce non-deterministic micro-stutters during 60 Hz drag-and-drop operations. To overcome this, our scene graph uses contiguous struct spans (ReadOnlySpan) allocated through unmanaged NativeMemory pools. Viewport updates trigger differential buffer invalidations rather than total scene reconstructs.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 858, - Width = 330, - Height = 22, - Text = "IV. ADAPTIVE SHADER RASTERIZATION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 882, - Width = 330, - Height = 120, - Text = "On high-DPI displays (Apple Retina and 4K Windows monitors), pixel fill rate dominates frame time. We introduce an adaptive tessellation shader that subdivides cubic Bézier curves based on local curvature κ(t) = |x'y'' - y'x''| / (x'^2 + y'^2)^{3/2}, minimizing emitted vertex counts.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 262, - Width = 330, - Height = 22, - Text = "V. GPU PIPELINE & BACKEND TOPOLOGY", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 415, - Y = 286, - Width = 330, - Height = 135, - Text = "The backend abstracts DirectX 12 on Windows, Metal on macOS, and Vulkan on Linux through Skia's GRContext handle. Document layout matrices are pre-computed in SIMD registers (Vector256), which batches up to 1,024 vector paths into a single GPU draw call invocation.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 415, - Y = 425, - Width = 330, - Height = 22, - Text = "VI. EMPIRICAL BENCHMARK EVALUATION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 415, - Y = 449, - Width = 330, - Height = 55, - Text = "We evaluated rendering throughput across 1,000 synthetic multi-layer vector documents containing complex paths, radial gradients, and typography on Apple M3 Max (Metal) and Intel Core i9 (DirectX 12).", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Benchmark Data Table - new PdfTableElement - { - X = 415, - Y = 508, - Width = 330, - Height = 165, - Headers = new List { "Framework", "FPS", "RAM", "P99 GC", "Draw Lat." }, - Rows = new List> - { - new() { "WPF Direct2D", "34.2", "420 MB", "18.4 ms", "4.2 ms" }, - new() { "Web Electron", "28.5", "680 MB", "24.1 ms", "6.8 ms" }, - new() { "Qt QPainter", "52.1", "210 MB", "0.0 ms", "1.9 ms" }, - new() { "Proposed .NET 9", "59.8", "148 MB", "1.2 ms", "0.8 ms" } - }, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#CBD5E1" - }, - - new PdfTextElement - { - X = 415, - Y = 686, - Width = 330, - Height = 22, - Text = "VII. MEMORY PRESSURE & CACHE COHERENCE", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 415, - Y = 710, - Width = 330, - Height = 145, - Text = "As documented in Table I, the zero-allocation pipeline avoids Gen2 GC collections during rapid zooming and pan gestures. Cache miss rates for the spatial R-tree dropped from 14.2% in pointer-heavy structures to 2.1% in flat contiguous memory blocks.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 415, - Y = 858, - Width = 330, - Height = 22, - Text = "VIII. ACKNOWLEDGMENTS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 415, - Y = 882, - Width = 330, - Height = 120, - Text = "This research was supported by National Science Foundation Grant CCF-2490182, the MIT Center for Advanced Computing, and the Open Source Cross-Platform Software Foundation. Hardware testbeds were generously provided by Microsoft Corporation and Apple Computer.", - FontSize = 9, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Justify - } - } - }; - - // ========================================================================= - // PAGE 2: Discussion, Multi-Device Scalability, Author Bios & References - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "IEEE TRANSACTIONS ON SOFTWARE ENGINEERING • VOL. 52, NO. 4", - FooterRight = "Page 2 of 2", - Elements = new List - { - // Top Header Metadata Strip - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "VANCE ET AL.: HIGH-PERFORMANCE VECTOR GRAPHICS ARCHITECTURE IN .NET", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 62, - Width = 330, - Height = 22, - Text = "IX. EXTENSIBILITY & VECTOR PLUGINS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 86, - Width = 330, - Height = 130, - Text = "Our architecture exposes a modular interface IPdfElementRenderer enabling custom procedural shaders, SVG path decoders, and hardware-accelerated QR matrix synthesis. Because element state is decoupled from rendering logic, extensions can execute on worker threads using task parallelism without acquiring UI thread locks [3].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 222, - Width = 330, - Height = 22, - Text = "X. MULTI-DEVICE RESPONSIVE LATENCY", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 246, - Width = 330, - Height = 65, - Text = "Table II benchmarks touch drag latency across diverse hardware form factors (Apple Silicon MacBook, Intel Surface Laptop, and ARM64 Linux tablets).", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Multi-device Latency Table - new PdfTableElement - { - X = 55, - Y = 315, - Width = 330, - Height = 150, - Headers = new List { "Platform", "OS Backend", "Touch Lat.", "FPS" }, - Rows = new List> - { - new() { "MacBook Pro M3", "Metal 3", "6.2 ms", "120 FPS" }, - new() { "Surface Laptop 5", "DirectX 12", "7.8 ms", "60 FPS" }, - new() { "Dell XPS 15", "Vulkan", "8.1 ms", "60 FPS" }, - new() { "Raspberry Pi 5", "GLES 3.1", "14.5 ms", "54 FPS" } - }, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#CBD5E1" - }, - - new PdfTextElement - { - X = 55, - Y = 478, - Width = 330, - Height = 22, - Text = "XI. CONCLUSION & FUTURE DIRECTIONS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 502, - Width = 330, - Height = 135, - Text = "We demonstrated that unified cross-platform desktop publishing in managed .NET runtimes achieves near-native performance through hardware-accelerated Skia surfaces and memory-conscious data structures. Future work will investigate real-time collaborative document synchronization via Conflict-Free Replicated Data Types (CRDTs) and neural font upscaling on WebGPU.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - - // References Section - new PdfTextElement - { - X = 415, - Y = 62, - Width = 330, - Height = 22, - Text = "REFERENCES", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 415, - Y = 85, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - new PdfTextElement - { - X = 415, - Y = 90, - Width = 330, - Height = 220, - Text = "[1] J. D. Foley, A. van Dam, et al., Computer Graphics: Principles and Practice, 3rd ed., Addison-Wesley, 2021.\n[2] M. Kilgard and J. Bolz, \"GPU-accelerated path rendering,\" ACM Trans. Graph., vol. 31, no. 6, pp. 172:1–172:10, 2022.\n[3] Google Skia Engine Architecture Whitepaper, Google LLC, 2025.\n[4] D. E. Knuth, The TeXbook, Addison-Wesley, 1984.\n[5] ISO 32000-2:2020, Document Management — Portable Document Format — Part 2: PDF 2.0.\n[6] E. Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1994.\n[7] Avalonia UI Architecture & Reactive Binding Framework Specification, 2026.\n[8] Microsoft .NET 9 Performance and Garbage Collector Improvements Guide, Microsoft Press, 2025.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Justify - }, - - // Author Biographies Header - new PdfTextElement - { - X = 415, - Y = 320, - Width = 330, - Height = 22, - Text = "AUTHOR BIOGRAPHIES", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 415, - Y = 340, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // Bio 1: John Doe - new PdfShapeElement - { - X = 415, - Y = 350, - Width = 36, - Height = 36, - CornerRadius = 18, - FillColorHex = "#0F6CBD", - Label = "JD", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 460, - Y = 348, - Width = 285, - Height = 85, - Text = "John Doe received the Ph.D. in Computer Science in 2020. He is currently a Principal Research Scientist specializing in GPU compilers and vector rendering systems.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - // Bio 2: Jane Doe - new PdfShapeElement - { - X = 415, - Y = 445, - Width = 36, - Height = 36, - CornerRadius = 18, - FillColorHex = "#0284C7", - Label = "JD", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 460, - Y = 443, - Width = 285, - Height = 85, - Text = "Jane Doe received the M.Sc. in Electrical Engineering in 2022. Her research focuses on zero-allocation memory pools and low-latency user interfaces.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - // Bio 3: Alex Doe - new PdfShapeElement - { - X = 415, - Y = 540, - Width = 36, - Height = 36, - CornerRadius = 18, - FillColorHex = "#7E22CE", - Label = "AD", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 460, - Y = 538, - Width = 285, - Height = 85, - Text = "Alex Doe is an Associate Professor at CodeFryDev Institute. His research interests include human-computer interaction, digital document security, and typography rendering algorithms.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Academic/HistoryResearchPaperTemplate.cs b/src/PdfEditorApp/Templates/Academic/HistoryResearchPaperTemplate.cs deleted file mode 100644 index d759772..0000000 --- a/src/PdfEditorApp/Templates/Academic/HistoryResearchPaperTemplate.cs +++ /dev/null @@ -1,540 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Academic; - -public class HistoryResearchPaperTemplate : ITemplateDefinition -{ - public string Id => "historyresearch"; - public string Name => "Historical Research & Archival Monograph"; - public string Description => "Full 2-page historical research monograph with primary ledger records, bullion flow tables, notary archives, and manuscript citations"; - public string Category => "Academic"; - public string IconKind => "Bookshelf"; - public string AccentColorHex => "#92400E"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Mediterranean_Maritime_Trade_1250_1450_Research.pdf", - Author = "Prof. Jane Doe", - Subject = "Economic History & Medieval Mediterranean Archival Trade Networks" - }; - - // ========================================================================= - // PAGE 1: Title, Abstract, Archival Context, Bullion & Port Ledger Table - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFDF9", - FooterLeft = "MEDIEVAL MEDITERRANEAN MONOGRAPHS • VOL. 48 • ISSN 0928-5520", - FooterCenter = "HISTORICAL RESEARCH MONOGRAPH", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Header Rule & Monograph Series Metadata - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "JOURNAL OF MEDIEVAL ECONOMIC HISTORY, VOL. 48, PP. 112–148 • DOI: 10.1163/15700674-12345", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#D97706" - }, - - // Main Paper Title - new PdfTextElement - { - X = 55, - Y = 58, - Width = 690, - Height = 48, - Text = "Maritime Trade Networks, Bullion Flows, and Commercial Treaties in the Mediterranean Basin (1250–1450 CE)", - FontSize = 16, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03", - Alignment = TextAlignmentMode.Center - }, - - // Author & Institutional Affiliation - new PdfTextElement - { - X = 55, - Y = 108, - Width = 690, - Height = 34, - Text = "Jane Doe, Ph.D.\nChair of Medieval Economic History, CodeFryDev Institute of Historical Studies", - FontSize = 9.5, - FontFamily = "Times New Roman", - TextColorHex = "#522504", - Alignment = TextAlignmentMode.Center - }, - - // Abstract & Archival Keywords Card - new PdfShapeElement - { - X = 75, - Y = 145, - Width = 650, - Height = 104, - CornerRadius = 4, - FillColorHex = "#FEF3C7", - StrokeColorHex = "#FDE68A", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 90, - Y = 150, - Width = 620, - Height = 68, - Text = "Abstract— Drawing upon newly transcribed notary cartularies from the Archivio di Stato di Venezia and the Datini Archives in Prato, this study examines the velocity of gold specie (ducats and florins) in financing Levantine spice and silk commerce between 1250 and 1450 CE. We demonstrate that merchant syndicates mitigated currency depreciation through sophisticated maritime loans and reciprocal credit instruments.", - FontSize = 9, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Justify - }, - new PdfTextElement - { - X = 90, - Y = 222, - Width = 620, - Height = 22, - Text = "Keywords— Mediterranean trade, Venetian notary registers, bullion flows, bills of exchange, Pax Mongolica.", - FontSize = 8.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#92400E" - }, - - // Column Split Divider - new PdfDividerElement - { - X = 55, - Y = 254, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#D97706" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 262, - Width = 330, - Height = 22, - Text = "I. HISTORIOGRAPHICAL CONTEXT & ARCHIVES", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfTextElement - { - X = 55, - Y = 286, - Width = 330, - Height = 110, - Text = "The Commercial Revolution of the thirteenth century reshaped the financial structures of Western Christendom [1]. Venetian and Genoese maritime hegemony relied not merely on naval superiority, but on legal innovations such as the colleganza and the development of double-entry bookkeeping recorded in notarized registers (cartulari notarili) across Mediterranean emporia [2].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 402, - Width = 330, - Height = 22, - Text = "II. BULLION FLOWS & CURRENCY SPECIE", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfTextElement - { - X = 55, - Y = 426, - Width = 330, - Height = 120, - Text = "The introduction of the gold ducat (ducato d'oro) in 1284 established an international standard of exchange. Our quantitative examination of 2,400 notarized contracts in Famagusta and Alexandria reveals that an estimated 350,000 to 500,000 gold ducats flowed annually through Levantine ports to purchase Indonesian spices and Persian silks [3].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Archival Quotation Callout - new PdfShapeElement - { - X = 55, - Y = 552, - Width = 330, - Height = 70, - CornerRadius = 3, - FillColorHex = "#FEF3C7", - StrokeColorHex = "#FDE68A", - StrokeThickness = 0.5 - }, - new PdfTextElement - { - X = 68, - Y = 558, - Width = 304, - Height = 58, - Text = "\"In nomine Domini, anno 1342... Ego Nicoleto de Contareno fateor recepisse a te... ducatos auri ducentos quinquaginta in colleganza pro navigando versus Alexandriam.\"\n— ASVe, Notarile, Busta 142, fol. 18r.", - FontSize = 8.5, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.3, - TextColorHex = "#78350F" - }, - - new PdfTextElement - { - X = 55, - Y = 632, - Width = 330, - Height = 22, - Text = "III. COMMERCIAL TREATIES & GUILD PRIVILEGES", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfTextElement - { - X = 55, - Y = 656, - Width = 330, - Height = 160, - Text = "Bilateral treaties signed between the Republic of Venice and Mamluk Sultans in Cairo (notably the 1345 and 1375 chrysobulls) fixed customs tariffs at 5% ad valorem for pepper and ginger, while granting extraterritorial legal jurisdiction to the Venetian consul (bailo) stationed in Alexandria and Aleppo [4].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 262, - Width = 330, - Height = 22, - Text = "IV. QUANTITATIVE TRADE DATA ACROSS PORTS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfTextElement - { - X = 415, - Y = 286, - Width = 330, - Height = 58, - Text = "Table I details the estimated annual gold specie inflows, primary exported commodities, and average customs duty rates across major Mediterranean trade depots based on consular customs ledgers (1300–1400 CE).", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Historical Archival Table - new PdfTableElement - { - X = 415, - Y = 348, - Width = 330, - Height = 145, - Headers = new List { "Trading Port", "Commodity", "Gold Flow (Ducats)", "Tariff" }, - Rows = new List> - { - new() { "Alexandria (Egypt)", "Spices / Pepper", "380,000", "5.0%" }, - new() { "Constantinople", "Silk / Grain", "240,000", "3.5%" }, - new() { "Famagusta (Cyprus)", "Sugar / Cotton", "160,000", "4.0%" }, - new() { "Ragusa (Dubrovnik)", "Silver / Timber", "120,000", "2.0%" } - }, - HeaderBackgroundHex = "#92400E", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#FEF3C7", - BorderColorHex = "#CBD5E1" - }, - - new PdfTextElement - { - X = 415, - Y = 502, - Width = 330, - Height = 22, - Text = "V. MARITIME LAW & BILLS OF EXCHANGE", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfTextElement - { - X = 415, - Y = 526, - Width = 330, - Height = 115, - Text = "The emergence of the lettera di cambio (bill of exchange) permitted merchant bankers in Florence, Venice, and Genoa to settle accounts without physically transporting silver and gold across pirate-infested shipping lanes, effectively multiplying liquidity across Europe [5].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 415, - Y = 648, - Width = 330, - Height = 22, - Text = "VI. CONVOY SHIPPING & THE MUDA SYSTEM", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfTextElement - { - X = 415, - Y = 672, - Width = 330, - Height = 145, - Text = "The Venetian State organized biannual state-sponsored galley convoys (mude) departing for Flanders, Beirut, and Alexandria. Auctioning cargo space on armed great galleys minimized piracy risks and created standardized freight insurance rates of 2.5%–4.0% [6].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - } - } - }; - - // ========================================================================= - // PAGE 2: Silk Road Links, Datini Archives, Author Bio & References - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFDF9", - FooterLeft = "MEDIEVAL MEDITERRANEAN MONOGRAPHS • VOL. 48", - FooterRight = "Page 2 of 2", - Elements = new List - { - // Top Header Metadata Strip - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "DE MONTMIRAIL: MEDITERRANEAN MARITIME TRADE (1250–1450 CE)", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#D97706" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 62, - Width = 330, - Height = 22, - Text = "VII. OVERLAND SILK ROAD LINKAGES", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfTextElement - { - X = 55, - Y = 86, - Width = 330, - Height = 135, - Text = "During the Pax Mongolica, maritime routes converged with overland caravan routes terminating at Trebizond, Tabriz, and Ayas. Venetian and Genoese factors established permanent trading houses (fondachi) in the Black Sea port of Caffa, exporting Crimean wheat and raw silk westward in exchange for Flemish woolen textiles and German silver ingots [7].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 228, - Width = 330, - Height = 22, - Text = "VIII. CONCLUSION & HISTORIOGRAPHY", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfTextElement - { - X = 55, - Y = 252, - Width = 330, - Height = 135, - Text = "The pre-modern Mediterranean was not an arena of irreconcilable religious clash, but a deeply integrated commercial network held together by institutional legal norms, shared currency weights, and mutual commercial interest. These notary registers prove that capital velocity was far higher than traditional historiography assumed.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Author Biography Header - new PdfTextElement - { - X = 55, - Y = 395, - Width = 330, - Height = 22, - Text = "AUTHOR BIOGRAPHY", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfDividerElement - { - X = 55, - Y = 415, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#D97706" - }, - - new PdfShapeElement - { - X = 55, - Y = 425, - Width = 36, - Height = 36, - CornerRadius = 18, - FillColorHex = "#92400E", - Label = "HM", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 100, - Y = 423, - Width = 285, - Height = 90, - Text = "Henriette de Montmirail is Professor of Medieval Economic History at EHESS Paris and Fellow of the Royal Historical Society. She has authored four books on Mediterranean maritime law and medieval banking cartularies.", - FontSize = 8.5, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 62, - Width = 330, - Height = 22, - Text = "PRIMARY SOURCES & BIBLIOGRAPHY", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#451A03" - }, - new PdfDividerElement - { - X = 415, - Y = 85, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#D97706" - }, - new PdfTextElement - { - X = 415, - Y = 90, - Width = 330, - Height = 270, - Text = "[1] F. Braudel, The Mediterranean and the Mediterranean World in the Age of Philip II, Harper & Row, 1972.\n[2] R. S. Lopez, The Commercial Revolution of the Middle Ages, 950–1350, Cambridge Univ. Press, 1976.\n[3] E. Ashtor, Levant Trade in the Later Middle Ages, Princeton Univ. Press, 1983.\n[4] F. C. Lane, Venice: A Maritime Republic, Johns Hopkins Univ. Press, 1973.\n[5] R. de Roover, The Rise and Decline of the Medici Bank, 1397–1494, Harvard Univ. Press, 1963.\n[6] D. Jacoby, Commercial Exchange Across the Mediterranean, Variorum, 2005.\n[7] M. Balard, La Romanie Génoise (XIIe–début du XVe siècle), École Française de Rome, 1978.\n[8] Archivio di Stato di Venezia (ASVe), Procuratori di San Marco, de Citra, Buste 140–162.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Justify - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Academic/MathResearchPaperTemplate.cs b/src/PdfEditorApp/Templates/Academic/MathResearchPaperTemplate.cs deleted file mode 100644 index 1ac3908..0000000 --- a/src/PdfEditorApp/Templates/Academic/MathResearchPaperTemplate.cs +++ /dev/null @@ -1,629 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Academic; - -public class MathResearchPaperTemplate : ITemplateDefinition -{ - public string Id => "mathresearch"; - public string Name => "Mathematics Research Paper"; - public string Description => "Full 2-page pure & applied mathematics paper with discrete Hodge Laplacians, theorem proofs, spectral tables, and citations"; - public string Category => "Academic"; - public string IconKind => "MathCompass"; - public string AccentColorHex => "#4338CA"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Discrete_Hodge_Laplacians_Simplicial_Manifolds_2026.pdf", - Author = "Prof. John Doe & Dr. Jane Doe", - Subject = "Discrete Differential Geometry & Topological Data Analysis" - }; - - // ========================================================================= - // PAGE 1: Title, Abstract, Discrete Hodge Formulation & Proofs - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "ANNALS OF MATHEMATICS • VOL. 198, NO. 3 • MSC2020: 55N31, 58J50, 05E45", - FooterCenter = "MATHEMATICAL RESEARCH ARTICLE", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Header Rule & Journal Metadata - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "ANNALS OF MATHEMATICS, VOL. 198 (2026), ISSUE 3, PP. 711–754", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // Main Paper Title - new PdfTextElement - { - X = 55, - Y = 58, - Width = 690, - Height = 48, - Text = "Discrete Hodge Laplacians and Persistent Cohomology on High-Dimensional Simplicial Manifolds", - FontSize = 17, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B", - Alignment = TextAlignmentMode.Center - }, - - // Authors & Institutional Affiliation - new PdfTextElement - { - X = 55, - Y = 108, - Width = 690, - Height = 34, - Text = "John Doe¹ and Jane Doe²\n¹Department of Mathematics, CodeFryDev Institute • ²Mathematical Institute, CodeFryDev Academy", - FontSize = 9.5, - FontFamily = "Times New Roman", - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Center - }, - - // Abstract & MSC Classification Card - new PdfShapeElement - { - X = 75, - Y = 145, - Width = 650, - Height = 104, - CornerRadius = 4, - FillColorHex = "#F5F3FF", - StrokeColorHex = "#DDD6FE", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 90, - Y = 150, - Width = 620, - Height = 68, - Text = "Abstract— We establish spectral convergence bounds for higher-order discrete Hodge Laplacians Δ_k acting on real cochain complexes of triangulated Riemannian manifolds. We prove that the persistent harmonic cochains of a filtration asymptotically recover the continuous L² de Rham cohomology, and provide explicit lower bounds on the first non-zero eigenvalue λ₁(Δ_k) in terms of combinatorial Ricci curvature.", - FontSize = 9, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#1E1B4B", - Alignment = TextAlignmentMode.Justify - }, - new PdfTextElement - { - X = 90, - Y = 222, - Width = 620, - Height = 22, - Text = "MSC 2020 Classification— 55N31 (Persistent homology), 58J50 (Spectral theory on manifolds), 05E45 (Simplicial complexes).", - FontSize = 8.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#4338CA" - }, - - // Column Split Divider - new PdfDividerElement - { - X = 55, - Y = 254, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 262, - Width = 330, - Height = 22, - Text = "1. INTRODUCTION & PRELIMINARIES", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 55, - Y = 286, - Width = 330, - Height = 110, - Text = "Let K be a finite, oriented simplicial complex of dimension n. For each integer k ∈ {0, …, n}, let C_k(K; ℝ) denote the vector space of real k-chains, endowed with the canonical inner product making the oriented simplices an orthonormal basis. The combinatorial coboundary operator δ_k: C^k(K; ℝ) → C^{k+1}(K; ℝ) is the adjoint of the boundary operator ∂_{k+1}.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 402, - Width = 330, - Height = 22, - Text = "2. DISCRETE HODGE LAPLACIAN", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 55, - Y = 426, - Width = 330, - Height = 45, - Text = "Definition 2.1. The k-th discrete Hodge Laplacian Δ_k: C^k(K; ℝ) → C^k(K; ℝ) is the self-adjoint, positive semi-definite operator defined by:", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Formal Hodge Laplacian Math Equation - new PdfMathElement - { - X = 55, - Y = 475, - Width = 330, - Height = 38, - Formula = @"\Delta_k = \delta_{k-1} \partial_k + \partial_{k+1} \delta_k = \Delta_k^{\text{down}} + \Delta_k^{\text{up}}", - FontSize = 10, - TextColorHex = "#1E1B4B", - BackgroundColorHex = "#F5F3FF", - BorderColorHex = "#DDD6FE", - BorderThickness = 0.5, - CornerRadius = 3, - ShowBackground = true, - ShowBorder = true, - ShowEquationNumber = true, - EquationNumber = "(2.1)", - Alignment = TextAlignmentMode.Center, - Category = MathCategory.Geometry, - PresetName = "Discrete Hodge Laplacian" - }, - - new PdfTextElement - { - X = 55, - Y = 516, - Width = 330, - Height = 22, - Text = "Theorem 2.2 (Discrete Hodge Decomposition).", - FontSize = 10, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 55, - Y = 540, - Width = 330, - Height = 145, - Text = "For any finite simplicial complex K, there exists an orthogonal direct sum decomposition:\n\nC^k(K; ℝ) = im(δ_{k-1}) ⊕ im(∂_{k+1}) ⊕ ker(Δ_k).\n\nFurthermore, there is a canonical isomorphism between the space of harmonic k-cochains and the k-th simplicial cohomology group: ker(Δ_k) ≅ H^k(K; ℝ), yielding dim ker(Δ_k) = β_k(K), where β_k is the k-th Betti number.", - FontSize = 9, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#1F2937", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 690, - Width = 330, - Height = 22, - Text = "3. SPECTRAL GAP & CURVATURE BOUNDS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 55, - Y = 714, - Width = 330, - Height = 140, - Text = "Lemma 3.1 (Bochner-Weitzenböck Formula). Let Ric_k(K) denote the Forman combinatorial Ricci curvature on k-simplices [4]. If Ric_k(K) ≥ κ > 0 for all σ ∈ K_k, then the first non-zero eigenvalue satisfies λ₁(Δ_k) ≥ κ. Consequently, whenever κ > 0, the k-th reduced cohomology group vanishes: H̃^k(K; ℝ) = 0.", - FontSize = 9, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 262, - Width = 330, - Height = 22, - Text = "4. NUMERICAL EIGENVALUE COMPUTATION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 415, - Y = 286, - Width = 330, - Height = 58, - Text = "We evaluated the spectral spectrum of Δ₁ on fine triangulations of standard smooth surfaces (2-Sphere S², Torus T², Klein Bottle K², and 3-Torus T³). Sparse Lanczos iterations were computed via ARPACK.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Analytical Spectral Table - new PdfTableElement - { - X = 415, - Y = 348, - Width = 330, - Height = 145, - Headers = new List { "Manifold (M)", "Dim", "β₁(M)", "λ₁(Δ₁)", "ρ(Δ₁)" }, - Rows = new List> - { - new() { "Sphere S²", "2", "0", "1.9842", "12.00" }, - new() { "Torus T²", "2", "2", "0.4931", "11.85" }, - new() { "Klein Bottle K²", "2", "1", "0.4810", "11.90" }, - new() { "3-Torus T³", "3", "3", "0.2465", "18.00" } - }, - HeaderBackgroundHex = "#4338CA", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F5F3FF", - BorderColorHex = "#CBD5E1" - }, - - new PdfTextElement - { - X = 415, - Y = 502, - Width = 330, - Height = 22, - Text = "5. PERSISTENT COHOMOLOGY ALGORITHM", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 415, - Y = 526, - Width = 330, - Height = 115, - Text = "Given a filtered simplicial complex ∅ = K₀ ⊂ K₁ ⊂ ⋯ ⊂ K_m = K, the persistence diagram Dgm_k is computed via matrix reduction on the boundary matrix. The harmonic representatives ω_i ∈ ker(Δ_k(K_t)) provide geometric localization for persistent topological features across filtration scales [3].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 415, - Y = 648, - Width = 330, - Height = 22, - Text = "6. CONTINUOUS ASYMPTOTIC CONVERGENCE", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 415, - Y = 672, - Width = 330, - Height = 145, - Text = "Theorem 6.1 (Spectral Convergence). Let (M, g) be a smooth compact Riemannian manifold and let {K_h}_{h>0} be a family of Delaunay triangulations with mesh size h → 0 satisfying the minimal angle condition. Then for each k, the spectrum of Δ_k(K_h) converges in the Hausdorff metric to the spectrum of the continuous Hodge-de Rham Laplacian Δ_k^M on differential k-forms.", - FontSize = 9, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#1F2937", - Alignment = TextAlignmentMode.Justify - } - } - }; - - // ========================================================================= - // PAGE 2: Heat Trace, Zeta Functions, Author Bios & References - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "ANNALS OF MATHEMATICS • VOL. 198, NO. 3", - FooterRight = "Page 2 of 2", - Elements = new List - { - // Top Header Metadata Strip - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "EISENBUD & MONTGOMERY: DISCRETE HODGE LAPLACIANS ON MANIFOLDS", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 62, - Width = 330, - Height = 22, - Text = "7. DISCRETE HEAT KERNEL & SPECTRAL ZETA", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 55, - Y = 86, - Width = 330, - Height = 70, - Text = "The discrete heat operator H_k(t) = exp(-t Δ_k) admits a spectral expansion governed by the eigenvalues 0 ≤ λ₁ ≤ λ₂ ≤ ⋯ ≤ λ_N. The combinatorial spectral zeta function is defined for Re(s) > 0 by:", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Formal Spectral Zeta Function Math Equation - new PdfMathElement - { - X = 55, - Y = 160, - Width = 330, - Height = 38, - Formula = @"\zeta_k(s) = \mathrm{Tr}\left( (\Delta_k |_{\ker(\Delta_k)^\perp})^{-s} \right) = \sum_{j=1}^{N - \beta_k} \lambda_j^{-s}", - FontSize = 9.5, - TextColorHex = "#1E1B4B", - BackgroundColorHex = "#F5F3FF", - BorderColorHex = "#DDD6FE", - BorderThickness = 0.5, - CornerRadius = 3, - ShowBackground = true, - ShowBorder = true, - ShowEquationNumber = true, - EquationNumber = "(7.1)", - Alignment = TextAlignmentMode.Center, - Category = MathCategory.Calculus, - PresetName = "Spectral Zeta Function" - }, - - new PdfTextElement - { - X = 55, - Y = 202, - Width = 330, - Height = 120, - Text = "The derivative at zero, ζ'_k(0), yields the discrete Ray-Singer analytic torsion, which we prove is a topological invariant of the complex K under simplicial subdivision and Reidemeister moves [5]. This extends Cheeger's theorem to combinatorial cell complexes.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 330, - Width = 330, - Height = 22, - Text = "8. CONCLUDING THEORETICAL REMARKS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfTextElement - { - X = 55, - Y = 354, - Width = 330, - Height = 125, - Text = "Our results establish that higher-order discrete Hodge Laplacians faithfully reflect the underlying geometry and topology of continuous Riemannian manifolds. Future extensions will investigate discrete spin Dirac operators D on simplicial spin manifolds and non-linear p-Laplacian eigenvalues.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Author Biographies (Left Column) - new PdfTextElement - { - X = 55, - Y = 490, - Width = 330, - Height = 22, - Text = "AUTHOR BIOGRAPHIES", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfDividerElement - { - X = 55, - Y = 510, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // Bio 1: David Eisenbud - new PdfShapeElement - { - X = 55, - Y = 520, - Width = 34, - Height = 34, - CornerRadius = 17, - FillColorHex = "#4338CA", - Label = "DE", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 98, - Y = 518, - Width = 287, - Height = 65, - Text = "David H. Eisenbud is Professor of Mathematics at UC Berkeley and former Director of MSRI. His work spans commutative algebra, algebraic geometry, and discrete Hodge theory.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - // Bio 2: Claire Montgomery - new PdfShapeElement - { - X = 55, - Y = 590, - Width = 34, - Height = 34, - CornerRadius = 17, - FillColorHex = "#6366F1", - Label = "CM", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 98, - Y = 588, - Width = 287, - Height = 65, - Text = "Claire Montgomery is a Royal Society University Research Fellow at Oxford University. Her research focuses on persistent spectral geometry and high-dimensional topological data analysis.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 62, - Width = 330, - Height = 22, - Text = "REFERENCES", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E1B4B" - }, - new PdfDividerElement - { - X = 415, - Y = 85, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - new PdfTextElement - { - X = 415, - Y = 90, - Width = 330, - Height = 270, - Text = "[1] W. V. D. Hodge, The Theory and Applications of Harmonic Integrals, Cambridge University Press, 1941.\n[2] H. Edelsbrunner and J. L. Harer, Computational Topology: An Introduction, American Mathematical Society, 2010.\n[3] G. Carlsson, \"Topology and data,\" Bull. Amer. Math. Soc., vol. 46, no. 2, pp. 255–308, 2009.\n[4] R. Forman, \"Bochner's method for cell complexes and combinatorial Ricci curvature,\" Discrete Comput. Geom., vol. 29, pp. 323–374, 2003.\n[5] M. F. Atiyah and I. M. Singer, \"The index of elliptic operators: I,\" Ann. of Math., vol. 87, pp. 484–530, 1968.\n[6] J. Cheeger, \"Analytic torsion and the heat equation,\" Ann. of Math., vol. 109, pp. 259–322, 1979.\n[7] D. B. Ray and I. M. Singer, \"R-torsion and the Laplacian on Riemannian manifolds,\" Adv. Math., vol. 7, pp. 145–210, 1971.\n[8] J. Dodziuk, \"Finite-difference approach to the Hodge theory of harmonic forms,\" Amer. J. Math., vol. 98, pp. 79–104, 1976.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Justify - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Academic/PhysicsResearchPaperTemplate.cs b/src/PdfEditorApp/Templates/Academic/PhysicsResearchPaperTemplate.cs deleted file mode 100644 index c240b58..0000000 --- a/src/PdfEditorApp/Templates/Academic/PhysicsResearchPaperTemplate.cs +++ /dev/null @@ -1,616 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Academic; - -public class PhysicsResearchPaperTemplate : ITemplateDefinition -{ - public string Id => "physicsresearch"; - public string Name => "Physics Research Paper"; - public string Description => "Full 2-page quantum physics paper with Hamiltonian equations, Lindblad dissipation, Wigner tomography, and spectroscopy data"; - public string Category => "Academic"; - public string IconKind => "Atom"; - public string AccentColorHex => "#0891B2"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Cavity_QED_Superconducting_Qubits_2026.pdf", - Author = "Dr. John Doe, Dr. Jane Doe, and Prof. Alex Doe", - Subject = "Quantum Electrodynamics & Non-Equilibrium Phase Transitions" - }; - - // ========================================================================= - // PAGE 1: Title, Abstract, Dicke Hamiltonian, Master Equation & Data - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "PHYSICAL REVIEW LETTERS • VOL. 136, 140401 (2026) • PACS: 03.67.Lx, 42.50.Pq, 85.25.Cp", - FooterCenter = "QUANTUM PHYSICS RESEARCH", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Header Rule & Journal Metadata - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "PHYSICAL REVIEW LETTERS 136, 140401 (2026) • DOI: 10.1103/PhysRevLett.136.140401", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // Main Paper Title - new PdfTextElement - { - X = 55, - Y = 58, - Width = 690, - Height = 48, - Text = "Cavity Quantum Electrodynamics and Non-Equilibrium Phase Transitions in Superconducting Qubits", - FontSize = 17, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490", - Alignment = TextAlignmentMode.Center - }, - - // Authors & Affiliation - new PdfTextElement - { - X = 55, - Y = 108, - Width = 690, - Height = 34, - Text = "John Doe¹, Jane Doe², and Alex Doe¹\n¹Quantum Photonics Laboratory, Department of Physics, CodeFryDev Institute\n²Center for Quantum Information, CodeFryDev Academy", - FontSize = 9.5, - FontFamily = "Times New Roman", - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Center - }, - - // Abstract & PACS Card - new PdfShapeElement - { - X = 75, - Y = 145, - Width = 650, - Height = 104, - CornerRadius = 4, - FillColorHex = "#ECFEFF", - StrokeColorHex = "#A5F3FC", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 90, - Y = 150, - Width = 620, - Height = 68, - Text = "Abstract— We report the experimental observation of a non-equilibrium superradiant phase transition in an array of 64 transmon qubits coupled to a high-Q 3D microwave cavity. In the ultra-strong coupling regime (g/ω_c ≈ 0.18), we measure a critical threshold in the cavity transmission spectrum, observing a 12.4 dB photon squeezing and extended ensemble dephasing times T₂* exceeding 180 μs.", - FontSize = 9, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#164E63", - Alignment = TextAlignmentMode.Justify - }, - new PdfTextElement - { - X = 90, - Y = 222, - Width = 620, - Height = 22, - Text = "PACS Numbers— 03.67.Lx (Quantum computation), 42.50.Pq (Cavity QED), 85.25.Cp (Josephson devices).", - FontSize = 8.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0891B2" - }, - - // Column Split Divider - new PdfDividerElement - { - X = 55, - Y = 254, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 262, - Width = 330, - Height = 22, - Text = "I. INTRODUCTION & EXPERIMENTAL SETUP", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 55, - Y = 286, - Width = 330, - Height = 110, - Text = "Circuit quantum electrodynamics (cQED) provides a macroscopic testbed for exploring light-matter interactions at the single-photon level [1, 2]. By embedding superconducting Josephson junction transmon qubits inside an aluminum 3D resonant cavity cooled to 12 mK in a dilution refrigerator, we suppress ambient thermal noise and achieve ultra-long photon coherence.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 402, - Width = 330, - Height = 22, - Text = "II. TAVIS-CUMMINGS HAMILTONIAN", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 55, - Y = 426, - Width = 330, - Height = 45, - Text = "The coherent dynamics of N identical two-level transmons coupled to a single cavity mode is described by the Dicke Hamiltonian:", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Dicke Hamiltonian Vector Math Equation - new PdfMathElement - { - X = 55, - Y = 475, - Width = 330, - Height = 44, - Formula = @"\hat{H} = \hbar \omega_c a^\dagger a + \frac{1}{2} \hbar \omega_q \sum_{j=1}^N \sigma_z^{(j)} + \frac{\hbar g}{\sqrt{N}} \sum_{j=1}^N (a^\dagger \sigma_-^{(j)} + a \sigma_+^{(j)})", - FontSize = 9.5, - TextColorHex = "#0E7490", - BackgroundColorHex = "#ECFEFF", - BorderColorHex = "#A5F3FC", - BorderThickness = 0.5, - CornerRadius = 3, - ShowBackground = true, - ShowBorder = true, - ShowEquationNumber = true, - EquationNumber = "(1)", - Alignment = TextAlignmentMode.Center, - Category = MathCategory.QuantumMechanics, - PresetName = "Dicke Cavity QED Hamiltonian" - }, - - new PdfTextElement - { - X = 55, - Y = 524, - Width = 330, - Height = 22, - Text = "III. LINDBLAD MASTER EQUATION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 55, - Y = 548, - Width = 330, - Height = 135, - Text = "Open-system quantum dissipation is treated via the Lindblad generator:\n\n∂_t ρ̂ = -i/ℏ [Ĥ, ρ̂] + κ 𝒟[â]ρ̂ + γ ∑ 𝒟[σ_-^(j)]ρ̂ + γ_φ ∑ 𝒟[σ_z^(j)]ρ̂,\n\nwhere 𝒟[L̂]ρ̂ = L̂ρ̂L̂† - ½ {L̂†L̂, ρ̂}. Here κ = 2π × 42 kHz represents the cavity photon leakage rate, γ = 2π × 8.5 kHz is the spontaneous qubit decay rate, and γ_φ is the pure dephasing rate.", - FontSize = 9, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 690, - Width = 330, - Height = 22, - Text = "IV. DECOHERENCE SUPPRESSION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 55, - Y = 714, - Width = 330, - Height = 140, - Text = "By tuning the qubit array frequency to the cavity anti-node (ω_q = 6.482 GHz), we observe cooperative subradiant states that effectively shield the quantum ensemble from local 1/f charge and flux noise fluctuations. This yields an order-of-magnitude enhancement in ensemble quantum state lifetime.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 262, - Width = 330, - Height = 22, - Text = "V. SPECTROSCOPY & EXPERIMENTAL DATA", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 415, - Y = 286, - Width = 330, - Height = 58, - Text = "Table I summarizes the calibrated experimental parameters across 4 sub-modules of the 64-qubit quantum processor measured at a base temperature T = 12.5 mK.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Experimental cQED Table - new PdfTableElement - { - X = 415, - Y = 348, - Width = 330, - Height = 145, - Headers = new List { "Module", "ω_q / 2π", "g / 2π", "T₁ (μs)", "T₂* (μs)" }, - Rows = new List> - { - new() { "Array α (N=16)", "6.482 GHz", "184 MHz", "192.4", "184.2" }, - new() { "Array β (N=16)", "6.485 GHz", "181 MHz", "188.1", "176.5" }, - new() { "Array γ (N=16)", "6.479 GHz", "186 MHz", "195.0", "189.0" }, - new() { "Array δ (N=16)", "6.481 GHz", "183 MHz", "190.2", "182.8" } - }, - HeaderBackgroundHex = "#0891B2", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#ECFEFF", - BorderColorHex = "#CBD5E1" - }, - - new PdfTextElement - { - X = 415, - Y = 502, - Width = 330, - Height = 22, - Text = "VI. QUANTUM PHASE TRANSITION OBSERVATION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 415, - Y = 526, - Width = 330, - Height = 115, - Text = "As the drive photon amplitude exceeds the critical threshold η_c = √(κγ)/2g, we detect macroscopic photon occupation within the cavity accompanied by spontaneous symmetry breaking of the collective spin polarization vector ⟨J_x⟩ ≠ 0 [3, 4].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 415, - Y = 648, - Width = 330, - Height = 22, - Text = "VII. TIME-RESOLVED PHOTON CORRELATIONS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 415, - Y = 672, - Width = 330, - Height = 145, - Text = "Second-order correlation measurements g^(2)(τ) = ⟨â†(t) â†(t+τ) â(t+τ) â(t)⟩ / ⟨↠â⟩² demonstrate strong non-classical antibunching (g^(2)(0) = 0.08 ± 0.02) below the threshold, transitioning smoothly into super-Poissonian photon bunching at criticality.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - } - } - }; - - // ========================================================================= - // PAGE 2: Wigner Tomography, Fault Tolerance, Author Bios & References - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "PHYSICAL REVIEW LETTERS • VOL. 136, 140401", - FooterRight = "Page 2 of 2", - Elements = new List - { - // Top Header Metadata Strip - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "BOHR, CHEN, AND THORNE: CAVITY QED IN SUPERCONDUCTING QUBITS", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 62, - Width = 330, - Height = 22, - Text = "VIII. WIGNER TOMOGRAPHY & SQUEEZING", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 55, - Y = 86, - Width = 330, - Height = 120, - Text = "Full quantum state tomography of the cavity mode reveals pronounced negativity in the reconstructed Wigner quasi-probability distribution W(α). Quadrature variance satisfies (ΔX_θ)² = e^{-2r} with squeeze parameter r = 1.43, demonstrating non-Gaussian quantum resource generation in macroscopic circuits [5].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 212, - Width = 330, - Height = 22, - Text = "IX. FAULT-TOLERANT QUANTUM COMPUTING", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 55, - Y = 236, - Width = 330, - Height = 130, - Text = "The collective protection against single-qubit relaxation opens direct pathways for bosonic quantum error correction codes, specifically Gottesman-Kitaev-Preskill (GKP) grid states and cat-qubit parity measurements. With average gate fidelities ℱ_gate > 99.82%, the architecture satisfies the fault-tolerance threshold.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 372, - Width = 330, - Height = 22, - Text = "X. CONCLUDING REMARKS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfTextElement - { - X = 55, - Y = 396, - Width = 330, - Height = 90, - Text = "In conclusion, we achieved ultra-strong collective light-matter coupling in multi-transmon systems. This platform provides an ideal simulator for non-equilibrium quantum criticality and high-fidelity quantum transducers.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Author Biographies Header - new PdfTextElement - { - X = 55, - Y = 494, - Width = 330, - Height = 22, - Text = "AUTHOR BIOGRAPHIES", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfDividerElement - { - X = 55, - Y = 515, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - - // Bio 1: Alexander Bohr - new PdfShapeElement - { - X = 55, - Y = 525, - Width = 34, - Height = 34, - CornerRadius = 17, - FillColorHex = "#0891B2", - Label = "AB", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 98, - Y = 523, - Width = 287, - Height = 65, - Text = "Alexander Bohr received his Ph.D. in Physics from Harvard in 2021. He leads experimental superconducting cQED research at the Harvard Quantum Optics Lab.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - // Bio 2: Evelyn Chen - new PdfShapeElement - { - X = 55, - Y = 595, - Width = 34, - Height = 34, - CornerRadius = 17, - FillColorHex = "#0E7490", - Label = "EC", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 98, - Y = 593, - Width = 287, - Height = 65, - Text = "Evelyn Chen is an Associate Professor of Electrical Engineering and Physics at MIT, focusing on Josephson junction parametric amplifiers and quantum noise limits.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 62, - Width = 330, - Height = 22, - Text = "REFERENCES", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#0E7490" - }, - new PdfDividerElement - { - X = 415, - Y = 85, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#CBD5E1" - }, - new PdfTextElement - { - X = 415, - Y = 90, - Width = 330, - Height = 270, - Text = "[1] S. Haroche and J.-M. Raimond, Exploring the Quantum: Atoms, Cavities, and Photons, Oxford Univ. Press, 2006.\n[2] A. Blais, S. M. Girvin, and R. J. Schoelkopf, \"Circuit quantum electrodynamics,\" Rev. Mod. Phys., vol. 93, 025005, 2021.\n[3] M. H. Devoret and R. J. Schoelkopf, \"Superconducting circuits for quantum information,\" Science, vol. 339, pp. 1169–1174, 2013.\n[4] R. H. Dicke, \"Coherence in spontaneous radiation processes,\" Phys. Rev., vol. 93, pp. 99–110, 1954.\n[5] P. Krantz et al., \"A quantum engineer's guide to superconducting qubits,\" Appl. Phys. Rev., vol. 6, 021318, 2019.\n[6] D. Gottesman, A. Kitaev, and J. Preskill, \"Encoding a qubit in an oscillator,\" Phys. Rev. A, vol. 64, 012310, 2001.\n[7] M. Mirrahimi et al., \"Dynamically protected cat-qubits,\" New J. Phys., vol. 16, 045014, 2014.\n[8] J. M. Fink et al., \"Climbing the Jaynes-Cummings ladder and observing its nonlinearity,\" Nature, vol. 454, pp. 315–318, 2008.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Justify - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Career/ResumeAcademicCvTemplate.cs b/src/PdfEditorApp/Templates/Career/ResumeAcademicCvTemplate.cs deleted file mode 100644 index bdc1bed..0000000 --- a/src/PdfEditorApp/Templates/Career/ResumeAcademicCvTemplate.cs +++ /dev/null @@ -1,490 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Career; - -public class ResumeAcademicCvTemplate : ITemplateDefinition -{ - public string Id => "resumeacademic"; - public string Name => "Academic & Scientific Curriculum Vitae"; - public string Description => "Comprehensive 2-page academic CV with grants table, peer-reviewed publications with DOIs, student advising, and editorial service"; - public string Category => "Career"; - public string IconKind => "SchoolOutline"; - public string AccentColorHex => "#78350F"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Prof_John_Doe_Academic_CV.pdf", - Author = "Prof. John Doe, Ph.D.", - Subject = "Curriculum Vitae • Computational Neuroscience & Applied Mathematics" - }; - - // ========================================================================= - // PAGE 1: Header, Appointments, Education, Grants Table & Major Publications - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "Prof. John Doe, Ph.D. • Curriculum Vitae | ORCID: 0000-0002-8910-4492", - FooterCenter = "FACULTY DOSSIER", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Header Rule - new PdfDividerElement - { - X = 55, - Y = 35, - Width = 690, - Height = 2, - Thickness = 2, - ColorHex = "#78350F" - }, - - // Scholar Name - new PdfTextElement - { - X = 55, - Y = 45, - Width = 520, - Height = 34, - Text = "JOHN DOE, Ph.D.", - FontSize = 24, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#1E293B" - }, - - // Academic Title & Institutional Affiliation - new PdfTextElement - { - X = 55, - Y = 78, - Width = 520, - Height = 44, - Text = "Associate Professor of Applied Mathematics & Computational Neuroscience\nInstitute for Advanced Theoretical Studies • CodeFryDev Institute of Technology\nEmail: j.doe@codefrydev.in • Web: codefrydev.in/people/john-doe", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#475569" - }, - - // Top Right ORCID / Google Scholar QR Code - new PdfQrCodeElement - { - X = 645, - Y = 42, - Width = 100, - Height = 85, - Content = "https://orcid.org/0000-0002-8910-4492", - Label = "ORCID RECORD", - DarkColorHex = "#78350F", - LightColorHex = "#FFFFFF" - }, - - // Header Divider - new PdfDividerElement - { - X = 55, - Y = 126, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - // Section: Research Interests - new PdfTextElement - { - X = 55, - Y = 135, - Width = 690, - Height = 20, - Text = "PRIMARY RESEARCH FIELDS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfTextElement - { - X = 55, - Y = 155, - Width = 690, - Height = 34, - Text = "Topological Data Analysis, Neural Differential Equations, High-Dimensional Stochastic Dynamical Systems, Geometric Deep Learning, and Spectral Graph Theory on Cortical Manifolds.", - FontSize = 9.5, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: Academic Appointments - new PdfTextElement - { - X = 55, - Y = 190, - Width = 690, - Height = 20, - Text = "ACADEMIC APPOINTMENTS & POSITIONS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 55, - Y = 208, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 214, - Width = 690, - Height = 68, - Text = "• Associate Professor (Tenured) — Mathematical Institute, Oxford University (2022 – Present)\n• Assistant Professor of Applied Mathematics — Stanford University (2018 – 2022)\n• Postdoctoral Research Fellow — Center for Brain Science, Harvard University (2015 – 2018)\n• Visiting Scholar — Max Planck Institute for Mathematics in the Sciences, Leipzig (2017)", - FontSize = 9, - FontFamily = "Times New Roman", - LineHeight = 1.4, - TextColorHex = "#334155" - }, - - // Section: Education - new PdfTextElement - { - X = 55, - Y = 285, - Width = 690, - Height = 20, - Text = "EDUCATION & DEGREES CONFERRED", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 55, - Y = 303, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 309, - Width = 690, - Height = 58, - Text = "• Ph.D. in Applied Mathematics — Harvard University (2011 – 2015)\n Dissertation: Spectral Geometry and Persistent Homology on Cortical Surfaces. Advisor: Prof. Shing-Tung Yau.\n• M.Sc. in Theoretical Physics — Cambridge University (Part III of Mathematical Tripos, Distinction, 2010 – 2011)\n• B.Sc. in Mathematics & Physics — MIT (Summa Cum Laude, GPA 4.00/4.00, 2006 – 2010)", - FontSize = 9, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: Major Research Grants & Funding - new PdfTextElement - { - X = 55, - Y = 370, - Width = 690, - Height = 20, - Text = "MAJOR FUNDED RESEARCH GRANTS ($4.2M TOTAL AWARDS)", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 55, - Y = 388, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - - // Table of Grants - new PdfTableElement - { - X = 55, - Y = 394, - Width = 690, - Height = 115, - Headers = new List { "Grant Agency & Title", "Grant Number", "Role", "Funding Period", "Total Amount" }, - Rows = new List> - { - new() { "ERC Consolidator Grant (Topological Brain Dynamics)", "ERC-2023-COG-948", "Principal Investigator", "2024 – 2029", "€2,000,000" }, - new() { "NSF DMS CAREER: Geometric Deep Learning on Manifolds", "DMS-2048910", "Sole PI", "2020 – 2025", "$520,000" }, - new() { "NIH BRAIN Initiative: High-Dimensional Cortical Tracking", "U01-MH12498", "Co-PI (with Stanford Med)", "2019 – 2023", "$1,450,000" } - }, - HeaderBackgroundHex = "#78350F", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#FFFDF9", - BorderColorHex = "#E2E8F0" - }, - - // Section: Selected Peer-Reviewed Publications (Part 1) - new PdfTextElement - { - X = 55, - Y = 520, - Width = 690, - Height = 20, - Text = "SELECTED PEER-REVIEWED PUBLICATIONS (OVER 45 PEER-REVIEWED PAPERS, h-index: 28)", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 55, - Y = 538, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 544, - Width = 690, - Height = 185, - Text = "1. Doe, J., & Chen, E. (2025). \"Persistent Spectral Cohomology on High-Dimensional Simplicial Manifolds.\" Journal of the American Mathematical Society, 38(2), pp. 412–458. DOI: 10.1090/jams/9842.\n2. Doe, J., Thorne, M., & Jenkins, S. (2024). \"Neural Differential Equations with Guaranteed Stability via Hodge Decomposition.\" Nature Machine Intelligence, 6(8), pp. 892–906. DOI: 10.1038/s42256-024-00892.\n3. Doe, J. (2023). \"Spectral Gap Concentration on Random Simplicial Complexes.\" Communications on Pure and Applied Mathematics, 76(4), pp. 789–834. DOI: 10.1002/cpa.22019.\n4. Vance, E., & Doe, J. (2022). \"Topological Decoding of Multi-Electrode Array Cortical Spikes.\" Proceedings of the National Academy of Sciences (PNAS), 119(14), e2119842119. DOI: 10.1073/pnas.2119842119.\n5. Doe, J., et al. (2020). \"Continuous Invertible Neural Flows on Riemannian Manifolds.\" IEEE Transactions on Pattern Analysis and Machine Intelligence (TPAMI), 42(11), pp. 2780–2794. DOI: 10.1109/TPAMI.2020.29841.", - FontSize = 8.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155" - } - } - }; - - // ========================================================================= - // PAGE 2: Additional Publications, Keynotes, Teaching, Advising & Service - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "Prof. John Doe, Ph.D. • Curriculum Vitae (Page 2)", - FooterCenter = "FACULTY DOSSIER", - FooterRight = "Page 2 of 2", - Elements = new List - { - // Top Header Rule - new PdfDividerElement - { - X = 55, - Y = 35, - Width = 690, - Height = 2, - Thickness = 2, - ColorHex = "#78350F" - }, - - // Section: Additional Publications (Part 2) - new PdfTextElement - { - X = 55, - Y = 48, - Width = 690, - Height = 20, - Text = "ADDITIONAL PEER-REVIEWED ARTICLES & MONOGRAPHS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 55, - Y = 70, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 76, - Width = 690, - Height = 150, - Text = "6. Doe, J., & Sterling, C. (2020). \"Discrete Bochner Laplacians and Curvature Bounds on Cell Complexes.\" SIAM Journal on Applied Mathematics, 80(3), pp. 1120–1145. DOI: 10.1137/19M1284102.\n7. Doe, J., & Tanaka, H. (2019). \"Diffusion Geometry on Non-Compact Symmetric Spaces.\" Journal of Machine Learning Research (JMLR), 20(84), pp. 1–38.\n8. Doe, J. (2018). \"Spectral Geometry of Cortical Folding Patterns.\" Cambridge University Press Research Monograph Series in Mathematical Biology, Vol. 14, 280 pages. ISBN 978-1-108-49210-4.\n9. Thorne, J., & Doe, J. (2017). \"Persistent Homology of Neural Population Codes.\" Physical Review E, 96(4), 042412. DOI: 10.1103/PhysRevE.96.042412.", - FontSize = 8.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: Invited Keynotes & Plenaries - new PdfTextElement - { - X = 55, - Y = 232, - Width = 690, - Height = 20, - Text = "INVITED KEYNOTES & PLENARY ADDRESSES", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 55, - Y = 254, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 260, - Width = 690, - Height = 65, - Text = "• Plenary Speaker, International Congress of Mathematicians (ICM 2026, Section on Applied Topology, Helsinki)\n• Invited Keynote, NeurIPS 2024 Workshop on Differential Geometry in Deep Learning (Vancouver, BC)\n• Euler Lecturer, Zurich Mathematics Colloquium, ETH Zurich (2023)\n• Courant Institute Annual Distinguished Lecture in Applied Mathematics, NYU (2022)\n• Invited Plenary, SIAM Conference on Mathematics of Data Science (MDS 2020)", - FontSize = 8.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: Ph.D. Advising & Postdoctoral Mentorship - new PdfTextElement - { - X = 55, - Y = 330, - Width = 330, - Height = 20, - Text = "PH.D. STUDENTS & POSTDOCS SUPERVISED", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 55, - Y = 352, - Width = 330, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 358, - Width = 330, - Height = 90, - Text = "• Dr. Clara Sterling (Ph.D. 2023, now Assistant Professor at Cambridge)\n• Dr. Julian Thorne (Ph.D. 2024, Postdoc at MIT Mathematics)\n• Dr. Hiroshi Tanaka (Postdoc 2021–2024, now Faculty at Tokyo Univ)\n• Currently advising 4 Ph.D. students and 2 postdoctoral fellows.", - FontSize = 8.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: Editorial Boards & Review Service - new PdfTextElement - { - X = 415, - Y = 330, - Width = 330, - Height = 20, - Text = "EDITORIAL BOARDS & SERVICE", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 415, - Y = 352, - Width = 330, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 415, - Y = 358, - Width = 330, - Height = 90, - Text = "• Associate Editor, SIAM Journal on Applied Mathematics (2021–Present)\n• Action Editor, Journal of Machine Learning Research (JMLR)\n• Program Chair, Computational Topology & Geometry 2025\n• NSF & ERC Panel Reviewer (Mathematics & Computing)", - FontSize = 8.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: University Teaching & Courses Taught - new PdfTextElement - { - X = 55, - Y = 455, - Width = 690, - Height = 20, - Text = "UNIVERSITY TEACHING & CURRICULUM DEVELOPMENT", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#78350F" - }, - new PdfDividerElement - { - X = 55, - Y = 477, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 483, - Width = 690, - Height = 65, - Text = "• MATH 842: High-Dimensional Stochastic Differential Equations (Oxford, Graduate Level, 2022–2026)\n• MATH 510: Computational Topology and Persistent Homology (Stanford / Oxford, 2019–2025)\n• Recipient of Oxford University Outstanding Graduate Teaching Award (2024)", - FontSize = 8.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155" - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Career/ResumeCreativeMinimalistTemplate.cs b/src/PdfEditorApp/Templates/Career/ResumeCreativeMinimalistTemplate.cs deleted file mode 100644 index 07d8888..0000000 --- a/src/PdfEditorApp/Templates/Career/ResumeCreativeMinimalistTemplate.cs +++ /dev/null @@ -1,472 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Career; - -public class ResumeCreativeMinimalistTemplate : ITemplateDefinition -{ - public string Id => "resumecreative"; - public string Name => "Creative UI/UX Design Director Resume"; - public string Description => "Minimalist design resume featuring creative philosophy, case study outcomes, and interactive Figma QR code"; - public string Category => "Career"; - public string IconKind => "PaletteOutline"; - public string AccentColorHex => "#0D9488"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Jane_Doe_Design_Director_Resume.pdf", - Author = "Jane Doe", - Subject = "Senior Design Director & UI/UX Specialist Resume" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FCFCFD", - FooterLeft = "Jane Doe • Creative Director & Product Designer | codefrydev.in", - FooterCenter = "PORTFOLIO & CASE STUDIES", - FooterRight = "Page 1 of 1", - Elements = new List - { - // Top Teal Accent Bar - new PdfShapeElement - { - X = 55, - Y = 40, - Width = 690, - Height = 4, - FillColorHex = "#0D9488", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - // Candidate Name & Creative Title - new PdfTextElement - { - X = 55, - Y = 54, - Width = 480, - Height = 38, - Text = "JANE DOE", - FontSize = 28, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 92, - Width = 480, - Height = 24, - Text = "Design Director • Principal Product & Interaction Designer", - FontSize = 12.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0D9488" - }, - - // Contact Strip - new PdfTextElement - { - X = 55, - Y = 116, - Width = 520, - Height = 20, - Text = "🌐 codefrydev.in • ✉️ jane.doe@codefrydev.in • 📍 New York, NY • 📱 +1 (555) 019-2834", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#64748B" - }, - - // Top Right Interactive Portfolio QR Code - new PdfQrCodeElement - { - X = 645, - Y = 48, - Width = 100, - Height = 85, - Content = "https://codefrydev.in/case-studies", - Label = "VIEW CASE STUDIES", - DarkColorHex = "#0D9488", - LightColorHex = "#FCFCFD" - }, - - // Header Divider Line - new PdfDividerElement - { - X = 55, - Y = 142, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - - // Creative Statement / Design Philosophy Box - new PdfShapeElement - { - X = 55, - Y = 152, - Width = 690, - Height = 72, - CornerRadius = 8, - FillColorHex = "#F0FDFA", - StrokeColorHex = "#99F6E4", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 72, - Y = 160, - Width = 656, - Height = 56, - Text = "\"Designing at the intersection of emotional elegance, strict accessibility, and data-driven usability. 10+ years shaping high-growth digital products adopted by 50M+ users globally, transforming complex enterprise workflows into intuitive, joyous experiences.\"", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsItalic = true, - LineHeight = 1.45, - TextColorHex = "#115E59" - }, - - // Section: Core Design Disciplines - new PdfTextElement - { - X = 55, - Y = 236, - Width = 690, - Height = 22, - Text = "CORE DESIGN DISCIPLINES & EXPERTISE", - FontSize = 12, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 258, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 265, - Width = 330, - Height = 65, - Text = "• Design Systems Architecture & Tokenization\n• End-to-End Product & Mobile App Strategy\n• User Research, Ethnography & Usability Testing\n• WCAG AAA Accessibility & Inclusive Design", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - new PdfTextElement - { - X = 415, - Y = 265, - Width = 330, - Height = 65, - Text = "• Interactive Prototyping & Motion Choreography\n• 3D Spatial & Canvas Interface Engineering\n• Cross-Functional Leadership (Design & Engineering)\n• Brand Identity, Editorial Direction & Storytelling", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: Design Leadership Experience - new PdfTextElement - { - X = 55, - Y = 340, - Width = 690, - Height = 22, - Text = "DESIGN LEADERSHIP EXPERIENCE", - FontSize = 12, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 362, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - - // Role 1 - new PdfTextElement - { - X = 55, - Y = 370, - Width = 490, - Height = 20, - Text = "Design Director | CodeFryDev Design Studio — New York, NY", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 550, - Y = 370, - Width = 195, - Height = 20, - Text = "2022 – Present", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0D9488", - Alignment = TextAlignmentMode.Right - }, - new PdfTextElement - { - X = 55, - Y = 390, - Width = 690, - Height = 85, - Text = "✦ Built and mentored a high-performing team of 14 product designers, motion artists, and design system engineers.\n✦ Directed the redesign of an enterprise fintech suite handling $12B+ in annual transaction volume, lifting Task Success Rate by +28% and NPS by +34 points.\n✦ Created the unified 'Aura' multiplatform design system deployed across iOS, Android, Web, and Desktop.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155" - }, - - // Role 2 - new PdfTextElement - { - X = 55, - Y = 485, - Width = 490, - Height = 20, - Text = "Principal Product Designer | CodeFryDev Interactive — San Francisco, CA", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 550, - Y = 485, - Width = 195, - Height = 20, - Text = "2018 – 2022", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0D9488", - Alignment = TextAlignmentMode.Right - }, - new PdfTextElement - { - X = 55, - Y = 505, - Width = 690, - Height = 85, - Text = "✦ Led end-to-end UX vision for new Host Onboarding flow across 220 countries, resulting in a +19.4% increase in completed active listings within the first 60 days.\n✦ Conducted in-depth contextual inquiry user studies in Tokyo, London, and Berlin to identify friction points in multi-calendar management.\n✦ Collaborated closely with VP of Design on strategic brand evolutionary guidelines.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155" - }, - - // Role 3 - new PdfTextElement - { - X = 55, - Y = 600, - Width = 490, - Height = 20, - Text = "Senior Interaction Designer | CodeFryDev Creative — New York, NY", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 550, - Y = 600, - Width = 195, - Height = 20, - Text = "2015 – 2018", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0D9488", - Alignment = TextAlignmentMode.Right - }, - new PdfTextElement - { - X = 55, - Y = 620, - Width = 690, - Height = 75, - Text = "✦ Crafted award-winning digital experiences and spatial interactive installations for clients including MoMA, The Atlantic, and Spotify.\n✦ Authored interactive brand design guides and dynamic typography specifications.\n✦ Recipient of Red Dot Best of the Best Award 2017.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155" - }, - - // Section: Featured Case Studies & Quantified Impact - new PdfTextElement - { - X = 55, - Y = 705, - Width = 690, - Height = 22, - Text = "FEATURED CASE STUDIES & MEASURED IMPACT", - FontSize = 12, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 727, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - - // Case Study Cards - new PdfShapeElement - { - X = 55, - Y = 735, - Width = 330, - Height = 90, - CornerRadius = 6, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 68, - Y = 742, - Width = 304, - Height = 78, - Text = "🏆 Global Fintech Workspace Reimagination\nRole: Lead Design Strategist • Client: Fortune 100\nImpact: +38% time savings on trades, $45M revenue impact, 0 accessibility violations across 48 modules.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - new PdfShapeElement - { - X = 415, - Y = 735, - Width = 330, - Height = 90, - CornerRadius = 6, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 428, - Y = 742, - Width = 304, - Height = 78, - Text = "🎨 Spatial Museum Interactive Experience\nRole: Creative Director • Client: Whitney Museum\nImpact: 1.4M visitors engaged, Featured in Fast Company Innovation by Design, Awwwards Site of the Year.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: Education & Recognition - new PdfTextElement - { - X = 55, - Y = 835, - Width = 330, - Height = 22, - Text = "EDUCATION & TRAINING", - FontSize = 12, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 857, - Width = 330, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 55, - Y = 864, - Width = 330, - Height = 65, - Text = "Master of Fine Arts (MFA) in Interaction Design\nSchool of Visual Arts (SVA), New York (2013 – 2015)\n\nB.A. in Graphic Design & Human-Computer Interaction\nCarnegie Mellon University • With University Honors", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - new PdfTextElement - { - X = 415, - Y = 835, - Width = 330, - Height = 22, - Text = "AWARDS & JURIES", - FontSize = 12, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 415, - Y = 857, - Width = 330, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - new PdfTextElement - { - X = 415, - Y = 864, - Width = 330, - Height = 65, - Text = "• D&AD Yellow Pencil Winner (Digital Design 2023)\n• Red Dot: Best of the Best Award (2017, 2021)\n• Jury Member, AIGA National Design Competitions\n• Fast Company 100 Most Creative People in Business", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Career/ResumeModernCleanTemplate.cs b/src/PdfEditorApp/Templates/Career/ResumeModernCleanTemplate.cs deleted file mode 100644 index df5533e..0000000 --- a/src/PdfEditorApp/Templates/Career/ResumeModernCleanTemplate.cs +++ /dev/null @@ -1,606 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Career; - -public class ResumeModernCleanTemplate : ITemplateDefinition -{ - public string Id => "resumemodern"; - public string Name => "Modern Tech & Product Resume"; - public string Description => "Two-column tech resume with skills sidebar, verified metrics, and interactive GitHub QR code"; - public string Category => "Career"; - public string IconKind => "AccountTieOutline"; - public string AccentColorHex => "#0284C7"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "John_Doe_Product_Engineer_Resume.pdf", - Author = "John Doe", - Subject = "Lead Product & Full-Stack Engineer Resume" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "John Doe • Lead Product Engineer | Portfolio: codefrydev.in", - FooterCenter = "CONFIDENTIAL", - FooterRight = "Page 1 of 1", - Elements = new List - { - // Left Column Background Sidebar (Width: 245) - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 245, - Height = 1131, - FillColorHex = "#F1F5F9", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - // Sidebar Right Border Line - new PdfDividerElement - { - IsVertical = true, - X = 245, - Y = 0, - Width = 1, - Height = 1131, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - // ========================================== - // SIDEBAR CONTENT (Left Column: X = 20, Width = 205) - // ========================================== - - // Monogram Initials Avatar Badge - new PdfShapeElement - { - X = 85, - Y = 45, - Width = 75, - Height = 75, - CornerRadius = 38, - FillColorHex = "#0284C7", - StrokeColorHex = "#0369A1", - StrokeThickness = 2, - Label = "JD", - LabelColorHex = "#FFFFFF", - LabelFontSize = 26 - }, - - // Contact Details Header - new PdfTextElement - { - X = 20, - Y = 135, - Width = 205, - Height = 22, - Text = "CONTACT", - FontSize = 11.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 20, - Y = 157, - Width = 205, - Height = 1, - Thickness = 1, - ColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 20, - Y = 162, - Width = 205, - Height = 100, - Text = "📧 john.doe@codefrydev.in\n📱 +1 (555) 019-2834\n📍 San Francisco, CA\n🌐 codefrydev.in\n🐙 github.com/codefrydev\n💼 linkedin.com/in/codefrydev", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.45, - TextColorHex = "#334155" - }, - - // Technical Skills Header - new PdfTextElement - { - X = 20, - Y = 272, - Width = 205, - Height = 22, - Text = "TECHNICAL SKILLS", - FontSize = 11.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 20, - Y = 294, - Width = 205, - Height = 1, - Thickness = 1, - ColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 20, - Y = 298, - Width = 205, - Height = 200, - Text = "LANGUAGES\n• TypeScript, C# / .NET 9, Go, Python, SQL\n\nFRONTEND & UI\n• React, Next.js, Avalonia UI, TailwindCSS, WebGL, Redux, Vite\n\nBACKEND & CLOUD\n• Node.js, ASP.NET Core, GraphQL, gRPC, Kafka, Redis, PostgreSQL\n\nDEVOPS & INFRA\n• AWS, Kubernetes, Docker, Terraform, GitHub Actions CI/CD", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Education Header - new PdfTextElement - { - X = 20, - Y = 508, - Width = 205, - Height = 22, - Text = "EDUCATION", - FontSize = 11.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 20, - Y = 530, - Width = 205, - Height = 1, - Thickness = 1, - ColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 20, - Y = 535, - Width = 205, - Height = 90, - Text = "B.S. in Computer Science\nUniversity of California, Berkeley\nGPA: 3.89 / 4.00 (2016 – 2020)\n• Regents & Chancellor's Scholar\n• Tau Beta Pi Engineering Honor", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Certifications Header - new PdfTextElement - { - X = 20, - Y = 635, - Width = 205, - Height = 22, - Text = "CERTIFICATIONS", - FontSize = 11.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 20, - Y = 657, - Width = 205, - Height = 1, - Thickness = 1, - ColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 20, - Y = 662, - Width = 205, - Height = 70, - Text = "• AWS Solutions Architect Professional\n• Certified Kubernetes Application Developer (CKAD)\n• Meta Front-End Certified Professional", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Sidebar Live Portfolio QR Code - new PdfQrCodeElement - { - X = 72, - Y = 745, - Width = 100, - Height = 100, - Content = "https://github.com/codefrydev", - Label = "SCAN FOR GITHUB", - DarkColorHex = "#0284C7", - LightColorHex = "#F1F5F9" - }, - - // Spoken Languages - new PdfTextElement - { - X = 20, - Y = 855, - Width = 205, - Height = 22, - Text = "LANGUAGES", - FontSize = 11.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 20, - Y = 875, - Width = 205, - Height = 1, - Thickness = 1, - ColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 20, - Y = 882, - Width = 205, - Height = 50, - Text = "• English (Native / Bilingual)\n• Mandarin Chinese (Fluent)\n• German (Conversational B1)", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // ========================================== - // MAIN BODY CONTENT (Right Column: X = 265, Width = 480) - // ========================================== - - // Candidate Name - new PdfTextElement - { - X = 265, - Y = 40, - Width = 480, - Height = 34, - Text = "JOHN DOE", - FontSize = 26, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // Subtitle / Professional Title - new PdfTextElement - { - X = 265, - Y = 74, - Width = 480, - Height = 22, - Text = "Lead Product Engineer • Full-Stack & Systems Architecture", - FontSize = 12.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0284C7" - }, - - // Executive Summary Card - new PdfTextElement - { - X = 265, - Y = 98, - Width = 480, - Height = 65, - Text = "High-velocity Lead Product Engineer with 7+ years of track record architecting mission-critical SaaS web & desktop applications. Proven expertise in zero-to-one product engineering, leading distributed frontend teams, and refactoring core ingestion pipelines to support 10M+ daily active users.", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#475569" - }, - - // Section: Professional Experience - new PdfTextElement - { - X = 265, - Y = 170, - Width = 480, - Height = 22, - Text = "PROFESSIONAL EXPERIENCE", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 265, - Y = 192, - Width = 480, - Height = 2, - Thickness = 1.5, - ColorHex = "#0284C7" - }, - - // Job 1 - new PdfTextElement - { - X = 265, - Y = 202, - Width = 330, - Height = 20, - Text = "Lead Product Engineer | CodeFryDev Technologies", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 600, - Y = 202, - Width = 145, - Height = 20, - Text = "2023 – Present • SF, CA", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0284C7", - Alignment = TextAlignmentMode.Right - }, - new PdfTextElement - { - X = 265, - Y = 224, - Width = 480, - Height = 85, - Text = "• Led a squad of 8 engineers architecting real-time collaboration canvas with WebGL & WebSockets, reducing latency by 45% for 2.4M enterprise users.\n• Built automated design token compilation engine across web and desktop clients, cutting designer-to-code iteration time by 60%.\n• Designed multi-tenant billing & seat provisioning microservice processing $35M+ annual recurring revenue with 99.99% uptime.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155" - }, - - // Job 2 - new PdfTextElement - { - X = 265, - Y = 315, - Width = 330, - Height = 20, - Text = "Senior Full-Stack Engineer | CodeFryDev Solutions", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 600, - Y = 315, - Width = 145, - Height = 20, - Text = "2021 – 2023 • SF, CA", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0284C7", - Alignment = TextAlignmentMode.Right - }, - new PdfTextElement - { - X = 265, - Y = 337, - Width = 480, - Height = 85, - Text = "• Spearheaded redesign of multi-currency onboarding KYC flow across 35 countries, improving checkout completion rate by +14.2%.\n• Optimized global GraphQL API caching layer with Redis cluster, dropping P95 API response times from 340ms to 48ms.\n• Mentored 5 junior and mid-level engineers through code reviews, tech talks, and comprehensive RFC design document templates.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155" - }, - - // Job 3 - new PdfTextElement - { - X = 265, - Y = 428, - Width = 330, - Height = 20, - Text = "Software Engineer | CodeFryDev Labs", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 600, - Y = 428, - Width = 145, - Height = 20, - Text = "2019 – 2021 • Berkeley, CA", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0284C7", - Alignment = TextAlignmentMode.Right - }, - new PdfTextElement - { - X = 265, - Y = 450, - Width = 480, - Height = 80, - Text = "• Engineered interactive data visualization dashboards rendering 500,000+ data points smoothly at 60 FPS using React & D3.js.\n• Migrated legacy monolith backend to containerized Docker services running on AWS ECS, decreasing deployment cycle from 3 days to 15 minutes.\n• Implemented automated Jest and Cypress test suites achieving 91% code coverage.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155" - }, - - // Section: Featured Open Source & Projects - new PdfTextElement - { - X = 265, - Y = 538, - Width = 480, - Height = 22, - Text = "FEATURED OPEN SOURCE & KEY PROJECTS", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 265, - Y = 560, - Width = 480, - Height = 2, - Thickness = 1.5, - ColorHex = "#0284C7" - }, - - // Project 1 - new PdfTextElement - { - X = 265, - Y = 570, - Width = 480, - Height = 20, - Text = "🚀 FastCanvas-Core (Creator & Maintainer — 3.2k GitHub Stars)", - FontSize = 10.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 265, - Y = 592, - Width = 480, - Height = 55, - Text = "Lightweight zero-allocation 2D vector drawing library built on WebAssembly and Skia. Adopted by over 40,000 active web applications worldwide with sub-millisecond layer caching.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Project 2 - new PdfTextElement - { - X = 265, - Y = 652, - Width = 480, - Height = 20, - Text = "⚡ Micro-State-Sync (Co-Author)", - FontSize = 10.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 265, - Y = 674, - Width = 480, - Height = 55, - Text = "CRDT-based real-time state synchronization engine for collaborative multi-user editing with conflict-free vector clocks and end-to-end encrypted payload verification.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section: Awards & Leadership - new PdfTextElement - { - X = 265, - Y = 736, - Width = 480, - Height = 22, - Text = "HONORS & LEADERSHIP", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 265, - Y = 758, - Width = 480, - Height = 2, - Thickness = 1.5, - ColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 265, - Y = 766, - Width = 480, - Height = 70, - Text = "• Winner, Stripe Global Hackathon 2022 (Real-time Settlement Engine)\n• Keynote Speaker, React Summit SF 2024 (\"Building 60 FPS Canvas Apps\")\n• Mentor, Women in Tech Engineering Cohort (Mentored 18 women engineers)", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155" - }, - - // Section: Patents & Publications - new PdfTextElement - { - X = 265, - Y = 845, - Width = 480, - Height = 22, - Text = "PATENTS & TECHNICAL PUBLICATIONS", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 265, - Y = 867, - Width = 480, - Height = 2, - Thickness = 1.5, - ColorHex = "#0284C7" - }, - new PdfTextElement - { - X = 265, - Y = 875, - Width = 480, - Height = 90, - Text = "• US Patent 11,842,109: \"Zero-Allocation Vector Tile Streaming in Distributed Browser Clients\"\n• Doe, J. (2024). \"Sub-Millisecond Canvas Geometry Rendering in Modern WebGL2 Environments.\" ACM SIGGRAPH Web3D Proceedings, pp. 45–56.\n• Available for advisory and engineering leadership consultations via codefrydev.in.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155" - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Career/ResumeTemplate.cs b/src/PdfEditorApp/Templates/Career/ResumeTemplate.cs deleted file mode 100644 index 67250e0..0000000 --- a/src/PdfEditorApp/Templates/Career/ResumeTemplate.cs +++ /dev/null @@ -1,525 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Career; - -public class ResumeTemplate : ITemplateDefinition -{ - public string Id => "resume"; - public string Name => "Executive Resume"; - public string Description => "Complete CV with QR code, competencies, and verified metrics"; - public string Category => "Career"; - public string IconKind => "AccountTieOutline"; - public string AccentColorHex => "#7C3AED"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "John_Doe_Executive_Resume.pdf", - Author = "John Doe", - Subject = "Principal Software Architect Resume" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "John Doe • Principal Software Architect | Portfolio: codefrydev.in", - FooterCenter = "CONFIDENTIAL & VERIFIED", - FooterRight = "Page 1 of 1", - Elements = new List - { - // 1. Monogram Initials Avatar Badge Top Left - new PdfShapeElement - { - X = 55, - Y = 45, - Width = 54, - Height = 54, - CornerRadius = 12, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#0C599B", - StrokeThickness = 0, - Label = "JD", - LabelColorHex = "#FFFFFF", - LabelFontSize = 22 - }, - - // 2. Candidate Full Name - new PdfTextElement - { - X = 120, - Y = 45, - Width = 510, - Height = 32, - Text = "JOHN DOE, M.Sc.", - FontSize = 22, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // 3. Professional Headline / Title - new PdfTextElement - { - X = 120, - Y = 75, - Width = 510, - Height = 22, - Text = "Principal Software Architect | Cloud & Distributed Systems", - FontSize = 12, - FontFamily = "Segoe UI", - TextColorHex = "#0F6CBD", - IsBold = true - }, - - // 4. Contact Information Strip (Line 1: Phone, Email, Location) - new PdfTextElement - { - X = 120, - Y = 96, - Width = 510, - Height = 20, - Text = "📧 john.doe@codefrydev.in • 📱 +1 (555) 019-2834 • 📍 Seattle, WA", - FontSize = 10, - FontFamily = "Segoe UI", - TextColorHex = "#475569" - }, - - // 5. Contact Information Strip (Line 2: Portfolio, LinkedIn, GitHub) - new PdfTextElement - { - X = 120, - Y = 114, - Width = 510, - Height = 20, - Text = "🔗 linkedin.com/in/codefrydev • 🌐 codefrydev.in • 🐙 github.com/codefrydev", - FontSize = 10, - FontFamily = "Segoe UI", - TextColorHex = "#475569" - }, - - // 6. Portfolio & Verification QR Code Badge Top Right - new PdfQrCodeElement - { - X = 645, - Y = 45, - Width = 100, - Height = 85, - Content = "https://codefrydev.in", - Label = "PORTFOLIO QR" - }, - - // 7. Header Accent Divider Line - new PdfDividerElement - { - X = 55, - Y = 142, - Width = 690, - Height = 2, - Thickness = 2, - ColorHex = "#0F6CBD" - }, - - // 8. Section: Executive Summary Title - new PdfTextElement - { - X = 55, - Y = 152, - Width = 690, - Height = 22, - Text = "EXECUTIVE SUMMARY", - FontSize = 12.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // 9. Executive Summary Callout Card - new PdfTextElement - { - X = 55, - Y = 174, - Width = 690, - Height = 68, - Text = "Strategic Principal Software Architect with 12+ years of expertise architecting high-throughput distributed systems, cross-platform enterprise desktop suites (.NET / Avalonia UI), and cloud-native microservices. Track record of leading 20+ member cross-functional engineering teams, orchestrating cloud migrations that reduced operational overhead by $1.4M annually, and delivering fault-tolerant platforms processing 100M+ transactions daily.", - FontSize = 10, - FontFamily = "Segoe UI", - LineHeight = 1.45, - TextColorHex = "#334155", - BackgroundColorHex = "#F8FAFC", - BorderColorHex = "#E2E8F0", - BorderThickness = 1.0, - CornerRadius = 6, - Padding = 8 - }, - - // 10. Section: Core Competencies & Skills Title - new PdfTextElement - { - X = 55, - Y = 246, - Width = 690, - Height = 22, - Text = "CORE COMPETENCIES & TECHNICAL EXPERTISE", - FontSize = 12.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // 11. Core Competencies Divider - new PdfDividerElement - { - X = 55, - Y = 268, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - - // 12. Skills Column Left - new PdfTextElement - { - X = 55, - Y = 276, - Width = 335, - Height = 64, - Text = "• Cloud & Distributed: Microsoft Azure, AWS, Kubernetes, Docker, Kafka, gRPC, Redis, Microservices, CI/CD\n• Architecture & Design: DDD, Event Sourcing, High Availability, Zero-Trust Security, System Resiliency, SOC2", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#374151" - }, - - // 13. Skills Column Right - new PdfTextElement - { - X = 410, - Y = 276, - Width = 335, - Height = 64, - Text = "• Languages & Frameworks: C# / .NET 9, Avalonia UI, WPF, TypeScript, Node.js, Go, Python, PostgreSQL\n• Engineering Leadership: Technical Roadmapping, Team Mentorship (20+ devs), Agile/Scrum, RFC Process", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#374151" - }, - - // 14. Section: Professional Experience Title - new PdfTextElement - { - X = 55, - Y = 348, - Width = 690, - Height = 22, - Text = "PROFESSIONAL EXPERIENCE", - FontSize = 12.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // 15. Professional Experience Divider - new PdfDividerElement - { - X = 55, - Y = 370, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - - // 16. Role 1: Title & Company - new PdfTextElement - { - X = 55, - Y = 378, - Width = 490, - Height = 20, - Text = "Principal Software Architect | CodeFryDev CloudScale Inc. — Seattle, WA", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - - // 17. Role 1: Date Range - new PdfTextElement - { - X = 550, - Y = 378, - Width = 195, - Height = 20, - Text = "Jan 2021 – Present", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD", - Alignment = TextAlignmentMode.Right - }, - - // 18. Role 1: Bullets & Achievements - new PdfTextElement - { - X = 55, - Y = 398, - Width = 690, - Height = 90, - Text = "✔ Spearheaded architecture of enterprise multiplatform desktop publishing suite using Avalonia UI and .NET 9, achieving 60 FPS rendering and reducing memory footprint by 45% across macOS, Windows, and Linux.\n✔ Led engineering transition of monolithic billing platform to Azure Kubernetes Service (AKS) microservices, lowering P99 latency by 55% and reducing annual cloud costs by $1.4M.\n✔ Mentored 22 software engineers, established automated CI/CD static security pipelines, and spearheaded RFC design review culture across 4 engineering pods.", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#374151" - }, - - // 19. Role 2: Title & Company - new PdfTextElement - { - X = 55, - Y = 498, - Width = 490, - Height = 20, - Text = "Staff Systems Engineer | CodeFryDev Data Systems — Redmond, WA", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - - // 20. Role 2: Date Range - new PdfTextElement - { - X = 550, - Y = 498, - Width = 195, - Height = 20, - Text = "Mar 2017 – Dec 2020", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD", - Alignment = TextAlignmentMode.Right - }, - - // 21. Role 2: Bullets & Achievements - new PdfTextElement - { - X = 55, - Y = 518, - Width = 690, - Height = 90, - Text = "✔ Architected distributed event streaming ingestion engine processing 75M+ transactions/day using Apache Kafka and .NET Core with 99.999% SLA availability.\n✔ Designed high-performance SkiaSharp hardware-accelerated rasterization backend for high-volume document exports and digital signature verification.\n✔ Partnered with Product and InfoSec teams to achieve SOC2 Type II compliance and implement end-to-end payload encryption.", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#374151" - }, - - // 22. Role 3: Title & Company - new PdfTextElement - { - X = 55, - Y = 618, - Width = 490, - Height = 20, - Text = "Senior Software Engineer | CodeFryDev Solutions Ltd — Seattle, WA", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#1E293B" - }, - - // 23. Role 3: Date Range - new PdfTextElement - { - X = 550, - Y = 618, - Width = 195, - Height = 20, - Text = "Jun 2014 – Feb 2017", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD", - Alignment = TextAlignmentMode.Right - }, - - // 24. Role 3: Bullets & Achievements - new PdfTextElement - { - X = 55, - Y = 638, - Width = 690, - Height = 76, - Text = "✔ Engineered RESTful web services and enterprise desktop clients adopted by 250,000+ business users globally.\n✔ Refactored legacy database queries and introduced distributed Redis caching layer, improving API response times from 850ms to 45ms.\n✔ Introduced automated integration testing with 88% code coverage, cutting QA regression cycles from 2 weeks to 3 days.", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#374151" - }, - - // 25. Section: Education & Credentials Title - new PdfTextElement - { - X = 55, - Y = 720, - Width = 690, - Height = 22, - Text = "EDUCATION & PROFESSIONAL CREDENTIALS", - FontSize = 12.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // 26. Education Divider - new PdfDividerElement - { - X = 55, - Y = 742, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - - // 27. Education Column Subheading - new PdfTextElement - { - X = 55, - Y = 750, - Width = 335, - Height = 20, - Text = "🎓 Academic Background", - FontSize = 10.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - - // 28. Education Details - new PdfTextElement - { - X = 55, - Y = 770, - Width = 335, - Height = 80, - Text = "Master of Science in Computer Science\nUniversity of Washington • GPA: 3.92 (2012 – 2014)\n\nBachelor of Science in Software Engineering\nSeattle University • Magna Cum Laude (2008 – 2012)", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#374151" - }, - - // 29. Certifications Column Subheading - new PdfTextElement - { - X = 410, - Y = 750, - Width = 335, - Height = 20, - Text = "🏆 Key Certifications & Honors", - FontSize = 10.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - - // 30. Certifications Details - new PdfTextElement - { - X = 410, - Y = 770, - Width = 335, - Height = 80, - Text = "• Microsoft Certified: Azure Solutions Architect Expert\n• AWS Certified Solutions Architect – Professional (SAP-C02)\n• Certified Kubernetes Administrator (CKA — Linux Foundation)\n• IEEE Senior Member & Open Source Contributor", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#374151" - }, - - // 31. Section: Notable Projects & Open Source - new PdfTextElement - { - X = 55, - Y = 860, - Width = 690, - Height = 22, - Text = "NOTABLE PROJECTS & OPEN SOURCE CONTRIBUTIONS", - FontSize = 12.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // 32. Projects Divider - new PdfDividerElement - { - X = 55, - Y = 882, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#E2E8F0" - }, - - // 33. Projects Callout Box - new PdfTextElement - { - X = 55, - Y = 890, - Width = 690, - Height = 90, - Text = "🚀 OpenPDF & CrossPlatform-UI Engine (Creator & Core Maintainer — 4.8k GitHub Stars)\nEngineered high-performance cross-platform vector rendering library for .NET with SkiaSharp integration.\n\n🌐 Enterprise Distributed Cache Mesh (Co-Author)\nLightweight, low-latency consensus protocol implementation for distributed multi-region memory stores.", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#166534", - BackgroundColorHex = "#F0FDF4", - BorderColorHex = "#BBF7D0", - BorderThickness = 1.0, - CornerRadius = 6, - Padding = 8 - }, - - // 34. Bottom Verification Strip Divider - new PdfDividerElement - { - X = 55, - Y = 992, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - // 35. References & Verification Notice - new PdfTextElement - { - X = 55, - Y = 1002, - Width = 690, - Height = 20, - Text = "References, patent publications, and verified code portfolio available upon request at codefrydev.in", - FontSize = 9, - FontFamily = "Segoe UI", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Certificates/CertificateNavyGoldTemplate.cs b/src/PdfEditorApp/Templates/Certificates/CertificateNavyGoldTemplate.cs deleted file mode 100644 index 7736665..0000000 --- a/src/PdfEditorApp/Templates/Certificates/CertificateNavyGoldTemplate.cs +++ /dev/null @@ -1,338 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Certificates; - -public class CertificateNavyGoldTemplate : ITemplateDefinition -{ - public string Id => "certificatenavygold"; - public string Name => "Executive Certificate of Honor"; - public string Description => "Prestigious corporate & executive recognition with navy & gold geometric crests"; - public string Category => "Certificates"; - public string IconKind => "ShieldCrownOutline"; - public string AccentColorHex => "#1E3A8A"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Executive_Certificate_of_Honor.pdf", - Author = "CodeFryDev Leadership Institute", - Subject = "Official Certificate of Distinction and Honor" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Landscape, - Width = 1131, - Height = 800, - BackgroundColorHex = "#FFFFFF", - ShowHeaderFooter = false, - Elements = new List - { - // Outer Navy Frame - new PdfShapeElement - { - X = 35, - Y = 35, - Width = 1061, - Height = 730, - FillColorHex = "#00000000", - StrokeColorHex = "#0F172A", - StrokeThickness = 4, - CornerRadius = 8, - ZIndex = 1 - }, - // Inner Gold Thin Inset Frame - new PdfShapeElement - { - X = 45, - Y = 45, - Width = 1041, - Height = 710, - FillColorHex = "#00000000", - StrokeColorHex = "#D97706", - StrokeThickness = 1.5, - CornerRadius = 6, - ZIndex = 2 - }, - - // Top Geometric Navy & Gold Corner Accents - new PdfShapeElement - { - X = 35, - Y = 35, - Width = 180, - Height = 180, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 0,0 L 180,0 L 0,180 Z", - FillColorHex = "#0F172A", - StrokeColorHex = "#00000000", - ZIndex = 3 - }, - new PdfShapeElement - { - X = 35, - Y = 35, - Width = 205, - Height = 205, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 180,0 L 205,0 L 0,205 L 0,180 Z", - FillColorHex = "#D97706", - StrokeColorHex = "#00000000", - ZIndex = 4 - }, - new PdfShapeElement - { - X = 916, - Y = 585, - Width = 180, - Height = 180, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 180,180 L 0,180 L 180,0 Z", - FillColorHex = "#0F172A", - StrokeColorHex = "#00000000", - ZIndex = 3 - }, - new PdfShapeElement - { - X = 891, - Y = 560, - Width = 205, - Height = 205, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 0,180 L 0,205 L 205,0 L 180,0 Z", - FillColorHex = "#D97706", - StrokeColorHex = "#00000000", - ZIndex = 4 - }, - - // Golden Shield Heraldic Crest - new PdfShapeElement - { - X = 525, - Y = 70, - Width = 80, - Height = 90, - ShapeType = ShapeType.ShieldBadge, - FillColorHex = "#FEF3C7", - StrokeColorHex = "#D97706", - StrokeThickness = 2.5, - Label = "CFD", - LabelColorHex = "#B45309", - LabelFontSize = 14, - ZIndex = 10 - }, - - // Main Title - new PdfTextElement - { - X = 150, - Y = 175, - Width = 831, - Height = 54, - Text = "CERTIFICATE OF DISTINCTION", - FontSize = 38, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 150, - Y = 230, - Width = 831, - Height = 26, - Text = "IN RECOGNITION OF EXCEPTIONAL EXECUTIVE LEADERSHIP & STRATEGIC VISION", - FontSize = 11.5, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#B45309", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Recipient Name - new PdfTextElement - { - X = 150, - Y = 280, - Width = 831, - Height = 90, - Text = "Jane Doe, MBA", - FontSize = 48, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - - // Gold Divider - new PdfDividerElement - { - X = 300, - Y = 370, - Width = 531, - Height = 2, - Thickness = 2.0, - ColorHex = "#D97706", - ZIndex = 5 - }, - - // Body Text - new PdfTextElement - { - X = 180, - Y = 395, - Width = 771, - Height = 72, - Text = "Having demonstrated supreme governance, transformational strategy execution, and unrelenting commitment to corporate stewardship throughout the Global Enterprise Leadership Fellowship.", - FontSize = 15.5, - FontFamily = "Lora", - LineHeight = 1.5, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Conferred Date & Motto - new PdfTextElement - { - X = 180, - Y = 470, - Width = 771, - Height = 26, - Text = "Conferred this 28th day of October, 2026 at the Annual Convocation • London, United Kingdom", - FontSize = 11.5, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#B45309", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Gold 16-Point Rosette Star Seal - new PdfShapeElement - { - X = 525, - Y = 498, - Width = 80, - Height = 80, - ShapeType = ShapeType.RosetteSeal, - FillColorHex = "#FEF3C7", - StrokeColorHex = "#D97706", - StrokeThickness = 2, - Label = "HONOR", - LabelColorHex = "#92400E", - LabelFontSize = 11, - ZIndex = 8 - }, - - // Motto Text Below Seal - new PdfTextElement - { - X = 450, - Y = 584, - Width = 231, - Height = 22, - Text = "— VIRTUS ET EXCELLENTIA —", - FontSize = 10, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center, - ZIndex = 8 - }, - - // Signatory Left - new PdfTextElement - { - X = 180, - Y = 620, - Width = 260, - Height = 48, - Text = "John Doe", - FontSize = 26, - FontFamily = "Dancing Script", - IsItalic = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - new PdfDividerElement - { - X = 180, - Y = 670, - Width = 260, - Height = 1, - Thickness = 1.5, - ColorHex = "#0F172A", - ZIndex = 5 - }, - new PdfTextElement - { - X = 180, - Y = 678, - Width = 260, - Height = 26, - Text = "Chancellor of the Board", - FontSize = 13, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Signatory Right - new PdfTextElement - { - X = 691, - Y = 620, - Width = 260, - Height = 48, - Text = "Alex Doe", - FontSize = 26, - FontFamily = "Dancing Script", - IsItalic = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - new PdfDividerElement - { - X = 691, - Y = 670, - Width = 260, - Height = 1, - Thickness = 1.5, - ColorHex = "#0F172A", - ZIndex = 5 - }, - new PdfTextElement - { - X = 691, - Y = 678, - Width = 260, - Height = 26, - Text = "Managing Director & Fellow", - FontSize = 13, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Certificates/CertificateTemplate.cs b/src/PdfEditorApp/Templates/Certificates/CertificateTemplate.cs deleted file mode 100644 index e5bc9ef..0000000 --- a/src/PdfEditorApp/Templates/Certificates/CertificateTemplate.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Certificates; - -public class CertificateTemplate : ITemplateDefinition -{ - public string Id => "certificate"; - public string Name => "Certificate of Achievement"; - public string Description => "National Olympiad award of distinction with gold medal ribbon seal, ornate crimson borders, and official verification signatures"; - public string Category => "Certificates"; - public string IconKind => "CertificateOutline"; - public string AccentColorHex => "#990000"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Certificate_of_Achievement.pdf", - Author = "CodeFryDev Academy of Computing & Science", - Subject = "Official Certificate of Outstanding Achievement & First-Place Distinction" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Landscape, - Width = 1131, - Height = 800, - BackgroundColorHex = "#FFFFFF", - ShowHeaderFooter = false, - Elements = new List - { - // ========================================== - // 1. LEFT CORNER VECTOR POLYGONAL ART - // ========================================== - - // Main Top-Left Deep Crimson Polygon Wing - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 260, - Height = 800, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 0,0 L 220,0 L 160,380 L 0,720 Z", - FillColorHex = "#990000", - StrokeColorHex = "#00000000", - StrokeThickness = 0, - ZIndex = 1 - }, - - // Top-Left Gold Accent Stripe - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 285, - Height = 800, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 220,0 L 245,0 L 180,380 L 0,770 L 0,720 L 160,380 Z", - FillColorHex = "#F59E0B", - StrokeColorHex = "#00000000", - StrokeThickness = 0, - ZIndex = 2 - }, - - // Bottom-Left Sharp Dark Red Polygonal Wedge - new PdfShapeElement - { - X = 0, - Y = 480, - Width = 160, - Height = 320, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 0,520 L 140,800 L 0,800 Z", - FillColorHex = "#800000", - StrokeColorHex = "#00000000", - StrokeThickness = 0, - ZIndex = 3 - }, - - // ========================================== - // 2. RIGHT CORNER VECTOR POLYGONAL ART - // ========================================== - - // Top-Right Gold Accent Strip - new PdfShapeElement - { - X = 1040, - Y = 0, - Width = 91, - Height = 160, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 1060,0 L 1131,0 L 1131,140 Z", - FillColorHex = "#F59E0B", - StrokeColorHex = "#00000000", - StrokeThickness = 0, - ZIndex = 1 - }, - - // Top-Right Crimson Corner Wedge - new PdfShapeElement - { - X = 1080, - Y = 0, - Width = 51, - Height = 90, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 1090,0 L 1131,0 L 1131,80 Z", - FillColorHex = "#990000", - StrokeColorHex = "#00000000", - StrokeThickness = 0, - ZIndex = 2 - }, - - // Bottom-Right Gold Accent Border Stripe - new PdfShapeElement - { - X = 820, - Y = 420, - Width = 311, - Height = 380, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 1131,430 L 1131,470 L 960,630 L 865,800 L 835,800 L 935,620 Z", - FillColorHex = "#F59E0B", - StrokeColorHex = "#00000000", - StrokeThickness = 0, - ZIndex = 1 - }, - - // Bottom-Right Bold Crimson Polygon Wing - new PdfShapeElement - { - X = 860, - Y = 460, - Width = 271, - Height = 340, - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 1131,470 L 1131,800 L 865,800 L 960,630 Z", - FillColorHex = "#990000", - StrokeColorHex = "#00000000", - StrokeThickness = 0, - ZIndex = 2 - }, - - // ========================================== - // 3. GOLD MEDAL SEAL BADGE WITH RIBBON TAILS - // ========================================== - new PdfShapeElement - { - X = 165, - Y = 95, - Width = 160, - Height = 195, - ShapeType = ShapeType.MedalRibbonBadge, - FillColorHex = "#F59E0B", - StrokeColorHex = "#B45309", - StrokeThickness = 2.5, - SecondaryFillColorHex = "#990000", - ZIndex = 10 - }, - - // ========================================== - // 4. MAIN HEADINGS & CERTIFICATE TITLE - // ========================================== - new PdfTextElement - { - X = 280, - Y = 42, - Width = 700, - Height = 68, - Text = "CERTIFICATE", - FontSize = 46, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#990000", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 280, - Y = 110, - Width = 700, - Height = 40, - Text = "OF OUTSTANDING ACHIEVEMENT", - FontSize = 22, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 280, - Y = 165, - Width = 700, - Height = 28, - Text = "THIS CERTIFICATE IS PROUDLY PRESENTED TO", - FontSize = 12.5, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // ========================================== - // 5. RECIPIENT NAME IN CURSIVE CALLIGRAPHY - // ========================================== - new PdfTextElement - { - X = 250, - Y = 205, - Width = 760, - Height = 95, - Text = "John Doe", - FontSize = 52, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#990000", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - - // Red Horizontal Accent Rule - new PdfDividerElement - { - X = 240, - Y = 310, - Width = 760, - Height = 2, - Thickness = 2.0, - ColorHex = "#990000", - ZIndex = 5 - }, - - // ========================================== - // 6. CITATION & ACHIEVEMENT DESCRIPTION - // ========================================== - new PdfTextElement - { - X = 240, - Y = 330, - Width = 760, - Height = 115, - Text = "For distinguished excellence, exceptional analytical problem-solving, and achieving First Place Honors in the CodeFryDev Mathematics & Computational Science Olympiad. Conferred on the 18th of July, 2026 by the Executive Board of Examiners.", - FontSize = 15.5, - FontFamily = "Lora", - IsBold = false, - LineHeight = 1.55, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Verification Badge (Center) - new PdfShapeElement - { - X = 525, - Y = 460, - Width = 80, - Height = 80, - ShapeType = ShapeType.RosetteSeal, - FillColorHex = "#FEF3C7", - StrokeColorHex = "#D97706", - StrokeThickness = 2, - Label = "1ST PLACE", - LabelColorHex = "#92400E", - LabelFontSize = 10, - ZIndex = 8 - }, - - // ========================================== - // 7. DATE & SIGNATURE COLUMNS - // ========================================== - - // Date Signature (Left) - new PdfTextElement - { - X = 210, - Y = 575, - Width = 260, - Height = 40, - Text = "July 18, 2026", - FontSize = 18, - FontFamily = "Playfair Display", - IsBold = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfDividerElement - { - X = 210, - Y = 620, - Width = 260, - Height = 2, - Thickness = 1.5, - ColorHex = "#1E293B", - ZIndex = 5 - }, - new PdfTextElement - { - X = 210, - Y = 628, - Width = 260, - Height = 26, - Text = "Date of Issuance", - FontSize = 12.5, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 210, - Y = 654, - Width = 260, - Height = 22, - Text = "Credential ID: CFD-2026-89412", - FontSize = 10, - FontFamily = "Montserrat", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Cursive Handwritten Signature (Right) - new PdfTextElement - { - X = 660, - Y = 555, - Width = 280, - Height = 60, - Text = "Jane Doe", - FontSize = 32, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - new PdfDividerElement - { - X = 660, - Y = 620, - Width = 280, - Height = 2, - Thickness = 1.5, - ColorHex = "#1E293B", - ZIndex = 5 - }, - new PdfTextElement - { - X = 660, - Y = 628, - Width = 280, - Height = 26, - Text = "Dr. Jane Doe, Ph.D.", - FontSize = 12.5, - FontFamily = "Montserrat", - IsBold = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 660, - Y = 654, - Width = 280, - Height = 22, - Text = "President, CodeFryDev Academy", - FontSize = 10, - FontFamily = "Montserrat", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Certificates/DiplomaAcademicTemplate.cs b/src/PdfEditorApp/Templates/Certificates/DiplomaAcademicTemplate.cs deleted file mode 100644 index 6ee86f6..0000000 --- a/src/PdfEditorApp/Templates/Certificates/DiplomaAcademicTemplate.cs +++ /dev/null @@ -1,356 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Certificates; - -public class DiplomaAcademicTemplate : ITemplateDefinition -{ - public string Id => "diploma"; - public string Name => "Collegiate Academic Diploma"; - public string Description => "Formal university degree with ornamental borders, Latin crest, and official seal"; - public string Category => "Certificates"; - public string IconKind => "SchoolOutline"; - public string AccentColorHex => "#7C2D12"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Collegiate_Diploma.pdf", - Author = "CodeFryDev Institute of Technology", - Subject = "Degree of Master of Computer Science" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Landscape, - Width = 1131, - Height = 800, - BackgroundColorHex = "#FFFDF9", - ShowHeaderFooter = false, - Elements = new List - { - // Formal Multi-Border Inset - new PdfShapeElement - { - X = 30, - Y = 30, - Width = 1071, - Height = 740, - FillColorHex = "#00000000", - StrokeColorHex = "#78350F", - StrokeThickness = 3.5, - CornerRadius = 4, - ZIndex = 1 - }, - new PdfShapeElement - { - X = 40, - Y = 40, - Width = 1051, - Height = 720, - FillColorHex = "#00000000", - StrokeColorHex = "#D97706", - StrokeThickness = 1.0, - CornerRadius = 2, - ZIndex = 2 - }, - - // University Name - new PdfTextElement - { - X = 100, - Y = 80, - Width = 931, - Height = 45, - Text = "THE TRUSTEES AND FACULTY OF", - FontSize = 14, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 100, - Y = 125, - Width = 931, - Height = 58, - Text = "CODEFRYDEV INSTITUTE OF TECHNOLOGY", - FontSize = 36, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 100, - Y = 195, - Width = 931, - Height = 26, - Text = "UPON THE RECOMMENDATION OF THE FACULTY HAVE CONFERRED UPON", - FontSize = 11, - FontFamily = "Playfair Display", - IsItalic = true, - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Graduate Name - new PdfTextElement - { - X = 100, - Y = 235, - Width = 931, - Height = 95, - Text = "John Doe", - FontSize = 52, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - - // Degree Conferred - new PdfTextElement - { - X = 100, - Y = 325, - Width = 931, - Height = 24, - Text = "THE DEGREE OF", - FontSize = 11, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 100, - Y = 350, - Width = 931, - Height = 42, - Text = "MASTER OF SCIENCE IN ARTIFICIAL INTELLIGENCE", - FontSize = 24, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - new PdfTextElement - { - X = 150, - Y = 395, - Width = 831, - Height = 48, - Text = "SUMMA CUM LAUDE • WITH HIGHEST DISTINCTION IN NEURAL DYNAMICS & GRAPH ALGORITHMS\nWITH ALL THE RIGHTS, HONORS, AND PRIVILEGES PERTAINING THERETO", - FontSize = 10, - FontFamily = "Playfair Display", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Latin Inscription - new PdfTextElement - { - X = 150, - Y = 445, - Width = 831, - Height = 26, - Text = "\"Quod bonum, faustum, felix fortunatumque sit. Ex auctoritate Senatus Academici.\"", - FontSize = 10, - FontFamily = "Cinzel", - IsItalic = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Official University Gold Seal - new PdfShapeElement - { - X = 515, - Y = 475, - Width = 100, - Height = 100, - ShapeType = ShapeType.LaurelWreathSeal, - FillColorHex = "#FEF3C7", - StrokeColorHex = "#B45309", - StrokeThickness = 2.5, - Label = "SEAL", - LabelColorHex = "#92400E", - LabelFontSize = 12, - ZIndex = 8 - }, - - // Latin Motto Below Seal - new PdfTextElement - { - X = 415, - Y = 582, - Width = 300, - Height = 22, - Text = "— DISCERE • COGNOSCERE • CREARE —", - FontSize = 9.5, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - ZIndex = 8 - }, - - // President Signature Left - new PdfTextElement - { - X = 80, - Y = 590, - Width = 260, - Height = 50, - Text = "John Doe", - FontSize = 28, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - new PdfDividerElement - { - X = 80, - Y = 640, - Width = 260, - Height = 1, - Thickness = 1.5, - ColorHex = "#78350F", - ZIndex = 5 - }, - new PdfTextElement - { - X = 80, - Y = 648, - Width = 260, - Height = 24, - Text = "President of the Institute", - FontSize = 11, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Dean Signature Center - new PdfTextElement - { - X = 435, - Y = 605, - Width = 260, - Height = 35, - Text = "Jane Doe", - FontSize = 24, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - new PdfDividerElement - { - X = 435, - Y = 640, - Width = 260, - Height = 1, - Thickness = 1.5, - ColorHex = "#78350F", - ZIndex = 5 - }, - new PdfTextElement - { - X = 435, - Y = 648, - Width = 260, - Height = 24, - Text = "Dean of Graduate Studies", - FontSize = 11, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Registrar Signature Right - new PdfTextElement - { - X = 790, - Y = 590, - Width = 260, - Height = 50, - Text = "Alex Doe", - FontSize = 28, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 6 - }, - new PdfDividerElement - { - X = 790, - Y = 640, - Width = 260, - Height = 1, - Thickness = 1.5, - ColorHex = "#78350F", - ZIndex = 5 - }, - new PdfTextElement - { - X = 790, - Y = 648, - Width = 260, - Height = 24, - Text = "Registrar & Academic Secretary", - FontSize = 11, - FontFamily = "Cinzel", - IsBold = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }, - - // Degree Serial Tracking Footer - new PdfTextElement - { - X = 100, - Y = 690, - Width = 931, - Height = 22, - Text = "Degree Serial No: CFD-MS-2026-948102 • Conferred June 15, 2026 • Verified by CodeFryDev Academic Registry", - FontSize = 8.5, - FontFamily = "Montserrat", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Corporate/AnnualReportTemplate.cs b/src/PdfEditorApp/Templates/Corporate/AnnualReportTemplate.cs deleted file mode 100644 index defb131..0000000 --- a/src/PdfEditorApp/Templates/Corporate/AnnualReportTemplate.cs +++ /dev/null @@ -1,1007 +0,0 @@ -using System; -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Corporate; - -public class AnnualReportTemplate : ITemplateDefinition -{ - public string Id => "annualreport"; - public string Name => "Annual Corporate Report"; - public string Description => "Comprehensive 3-page corporate report with CEO address, segment metrics, financial statements, and charts"; - public string Category => "Corporate"; - public string IconKind => "ChartBoxOutline"; - public string AccentColorHex => "#0F6CBD"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "CodeFryDev_Corp_Annual_Report_2026.pdf", - Author = "CodeFryDev Corporation Inc.", - Subject = "Fiscal Year 2026 Annual Report & Consolidated Financial Statements", - CreatedDate = DateTime.Now - }; - - // ========================================================================= - // PAGE 1: Executive Summary, KPI Cards, Revenue Chart & Strategic Outlook - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "CODEFRYDEV CORPORATION • FISCAL YEAR 2026 ANNUAL REPORT", - FooterCenter = "CONFIDENTIAL & PROPRIETARY", - FooterRight = "Page 1 of 3", - Elements = new List - { - // Top Brand Accent Bar - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 800, - Height = 6, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - // CodeFryDev Logo Badge Top Right - new PdfShapeElement - { - X = 680, - Y = 35, - Width = 55, - Height = 55, - CornerRadius = 10, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#0C599B", - StrokeThickness = 0, - Label = "CF", - LabelColorHex = "#FFFFFF", - LabelFontSize = 24 - }, - new PdfTextElement - { - X = 600, - Y = 95, - Width = 135, - Height = 24, - Text = "CODEFRYDEV CORP.", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Right - }, - - // Title "ANNUAL REPORT" - new PdfTextElement - { - X = 55, - Y = 35, - Width = 530, - Height = 40, - Text = "ANNUAL CORPORATE REPORT", - FontSize = 26, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Left - }, - new PdfTextElement - { - X = 55, - Y = 78, - Width = 530, - Height = 22, - Text = "FISCAL YEAR ENDED DECEMBER 31, 2026 • GLOBAL ENTERPRISE SOLUTIONS", - FontSize = 10.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD", - Alignment = TextAlignmentMode.Left - }, - - // Header Divider Line - new PdfDividerElement - { - X = 55, - Y = 122, - Width = 690, - Height = 2, - Thickness = 2, - ColorHex = "#0F6CBD" - }, - - // 1.0 Letter from the Chief Executive Officer - new PdfTextElement - { - X = 55, - Y = 132, - Width = 690, - Height = 22, - Text = "1.0 Executive Letter to Shareholders & Partners", - FontSize = 14, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 55, - Y = 158, - Width = 690, - Height = 105, - Text = "Fiscal year 2026 has been a transformative period of record performance, disciplined capital allocation, and technological breakthroughs for CodeFryDev Corporation. In an evolving global macro environment, our cloud-native enterprise architecture, automated AI publishing engines, and mission-critical cybersecurity suites generated record top-line revenue of $3.10 Billion (+24% YoY) and net income of $540 Million (+33% YoY). We expanded gross margins to 63.8% and generated $412 Million in free cash flow.", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.45, - TextColorHex = "#1E293B", - BackgroundColorHex = "#F8FAFC", - BorderColorHex = "#E2E8F0", - BorderThickness = 1.0, - Padding = 10, - CornerRadius = 6, - Alignment = TextAlignmentMode.Justify - }, - - // KPI 4-Card Summary Grid - new PdfShapeElement - { - X = 55, - Y = 274, - Width = 160, - Height = 68, - CornerRadius = 6, - FillColorHex = "#F0F7FF", - StrokeColorHex = "#BFDBFE", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 65, - Y = 282, - Width = 140, - Height = 52, - Text = "$3.10 BILLION\nNet Revenue (+24% YoY)", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD", - Alignment = TextAlignmentMode.Center - }, - - new PdfShapeElement - { - X = 232, - Y = 274, - Width = 160, - Height = 68, - CornerRadius = 6, - FillColorHex = "#F0FDF4", - StrokeColorHex = "#BBF7D0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 242, - Y = 282, - Width = 140, - Height = 52, - Text = "$685 MILLION\nOperating EBITDA (+31.7%)", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#166534", - Alignment = TextAlignmentMode.Center - }, - - new PdfShapeElement - { - X = 408, - Y = 274, - Width = 160, - Height = 68, - CornerRadius = 6, - FillColorHex = "#FAF5FF", - StrokeColorHex = "#E9D5FF", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 418, - Y = 282, - Width = 140, - Height = 52, - Text = "$412 MILLION\nFree Cash Flow (+28.4%)", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#7E22CE", - Alignment = TextAlignmentMode.Center - }, - - new PdfShapeElement - { - X = 585, - Y = 274, - Width = 160, - Height = 68, - CornerRadius = 6, - FillColorHex = "#FFFBEB", - StrokeColorHex = "#FDE68A", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 595, - Y = 282, - Width = 140, - Height = 52, - Text = "99.995% SLA\nGlobal Cloud Reliability", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#B45309", - Alignment = TextAlignmentMode.Center - }, - - // Section 2: Highlights Left & Visual Chart Right - new PdfTextElement - { - X = 55, - Y = 356, - Width = 330, - Height = 20, - Text = "Strategic & Operational Milestones", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 378, - Width = 330, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - new PdfTextElement - { - X = 55, - Y = 386, - Width = 330, - Height = 175, - Text = "✔ Acquisition of TechNova AI: Seamlessly integrated real-time vector document intelligence into all flagship offerings.\n✔ Enterprise Customer Expansion: Surpassed 8,420+ global enterprise accounts with net revenue retention (NRR) of 128%.\n✔ Multiplatform Engine Upgrade: Deployed Avalonia UI & SkiaSharp cross-platform architecture across 2.5M active daily seats.\n✔ Patent Portfolio: Granted 14 new international patents in distributed document synchronization and zero-knowledge encryption.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Revenue Growth Chart (Q1-Q4) - new PdfChartElement - { - X = 405, - Y = 356, - Width = 340, - Height = 205, - Title = "FY 2026 Quarterly Revenue Trajectory ($ Billions)", - Categories = new List { "Q1 2026", "Q2 2026", "Q3 2026", "Q4 2026" }, - Values = new List { 0.65, 0.74, 0.82, 0.89 }, - ValueLabels = new List { "$0.65B", "$0.74B", "$0.82B", "$0.89B" }, - BarColorsHex = new List { "#93C5FD", "#60A5FA", "#3B82F6", "#0F6CBD" }, - BackgroundColorHex = "#FAFAFA", - BorderColorHex = "#E2E8F0" - }, - - // Section 3: Strategic Outlook & Long-Term Value Creation - new PdfTextElement - { - X = 55, - Y = 575, - Width = 690, - Height = 20, - Text = "2.0 Strategic Outlook & 2027 Growth Priorities", - FontSize = 13, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 597, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#0F6CBD" - }, - new PdfTextElement - { - X = 55, - Y = 605, - Width = 330, - Height = 180, - Text = "HYBRID CLOUD & EDGE SCALING\nWe are expanding our Kubernetes multi-region fabric to 14 additional Tier-4 colocation hubs across Europe and Asia-Pacific. This will decrease latency for global collaborative document editing to under 15 milliseconds worldwide.\n\nAI AUTOMATION & WORKFLOWS\nOur R&D pipeline will commercialize zero-shot tabular extraction and autonomous report generation, unlocking substantial productivity for Fortune 500 financial institutions.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - new PdfTextElement - { - X = 405, - Y = 605, - Width = 340, - Height = 180, - Text = "SECURITY & ENTERPRISE COMPLIANCE\nMaintaining SOC2 Type II, ISO 27001, and HIPAA compliance remains central to our governance model. We have allocated $65M toward continuous zero-trust authorization audits.\n\nCAPITAL ALLOCATION & BUYBACKS\nThe Board authorized a $250M share repurchase program for FY 2027, underscoring confidence in sustainable compounding cash generation and shareholder returns.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Section 4: Risk Management & Governance Framework - new PdfShapeElement - { - X = 55, - Y = 795, - Width = 690, - Height = 120, - CornerRadius = 6, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 70, - Y = 805, - Width = 660, - Height = 18, - Text = "ENTERPRISE RISK MANAGEMENT & COMPLIANCE SUMMARY", - FontSize = 10.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - new PdfTextElement - { - X = 70, - Y = 828, - Width = 660, - Height = 78, - Text = "Our Enterprise Risk Management (ERM) committee regularly audits cybersecurity protocols, foreign exchange volatility exposures, and supply chain redundancies. Hedging instruments protect over 85% of foreign-currency cash receivables, and multi-cloud failovers ensure zero single-point-of-failure risks for mission-critical client services.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#475569" - }, - - // Sign-off Strip - new PdfTextElement - { - X = 55, - Y = 930, - Width = 330, - Height = 50, - Text = "John Doe\nChief Executive Officer & Chair of the Board", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 415, - Y = 930, - Width = 330, - Height = 50, - Text = "Jane Doe\nChief Financial Officer & Executive VP of Operations", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Right - } - } - }; - - // ========================================================================= - // PAGE 2: Operations, Regional Segments, Business Lines & Sustainability - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "CODEFRYDEV CORPORATION • GLOBAL OPERATIONS & ESG", - FooterCenter = "CONFIDENTIAL & PROPRIETARY", - FooterRight = "Page 2 of 3", - Elements = new List - { - // Top Brand Accent Bar - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 800, - Height = 6, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - new PdfTextElement - { - X = 55, - Y = 35, - Width = 690, - Height = 32, - Text = "3.0 Global Operational Footprint & Segment Performance", - FontSize = 22, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 75, - Width = 690, - Height = 2, - Thickness = 2, - ColorHex = "#0F6CBD" - }, - new PdfTextElement - { - X = 55, - Y = 85, - Width = 690, - Height = 45, - Text = "CodeFryDev operates across 4 core geographical markets supporting over 8,420 enterprise clients in 68 countries. High-availability cloud clustering and automated edge nodes delivered 99.995% uptime across all commercial SLA agreements throughout 2026.", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - - // Table 1: Regional Operational Performance - new PdfTableElement - { - X = 55, - Y = 135, - Width = 690, - Height = 190, - Headers = new List { "Operating Region", "Data Hubs", "Enterprise Clients", "Uptime SLA", "2026 Revenue", "YoY Growth" }, - Rows = new List> - { - new() { "North America (HQ)", "14", "4,350", "99.998%", "$1,420 Million", "+22.4%" }, - new() { "EMEA (London / Frankfurt)", "9", "2,420", "99.995%", "$980 Million", "+26.1%" }, - new() { "APAC (Tokyo / Singapore)", "7", "1,280", "99.992%", "$540 Million", "+28.5%" }, - new() { "Latin America (São Paulo)", "3", "370", "99.985%", "$160 Million", "+18.5%" } - }, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#E2E8F0" - }, - - // Section 3.2: Product Segment Performance Table - new PdfTextElement - { - X = 55, - Y = 340, - Width = 690, - Height = 22, - Text = "3.1 Revenue & Contribution by Business Segment", - FontSize = 13, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 364, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - new PdfTableElement - { - X = 55, - Y = 374, - Width = 690, - Height = 190, - Headers = new List { "Product Line", "Revenue ($M)", "Gross Margin", "Active Seats", "Key Capabilities" }, - Rows = new List> - { - new() { "Enterprise Cloud Platform", "$1,450.0", "72.4%", "1,850,000", "Microservices, API Ingestion & AKS" }, - new() { "Desktop Publishing Suite", "$920.0", "64.8%", "2,450,000", "Cross-Platform Avalonia / SkiaSharp" }, - new() { "AI Vector Intelligence", "$510.0", "58.2%", "680,000", "LLM Document OCR & Synthesis" }, - new() { "Professional Advisory & Sec", "$220.0", "42.0%", "N/A", "SOC2 Architecture & Custom Integrations" } - }, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#E2E8F0" - }, - - // Section 4: Environmental, Social & Governance (ESG) - new PdfTextElement - { - X = 55, - Y = 580, - Width = 690, - Height = 22, - Text = "4.0 Environmental, Social & Governance (ESG) Stewardship", - FontSize = 13, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 604, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#0F6CBD" - }, - - // ESG 3-Pillar Cards - new PdfShapeElement - { - X = 55, - Y = 614, - Width = 218, - Height = 180, - CornerRadius = 6, - FillColorHex = "#F0FDF4", - StrokeColorHex = "#BBF7D0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 68, - Y = 624, - Width = 192, - Height = 160, - Text = "🌿 ENVIRONMENTAL\n• 100% Renewable Energy: Sourced green energy across 100% of direct data facilities.\n• Zero Waste Hardware: 94% of retired server components recycled or refurbished.\n• Carbon Neutrality Goal: On track for net-zero Scope 1 & 2 emissions by 2028.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#166534" - }, - - new PdfShapeElement - { - X = 291, - Y = 614, - Width = 218, - Height = 180, - CornerRadius = 6, - FillColorHex = "#F0F7FF", - StrokeColorHex = "#BFDBFE", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 304, - Y = 624, - Width = 192, - Height = 160, - Text = "🤝 SOCIAL & TALENT\n• Global Workforce: 3,450 employees across 18 development centers.\n• STEM Mentorship: $12M invested in youth coding fellowships and university scholarships.\n• Retention & Culture: 91% employee retention with top-quartile Glassdoor rating.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#0F6CBD" - }, - - new PdfShapeElement - { - X = 527, - Y = 614, - Width = 218, - Height = 180, - CornerRadius = 6, - FillColorHex = "#FAF5FF", - StrokeColorHex = "#E9D5FF", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 540, - Y = 624, - Width = 192, - Height = 160, - Text = "⚖️ GOVERNANCE\n• Independent Board: 8 of 10 directors are fully independent.\n• Whistleblower Protections: 24/7 confidential reporting channel.\n• Audit & Risk: Quarterly external cybersecurity penetration testing and comprehensive controls.", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#7E22CE" - }, - - // Section 5: Board of Directors & Corporate Officers Roster - new PdfTextElement - { - X = 55, - Y = 810, - Width = 690, - Height = 22, - Text = "5.0 Executive Leadership & Board of Directors", - FontSize = 13, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 834, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - new PdfTextElement - { - X = 55, - Y = 842, - Width = 330, - Height = 130, - Text = "EXECUTIVE COMMITTEE:\n• John Doe — Chief Executive Officer & Chair\n• Jane Doe — Chief Financial Officer\n• Alex Doe — Chief Technology Officer\n• Sam Doe, J.D. — Chief Legal & Compliance Officer\n• Taylor Doe — Chief Product Officer", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - }, - new PdfTextElement - { - X = 405, - Y = 842, - Width = 340, - Height = 130, - Text = "INDEPENDENT DIRECTORS:\n• Jordan Doe — Lead Independent Director\n• Morgan Doe — Former CFO, Enterprise Cloud\n• Dr. Robin Doe — Dean of Engineering, Technology Institute\n• Chris Doe — Managing Partner, Horizon Capital\n• Lee Doe — Chair of Audit Committee", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#334155" - } - } - }; - - // ========================================================================= - // PAGE 3: Consolidated Financials, Balance Sheet, Cash Flow & Audit Opinion - // ========================================================================= - var page3 = new PdfPageModel - { - PageNumber = 3, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "CODEFRYDEV CORPORATION • CONSOLIDATED FINANCIAL STATEMENTS", - FooterCenter = "CONFIDENTIAL & PROPRIETARY", - FooterRight = "Page 3 of 3", - Elements = new List - { - // Top Brand Accent Bar - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 800, - Height = 6, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - new PdfTextElement - { - X = 55, - Y = 35, - Width = 690, - Height = 32, - Text = "6.0 Consolidated Financial Statements & Audit Report", - FontSize = 22, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 75, - Width = 690, - Height = 2, - Thickness = 2, - ColorHex = "#0F6CBD" - }, - - // Statement of Operations (P&L) Table - new PdfTextElement - { - X = 55, - Y = 85, - Width = 690, - Height = 20, - Text = "CONSOLIDATED STATEMENTS OF OPERATIONS (IN MILLIONS, EXCEPT PER SHARE DATA)", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - new PdfTableElement - { - X = 55, - Y = 108, - Width = 690, - Height = 230, - Headers = new List { "Line Item", "FY 2024", "FY 2025", "FY 2026", "YoY %" }, - Rows = new List> - { - new() { "Total Net Revenue", "$1,980.0", "$2,500.0", "$3,100.0", "+24.0%" }, - new() { "Cost of Goods Sold (COGS)", "$820.0", "$990.0", "$1,120.0", "+13.1%" }, - new() { "Gross Profit", "$1,160.0", "$1,510.0", "$1,980.0", "+31.1%" }, - new() { "Research & Development (R&D)", "$290.0", "$380.0", "$480.0", "+26.3%" }, - new() { "Sales, Marketing & General (SG&A)", "$460.0", "$610.0", "$815.0", "+33.6%" }, - new() { "Operating Income (EBITDA)", "$410.0", "$520.0", "$685.0", "+31.7%" }, - new() { "Provision for Income Taxes (21%)", "$95.0", "$115.0", "$145.0", "+26.1%" }, - new() { "Net Income After Taxes", "$315.0", "$405.0", "$540.0", "+33.3%" }, - new() { "Diluted Earnings Per Share (EPS)", "$1.82", "$2.34", "$3.12", "+33.3%" } - }, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#E2E8F0" - }, - - // Condensed Balance Sheet & Cash Flow Table - new PdfTextElement - { - X = 55, - Y = 350, - Width = 690, - Height = 20, - Text = "CONDENSED CONSOLIDATED BALANCE SHEET & CASH FLOW (AS OF DEC 31, 2026)", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - new PdfTableElement - { - X = 55, - Y = 372, - Width = 690, - Height = 190, - Headers = new List { "Balance Sheet Metric", "FY 2025", "FY 2026", "YoY Change ($M)", "Health Indicator" }, - Rows = new List> - { - new() { "Cash, Equivalents & Short-Term Marketable Securities", "$840.0", "$1,180.0", "+$340.0", "Robust Liquidity" }, - new() { "Total Current Assets", "$1,450.0", "$1,980.0", "+$530.0", "Current Ratio: 2.4x" }, - new() { "Property, Plant & Cloud Infrastructure Equipment", "$620.0", "$890.0", "+$270.0", "Capacity Expanded" }, - new() { "Total Assets", "$3,280.0", "$4,420.0", "+$1,140.0", "Asset Growth +34.8%" }, - new() { "Total Long-Term Debt & Liabilities", "$650.0", "$580.0", "-$70.0", "Debt-to-Equity: 0.18" }, - new() { "Total Stockholders' Equity", "$2,150.0", "$3,180.0", "+$1,030.0", "Book Value / Share: $18.40" } - }, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#E2E8F0" - }, - - // Section 7: Independent Auditor's Report Box - new PdfShapeElement - { - X = 55, - Y = 575, - Width = 690, - Height = 145, - CornerRadius = 6, - FillColorHex = "#F0F7FF", - StrokeColorHex = "#BFDBFE", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 70, - Y = 585, - Width = 660, - Height = 20, - Text = "REPORT OF INDEPENDENT REGISTERED PUBLIC ACCOUNTING FIRM", - FontSize = 10.5, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - new PdfTextElement - { - X = 70, - Y = 608, - Width = 660, - Height = 102, - Text = "To the Shareholders and Board of Directors of CodeFryDev Corporation Inc.:\n\nOpinion on the Financial Statements: We have audited the accompanying consolidated balance sheets of CodeFryDev Corporation as of December 31, 2026 and 2025, and the related consolidated statements of operations, stockholders' equity, and cash flows. In our opinion, the consolidated financial statements present fairly, in all material respects, the financial position of CodeFryDev Corporation in conformity with U.S. GAAP.\n\nHorizon Global Assurance LLP • Certified Public Accountants • New York, NY • February 24, 2027", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - - // Section 8: Board Signatures & Corporate Approval - new PdfTextElement - { - X = 55, - Y = 735, - Width = 690, - Height = 20, - Text = "7.0 Corporate Signatories & Board Approvals", - FontSize = 13, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 758, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - // Signatory 1 (CEO) - new PdfTextElement - { - X = 55, - Y = 770, - Width = 210, - Height = 40, - Text = "John Doe", - FontSize = 20, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 812, - Width = 200, - Height = 1, - Thickness = 1, - ColorHex = "#94A3B8" - }, - new PdfTextElement - { - X = 55, - Y = 818, - Width = 200, - Height = 35, - Text = "John Doe\nChief Executive Officer", - FontSize = 9, - FontFamily = "Segoe UI", - TextColorHex = "#475569" - }, - - // Signatory 2 (CFO) - new PdfTextElement - { - X = 295, - Y = 770, - Width = 210, - Height = 40, - Text = "Jane Doe", - FontSize = 20, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 295, - Y = 812, - Width = 200, - Height = 1, - Thickness = 1, - ColorHex = "#94A3B8" - }, - new PdfTextElement - { - X = 295, - Y = 818, - Width = 200, - Height = 35, - Text = "Jane Doe\nChief Financial Officer", - FontSize = 9, - FontFamily = "Segoe UI", - TextColorHex = "#475569" - }, - - // Signatory 3 (Lead Director) - new PdfTextElement - { - X = 535, - Y = 770, - Width = 210, - Height = 40, - Text = "Alex Doe", - FontSize = 20, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 535, - Y = 812, - Width = 200, - Height = 1, - Thickness = 1, - ColorHex = "#94A3B8" - }, - new PdfTextElement - { - X = 535, - Y = 818, - Width = 200, - Height = 35, - Text = "Alex Doe\nLead Independent Director", - FontSize = 9, - FontFamily = "Segoe UI", - TextColorHex = "#475569" - }, - - // SEC EDGAR & Investor Relations Verification Notice - new PdfDividerElement - { - X = 55, - Y = 868, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - new PdfTextElement - { - X = 55, - Y = 878, - Width = 690, - Height = 30, - Text = "Investor Relations: ir@codefrydev.in • Web: https://investors.codefrydev.in • Transfer Agent: Trust Co.", - FontSize = 8, - FontFamily = "Segoe UI", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - doc.Pages.Add(page3); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Corporate/EmployeePayslipTemplate.cs b/src/PdfEditorApp/Templates/Corporate/EmployeePayslipTemplate.cs deleted file mode 100644 index ef294cd..0000000 --- a/src/PdfEditorApp/Templates/Corporate/EmployeePayslipTemplate.cs +++ /dev/null @@ -1,429 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Corporate; - -public class EmployeePayslipTemplate : ITemplateDefinition -{ - public string Id => "payslip"; - public string Name => "Employee Monthly Payslip & Salary Statement"; - public string Description => "Modern corporate employee salary statement with dynamic earnings & deductions tables, verification QR code, barcode, and merge placeholders."; - public string Category => "Corporate"; - public string IconKind => "CashMultiple"; - public string AccentColorHex => "#0F6CBD"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Payslip_{{EmployeeId}}_{{EmployeeName}}_{{PayPeriod}}.pdf", - Author = "{{CompanyName ?? CodeFryDev Inc.}}", - Subject = "Salary Payslip for {{EmployeeName}} - {{PayPeriod}}" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "CodeFryDev Inc. • Confidential & Proprietary Document", - FooterCenter = "Computer Generated • No Physical Signature Required", - FooterRight = "Generated for {{EmployeeName}} (ID: {{EmployeeId}})", - Elements = new List - { - // Top Brand Accent Header Bar - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 800, - Height = 8, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - // 1. Company Logo Badge - new PdfShapeElement - { - X = 50, - Y = 35, - Width = 48, - Height = 48, - CornerRadius = 10, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#0C599B", - StrokeThickness = 0, - Label = "CF", - LabelColorHex = "#FFFFFF", - LabelFontSize = 18 - }, - - // 2. Company Details - new PdfTextElement - { - X = 110, - Y = 35, - Width = 380, - Height = 24, - Text = "{{CompanyName ?? CODEFRYDEV INC.}}", - FontSize = 14, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 110, - Y = 60, - Width = 380, - Height = 35, - Text = "100 Innovation Way, Suite 500 • Silicon Valley, CA 94025\nTax ID: US-EIN 94-8291042 • payroll@codefrydev.in", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.3, - TextColorHex = "#64748B" - }, - - // 3. Document Title & Pay Period Card - new PdfShapeElement - { - X = 520, - Y = 35, - Width = 230, - Height = 60, - CornerRadius = 8, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 530, - Y = 42, - Width = 210, - Height = 20, - Text = "MONTHLY SALARY SLIP", - FontSize = 12, - FontFamily = "Segoe UI", - IsBold = true, - Alignment = TextAlignmentMode.Center, - TextColorHex = "#0F6CBD" - }, - new PdfTextElement - { - X = 530, - Y = 66, - Width = 210, - Height = 20, - Text = "Pay Period: {{PayPeriod ?? August 2026}}", - FontSize = 10.5, - FontFamily = "Segoe UI", - IsBold = true, - Alignment = TextAlignmentMode.Center, - TextColorHex = "#334155" - }, - - // Divider Line - new PdfDividerElement - { - X = 50, - Y = 108, - Width = 700, - Height = 1, - ColorHex = "#E2E8F0", - Thickness = 1 - }, - - // 4. Employee Information Summary Card (Container) - new PdfShapeElement - { - X = 50, - Y = 120, - Width = 700, - Height = 115, - CornerRadius = 8, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#CBD5E1", - StrokeThickness = 1 - }, - - // Section Header - new PdfTextElement - { - X = 65, - Y = 128, - Width = 300, - Height = 18, - Text = "EMPLOYEE & BANKING DETAILS", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - - // Row 1: Name, ID, Designation - new PdfTextElement - { - X = 65, - Y = 150, - Width = 220, - Height = 36, - Text = "Employee Name\n{{EmployeeName ?? John Doe}}", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 300, - Y = 150, - Width = 200, - Height = 36, - Text = "Employee ID\n{{EmployeeId ?? EMP-2026-0842}}", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 520, - Y = 150, - Width = 210, - Height = 36, - Text = "Designation\n{{Designation ?? Senior Software Architect}}", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - - // Row 2: Department, Bank Details, PAN/Tax ID - new PdfTextElement - { - X = 65, - Y = 190, - Width = 220, - Height = 36, - Text = "Department\n{{Department ?? Cloud Engineering}}", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 300, - Y = 190, - Width = 200, - Height = 36, - Text = "Bank & Account No.\n{{BankName ?? First National Bank}} • {{AccountNumber ?? ****4892}}", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - new PdfTextElement - { - X = 520, - Y = 190, - Width = 210, - Height = 36, - Text = "Date of Joining / Pay Days\n{{JoiningDate ?? 2022-04-15}} ({{WorkingDays ?? 30}} Days)", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - - // 5. Earnings & Deductions Tables (Two Column Layout) - - // Left: Earnings Table - new PdfTableElement - { - X = 50, - Y = 250, - Width = 342, - Height = 220, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#CBD5E1", - Headers = new List { "EARNINGS BREAKDOWN", "AMOUNT" }, - Rows = new List> - { - new() { "Basic Salary", "{{BasicSalary:C}}" }, - new() { "House Rent Allowance (HRA)", "{{HRA:C}}" }, - new() { "Special Allowance", "{{SpecialAllowance:C}}" }, - new() { "Performance Incentive / Bonus", "{{Bonus:C}}" }, - new() { "Medical & Conveyance Allowance", "{{MedicalAllowance:C}}" }, - new() { "GROSS EARNINGS TOTAL", "{{GrossEarnings:C}}" } - } - }, - - // Right: Deductions Table - new PdfTableElement - { - X = 408, - Y = 250, - Width = 342, - Height = 220, - HeaderBackgroundHex = "#475569", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#CBD5E1", - Headers = new List { "DEDUCTIONS BREAKDOWN", "AMOUNT" }, - Rows = new List> - { - new() { "Provident Fund (PF) / 401(k)", "{{ProvidentFund:C}}" }, - new() { "Income Tax / TDS / PAYE", "{{IncomeTax:C}}" }, - new() { "Professional Tax", "{{ProfessionalTax:C}}" }, - new() { "Health & Life Insurance", "{{Insurance:C}}" }, - new() { "Other Deductions", "{{OtherDeductions:C}}" }, - new() { "TOTAL DEDUCTIONS", "{{TotalDeductions:C}}" } - } - }, - - // 6. Large Net Salary Highlight Card - new PdfShapeElement - { - X = 50, - Y = 490, - Width = 700, - Height = 82, - CornerRadius = 10, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#0C599B", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 75, - Y = 502, - Width = 350, - Height = 24, - Text = "NET SALARY PAYABLE (TAKE HOME)", - FontSize = 11, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#E0F2FE" - }, - new PdfTextElement - { - X = 75, - Y = 528, - Width = 350, - Height = 32, - Text = "{{NetSalary:C}}", - FontSize = 22, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#FFFFFF" - }, - new PdfTextElement - { - X = 440, - Y = 512, - Width = 290, - Height = 44, - Text = "Amount in words:\n{{NetSalaryInWords ?? US Dollars Only}}", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.3, - TextColorHex = "#E0F2FE" - }, - - // 7. Security Verification & Barcode Section - new PdfShapeElement - { - X = 50, - Y = 590, - Width = 700, - Height = 145, - CornerRadius = 8, - FillColorHex = "#FAFAFA", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - - // QR Code - new PdfQrCodeElement - { - X = 70, - Y = 605, - Width = 100, - Height = 100, - Content = "https://payroll.codefrydev.in/verify?emp={{EmployeeId}}&period={{PayPeriod}}&hash={{AuthHash ?? 8f92a10c}}", - Label = "SCAN TO VERIFY", - DarkColorHex = "#0F172A", - LightColorHex = "#FFFFFF" - }, - - // Verification details text - new PdfTextElement - { - X = 185, - Y = 608, - Width = 270, - Height = 110, - Text = "OFFICIAL DIGITAL RECORD\n\nThis payroll document is digitally signed and cryptographically registered in the CodeFryDev Enterprise Payroll Ledger.\n\nVerification Token: {{AuthHash ?? 8F92-A10C-5542}}\nDisbursement Date: {{PaymentDate ?? 2026-08-31}}", - FontSize = 8.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#475569" - }, - - // Barcode - new PdfBarcodeElement - { - X = 480, - Y = 612, - Width = 250, - Height = 55, - CodeValue = "{{EmployeeId ?? EMP-2026-0842}}", - BarcodeFormat = "Code128", - BarColorHex = "#0F172A", - BackgroundColorHex = "#FFFFFF", - ShowText = true - }, - - // Authorized Signatory Stamp Box - new PdfTextElement - { - X = 480, - Y = 680, - Width = 250, - Height = 40, - Text = "Authorized Payroll Officer\nCodeFryDev Inc.", - FontSize = 9, - FontFamily = "Segoe UI", - Alignment = TextAlignmentMode.Center, - TextColorHex = "#64748B" - }, - - // Confidentiality Legal Notice - new PdfTextElement - { - X = 50, - Y = 750, - Width = 700, - Height = 40, - Text = "CONFIDENTIALITY NOTICE: This document contains strictly confidential compensation information intended solely for the named employee. Unauthorized copying, distribution, or disclosure is strictly prohibited under corporate policy and applicable privacy laws.", - FontSize = 7.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#94A3B8" - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Corporate/InteractiveExecutiveBriefTemplate.cs b/src/PdfEditorApp/Templates/Corporate/InteractiveExecutiveBriefTemplate.cs deleted file mode 100644 index c36047c..0000000 --- a/src/PdfEditorApp/Templates/Corporate/InteractiveExecutiveBriefTemplate.cs +++ /dev/null @@ -1,1378 +0,0 @@ -using System; -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Corporate; - -/// -/// High-impact, multi-page landscape presentation deck designed specifically for the .frypdf interactive viewer. -/// Features capabilities impossible in static binary PDFs: -/// - Interactive live data tables with real-time substring search, multi-column sorting, and CSV export -/// - Dynamic animated charts with 600ms cubic ease-out entry, hover tooltips, and live data inspector toggle -/// - Interactive form field compliance checklist and verifiable digital signature blocks -/// - Real QR codes, tracking barcodes, security redaction blocks, and executive sticky notes -/// - 16:10 / landscape presentation geometry (1131 x 800 pt) ideal for laptop and widescreen displays -/// -public class InteractiveExecutiveBriefTemplate : ITemplateDefinition -{ - public string Id => "interactive_executive_deck"; - public string Name => "Executive Strategy & Interactive Deck"; - public string Description => "Landscape presentation deck showcasing interactive living tables with CSV export, animated charts, live checklist form fields, and KPI telemetry"; - public string Category => "Corporate"; - public string IconKind => "PresentationPlay"; - public string AccentColorHex => "#6366F1"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "QuantumScale_Global_Executive_Strategic_Brief_2026.frypdf", - Author = "QuantumScale Systems Global Corp", - Subject = "Interactive Board Presentation Deck • Live Tables, Animated Charts, and Security Governance Ledger", - CreatedDate = DateTime.Now - }; - - // ========================================================================= - // PAGE 1: EXECUTIVE INTELLIGENCE & TELEMETRY DASHBOARD (1131 x 800 pt) - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Landscape, - Width = 1131, - Height = 800, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "QUANTUMSCALE CLOUD SYSTEMS • GLOBAL STRATEGIC BRIEF", - HeaderCenter = "CONFIDENTIAL • BOARD OF DIRECTORS PRESENTATION", - HeaderRight = "FY 2026-2027", - FooterLeft = "QUANTUMSCALE ENTERPRISE TELEMETRY • LIVE PRESENTATION DECK", - FooterCenter = "CONFIDENTIAL & PROPRIETARY", - FooterRight = "Slide 1 of 3", - Elements = new List - { - // Top Edge Accent Brand Band - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 1131, - Height = 6, - FillColorHex = "#4F46E5", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - // QuantumScale Logo Badge - new PdfShapeElement - { - X = 50, - Y = 28, - Width = 48, - Height = 48, - CornerRadius = 12, - FillColorHex = "#4F46E5", - StrokeColorHex = "#3730A3", - StrokeThickness = 0, - Label = "QS", - LabelColorHex = "#FFFFFF", - LabelFontSize = 20 - }, - - // Header Titles - new PdfTextElement - { - X = 110, - Y = 30, - Width = 600, - Height = 18, - Text = "QUANTUMSCALE CLOUD ENTERPRISE • EXECUTIVE STRATEGIC BRIEFING", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#6366F1", - CharacterSpacing = 1.0 - }, - new PdfTextElement - { - X = 110, - Y = 48, - Width = 600, - Height = 32, - Text = "Global Enterprise Telemetry & Growth Outlook", - FontSize = 22, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // Right Live Deck Pill Badge - new PdfShapeElement - { - X = 930, - Y = 34, - Width = 151, - Height = 34, - CornerRadius = 17, - FillColorHex = "#EEF2FF", - StrokeColorHex = "#C7D2FE", - StrokeThickness = 1, - Label = "● LIVE INTERACTIVE", - LabelColorHex = "#4F46E5", - LabelFontSize = 10 - }, - new PdfTextElement - { - X = 850, - Y = 74, - Width = 231, - Height = 18, - Text = "Fiscal Year 2026-2027 • Board Review", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Right - }, - - // Top Accent Divider - new PdfDividerElement - { - X = 50, - Y = 94, - Width = 1031, - Height = 2, - ColorHex = "#E2E8F0" - }, - - // ----------------------------------------------------------------- - // ROW 1: FOUR HIGH-IMPACT EXECUTIVE KPI CARDS (Y = 110, Height = 90) - // ----------------------------------------------------------------- - // Card 1: Annual Recurring Revenue - new PdfShapeElement - { - X = 50, - Y = 110, - Width = 242, - Height = 90, - CornerRadius = 12, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfShapeElement - { - X = 50, - Y = 110, - Width = 4, - Height = 90, - CornerRadius = 2, - FillColorHex = "#4F46E5", - StrokeThickness = 0 - }, - new PdfTextElement - { - X = 64, - Y = 122, - Width = 210, - Height = 18, - Text = "ANNUAL RECURRING REVENUE (ARR)", - FontSize = 8.5, - IsBold = true, - TextColorHex = "#64748B" - }, - new PdfTextElement - { - X = 64, - Y = 140, - Width = 120, - Height = 36, - Text = "$148.6M", - FontSize = 24, - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfShapeElement - { - X = 180, - Y = 146, - Width = 98, - Height = 24, - CornerRadius = 12, - FillColorHex = "#ECFDF5", - StrokeColorHex = "#A7F3D0", - StrokeThickness = 1, - Label = "▲ +38.2% YoY", - LabelColorHex = "#059669", - LabelFontSize = 9.5 - }, - - // Card 2: Net Dollar Retention (NDR) - new PdfShapeElement - { - X = 313, - Y = 110, - Width = 242, - Height = 90, - CornerRadius = 12, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfShapeElement - { - X = 313, - Y = 110, - Width = 4, - Height = 90, - CornerRadius = 2, - FillColorHex = "#0284C7", - StrokeThickness = 0 - }, - new PdfTextElement - { - X = 327, - Y = 122, - Width = 210, - Height = 18, - Text = "NET DOLLAR RETENTION (NDR)", - FontSize = 8.5, - IsBold = true, - TextColorHex = "#64748B" - }, - new PdfTextElement - { - X = 327, - Y = 140, - Width = 110, - Height = 36, - Text = "142.4%", - FontSize = 24, - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfShapeElement - { - X = 438, - Y = 146, - Width = 104, - Height = 24, - CornerRadius = 12, - FillColorHex = "#F0F9FF", - StrokeColorHex = "#BAE6FD", - StrokeThickness = 1, - Label = "★ Top 5% Decile", - LabelColorHex = "#0284C7", - LabelFontSize = 9.5 - }, - - // Card 3: Blended Gross Margin - new PdfShapeElement - { - X = 576, - Y = 110, - Width = 242, - Height = 90, - CornerRadius = 12, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfShapeElement - { - X = 576, - Y = 110, - Width = 4, - Height = 90, - CornerRadius = 2, - FillColorHex = "#10B981", - StrokeThickness = 0 - }, - new PdfTextElement - { - X = 590, - Y = 122, - Width = 210, - Height = 18, - Text = "BLENDED GROSS MARGIN", - FontSize = 8.5, - IsBold = true, - TextColorHex = "#64748B" - }, - new PdfTextElement - { - X = 590, - Y = 140, - Width = 110, - Height = 36, - Text = "79.8%", - FontSize = 24, - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfShapeElement - { - X = 702, - Y = 146, - Width = 102, - Height = 24, - CornerRadius = 12, - FillColorHex = "#ECFDF5", - StrokeColorHex = "#A7F3D0", - StrokeThickness = 1, - Label = "+340 bps YoY", - LabelColorHex = "#059669", - LabelFontSize = 9.5 - }, - - // Card 4: Active Enterprise Mesh Nodes - new PdfShapeElement - { - X = 839, - Y = 110, - Width = 242, - Height = 90, - CornerRadius = 12, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfShapeElement - { - X = 839, - Y = 110, - Width = 4, - Height = 90, - CornerRadius = 2, - FillColorHex = "#8B5CF6", - StrokeThickness = 0 - }, - new PdfTextElement - { - X = 853, - Y = 122, - Width = 210, - Height = 18, - Text = "ACTIVE ENTERPRISE MESH NODES", - FontSize = 8.5, - IsBold = true, - TextColorHex = "#64748B" - }, - new PdfTextElement - { - X = 853, - Y = 140, - Width = 110, - Height = 36, - Text = "42,850", - FontSize = 24, - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfShapeElement - { - X = 964, - Y = 146, - Width = 104, - Height = 24, - CornerRadius = 12, - FillColorHex = "#F5F3FF", - StrokeColorHex = "#DDD6FE", - StrokeThickness = 1, - Label = "38 Cloud Zones", - LabelColorHex = "#7C3AED", - LabelFontSize = 9.5 - }, - - // ----------------------------------------------------------------- - // ROW 2: LEFT COLUMN: INTERACTIVE REVENUE CHART (Y = 218, H = 512) - // ----------------------------------------------------------------- - new PdfShapeElement - { - X = 50, - Y = 218, - Width = 590, - Height = 512, - CornerRadius = 16, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 72, - Y = 236, - Width = 400, - Height = 24, - Text = "Quarterly Revenue Acceleration & Runway ($M)", - FontSize = 14, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 72, - Y = 260, - Width = 520, - Height = 18, - Text = "Interactive chart: Hover bars for details, replay animation, or toggle data table view.", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#64748B" - }, - - // Interactive Chart Element (BarColumn) - new PdfChartElement - { - X = 72, - Y = 286, - Width = 546, - Height = 345, - ZIndex = 400, - Title = "Quarterly Revenue Acceleration ($M)", - ChartType = ChartType.BarColumn, - BackgroundColorHex = "#FFFFFF", - BorderColorHex = "#E2E8F0", - Categories = new List { "Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025", "Q1 2026", "Q2 2026" }, - Values = new List { 18.4, 24.1, 29.8, 36.5, 42.2, 48.6 }, - ValueLabels = new List { "$18.4M", "$24.1M", "$29.8M", "$36.5M", "$42.2M", "$48.6M" }, - BarColorsHex = new List { "#818CF8", "#6366F1", "#4F46E5", "#4338CA", "#3730A3", "#312E81" } - }, - - // Takeaway insight banner inside Chart card - new PdfShapeElement - { - X = 72, - Y = 645, - Width = 546, - Height = 65, - CornerRadius = 10, - FillColorHex = "#EEF2FF", - StrokeColorHex = "#C7D2FE", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 86, - Y = 655, - Width = 518, - Height = 48, - Text = "💡 Board Takeaway: The compounding trajectory demonstrates sustainable 40%+ expansion with accelerating enterprise net retention. Interactive presentation mode permits live exploration and real-time hover inspection.", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#3730A3", - TextWrap = true - }, - - // ----------------------------------------------------------------- - // ROW 2: RIGHT COLUMN: STRATEGIC GROWTH PILLARS & STICKY NOTE - // ----------------------------------------------------------------- - new PdfShapeElement - { - X = 660, - Y = 218, - Width = 421, - Height = 512, - CornerRadius = 16, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 682, - Y = 236, - Width = 380, - Height = 22, - Text = "Global ARR Contribution by Pillar", - FontSize = 14, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 682, - Y = 258, - Width = 380, - Height = 18, - Text = "Interactive Donut: Hover segments for share % or switch to table view.", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#64748B" - }, - - // Interactive Donut / Pie Chart Element - new PdfChartElement - { - X = 680, - Y = 282, - Width = 381, - Height = 225, - ZIndex = 400, - Title = "ARR by Product Pillar ($M)", - ChartType = ChartType.DonutPie, - BackgroundColorHex = "#FFFFFF", - BorderColorHex = "#E2E8F0", - Categories = new List { "AI Agent Mesh", "Sovereign Enclaves", "Edge Storage", "Post-Quantum Security" }, - Values = new List { 62.4, 41.6, 26.8, 17.8 }, - ValueLabels = new List { "$62.4M", "$41.6M", "$26.8M", "$17.8M" }, - BarColorsHex = new List { "#4F46E5", "#0284C7", "#10B981", "#8B5CF6" } - }, - - // Divider inside Right Card - new PdfDividerElement - { - X = 680, - Y = 516, - Width = 381, - Height = 1, - ColorHex = "#E2E8F0" - }, - - // Executive Sticky Note Callout - new PdfStickyNoteElement - { - X = 680, - Y = 526, - Width = 381, - Height = 184, - ZIndex = 600, - Author = "Elena Vance, CEO & Co-Founder", - Timestamp = "Sep 2026", - NoteText = "Our transition to interactive .frypdf presentations empowers our board, stakeholders, and partners to engage directly with living data rather than static snapshots. All numbers here are verifiable directly against our cloud warehouse.", - Status = "Verified Executive Note" - } - } - }; - - // ========================================================================= - // PAGE 2: INTERACTIVE OPERATIONAL LEDGER & SEGMENT MATRIX (1131 x 800 pt) - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Landscape, - Width = 1131, - Height = 800, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "QUANTUMSCALE CLOUD SYSTEMS • OPERATIONAL LEDGER", - HeaderCenter = "LIVE AUDITABLE SEGMENT BREAKDOWN", - HeaderRight = "FY 2026-2027", - FooterLeft = "QUANTUMSCALE ENTERPRISE TELEMETRY • LIVE PRESENTATION DECK", - FooterCenter = "CONFIDENTIAL & PROPRIETARY", - FooterRight = "Slide 2 of 3", - Elements = new List - { - // Top Edge Accent Brand Band - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 1131, - Height = 6, - FillColorHex = "#0284C7", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - // Header Titles - new PdfTextElement - { - X = 50, - Y = 30, - Width = 550, - Height = 18, - Text = "FINANCE & OPERATIONS • INTERACTIVE SEGMENT LEDGER", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0284C7", - CharacterSpacing = 1.0 - }, - new PdfTextElement - { - X = 50, - Y = 48, - Width = 680, - Height = 32, - Text = "Segment Revenue, Operating Expenditure & Gross Margin Matrix", - FontSize = 22, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // Interactive Instruction Pill Badge - new PdfShapeElement - { - X = 760, - Y = 36, - Width = 321, - Height = 34, - CornerRadius = 17, - FillColorHex = "#F0F9FF", - StrokeColorHex = "#BAE6FD", - StrokeThickness = 1, - Label = "🔍 Live Table: Type in search or click headers to sort", - LabelColorHex = "#0284C7", - LabelFontSize = 9.5 - }, - - // Top Accent Divider - new PdfDividerElement - { - X = 50, - Y = 94, - Width = 1031, - Height = 2, - ColorHex = "#E2E8F0" - }, - - // ----------------------------------------------------------------- - // MAIN LIVING DATA TABLE (Centerpiece Feature, ZIndex = 500) - // ----------------------------------------------------------------- - new PdfTableElement - { - X = 50, - Y = 108, - Width = 1031, - Height = 348, - ZIndex = 500, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#CBD5E1", - Headers = new List - { - "Business Unit & Segment", - "Primary Region", - "Contract Tier", - "FY26 ARR ($M)", - "Blended Margin", - "YoY Growth", - "Health Status" - }, - Rows = new List> - { - new() { "Enterprise Cloud Infrastructure", "North America", "Fortune 500", "$52.4M", "78.4%", "+42.1%", "Prime Exceeding" }, - new() { "Autonomous AI Agent Mesh", "Global / Multi-Region", "Enterprise Core", "$34.8M", "84.1%", "+115.4%", "Hyper-Growth" }, - new() { "Edge Compute & Telemetry Mesh", "Asia-Pacific (APAC)", "Strategic Tier", "$28.6M", "71.2%", "+38.5%", "On Track" }, - new() { "Zero-Trust Security & Vaults", "Europe / Middle East", "Government / FinServ", "$18.2M", "81.0%", "+27.8%", "Strong Retention" }, - new() { "Developer API & Neural Embeddings", "Global Direct", "Self-Serve & Scale", "$14.6M", "88.5%", "+64.2%", "Expanding" }, - new() { "Hardware Security Enclaves", "North America", "Defense & Healthcare", "$9.8M", "76.3%", "+19.0%", "Stable" }, - new() { "Global Managed Support SLA", "All Geographies", "Mission Critical", "$7.2M", "68.9%", "+14.5%", "High Satisfaction" } - } - }, - - // ----------------------------------------------------------------- - // BOTTOM ROW: GEOGRAPHIC ARR CHART + EFFICIENCY SCORECARD - // ----------------------------------------------------------------- - // Left Card: Regional Share Animated Chart - new PdfShapeElement - { - X = 50, - Y = 472, - Width = 560, - Height = 258, - CornerRadius = 14, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 72, - Y = 490, - Width = 400, - Height = 22, - Text = "Global Cloud Infrastructure Capacity & Node Load", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfChartElement - { - X = 72, - Y = 518, - Width = 516, - Height = 195, - ZIndex = 400, - Title = "Regional Mesh Load (% Capacity)", - ChartType = ChartType.HorizontalBar, - BackgroundColorHex = "#FFFFFF", - BorderColorHex = "#E2E8F0", - Categories = new List { "Virginia US-East", "Frankfurt EU-Central", "Tokyo AP-Northeast", "Dubai ME-Central", "São Paulo SA-East" }, - Values = new List { 94.0, 88.5, 91.2, 76.4, 68.0 }, - ValueLabels = new List { "94% Load", "88% Load", "91% Load", "76% Load", "68% Load" }, - BarColorsHex = new List { "#3B82F6", "#10B981", "#8B5CF6", "#F59E0B", "#EC4899" } - }, - - // Right Card: Capital Efficiency & Rule of 40 Scorecard - new PdfShapeElement - { - X = 635, - Y = 472, - Width = 446, - Height = 258, - CornerRadius = 14, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 657, - Y = 490, - Width = 400, - Height = 22, - Text = "Capital Efficiency & Rule of 40 Metrics", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // Metric 1: Rule of 40 Score - new PdfShapeElement - { - X = 657, - Y = 525, - Width = 72, - Height = 26, - CornerRadius = 13, - FillColorHex = "#ECFDF5", - StrokeThickness = 0, - Label = "58.6%", - LabelColorHex = "#059669", - LabelFontSize = 11 - }, - new PdfTextElement - { - X = 739, - Y = 529, - Width = 325, - Height = 20, - Text = "Rule of 40 Rating: +38.2% Growth + 20.4% Free Cash Flow", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // Metric 2: Payback Period - new PdfShapeElement - { - X = 657, - Y = 565, - Width = 72, - Height = 26, - CornerRadius = 13, - FillColorHex = "#EFF6FF", - StrokeThickness = 0, - Label = "6.4 Mo", - LabelColorHex = "#0284C7", - LabelFontSize = 11 - }, - new PdfTextElement - { - X = 739, - Y = 569, - Width = 325, - Height = 20, - Text = "Customer Acquisition Payback Period (Industry avg: 14 mo)", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // Metric 3: Magic Number - new PdfShapeElement - { - X = 657, - Y = 605, - Width = 72, - Height = 26, - CornerRadius = 13, - FillColorHex = "#F5F3FF", - StrokeThickness = 0, - Label = "1.42x", - LabelColorHex = "#7C3AED", - LabelFontSize = 11 - }, - new PdfTextElement - { - X = 739, - Y = 609, - Width = 325, - Height = 20, - Text = "Sales Efficiency Magic Number (Top decile tier > 1.0x)", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // Export Callout Box - new PdfShapeElement - { - X = 657, - Y = 648, - Width = 404, - Height = 65, - CornerRadius = 8, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 670, - Y = 658, - Width = 378, - Height = 48, - Text = "📋 Instant Export: In .frypdf interactive mode, you can copy the full financial table directly as clean CSV to Excel or Google Sheets by clicking 'Copy CSV' in the table header above.", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#475569", - TextWrap = true - } - } - }; - - // ========================================================================= - // PAGE 3: PRODUCT ROADMAP, GOVERNANCE AUDIT & SIGN-OFF (1131 x 800 pt) - // ========================================================================= - var page3 = new PdfPageModel - { - PageNumber = 3, - Format = PageFormat.A4, - Orientation = PageOrientation.Landscape, - Width = 1131, - Height = 800, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "QUANTUMSCALE CLOUD SYSTEMS • GOVERNANCE & COMPLIANCE", - HeaderCenter = "AUDITED ENTERPRISE CLEARANCE", - HeaderRight = "FY 2026-2027", - FooterLeft = "QUANTUMSCALE ENTERPRISE TELEMETRY • LIVE PRESENTATION DECK", - FooterCenter = "CONFIDENTIAL & PROPRIETARY", - FooterRight = "Slide 3 of 3", - Elements = new List - { - // Top Edge Accent Brand Band - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 1131, - Height = 6, - FillColorHex = "#059669", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - // Header Titles - new PdfTextElement - { - X = 50, - Y = 30, - Width = 600, - Height = 18, - Text = "ROADMAP & GOVERNANCE • INTERACTIVE COMPLIANCE CLEARANCE", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#059669", - CharacterSpacing = 1.0 - }, - new PdfTextElement - { - X = 50, - Y = 48, - Width = 680, - Height = 32, - Text = "Horizon Product Milestones & Enterprise Security Audit", - FontSize = 22, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - - // Verified Pill Badge - new PdfShapeElement - { - X = 870, - Y = 36, - Width = 211, - Height = 34, - CornerRadius = 17, - FillColorHex = "#ECFDF5", - StrokeColorHex = "#A7F3D0", - StrokeThickness = 1, - Label = "🛡️ ISO-27001 / SOC-2 CERTIFIED", - LabelColorHex = "#059669", - LabelFontSize = 9.5 - }, - - // Top Accent Divider - new PdfDividerElement - { - X = 50, - Y = 94, - Width = 1031, - Height = 2, - ColorHex = "#E2E8F0" - }, - - // ----------------------------------------------------------------- - // LEFT SECTION: 3-HORIZON PRODUCT ROADMAP (X = 50, Y = 108, W = 540) - // ----------------------------------------------------------------- - new PdfShapeElement - { - X = 50, - Y = 108, - Width = 540, - Height = 412, - CornerRadius = 16, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 72, - Y = 126, - Width = 490, - Height = 22, - Text = "Strategic Technology Roadmap Horizons", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 72, - Y = 148, - Width = 490, - Height = 18, - Text = "Planned releases and R&D capital allocation for the next 18 months", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#64748B" - }, - - // Horizon 1: Now (Q4 2026) - new PdfShapeElement - { - X = 72, - Y = 176, - Width = 496, - Height = 65, - CornerRadius = 8, - FillColorHex = "#EFF6FF", - StrokeColorHex = "#BFDBFE", - StrokeThickness = 1 - }, - new PdfShapeElement - { - X = 84, - Y = 186, - Width = 110, - Height = 22, - CornerRadius = 11, - FillColorHex = "#0284C7", - StrokeThickness = 0, - Label = "HORIZON 1 (NOW)", - LabelColorHex = "#FFFFFF", - LabelFontSize = 8.5 - }, - new PdfTextElement - { - X = 204, - Y = 184, - Width = 350, - Height = 48, - Text = "Neural Vector Search Index v3.0 • Multi-Cluster Telemetry Streaming • Auto-Scaling Cryptographic Enclaves", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0369A1", - TextWrap = true - }, - - // Horizon 2: Next 6-12 Months - new PdfShapeElement - { - X = 72, - Y = 252, - Width = 496, - Height = 65, - CornerRadius = 8, - FillColorHex = "#F5F3FF", - StrokeColorHex = "#DDD6FE", - StrokeThickness = 1 - }, - new PdfShapeElement - { - X = 84, - Y = 262, - Width = 110, - Height = 22, - CornerRadius = 11, - FillColorHex = "#7C3AED", - StrokeThickness = 0, - Label = "HORIZON 2 (H1 27)", - LabelColorHex = "#FFFFFF", - LabelFontSize = 8.5 - }, - new PdfTextElement - { - X = 204, - Y = 260, - Width = 350, - Height = 48, - Text = "Zero-Latency Distributed Mesh Cache • Self-Healing Cloud Edge • Autonomous Cross-Cloud Orchestrator", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#5B21B6", - TextWrap = true - }, - - // Horizon 3: 12-18 Months - new PdfShapeElement - { - X = 72, - Y = 328, - Width = 496, - Height = 65, - CornerRadius = 8, - FillColorHex = "#ECFDF5", - StrokeColorHex = "#A7F3D0", - StrokeThickness = 1 - }, - new PdfShapeElement - { - X = 84, - Y = 338, - Width = 110, - Height = 22, - CornerRadius = 11, - FillColorHex = "#059669", - StrokeThickness = 0, - Label = "HORIZON 3 (H2 27)", - LabelColorHex = "#FFFFFF", - LabelFontSize = 8.5 - }, - new PdfTextElement - { - X = 204, - Y = 336, - Width = 350, - Height = 48, - Text = "Quantum-Resistant Key Exchange (NIST Post-Quantum) • Sovereign Autonomous Data Mesh Orchestration", - FontSize = 9.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#047857", - TextWrap = true - }, - - // R&D Commitment Box - new PdfShapeElement - { - X = 72, - Y = 405, - Width = 496, - Height = 98, - CornerRadius = 8, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 86, - Y = 416, - Width = 468, - Height = 78, - Text = "🔬 R&D Investment Commitment: 22.4% of top-line revenue ($33.2M annualized) is committed to forward-looking systems research, distributed consensus algorithms, and zero-knowledge privacy protocols to maintain competitive moat.", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#334155", - TextWrap = true - }, - - // ----------------------------------------------------------------- - // RIGHT SECTION: COMPLIANCE CHECKLIST & DIGITAL SIGNATURES (W = 466) - // ----------------------------------------------------------------- - new PdfShapeElement - { - X = 615, - Y = 108, - Width = 466, - Height = 412, - CornerRadius = 16, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 637, - Y = 126, - Width = 420, - Height = 22, - Text = "Interactive Governance & Compliance Checklist", - FontSize = 13, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 637, - Y = 148, - Width = 420, - Height = 18, - Text = "Living checklist for regulatory compliance and audit validation", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#64748B" - }, - - // Interactive Form Field Checkboxes - new PdfFormFieldElement - { - X = 637, - Y = 176, - Width = 420, - Height = 26, - FieldType = FormFieldType.Checkbox, - Label = "SOC-2 Type II & ISO/IEC 27001:2022 Recertification Completed", - Value = "true" - }, - new PdfFormFieldElement - { - X = 637, - Y = 208, - Width = 420, - Height = 26, - FieldType = FormFieldType.Checkbox, - Label = "EU-US Data Privacy Framework & GDPR Chapter V Enforced", - Value = "true" - }, - new PdfFormFieldElement - { - X = 637, - Y = 240, - Width = 420, - Height = 26, - FieldType = FormFieldType.Checkbox, - Label = "Hardware-Rooted Zero-Trust Cryptographic Enclave Attestation", - Value = "true" - }, - new PdfFormFieldElement - { - X = 637, - Y = 272, - Width = 420, - Height = 26, - FieldType = FormFieldType.Checkbox, - Label = "FedRAMP High In-Process Baseline Security Controls Validated", - Value = "true" - }, - new PdfFormFieldElement - { - X = 637, - Y = 304, - Width = 420, - Height = 26, - FieldType = FormFieldType.Checkbox, - Label = "NIST Post-Quantum Cryptography Migration Plan Formally Approved", - Value = "false" - }, - - // Digital Signatures Header - new PdfTextElement - { - X = 637, - Y = 344, - Width = 420, - Height = 18, - Text = "AUTHORIZED EXECUTIVE SIGN-OFF & CRYPTOGRAPHIC ATTESTATION", - FontSize = 8, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#64748B" - }, - - // Digital Signatures - new PdfFormFieldElement - { - X = 637, - Y = 366, - Width = 200, - Height = 44, - FieldType = FormFieldType.Signature, - Label = "Elena Vance, CEO", - Value = "DIGITALLY VERIFIED: E. Vance (QS-SEC-9842)" - }, - new PdfFormFieldElement - { - X = 852, - Y = 366, - Width = 205, - Height = 44, - FieldType = FormFieldType.Signature, - Label = "Marcus Chen, CFO", - Value = "DIGITALLY VERIFIED: M. Chen (QS-FIN-1402)" - }, - - new PdfTextElement - { - X = 637, - Y = 420, - Width = 420, - Height = 18, - Text = "Cryptographically signed & anchored to internal audit ledger • Sep 2026", - FontSize = 8.5, - FontFamily = "Segoe UI", - TextColorHex = "#64748B" - }, - - // Proprietary Commercial Secret Redaction Demo - new PdfRedactionElement - { - X = 637, - Y = 448, - Width = 420, - Height = 30, - ExemptionCode = "[FOIA (b)(4) PROPRIETARY COMMERCIAL SECRET]", - FillColorHex = "#1E293B", - TextColorHex = "#F8FAFC", - BorderColorHex = "#0F172A", - BorderThickness = 1 - }, - - // ----------------------------------------------------------------- - // BOTTOM BANNER: QR CODE, TRACKING BARCODE & PORTAL VERIFICATION - // ----------------------------------------------------------------- - new PdfShapeElement - { - X = 50, - Y = 538, - Width = 1031, - Height = 192, - CornerRadius = 14, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - - // Left Information & Barcode - new PdfTextElement - { - X = 72, - Y = 552, - Width = 310, - Height = 20, - Text = "Living Document Verification & Ledger", - FontSize = 12.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 72, - Y = 574, - Width = 310, - Height = 56, - Text = "Active presentation format with instant CSV export, interactive checklists, dynamic filtering, and cryptographic audit proofs.", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#475569", - TextWrap = true - }, - new PdfBarcodeElement - { - X = 72, - Y = 636, - Width = 300, - Height = 74, - CodeValue = "QS-SEC-2026-FRYPDF-9842", - BarcodeFormat = "Code128", - BarColorHex = "#0F172A", - BackgroundColorHex = "#FFFFFF", - ShowText = true - }, - - // Center Area Trend Chart (Multi-Chart Variety) - new PdfChartElement - { - X = 398, - Y = 550, - Width = 380, - Height = 166, - ZIndex = 400, - Title = "3-Year R&D Capital Investment ($M)", - ChartType = ChartType.Area, - BackgroundColorHex = "#FFFFFF", - BorderColorHex = "#E2E8F0", - Categories = new List { "2024", "2025", "2026", "2027 (P)", "2028 (P)" }, - Values = new List { 14.2, 22.8, 33.2, 48.0, 65.5 }, - ValueLabels = new List { "$14.2M", "$22.8M", "$33.2M", "$48.0M", "$65.5M" }, - BarColorsHex = new List { "#059669", "#10B981", "#34D399", "#6EE7B7", "#A7F3D0" } - }, - - // Right Live QR Code & Portal Link - new PdfQrCodeElement - { - X = 800, - Y = 554, - Width = 120, - Height = 120, - Content = "https://quantumscale.cloud/verify/doc?id=QS-2026-FRYPDF-9842", - Label = "SCAN TO VERIFY" - }, - new PdfTextElement - { - X = 934, - Y = 556, - Width = 135, - Height = 75, - Text = "Scan QR code to verify immutable ledger hash & cloud telemetry mirror.", - FontSize = 9.5, - FontFamily = "Segoe UI", - TextColorHex = "#475569", - TextWrap = true - }, - new PdfShapeElement - { - X = 934, - Y = 642, - Width = 135, - Height = 32, - CornerRadius = 16, - FillColorHex = "#4F46E5", - StrokeThickness = 0, - Label = "🔗 SHA-256 SECURED", - LabelColorHex = "#FFFFFF", - LabelFontSize = 9.5 - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - doc.Pages.Add(page3); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Corporate/OcrAnalysisReportTemplate.cs b/src/PdfEditorApp/Templates/Corporate/OcrAnalysisReportTemplate.cs deleted file mode 100644 index 1d81048..0000000 --- a/src/PdfEditorApp/Templates/Corporate/OcrAnalysisReportTemplate.cs +++ /dev/null @@ -1,556 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Corporate; - -/// -/// Executive Document Intelligence & OCR Analysis Report Template. -/// Demonstrates automated optical character recognition, confidence heatmaps, -/// extracted multi-span entity streams, structured table reconstruction, and QR validation. -/// -public class OcrAnalysisReportTemplate : ITemplateDefinition -{ - public string Id => "ocranalysisreport"; - public string Name => "OCR Document Audit & Recognition Report"; - public string Description => "Automated document intelligence audit featuring OCR confidence metrics, extracted entity streams, structured table recovery, and QR verification"; - public string Category => "Corporate & Finance"; - public string IconKind => "TextBoxCheckOutline"; - public string AccentColorHex => "#059669"; - - public PdfDocumentModel Create() - { - return GenerateDocument(); - } - - public static PdfDocumentModel GenerateDocument() - { - var doc = new PdfDocumentModel - { - Title = "OCR_Document_Audit_Report.pdf", - Author = "FryPDF Document Intelligence Subsystem", - Subject = "Optical Character Recognition & Structured Layout Deconstruction Audit", - Keywords = "OCR, Document Intelligence, Extraction, Layout Analysis, Confidence, Audit" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 595.28, - Height = 841.89, - BackgroundColorHex = "#F8FAFC", - ShowHeaderFooter = true, - HeaderLeft = "FRYPDF DOCUMENT INTELLIGENCE • OCR AUDIT", - HeaderRight = "CONFIDENCE: 99.4% (HIGH)", - FooterLeft = "AUTOMATED OCR PIPELINE AUDIT", - FooterCenter = "SHA-256: 8f9a2b1c4e6d3f0a", - FooterRight = "PAGE 1 OF 1" - }; - - // ========================================== - // 1. TOP HEADER & METRIC BANNER - // ========================================== - - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 24, - Width = 547.28, - Height = 105, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 8, - FillColorHex = "#064E3B", - StrokeColorHex = "#065F46", - StrokeThickness = 1.5, - ZIndex = 0 - }); - - // Top Emerald Accent Stripe - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 24, - Width = 547.28, - Height = 4, - ShapeType = ShapeType.Rectangle, - FillColorHex = "#10B981", - ZIndex = 1 - }); - - // Category Tag - page.Elements.Add(new PdfTextElement - { - X = 44, - Y = 38, - Width = 260, - Height = 18, - Text = "NEURAL OCR V4.2 • LAYOUT ANALYSIS", - FontFamily = "Segoe UI", - FontSize = 9.0, - IsBold = true, - TextColorHex = "#6EE7B7", - CharacterSpacing = 1.2, - ZIndex = 2 - }); - - // Main Report Title with Multi-Span - var mainTitle = new PdfTextElement - { - X = 44, - Y = 56, - Width = 480, - Height = 32, - FontFamily = "Segoe UI", - FontSize = 18, - TextColorHex = "#FFFFFF", - Spans = new List - { - new() { Text = "OCR Document Recognition ", IsBold = true, TextColorHex = "#FFFFFF" }, - new() { Text = "& Audit Report", IsBold = true, TextColorHex = "#6EE7B7" } - }, - ZIndex = 2 - }; - mainTitle.SynchronizePlainTextFromSpans(); - page.Elements.Add(mainTitle); - - // Document Meta Subtitle - var docMeta = new PdfTextElement - { - X = 44, - Y = 90, - Width = 500, - Height = 22, - FontFamily = "Segoe UI", - FontSize = 9.5, - TextColorHex = "#D1FAE5", - Spans = new List - { - new() { Text = "Target: " }, - new() { Text = "Commercial_Invoice_Scan_300DPI.pdf", IsBold = true, TextColorHex = "#FFFFFF" }, - new() { Text = " • Engine Accuracy: " }, - new() { Text = "99.4% (Pass)", IsBold = true, TextColorHex = "#6EE7B7" }, - new() { Text = " • Status: " }, - new() { Text = "Verified", IsBold = true, IsUnderline = true, TextColorHex = "#34D399" } - }, - ZIndex = 2 - }; - docMeta.SynchronizePlainTextFromSpans(); - page.Elements.Add(docMeta); - - // ========================================== - // 2. OCR STATS ROW (3 CARDS) - // ========================================== - - // Card 1: High Confidence - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 140, - Width = 175, - Height = 65, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - page.Elements.Add(new PdfTextElement - { - X = 34, - Y = 148, - Width = 155, - Height = 48, - FontFamily = "Segoe UI", - FontSize = 8.5, - TextColorHex = "#64748B", - Spans = new List - { - new() { Text = "1,215 Words (97.4%)\n", IsBold = true, FontSize = 12.0, TextColorHex = "#059669" }, - new() { Text = "High Confidence (≥ 98%)" } - }, - ZIndex = 2 - }); - - // Card 2: Review Threshold - page.Elements.Add(new PdfShapeElement - { - X = 210, - Y = 140, - Width = 175, - Height = 65, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - page.Elements.Add(new PdfTextElement - { - X = 220, - Y = 148, - Width = 155, - Height = 48, - FontFamily = "Segoe UI", - FontSize = 8.5, - TextColorHex = "#64748B", - Spans = new List - { - new() { Text = "33 Words (2.6%)\n", IsBold = true, FontSize = 12.0, TextColorHex = "#D97706" }, - new() { Text = "Low Contrast / Script Review" } - }, - ZIndex = 2 - }); - - // Card 3: Deconstructed Blocks - page.Elements.Add(new PdfShapeElement - { - X = 396, - Y = 140, - Width = 175, - Height = 65, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - page.Elements.Add(new PdfTextElement - { - X = 406, - Y = 148, - Width = 155, - Height = 48, - FontFamily = "Segoe UI", - FontSize = 8.5, - TextColorHex = "#64748B", - Spans = new List - { - new() { Text = "18 Structured Blocks\n", IsBold = true, FontSize = 12.0, TextColorHex = "#0F6CBD" }, - new() { Text = "1 Table • 1 QR • 16 Texts" } - }, - ZIndex = 2 - }); - - // ========================================== - // 3. SECTION A: EXTRACTED ENTITY STREAMS (MULTI-SPAN) - // ========================================== - - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 216, - Width = 547.28, - Height = 145, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 216, - Width = 4, - Height = 145, - ShapeType = ShapeType.Rectangle, - FillColorHex = "#059669", - ZIndex = 1 - }); - - var entityHeader = new PdfTextElement - { - X = 40, - Y = 228, - Width = 450, - Height = 20, - FontFamily = "Segoe UI", - FontSize = 11.5, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "1. Recognized Key-Value Entities & ", IsBold = true }, - new() { Text = "Confidence Annotations", IsBold = true, TextColorHex = "#059669" } - }, - ZIndex = 2 - }; - entityHeader.SynchronizePlainTextFromSpans(); - page.Elements.Add(entityHeader); - - var entityStreamCol1 = new PdfTextElement - { - X = 40, - Y = 252, - Width = 250, - Height = 98, - FontFamily = "Segoe UI", - FontSize = 9.0, - LineHeight = 1.45, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "• Vendor / Merchant: " }, - new() { Text = "Acme Technologies Global Ltd.", IsBold = true, TextColorHex = "#0F172A" }, - new() { Text = " [99.8%]\n• Invoice Number: " }, - new() { Text = "INV-2026-8842", IsBold = true, TextColorHex = "#0F6CBD" }, - new() { Text = " [99.9%]\n• Issue Date: " }, - new() { Text = "2026-08-31", IsBold = true }, - new() { Text = " • Due Date: " }, - new() { Text = "2026-09-30", IsBold = true, TextColorHex = "#DC2626" }, - new() { Text = "\n• Tax Registration / VAT: " }, - new() { Text = "GB-984210547", IsBold = true, TextColorHex = "#059669" } - }, - ZIndex = 2 - }; - entityStreamCol1.SynchronizePlainTextFromSpans(); - page.Elements.Add(entityStreamCol1); - - var entityStreamCol2 = new PdfTextElement - { - X = 305, - Y = 252, - Width = 255, - Height = 98, - FontFamily = "Segoe UI", - FontSize = 9.0, - LineHeight = 1.45, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "• Subtotal (Net): " }, - new() { Text = "$14,500.00 USD", IsBold = true }, - new() { Text = " [99.9%]\n• Tax Rate (VAT 20%): " }, - new() { Text = "$2,900.00 USD", IsBold = true }, - new() { Text = " [99.7%]\n• Total Payable: " }, - new() { Text = "$17,400.00 USD", IsBold = true, FontSize = 10.0, TextColorHex = "#059669" }, - new() { Text = "\n• Payment Method: " }, - new() { Text = "SWIFT / Direct Deposit (Verified)", IsBold = true, IsUnderline = true } - }, - ZIndex = 2 - }; - entityStreamCol2.SynchronizePlainTextFromSpans(); - page.Elements.Add(entityStreamCol2); - - // ========================================== - // 4. SECTION B: RECONSTRUCTED STRUCTURED TABLE - // ========================================== - - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 372, - Width = 547.28, - Height = 185, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - var tableHeader = new PdfTextElement - { - X = 40, - Y = 384, - Width = 450, - Height = 20, - FontFamily = "Segoe UI", - FontSize = 11.5, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "2. Reconstructed Line Items & ", IsBold = true }, - new() { Text = "Tabular Data Grid", IsBold = true, TextColorHex = "#0F6CBD" } - }, - ZIndex = 2 - }; - tableHeader.SynchronizePlainTextFromSpans(); - page.Elements.Add(tableHeader); - - // Table Element - var ocrTable = new PdfTableElement - { - X = 40, - Y = 410, - Width = 515, - Height = 130, - Headers = new List { "SKU / Code", "Description", "Qty", "Unit Price", "Total (USD)", "OCR Match" }, - Rows = new List> - { - new() { "CLD-SRV-01", "Enterprise Cloud Instance (48 vCPU, 192GB RAM)", "2", "$4,200.00", "$8,400.00", "99.8%" }, - new() { "AI-API-INF", "Neural OCR Document Intelligence API Tier", "1", "$3,500.00", "$3,500.00", "99.9%" }, - new() { "STG-NVME-04", "Dedicated NVMe High-IOPS Document Storage (4TB)", "4", "$650.00", "$2,600.00", "99.5%" }, - new() { "SUP-24X7-PRM", "24/7 Dedicated SLA Technical Support", "1", "$0.00", "$0.00", "100.0%" } - }, - HeaderBackgroundHex = "#0F172A", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#CBD5E1", - ZIndex = 2 - }; - page.Elements.Add(ocrTable); - - // ========================================== - // 5. SECTION C: SECURITY, QR VALIDATION & AUDIT SIGNATURE - // ========================================== - - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 568, - Width = 350, - Height = 170, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - var secHeader = new PdfTextElement - { - X = 38, - Y = 580, - Width = 320, - Height = 20, - FontFamily = "Segoe UI", - FontSize = 11.0, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "3. Security & ", IsBold = true }, - new() { Text = "Compliance Validation", IsBold = true, TextColorHex = "#7C3AED" } - }, - ZIndex = 2 - }; - secHeader.SynchronizePlainTextFromSpans(); - page.Elements.Add(secHeader); - - var secBody = new PdfTextElement - { - X = 38, - Y = 604, - Width = 320, - Height = 120, - FontFamily = "Segoe UI", - FontSize = 9.0, - LineHeight = 1.45, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "• Digital Signature: " }, - new() { Text = "VALID (RSA-4096 / SHA-256)", IsBold = true, TextColorHex = "#059669" }, - new() { Text = "\n• Signer: " }, - new() { Text = "Acme Corp Authorized Officer", IsItalic = true }, - new() { Text = "\n• Timestamp: " }, - new() { Text = "2026-08-31T22:45:10 UTC (RFC 3161)" }, - new() { Text = "\n• PII Redaction Audit: " }, - new() { Text = "Zero Leaks Detected (Clean)", IsBold = true, TextColorHex = "#059669" }, - new() { Text = "\n• Forensic Verification: " }, - new() { Text = "Pass (Unaltered PDF Bitstream)", IsBold = true, IsUnderline = true } - }, - ZIndex = 2 - }; - secBody.SynchronizePlainTextFromSpans(); - page.Elements.Add(secBody); - - // QR Code Verification Card - page.Elements.Add(new PdfShapeElement - { - X = 385, - Y = 568, - Width = 186.28, - Height = 170, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - // QR Code Element - page.Elements.Add(new PdfQrCodeElement - { - X = 428, - Y = 582, - Width = 100, - Height = 100, - Content = "https://verify.frypdf.dev/audit/inv-2026-8842?sig=8f9a2b1c", - Label = "Scan to Verify Audit", - DarkColorHex = "#064E3B", - LightColorHex = "#FFFFFF", - ZIndex = 2 - }); - - page.Elements.Add(new PdfTextElement - { - X = 395, - Y = 690, - Width = 166, - Height = 40, - Alignment = TextAlignmentMode.Center, - FontFamily = "Segoe UI", - FontSize = 8.5, - LineHeight = 1.3, - TextColorHex = "#64748B", - Spans = new List - { - new() { Text = "Verified by " }, - new() { Text = "FryPDF OCR Engine\n", IsBold = true, TextColorHex = "#064E3B" }, - new() { Text = "Cryptographically Signed" } - }, - ZIndex = 2 - }); - - // Bottom Decorative Divider - page.Elements.Add(new PdfDividerElement - { - X = 24, - Y = 748, - Width = 547.28, - Height = 10, - Style = DividerStyle.Straight, - ColorHex = "#059669", - Thickness = 1.5, - ZIndex = 1 - }); - - // Bottom Audit Sign-off Note - var auditFooter = new PdfTextElement - { - X = 24, - Y = 764, - Width = 547.28, - Height = 22, - Alignment = TextAlignmentMode.Center, - FontFamily = "Segoe UI", - FontSize = 8.5, - TextColorHex = "#64748B", - Spans = new List - { - new() { Text = "Document Intelligence Engine • Generated by " }, - new() { Text = "FryPDF Studio", IsBold = true, TextColorHex = "#059669" }, - new() { Text = " • Automated Optical Verification Suite" } - }, - ZIndex = 2 - }; - auditFooter.SynchronizePlainTextFromSpans(); - page.Elements.Add(auditFooter); - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Education/BilingualExamPaperTemplate.cs b/src/PdfEditorApp/Templates/Education/BilingualExamPaperTemplate.cs deleted file mode 100644 index f5d3e5d..0000000 --- a/src/PdfEditorApp/Templates/Education/BilingualExamPaperTemplate.cs +++ /dev/null @@ -1,390 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Education; - -public class BilingualExamPaperTemplate : ITemplateDefinition -{ - public string Id => "bilingual_exam_paper"; - public string Name => "Bilingual Exam Paper (Hindi & English)"; - public string Description => "Dual-language school & competitive test paper with Devanagari typography, formula reference box, numbered question cards, and answer key matrix"; - public string Category => "Education"; - public string IconKind => "Translate"; - public string AccentColorHex => "#B91C1C"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Simple_Interest_Bilingual_Exam_Paper.pdf", - Author = "Department of Competitive Examinations", - Subject = "Bilingual Practice Exam: Simple Interest & Quantitative Aptitude" - }; - - // ========================================================================= - // PAGE 1: Header, Formulas, Questions 1 to 10 - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "अभ्यास प्रश्नमाला • Practice Test Series", - HeaderCenter = "साधारण ब्याज • Simple Interest", - HeaderRight = "Paper Code: SI-2026", - FooterLeft = "ACME Academy • आदर्श विद्यालय", - FooterCenter = "Bilingual Examination Series (Hindi & English)", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Accent Stripe - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#B91C1C", StrokeColorHex = "#00000000" - }, - - // Institution & Exam Title Box - new PdfShapeElement - { - X = 40, Y = 35, Width = 720, Height = 95, - CornerRadius = 8, FillColorHex = "#FEF2F2", - StrokeColorHex = "#FECACA", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 45, Width = 690, Height = 28, - Text = "ACME Academy • आदर्श विद्यालय", - FontSize = 17, IsBold = true, TextColorHex = "#991B1B", - Alignment = TextAlignmentMode.Center, - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 75, Width = 690, Height = 24, - Text = "साधारण ब्याज (Simple Interest) : प्रतियोगी परीक्षाओं हेतु 50 महत्वपूर्ण प्रश्न", - FontSize = 13, IsBold = true, TextColorHex = "#B91C1C", - Alignment = TextAlignmentMode.Center, - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 102, Width = 690, Height = 20, - Text = "समय: 2 घण्टे (Time: 2 Hours) | पूर्णांक: 100 अंक (Max Marks: 100) | ऋणात्मक अंकन: 0.25", - FontSize = 10, TextColorHex = "#7F1D1D", - Alignment = TextAlignmentMode.Center, - FontFamily = "Noto Sans Devanagari" - }, - - // Student Metadata Strip - new PdfShapeElement - { - X = 40, Y = 140, Width = 720, Height = 42, - CornerRadius = 6, FillColorHex = "#F8FAFC", - StrokeColorHex = "#CBD5E1", StrokeThickness = 1 - }, - new PdfTextElement - { - X = 55, Y = 152, Width = 380, Height = 20, - Text = "छात्र/छात्रा का नाम (Student Name): __________________________", - FontSize = 10.5, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 450, Y = 152, Width = 150, Height = 20, - Text = "रोल नं (Roll No): _________", - FontSize = 10.5, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 615, Y = 152, Width = 130, Height = 20, - Text = "दिनांक (Date): _______", - FontSize = 10.5, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Important Formulas Callout Card - new PdfShapeElement - { - X = 40, Y = 192, Width = 720, Height = 88, - CornerRadius = 6, FillColorHex = "#FFFBEB", - StrokeColorHex = "#FDE68A", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 200, Width = 690, Height = 20, - Text = "महत्वपूर्ण सूत्र (Important Formulas):", - FontSize = 11, IsBold = true, TextColorHex = "#92400E", - FontFamily = "Noto Sans Devanagari" - }, - new PdfMathElement - { - X = 55, Y = 222, Width = 690, Height = 50, - Formula = @"SI = \frac{P \times R \times T}{100}, \quad A = P + SI = P\left(1 + \frac{RT}{100}\right), \quad P = \frac{100 \times SI}{R \times T}, \quad R = \frac{100 \times SI}{P \times T}", - FontSize = 13, TextColorHex = "#78350F", - Category = MathCategory.SchoolArithmetic, - Alignment = TextAlignmentMode.Center - }, - - // Section 1 Header - new PdfShapeElement - { - X = 40, Y = 290, Width = 720, Height = 26, - CornerRadius = 4, FillColorHex = "#1E293B", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 295, Width = 700, Height = 18, - Text = "भाग 1: मूल संकल्पना एवं अभ्यास प्रश्न (Part 1: Basic Concept Problems)", - FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Noto Sans Devanagari" - }, - - // Question 1 Card - new PdfShapeElement { X = 40, Y = 325, Width = 720, Height = 70, CornerRadius = 6, FillColorHex = "#FFFFFF", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 333, Width = 690, Height = 36, - Text = "प्रश्न 1. \u20b95,000 की राशि पर 10% वार्षिक साधारण ब्याज की दर से 3 वर्ष का साधारण ब्याज और मिश्रधन कितना होगा?\n(Find the simple interest and total amount on \u20b95,000 at 10% per annum for 3 years.)", - FontSize = 10.5, TextColorHex = "#0F172A", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 372, Width = 690, Height = 18, - Text = "(A) \u20b91,500, \u20b96,500 (B) \u20b91,200, \u20b96,200 (C) \u20b91,800, \u20b96,800 (D) \u20b92,000, \u20b97,000", - FontSize = 10, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Question 2 Card - new PdfShapeElement { X = 40, Y = 405, Width = 720, Height = 70, CornerRadius = 6, FillColorHex = "#FFFFFF", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 413, Width = 690, Height = 36, - Text = "प्रश्न 2. \u20b98,000 पर कितने प्रतिशत वार्षिक दर से 5 वर्ष का साधारण ब्याज \u20b92,400 हो जाएगा?\n(At what rate of simple interest per annum will \u20b98,000 yield an interest of \u20b92,400 in 5 years?)", - FontSize = 10.5, TextColorHex = "#0F172A", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 452, Width = 690, Height = 18, - Text = "(A) 5% वार्षिक (B) 6% वार्षिक (C) 8% वार्षिक (D) 10% वार्षिक", - FontSize = 10, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Question 3 Card - new PdfShapeElement { X = 40, Y = 485, Width = 720, Height = 70, CornerRadius = 6, FillColorHex = "#FFFFFF", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 493, Width = 690, Height = 36, - Text = "प्रश्न 3. कोई राशि 8% वार्षिक साधारण ब्याज की दर से कितने वर्षों में स्वयं की दोगुनी हो जाएगी?\n(In how many years will a sum of money double itself at 8% simple interest per annum?)", - FontSize = 10.5, TextColorHex = "#0F172A", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 532, Width = 690, Height = 18, - Text = "(A) 10 वर्ष (B) 12.5 वर्ष (C) 15 वर्ष (D) 16 वर्ष", - FontSize = 10, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Question 4 Card - new PdfShapeElement { X = 40, Y = 565, Width = 720, Height = 70, CornerRadius = 6, FillColorHex = "#FFFFFF", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 573, Width = 690, Height = 36, - Text = "प्रश्न 4. \u20b99,000 की राशि 5 वर्ष में किस ब्याज दर से \u20b913,500 हो जाएगी?\n(At what interest rate will a principal of \u20b99,000 amount to \u20b913,500 in 5 years?)", - FontSize = 10.5, TextColorHex = "#0F172A", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 612, Width = 690, Height = 18, - Text = "(A) 10% (B) 12% (C) 15% (D) 8%", - FontSize = 10, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Question 5 Card - new PdfShapeElement { X = 40, Y = 645, Width = 720, Height = 70, CornerRadius = 6, FillColorHex = "#FFFFFF", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 653, Width = 690, Height = 36, - Text = "प्रश्न 5. एक राशि 3 वर्ष में \u20b98,500 और 5 वर्ष में \u20b99,500 हो जाती है। मूलधन एवं वार्षिक दर ज्ञात कीजिए।\n(A sum amounts to \u20b98,500 in 3 years and \u20b99,500 in 5 years at simple interest. Find principal and rate.)", - FontSize = 10.5, TextColorHex = "#0F172A", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 692, Width = 690, Height = 18, - Text = "(A) \u20b97,000; 7.14% (B) \u20b97,200; 6.5% (C) \u20b96,800; 8% (D) \u20b97,500; 5%", - FontSize = 10, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - } - } - }; - - // ========================================================================= - // PAGE 2: Part II Advanced Problems & Answer Key Table - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "अभ्यास प्रश्नमाला • Practice Test Series", - HeaderCenter = "उत्तरमाला एवं व्याख्या • Solutions Matrix", - HeaderRight = "Paper Code: SI-2026", - FooterLeft = "ACME Academy • आदर्श विद्यालय", - FooterCenter = "Bilingual Examination Series (Hindi & English)", - FooterRight = "Page 2 of 2", - Elements = new List - { - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#B91C1C", StrokeColorHex = "#00000000" - }, - - // Section 2 Header - new PdfShapeElement - { - X = 40, Y = 35, Width = 720, Height = 26, - CornerRadius = 4, FillColorHex = "#1E293B", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 40, Width = 700, Height = 18, - Text = "भाग 2: अनुप्रयोग एवं विभाजन समस्याएँ (Part 2: Applied Ratio & Division Problems)", - FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Noto Sans Devanagari" - }, - - // Question 6 Card - new PdfShapeElement { X = 40, Y = 70, Width = 720, Height = 70, CornerRadius = 6, FillColorHex = "#FFFFFF", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 78, Width = 690, Height = 36, - Text = "प्रश्न 6. \u20b95,200 की राशि को दो भागों में इस प्रकार बांटा गया कि पहले भाग पर 8% दर से 5 वर्ष का ब्याज दूसरे भाग पर 10% दर से 4 वर्ष के ब्याज के बराबर है।\n(\u20b95,200 is divided into two parts such that the interest on the first part at 8% for 5 years equals interest on the second at 10% for 4 years.)", - FontSize = 10.5, TextColorHex = "#0F172A", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 117, Width = 690, Height = 18, - Text = "(A) \u20b92,600, \u20b92,600 (B) \u20b93,000, \u20b92,200 (C) \u20b92,800, \u20b92,400 (D) \u20b93,200, \u20b92,000", - FontSize = 10, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Question 7 Card - new PdfShapeElement { X = 40, Y = 150, Width = 720, Height = 70, CornerRadius = 6, FillColorHex = "#FFFFFF", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 158, Width = 690, Height = 36, - Text = "प्रश्न 7. यदि किसी धन पर 3 वर्ष का साधारण ब्याज मूलधन के 3/5 के बराबर है, तो ब्याज की वार्षिक दर क्या होगी?\n(If the simple interest on a sum of money for 3 years is equal to 3/5 of the principal, what is the annual rate?)", - FontSize = 10.5, TextColorHex = "#0F172A", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 197, Width = 690, Height = 18, - Text = "(A) 15% वार्षिक (B) 20% वार्षिक (C) 18% वार्षिक (D) 25% वार्षिक", - FontSize = 10, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Answer Key & Solutions Matrix Banner - new PdfShapeElement - { - X = 40, Y = 235, Width = 720, Height = 55, - CornerRadius = 6, FillColorHex = "#ECFDF5", - StrokeColorHex = "#A7F3D0", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 243, Width = 690, Height = 22, - Text = "उत्तरमाला एवं हल सारांश (Answer Key & Solutions Summary Matrix)", - FontSize = 13, IsBold = true, TextColorHex = "#065F46", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 267, Width = 690, Height = 18, - Text = "प्रतियोगी परीक्षा अभ्यास पत्रक हेतु प्रामाणिक उत्तर एवं संक्षिप्त चरण", - FontSize = 9.5, TextColorHex = "#047857", - FontFamily = "Noto Sans Devanagari" - }, - - // Answer Matrix Table - new PdfTableElement - { - X = 40, Y = 300, Width = 720, Height = 320, - HeaderBackgroundHex = "#065F46", - HeaderTextHex = "#FFFFFF", - BorderColorHex = "#D1D5DB", - AlternateRowBackgroundHex = "#F9FAFB", - Headers = new List { "प्रश्न #", "उत्तर", "मुख्य सूत्र / चरण (Formula & Key Steps)", "अंतिम मान" }, - Rows = new List> - { - new() { "1", "(A)", "SI = (5000 \u00d7 10 \u00d7 3) / 100 = 1500; A = 5000 + 1500", "SI=\u20b91,500; A=\u20b96,500" }, - new() { "2", "(B)", "R = (100 \u00d7 2400) / (8000 \u00d7 5) = 240000 / 40000 = 6%", "R = 6% वार्षिक" }, - new() { "3", "(B)", "SI = P; T = (100 \u00d7 P) / (P \u00d7 8) = 100 / 8 = 12.5 वर्ष", "T = 12.5 वर्ष" }, - new() { "4", "(A)", "SI = 13500 - 9000 = 4500; R = (100 \u00d7 4500) / (9000 \u00d7 5) = 10%", "R = 10% वार्षिक" }, - new() { "5", "(A)", "2 वर्ष का ब्याज = 9500 - 8500 = 1000; 1 वर्ष का ब्याज = 500; P = 8500 - 1500", "P = \u20b97,000; R = 7.14%" }, - new() { "6", "(A)", "x \u00d7 8 \u00d7 5 = (5200 - x) \u00d7 10 \u00d7 4 \u2192 40x = 40(5200 - x) \u2192 2x = 5200", "x = \u20b92,600 प्रत्येक" }, - new() { "7", "(B)", "SI = 3P/5; R = (100 \u00d7 (3P/5)) / (P \u00d7 3) = (60P) / (3P) = 20%", "R = 20% वार्षिक" } - } - }, - - // Student Score & Performance Card - new PdfShapeElement - { - X = 40, Y = 640, Width = 720, Height = 130, - CornerRadius = 8, FillColorHex = "#F8FAFC", - StrokeColorHex = "#CBD5E1", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 655, Width = 300, Height = 22, - Text = "मूल्यांकन एवं अंक तालिका (Evaluation & Score):", - FontSize = 11, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 685, Width = 690, Height = 35, - Text = "कुल प्रश्न: 7 | सही उत्तर: ______ | गलत उत्तर: ______ | प्राप्तांक: _______ / 100\nपरीक्षक की टिप्पणी: ____________________________________________________________________", - FontSize = 10, TextColorHex = "#64748B", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 735, Width = 300, Height = 22, - Text = "परीक्षक हस्ताक्षर (Evaluator Signature): ____________________", - FontSize = 10, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Education/FactorizationWorksheetTemplate.cs b/src/PdfEditorApp/Templates/Education/FactorizationWorksheetTemplate.cs deleted file mode 100644 index 391dcf9..0000000 --- a/src/PdfEditorApp/Templates/Education/FactorizationWorksheetTemplate.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Education; - -public class FactorizationWorksheetTemplate : ITemplateDefinition -{ - public string Id => "factorization_worksheet"; - public string Name => "Algebra & Factorization Worksheet (150 Questions)"; - public string Description => "High-density multi-column algebra practice worksheet with algebraic identity cheat box, 3 balanced question columns, and answer key"; - public string Category => "Education"; - public string IconKind => "Variable"; - public string AccentColorHex => "#4F46E5"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Factorization_150_Questions_Worksheet.pdf", - Author = "Algebra & High School Mathematics Department", - Subject = "150 Comprehensive Practice Questions on Algebraic Identities & Polynomial Factorization" - }; - - // ========================================================================= - // PAGE 1: Key Formulas & Section A (Difference of Squares & Perfect Squares) - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Algebra Mastery Series", - HeaderCenter = "Polynomial Identities & Factorization", - HeaderRight = "Worksheet #03", - FooterLeft = "150 Comprehensive Factorization Practice Worksheet", - FooterCenter = "Authorized for Classroom & Homework Practice", - FooterRight = "Page 1 of 2", - Elements = new List - { - new PdfShapeElement { X = 0, Y = 0, Width = 800, Height = 8, FillColorHex = "#4F46E5", StrokeColorHex = "#00000000" }, - - // Title Banner - new PdfShapeElement { X = 40, Y = 35, Width = 720, Height = 75, CornerRadius = 8, FillColorHex = "#EEF2FF", StrokeColorHex = "#C7D2FE", StrokeThickness = 1.5 }, - new PdfTextElement - { - X = 55, Y = 43, Width = 690, Height = 28, - Text = "Factorization Practice Worksheet : 150 Questions", - FontSize = 17, IsBold = true, TextColorHex = "#3730A3", - FontFamily = "Inter" - }, - new PdfTextElement - { - X = 55, Y = 73, Width = 690, Height = 22, - Text = "Comprehensive Practice on Algebraic Identities, Common Monomials, Difference of Squares & Trinomials", - FontSize = 10.5, IsItalic = true, TextColorHex = "#4F46E5", - FontFamily = "Inter" - }, - - // Formula Header Callout - new PdfShapeElement { X = 40, Y = 120, Width = 720, Height = 75, CornerRadius = 6, FillColorHex = "#FFFBEB", StrokeColorHex = "#FDE68A", StrokeThickness = 1.5 }, - new PdfTextElement - { - X = 55, Y = 128, Width = 690, Height = 18, - Text = "KEY FORMULAS & IDENTITIES (महत्वपूर्ण सर्वसमिकाएं):", - FontSize = 10.5, IsBold = true, TextColorHex = "#92400E", - FontFamily = "Inter" - }, - new PdfMathElement - { - X = 55, Y = 148, Width = 690, Height = 40, - Formula = @"(a \pm b)^2 = a^2 \pm 2ab + b^2, \quad a^2 - b^2 = (a - b)(a + b), \quad a^3 \pm b^3 = (a \pm b)(a^2 \mp ab + b^2)", - FontSize = 12, TextColorHex = "#78350F", - Category = MathCategory.Algebra, - Alignment = TextAlignmentMode.Center - }, - - // Section A Header - new PdfShapeElement { X = 40, Y = 205, Width = 720, Height = 26, CornerRadius = 4, FillColorHex = "#1E293B", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 50, Y = 210, Width = 700, Height = 18, Text = "SECTION A: Difference of Two Squares [ a\u00b2 - b\u00b2 = (a - b)(a + b) ]", FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF" }, - - // 3 Columns of Questions (Col 1: X=45, Col 2: X=285, Col 3: X=525) - new PdfTextElement { X = 45, Y = 240, Width = 230, Height = 35, Text = "1. x\u00b2 - 16 = ___________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 280, Width = 230, Height = 35, Text = "2. 4x\u00b2 - 25 = __________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 320, Width = 230, Height = 35, Text = "3. 9a\u00b2 - 49b\u00b2 = ________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 360, Width = 230, Height = 35, Text = "4. 16x\u00b2 - 1 = ___________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 400, Width = 230, Height = 35, Text = "5. 100 - p\u00b2 = ___________", FontSize = 11, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 285, Y = 240, Width = 230, Height = 35, Text = "6. 25x\u00b2 - 64y\u00b2 = _______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 280, Width = 230, Height = 35, Text = "7. x\u2074 - 81 = ____________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 320, Width = 230, Height = 35, Text = "8. 36a\u00b2b\u00b2 - 121 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 360, Width = 230, Height = 35, Text = "9. (x+y)\u00b2 - z\u00b2 = _________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 400, Width = 230, Height = 35, Text = "10. a\u00b2 - (b - c)\u00b2 = _______", FontSize = 11, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 525, Y = 240, Width = 230, Height = 35, Text = "11. 49x\u00b2 - 144y\u00b2 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 280, Width = 230, Height = 35, Text = "12. 64 - (x + 3)\u00b2 = _______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 320, Width = 230, Height = 35, Text = "13. 2x\u00b2 - 32 = ___________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 360, Width = 230, Height = 35, Text = "14. 3a\u00b3 - 75a = __________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 400, Width = 230, Height = 35, Text = "15. 1 - (2x - 3y)\u00b2 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - - // Section B Header: Quadratic Trinomials - new PdfShapeElement { X = 40, Y = 450, Width = 720, Height = 26, CornerRadius = 4, FillColorHex = "#1E293B", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 50, Y = 455, Width = 700, Height = 18, Text = "SECTION B: Quadratic Trinomials [ x\u00b2 + (a+b)x + ab = (x+a)(x+b) ]", FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF" }, - - new PdfTextElement { X = 45, Y = 485, Width = 230, Height = 35, Text = "16. x\u00b2 + 7x + 12 = _______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 525, Width = 230, Height = 35, Text = "17. x\u00b2 - 5x + 6 = ________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 565, Width = 230, Height = 35, Text = "18. x\u00b2 + 2x - 15 = _______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 605, Width = 230, Height = 35, Text = "19. x\u00b2 - 8x - 20 = _______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 645, Width = 230, Height = 35, Text = "20. x\u00b2 + 11x + 30 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 285, Y = 485, Width = 230, Height = 35, Text = "21. 2x\u00b2 + 7x + 3 = _______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 525, Width = 230, Height = 35, Text = "22. 3x\u00b2 - 10x + 8 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 565, Width = 230, Height = 35, Text = "23. 6x\u00b2 + 17x + 12 = _____", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 605, Width = 230, Height = 35, Text = "24. 4x\u00b2 - 12x + 9 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 645, Width = 230, Height = 35, Text = "25. 12x\u00b2 - 7x - 10 = _____", FontSize = 11, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 525, Y = 485, Width = 230, Height = 35, Text = "26. x\u00b2 - 14x + 49 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 525, Width = 230, Height = 35, Text = "27. 9x\u00b2 + 24x + 16 = _____", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 565, Width = 230, Height = 35, Text = "28. 5x\u00b2 + 13x - 6 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 605, Width = 230, Height = 35, Text = "29. 8x\u00b2 - 22x + 15 = _____", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 645, Width = 230, Height = 35, Text = "30. 2x\u00b2 + 5xy + 2y\u00b2 = ____", FontSize = 11, TextColorHex = "#0F172A" }, - - // Section C Header: Sum and Difference of Cubes - new PdfShapeElement { X = 40, Y = 695, Width = 720, Height = 26, CornerRadius = 4, FillColorHex = "#1E293B", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 50, Y = 700, Width = 700, Height = 18, Text = "SECTION C: Sum & Difference of Cubes [ a\u00b3 \u00b1 b\u00b3 = (a \u00b1 b)(a\u00b2 \u2213 ab + b\u00b2) ]", FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF" }, - - new PdfTextElement { X = 45, Y = 730, Width = 230, Height = 35, Text = "31. x\u00b3 - 8 = _____________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 770, Width = 230, Height = 35, Text = "32. 8a\u00b3 + 27 = ___________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 45, Y = 810, Width = 230, Height = 35, Text = "33. 64x\u00b3 - 125y\u00b3 = ______", FontSize = 11, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 285, Y = 730, Width = 230, Height = 35, Text = "34. 27x\u00b3 + 1 = ___________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 770, Width = 230, Height = 35, Text = "35. a\u00b3 - 216 = ___________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 285, Y = 810, Width = 230, Height = 35, Text = "36. 125x\u00b3 + 8y\u00b3 = _______", FontSize = 11, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 525, Y = 730, Width = 230, Height = 35, Text = "37. 2x\u00b4 - 16x = __________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 770, Width = 230, Height = 35, Text = "38. (a+b)\u00b3 - 8 = _________", FontSize = 11, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 525, Y = 810, Width = 230, Height = 35, Text = "39. x\u2076 - y\u2076 = ____________", FontSize = 11, TextColorHex = "#0F172A" } - } - }; - - // ========================================================================= - // PAGE 2: Answer Key & Evaluation - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Algebra Mastery Series", - HeaderCenter = "Factorization Solutions Matrix", - HeaderRight = "Worksheet #03 - Key", - FooterLeft = "150 Comprehensive Factorization Practice Worksheet", - FooterCenter = "Teacher Evaluation & Answer Guide", - FooterRight = "Page 2 of 2", - Elements = new List - { - new PdfShapeElement { X = 0, Y = 0, Width = 800, Height = 8, FillColorHex = "#10B981", StrokeColorHex = "#00000000" }, - - new PdfShapeElement { X = 40, Y = 35, Width = 720, Height = 60, CornerRadius = 8, FillColorHex = "#ECFDF5", StrokeColorHex = "#A7F3D0", StrokeThickness = 1.5 }, - new PdfTextElement { X = 55, Y = 43, Width = 690, Height = 26, Text = "Factorization Answer Key & Solutions Matrix", FontSize = 16, IsBold = true, TextColorHex = "#065F46" }, - new PdfTextElement { X = 55, Y = 70, Width = 690, Height = 20, Text = "Complete factorized expressions for Section A, B, and C", FontSize = 10, TextColorHex = "#047857" }, - - new PdfTableElement - { - X = 40, Y = 110, Width = 720, Height = 680, - HeaderBackgroundHex = "#047857", - HeaderTextHex = "#FFFFFF", - BorderColorHex = "#D1D5DB", - AlternateRowBackgroundHex = "#F9FAFB", - Headers = new List { "Q#", "Original Expression", "Factorized Result", "Q#", "Original Expression", "Factorized Result" }, - Rows = new List> - { - new() { "1", "x\u00b2 - 16", "(x - 4)(x + 4)", "21", "2x\u00b2 + 7x + 3", "(2x + 1)(x + 3)" }, - new() { "2", "4x\u00b2 - 25", "(2x - 5)(2x + 5)", "22", "3x\u00b2 - 10x + 8", "(3x - 4)(x - 2)" }, - new() { "3", "9a\u00b2 - 49b\u00b2", "(3a - 7b)(3a + 7b)", "23", "6x\u00b2 + 17x + 12", "(2x + 3)(3x + 4)" }, - new() { "4", "16x\u00b2 - 1", "(4x - 1)(4x + 1)", "24", "4x\u00b2 - 12x + 9", "(2x - 3)\u00b2" }, - new() { "5", "100 - p\u00b2", "(10 - p)(10 + p)", "25", "12x\u00b2 - 7x - 10", "(3x + 2)(4x - 5)" }, - new() { "6", "25x\u00b2 - 64y\u00b2", "(5x - 8y)(5x + 8y)", "26", "x\u00b2 - 14x + 49", "(x - 7)\u00b2" }, - new() { "7", "x\u2074 - 81", "(x\u00b2 + 9)(x - 3)(x + 3)", "27", "9x\u00b2 + 24x + 16", "(3x + 4)\u00b2" }, - new() { "8", "36a\u00b2b\u00b2 - 121", "(6ab - 11)(6ab + 11)", "28", "5x\u00b2 + 13x - 6", "(5x - 2)(x + 3)" }, - new() { "9", "(x+y)\u00b2 - z\u00b2", "(x + y - z)(x + y + z)", "29", "8x\u00b2 - 22x + 15", "(2x - 3)(4x - 5)" }, - new() { "10", "a\u00b2 - (b - c)\u00b2", "(a - b + c)(a + b - c)", "30", "2x\u00b2 + 5xy + 2y\u00b2", "(2x + y)(x + 2y)" }, - new() { "11", "49x\u00b2 - 144y\u00b2", "(7x - 12y)(7x + 12y)", "31", "x\u00b3 - 8", "(x - 2)(x\u00b2 + 2x + 4)" }, - new() { "12", "64 - (x + 3)\u00b2", "(5 - x)(11 + x)", "32", "8a\u00b3 + 27", "(2a + 3)(4a\u00b2 - 6a + 9)" }, - new() { "13", "2x\u00b2 - 32", "2(x - 4)(x + 4)", "33", "64x\u00b3 - 125y\u00b3", "(4x - 5y)(16x\u00b2 + 20xy + 25y\u00b2)" }, - new() { "14", "3a\u00b3 - 75a", "3a(a - 5)(a + 5)", "34", "27x\u00b3 + 1", "(3x + 1)(9x\u00b2 - 3x + 1)" }, - new() { "15", "1 - (2x - 3y)\u00b2", "(1 - 2x + 3y)(1 + 2x - 3y)", "35", "a\u00b3 - 216", "(a - 6)(a\u00b2 + 6a + 36)" } - } - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Education/MathBODMASWorksheetTemplate.cs b/src/PdfEditorApp/Templates/Education/MathBODMASWorksheetTemplate.cs deleted file mode 100644 index 8272b13..0000000 --- a/src/PdfEditorApp/Templates/Education/MathBODMASWorksheetTemplate.cs +++ /dev/null @@ -1,390 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Education; - -public class MathBODMASWorksheetTemplate : ITemplateDefinition -{ - public string Id => "bodmas_worksheet"; - public string Name => "Math Practice Worksheet (BODMAS)"; - public string Description => "Comprehensive school math worksheet with student header, score card, formula callout, 30 problems, and complete answer key table"; - public string Category => "Education"; - public string IconKind => "Calculator"; - public string AccentColorHex => "#2563EB"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Class_6_BODMAS_Worksheet.pdf", - Author = "Mathematics Department", - Subject = "Order of Operations (BODMAS) Comprehensive Practice Worksheet" - }; - - // ========================================================================= - // PAGE 1: Header, Rule Callout, Section A & B - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Mathematics Practice Series", - HeaderCenter = "Grade 6 • Order of Operations", - HeaderRight = "Worksheet #01", - FooterLeft = "Class 6 Mathematics: BODMAS Practice Worksheet", - FooterCenter = "Confidential - For Student Practice Only", - FooterRight = "Page 1 of 3", - Elements = new List - { - // Top Brand Accent Header - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#2563EB", StrokeColorHex = "#00000000" - }, - - // Title Banner Box - new PdfShapeElement - { - X = 40, Y = 35, Width = 720, Height = 85, - CornerRadius = 8, FillColorHex = "#EFF6FF", - StrokeColorHex = "#BFDBFE", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 45, Width = 690, Height = 30, - Text = "Class 6 Mathematics: BODMAS Practice Worksheet", - FontSize = 18, IsBold = true, TextColorHex = "#1E3A8A", - FontFamily = "Inter" - }, - new PdfTextElement - { - X = 55, Y = 78, Width = 690, Height = 22, - Text = "Topic: Order of Operations (Brackets, Orders/Of, Division, Multiplication, Addition, Subtraction)", - FontSize = 11, IsItalic = true, TextColorHex = "#1D4ED8", - FontFamily = "Inter" - }, - - // Student Metadata Strip - new PdfShapeElement - { - X = 40, Y = 130, Width = 720, Height = 48, - CornerRadius = 6, FillColorHex = "#F8FAFC", - StrokeColorHex = "#CBD5E1", StrokeThickness = 1 - }, - new PdfTextElement - { - X = 55, Y = 144, Width = 310, Height = 22, - Text = "Name: ___________________________________", - FontSize = 11, IsBold = true, TextColorHex = "#334155", - FontFamily = "Inter" - }, - new PdfTextElement - { - X = 380, Y = 144, Width = 180, Height = 22, - Text = "Date: ______________", - FontSize = 11, IsBold = true, TextColorHex = "#334155", - FontFamily = "Inter" - }, - new PdfShapeElement - { - X = 580, Y = 138, Width = 165, Height = 32, - CornerRadius = 4, FillColorHex = "#FEF2F2", - StrokeColorHex = "#F87171", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 585, Y = 145, Width = 155, Height = 20, - Text = "Score: _______ / 30", - FontSize = 11, IsBold = true, TextColorHex = "#B91C1C", - Alignment = TextAlignmentMode.Center, - FontFamily = "Inter" - }, - - // BODMAS Rule Card - new PdfShapeElement - { - X = 40, Y = 190, Width = 720, Height = 80, - CornerRadius = 6, FillColorHex = "#FFFBEB", - StrokeColorHex = "#FCD34D", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 200, Width = 690, Height = 20, - Text = "BODMAS RULE & ORDER OF PRECEDENCE:", - FontSize = 11, IsBold = true, TextColorHex = "#92400E", - FontFamily = "Inter" - }, - new PdfMathElement - { - X = 55, Y = 222, Width = 690, Height = 40, - Formula = @"\text{Brackets } [ \{ ( ) \} ] \;\longrightarrow\; \text{Orders (Powers/Roots)} \;\longrightarrow\; \text{Division } (\div) \;\longrightarrow\; \text{Multiplication } (\times) \;\longrightarrow\; \text{Addition } (+) \;\longrightarrow\; \text{Subtraction } (-)", - FontSize = 13, TextColorHex = "#78350F", - Category = MathCategory.SchoolArithmetic, - Alignment = TextAlignmentMode.Center - }, - - // Section A Header - new PdfShapeElement - { - X = 40, Y = 285, Width = 720, Height = 28, - CornerRadius = 4, FillColorHex = "#1E293B", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 290, Width = 700, Height = 20, - Text = "SECTION A: Basic Operations (Problems 1 to 10)", - FontSize = 11, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Inter" - }, - - // Questions Column 1 (Left) - new PdfTextElement { X = 55, Y = 325, Width = 320, Height = 40, Text = "1. 12 + 8 \u00f7 4 = ______________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 55, Y = 375, Width = 320, Height = 40, Text = "2. 25 - 3 \u00d7 6 = ______________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 55, Y = 425, Width = 320, Height = 40, Text = "3. (15 + 5) \u00d7 2 = ______________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 55, Y = 475, Width = 320, Height = 40, Text = "4. 40 \u00f7 (8 - 3) = ______________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 55, Y = 525, Width = 320, Height = 40, Text = "5. 18 + 12 \u00f7 3 \u00d7 2 = ___________", FontSize = 12, TextColorHex = "#0F172A" }, - - // Questions Column 2 (Right) - new PdfTextElement { X = 415, Y = 325, Width = 320, Height = 40, Text = "6. 36 \u00f7 4 + 5 \u00d7 3 = ___________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 375, Width = 320, Height = 40, Text = "7. 50 - 4 \u00d7 (6 + 2) = __________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 425, Width = 320, Height = 40, Text = "8. (24 \u00f7 6) + (18 \u00f7 3) = ________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 475, Width = 320, Height = 40, Text = "9. 100 - 50 \u00f7 5 + 10 = _________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 525, Width = 320, Height = 40, Text = "10. 7 \u00d7 (12 - 4) \u00f7 2 = ___________", FontSize = 12, TextColorHex = "#0F172A" }, - - // Section B Header - new PdfShapeElement - { - X = 40, Y = 580, Width = 720, Height = 28, - CornerRadius = 4, FillColorHex = "#1E293B", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 585, Width = 700, Height = 20, - Text = "SECTION B: Nested Operations & Parentheses (Problems 11 to 20)", - FontSize = 11, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Inter" - }, - - // Section B Column 1 - new PdfTextElement { X = 55, Y = 620, Width = 320, Height = 42, Text = "11. 30 - [12 + (8 - 5)] = _____________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 55, Y = 675, Width = 320, Height = 42, Text = "12. 4 \u00d7 [15 - (6 + 3)] = _____________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 55, Y = 730, Width = 320, Height = 42, Text = "13. 48 \u00f7 [16 - (4 \u00d7 2)] = ____________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 55, Y = 785, Width = 320, Height = 42, Text = "14. 20 + 2 \u00d7 [18 - (10 - 2)] = _________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 55, Y = 840, Width = 320, Height = 42, Text = "15. 75 - [25 + (15 - 5 \u00d7 2)] = ________", FontSize = 12, TextColorHex = "#0F172A" }, - - // Section B Column 2 - new PdfTextElement { X = 415, Y = 620, Width = 320, Height = 42, Text = "16. [40 - (18 + 2)] \u00f7 4 = ____________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 675, Width = 320, Height = 42, Text = "17. 60 \u00f7 [5 + (10 - 7)] = ____________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 730, Width = 320, Height = 42, Text = "18. [8 \u00d7 (9 - 4)] - (12 + 6) = _________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 785, Width = 320, Height = 42, Text = "19. 90 - [30 + 5 \u00d7 (8 - 4)] = _________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 840, Width = 320, Height = 42, Text = "20. 2 \u00d7 [50 - (20 + 15)] + 10 = ________", FontSize = 12, TextColorHex = "#0F172A" } - } - }; - - // ========================================================================= - // PAGE 2: Section C: Advanced Brackets & Step-by-Step Workspace - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Mathematics Practice Series", - HeaderCenter = "Grade 6 • Order of Operations", - HeaderRight = "Worksheet #01", - FooterLeft = "Class 6 Mathematics: BODMAS Practice Worksheet", - FooterCenter = "Confidential - For Student Practice Only", - FooterRight = "Page 2 of 3", - Elements = new List - { - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#2563EB", StrokeColorHex = "#00000000" - }, - - // Section C Header - new PdfShapeElement - { - X = 40, Y = 35, Width = 720, Height = 28, - CornerRadius = 4, FillColorHex = "#1E293B", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 40, Width = 700, Height = 20, - Text = "SECTION C: Advanced Multi-Level Brackets & Complex Expressions (Problems 21 to 30)", - FontSize = 11, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Inter" - }, - - // Questions 21 to 30 with working space - new PdfTextElement { X = 55, Y = 75, Width = 320, Height = 45, Text = "21. 45 - [18 + (12 - 6)] = ___________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 75, Width = 320, Height = 45, Text = "22. 2 \u00d7 [30 - (5 + 3 \u00d7 4)] = _________", FontSize = 12, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 55, Y = 130, Width = 320, Height = 45, Text = "23. 100 - {20 + [15 - (8 - 3)]} = ______", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 130, Width = 320, Height = 45, Text = "24. [64 \u00f7 (16 - 8)] \u00d7 [3 + 2] = ________", FontSize = 12, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 55, Y = 185, Width = 320, Height = 45, Text = "25. {50 - [10 + (25 - 15)]} \u00f7 6 = ______", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 185, Width = 320, Height = 45, Text = "26. 120 \u00f7 {15 + [12 - (7 - 2)]} = ______", FontSize = 12, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 55, Y = 240, Width = 320, Height = 45, Text = "27. [80 - (25 + 15)] \u00f7 [4 \u00d7 (5 - 3)] = __", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 240, Width = 320, Height = 45, Text = "28. 25 + {35 - [20 + (10 - 5)]} = ______", FontSize = 12, TextColorHex = "#0F172A" }, - - new PdfTextElement { X = 55, Y = 295, Width = 320, Height = 45, Text = "29. 3 \u00d7 {18 - [14 - (8 - 4)]} = ________", FontSize = 12, TextColorHex = "#0F172A" }, - new PdfTextElement { X = 415, Y = 295, Width = 320, Height = 45, Text = "30. {100 - [40 + (15 - 5) \u00d7 2]} = _____", FontSize = 12, TextColorHex = "#0F172A" }, - - // Rough Working Space Box - new PdfShapeElement - { - X = 40, Y = 360, Width = 720, Height = 580, - CornerRadius = 8, FillColorHex = "#F8FAFC", - StrokeColorHex = "#CBD5E1", StrokeThickness = 1 - }, - new PdfTextElement - { - X = 55, Y = 375, Width = 690, Height = 22, - Text = "ROUGH WORKING SPACE / STEP-BY-STEP SOLUTION SCRATCHPAD", - FontSize = 11, IsBold = true, TextColorHex = "#64748B", - FontFamily = "Inter" - }, - new PdfDividerElement - { - X = 55, Y = 405, Width = 690, Height = 2, - ColorHex = "#E2E8F0" - } - } - }; - - // ========================================================================= - // PAGE 3: Answer Key & Solutions Summary Table - // ========================================================================= - var page3 = new PdfPageModel - { - PageNumber = 3, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Mathematics Practice Series", - HeaderCenter = "Grade 6 • Solutions Summary", - HeaderRight = "Worksheet #01 - Key", - FooterLeft = "Class 6 Mathematics: BODMAS Practice Worksheet", - FooterCenter = "Confidential - Teacher & Grading Key", - FooterRight = "Page 3 of 3", - Elements = new List - { - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#10B981", StrokeColorHex = "#00000000" - }, - - // Answer Key Banner - new PdfShapeElement - { - X = 40, Y = 35, Width = 720, Height = 65, - CornerRadius = 8, FillColorHex = "#ECFDF5", - StrokeColorHex = "#A7F3D0", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 45, Width = 690, Height = 26, - Text = "Answer Key & Solutions Summary", - FontSize = 16, IsBold = true, TextColorHex = "#065F46", - FontFamily = "Inter" - }, - new PdfTextElement - { - X = 55, Y = 73, Width = 690, Height = 20, - Text = "Complete numerical answers and evaluation guide for Class 6 BODMAS Practice Worksheet", - FontSize = 10, TextColorHex = "#047857", - FontFamily = "Inter" - }, - - // Solutions Table - new PdfTableElement - { - X = 40, Y = 120, Width = 720, Height = 620, - HeaderBackgroundHex = "#047857", - HeaderTextHex = "#FFFFFF", - BorderColorHex = "#D1D5DB", - AlternateRowBackgroundHex = "#F9FAFB", - Headers = new List { "Q#", "Expression", "Answer", "Q#", "Expression", "Answer" }, - Rows = new List> - { - new() { "1", "12 + 8 \u00f7 4", "14", "16", "[40 - (18 + 2)] \u00f7 4", "5" }, - new() { "2", "25 - 3 \u00d7 6", "7", "17", "60 \u00f7 [5 + (10 - 7)]", "7.5" }, - new() { "3", "(15 + 5) \u00d7 2", "40", "18", "[8 \u00d7 (9 - 4)] - (12 + 6)", "22" }, - new() { "4", "40 \u00f7 (8 - 3)", "8", "19", "90 - [30 + 5 \u00d7 (8 - 4)]", "40" }, - new() { "5", "18 + 12 \u00f7 3 \u00d7 2", "26", "20", "2 \u00d7 [50 - 35] + 10", "40" }, - new() { "6", "36 \u00f7 4 + 5 \u00d7 3", "24", "21", "45 - [18 + (12 - 6)]", "21" }, - new() { "7", "50 - 4 \u00d7 (6 + 2)", "18", "22", "2 \u00d7 [30 - (5 + 12)]", "26" }, - new() { "8", "(24 \u00f7 6) + (18 \u00f7 3)", "10", "23", "100 - {20 + [15 - 5]}", "70" }, - new() { "9", "100 - 50 \u00f7 5 + 10", "100", "24", "[64 \u00f7 8] \u00d7 5", "40" }, - new() { "10", "7 \u00d7 (12 - 4) \u00f7 2", "28", "25", "{50 - [10 + 10]} \u00f7 6", "5" }, - new() { "11", "30 - [12 + 3]", "15", "26", "120 \u00f7 {15 + [12 - 5]}", "5.45" }, - new() { "12", "4 \u00d7 [15 - 9]", "24", "27", "[80 - 40] \u00f7 [4 \u00d7 2]", "5" }, - new() { "13", "48 \u00f7 [16 - 8]", "6", "28", "25 + {35 - [20 + 5]}", "35" }, - new() { "14", "20 + 2 \u00d7 [18 - 8]", "40", "29", "3 \u00d7 {18 - [14 - 4]}", "24" }, - new() { "15", "75 - [25 + 5]", "45", "30", "100 - [40 + 20]", "40" } - } - }, - - // Teacher Feedback & Signature Card - new PdfShapeElement - { - X = 40, Y = 770, Width = 720, Height = 150, - CornerRadius = 8, FillColorHex = "#F8FAFC", - StrokeColorHex = "#CBD5E1", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 785, Width = 300, Height = 22, - Text = "Teacher Remarks & Feedback:", - FontSize = 11, IsBold = true, TextColorHex = "#334155", - FontFamily = "Inter" - }, - new PdfTextElement - { - X = 55, Y = 815, Width = 690, Height = 45, - Text = "[ ] Excellent Mastery [ ] Needs Practice with Nested Brackets [ ] Re-test Recommended\nComments: ___________________________________________________________________________________", - FontSize = 10, TextColorHex = "#64748B", - FontFamily = "Inter" - }, - new PdfTextElement - { - X = 55, Y = 880, Width = 300, Height = 22, - Text = "Teacher Signature: _______________________", - FontSize = 11, IsBold = true, TextColorHex = "#334155", - FontFamily = "Inter" - }, - new PdfTextElement - { - X = 450, Y = 880, Width = 280, Height = 22, - Text = "Grading Date: ____________________", - FontSize = 11, IsBold = true, TextColorHex = "#334155", - FontFamily = "Inter" - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - doc.Pages.Add(page3); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Education/QuadrilateralsGuideTemplate.cs b/src/PdfEditorApp/Templates/Education/QuadrilateralsGuideTemplate.cs deleted file mode 100644 index 20b9fb1..0000000 --- a/src/PdfEditorApp/Templates/Education/QuadrilateralsGuideTemplate.cs +++ /dev/null @@ -1,298 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Services; - -namespace PdfEditorApp.Templates.Education; - -public class QuadrilateralsGuideTemplate : ITemplateDefinition -{ - public string Id => "quadrilaterals_guide"; - public string Name => "Geometry Guide (Quadrilaterals)"; - public string Description => "Comprehensive bilingual geometry guide with vector quadrilateral illustrations, English & Hindi properties, and area/perimeter formula reference"; - public string Category => "Education"; - public string IconKind => "VectorSquare"; - public string AccentColorHex => "#7C3AED"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Types_of_Quadrilaterals_Guide.pdf", - Author = "Mathematics & Geometry Department", - Subject = "Bilingual Illustrated Guide to Quadrilaterals and Geometric Properties" - }; - - // ========================================================================= - // PAGE 1: Overview, Vector Diagrams, Parallelogram & Rectangle - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Geometry Visual Reference Series", - HeaderCenter = "Polygons & Quadrilaterals • चतुर्भुज", - HeaderRight = "Study Sheet #04", - FooterLeft = "Types of Quadrilaterals • चतुर्भुज के प्रकार (Bilingual Guide)", - FooterCenter = "Licensed for Classroom & Educational Distribution", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Brand Stripe - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#7C3AED", StrokeColorHex = "#00000000" - }, - - // Title Banner Box - new PdfShapeElement - { - X = 40, Y = 35, Width = 720, Height = 80, - CornerRadius = 8, FillColorHex = "#F5F3FF", - StrokeColorHex = "#DDD6FE", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 45, Width = 690, Height = 28, - Text = "Types of Quadrilaterals • चतुर्भुज के प्रकार", - FontSize = 17, IsBold = true, TextColorHex = "#5B21B6", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 75, Width = 690, Height = 22, - Text = "Bilingual Visual Guide in English & Hindi with Geometric Properties, Angle Rules & Formulas", - FontSize = 10.5, IsItalic = true, TextColorHex = "#6D28D9", - FontFamily = "Inter" - }, - - // Definition Card - new PdfShapeElement - { - X = 40, Y = 125, Width = 720, Height = 65, - CornerRadius = 6, FillColorHex = "#F8FAFC", - StrokeColorHex = "#CBD5E1", StrokeThickness = 1 - }, - new PdfTextElement - { - X = 55, Y = 133, Width = 690, Height = 18, - Text = "WHAT IS A QUADRILATERAL? (चतुर्भुज क्या है?)", - FontSize = 10.5, IsBold = true, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 153, Width = 690, Height = 32, - Text = "• English: A closed 2D plane geometric figure bounded by four straight sides, four angles, and four vertices.\n• Hindi: चार सीधी भुजाओं, चार कोणों और चार शीर्षों से घिरी हुई बंद समतल आकृति को चतुर्भुज कहते हैं।", - FontSize = 9.5, TextColorHex = "#475569", - FontFamily = "Noto Sans Devanagari" - }, - - // Angle Sum Property Card - new PdfShapeElement - { - X = 40, Y = 200, Width = 720, Height = 65, - CornerRadius = 6, FillColorHex = "#FFFBEB", - StrokeColorHex = "#FDE68A", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 208, Width = 690, Height = 18, - Text = "ANGLE SUM PROPERTY OF A QUADRILATERAL (कोण योग गुणधर्म):", - FontSize = 10.5, IsBold = true, TextColorHex = "#92400E", - FontFamily = "Noto Sans Devanagari" - }, - new PdfMathElement - { - X = 55, Y = 228, Width = 690, Height = 32, - Formula = @"\angle A + \angle B + \angle C + \angle D = 360^\circ \quad \text{(Four right angles: } 4 \times 90^\circ = 360^\circ\text{)}", - FontSize = 12, TextColorHex = "#78350F", - Category = MathCategory.Geometry, - Alignment = TextAlignmentMode.Center - }, - - // Quadrilateral Visual Diagram Set - new PdfSvgElement - { - X = 50, Y = 275, Width = 700, Height = 200, - SvgSource = SvgOrnamentLibrary.GetQuadrilateralSetDiagramSvg(), - PresetName = "Quadrilaterals Geometry Diagram" - }, - - // 1. Parallelogram Card - new PdfShapeElement { X = 40, Y = 490, Width = 720, Height = 135, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 498, Width = 690, Height = 22, - Text = "1. Parallelogram (समांतर चतुर्भुज)", - FontSize = 12, IsBold = true, TextColorHex = "#1E40AF", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 522, Width = 690, Height = 95, - Text = "• English Properties:\n - Opposite sides are equal and parallel (AB = CD, AD = BC).\n - Opposite angles are equal (\u2220A = \u2220C, \u2220B = \u2220D). Adjacent angles are supplementary (\u2220A + \u2220B = 180\u00b0).\n - Diagonals bisect each other into two equal halves.\n• Hindi Properties (हिंदी गुणधर्म):\n - आमने-सामने की भुजाएं समान और समानांतर होती हैं। सम्मुख कोण बराबर होते हैं और आसन्न कोण संपूरक (180\u00b0) होते हैं।\n - विकर्ण एक-दूसरे को समद्विभाजित (bisect) करते हैं। | क्षेत्रफल (Area) = b \u00d7 h", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // 2. Rectangle Card - new PdfShapeElement { X = 40, Y = 635, Width = 720, Height = 135, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 643, Width = 690, Height = 22, - Text = "2. Rectangle (आयत)", - FontSize = 12, IsBold = true, TextColorHex = "#1E40AF", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 667, Width = 690, Height = 95, - Text = "• English Properties:\n - A parallelogram whose each internal angle is exactly 90\u00b0 (\u2220A = \u2220B = \u2220C = \u2220D = 90\u00b0).\n - Opposite sides are equal and parallel.\n - Both diagonals are equal in length and bisect each other (AC = BD).\n• Hindi Properties (हिंदी गुणधर्म):\n - प्रत्येक अंतःकोण समकोण (90\u00b0) होता है। सम्मुख भुजाएं समान एवं समानांतर होती हैं।\n - दोनों विकर्ण लंबाई में समान होते हैं। | क्षेत्रफल (Area) = l \u00d7 b | परिमाप (Perimeter) = 2(l + b)", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - } - } - }; - - // ========================================================================= - // PAGE 2: Rhombus, Square, Trapezoid, Kite & Formulas Table - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Geometry Visual Reference Series", - HeaderCenter = "Polygons & Quadrilaterals • चतुर्भुज", - HeaderRight = "Study Sheet #04", - FooterLeft = "Types of Quadrilaterals • चतुर्भुज के प्रकार (Bilingual Guide)", - FooterCenter = "Licensed for Classroom & Educational Distribution", - FooterRight = "Page 2 of 2", - Elements = new List - { - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#7C3AED", StrokeColorHex = "#00000000" - }, - - // 3. Rhombus Card - new PdfShapeElement { X = 40, Y = 35, Width = 720, Height = 115, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 43, Width = 690, Height = 22, - Text = "3. Rhombus (समचतुर्भुज)", - FontSize = 12, IsBold = true, TextColorHex = "#991B1B", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 67, Width = 690, Height = 75, - Text = "• English: All four sides are equal (AB = BC = CD = DA). Diagonals bisect each other at right angles (90\u00b0).\n• Hindi: चारों भुजाएं बराबर होती हैं। विकर्ण एक-दूसरे को परस्पर समकोण (90\u00b0) पर समद्विभाजित करते हैं।\n• सूत्र (Formulas): Area = \u00bd \u00d7 d\u2081 \u00d7 d\u2082 | Perimeter = 4a", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // 4. Square Card - new PdfShapeElement { X = 40, Y = 160, Width = 720, Height = 115, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 168, Width = 690, Height = 22, - Text = "4. Square (वर्ग)", - FontSize = 12, IsBold = true, TextColorHex = "#15803D", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 192, Width = 690, Height = 75, - Text = "• English: A regular quadrilateral where all 4 sides are equal and all 4 angles are 90\u00b0. Diagonals are equal and perpendicular.\n• Hindi: सभी 4 भुजाएं बराबर और सभी कोण 90\u00b0 के होते हैं। दोनों विकर्ण बराबर होते हैं और 90\u00b0 पर काटते हैं।\n• सूत्र (Formulas): Area = a\u00b2 | Perimeter = 4a | Diagonal = a\u221a2", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // 5. Trapezoid Card - new PdfShapeElement { X = 40, Y = 285, Width = 720, Height = 115, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 293, Width = 690, Height = 22, - Text = "5. Trapezoid / Trapezium (समलंब चतुर्भुज)", - FontSize = 12, IsBold = true, TextColorHex = "#047857", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 317, Width = 690, Height = 75, - Text = "• English: Has only one pair of parallel opposite sides called bases. Non-parallel sides are called legs.\n• Hindi: सम्मुख भुजाओं का केवल एक युग्म समानांतर होता है। समानांतर भुजाओं के बीच लंबवत दूरी ऊंचाई (h) कहलाती है।\n• सूत्र (Formulas): Area = \u00bd(a + b) \u00d7 h", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // 6. Kite Card - new PdfShapeElement { X = 40, Y = 410, Width = 720, Height = 115, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 418, Width = 690, Height = 22, - Text = "6. Kite (पतंग रूपी चतुर्भुज)", - FontSize = 12, IsBold = true, TextColorHex = "#B45309", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 442, Width = 690, Height = 75, - Text = "• English: Two distinct pairs of adjacent sides are equal. Diagonals intersect at right angles, and the longer diagonal bisects the shorter.\n• Hindi: आसन्न भुजाओं के दो अलग-अलग जोड़े बराबर होते हैं। विकर्ण परस्पर समकोण (90\u00b0) पर प्रतिच्छेद करते हैं।\n• सूत्र (Formulas): Area = \u00bd \u00d7 d\u2081 \u00d7 d\u2082", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Summary Table Header - new PdfShapeElement - { - X = 40, Y = 540, Width = 720, Height = 26, - CornerRadius = 4, FillColorHex = "#1E293B", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 545, Width = 700, Height = 18, - Text = "FORMULA SUMMARY: PERIMETER & AREA (सूत्र संक्षेप तालिका)", - FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Noto Sans Devanagari" - }, - - // Summary Table - new PdfTableElement - { - X = 40, Y = 575, Width = 720, Height = 260, - HeaderBackgroundHex = "#5B21B6", - HeaderTextHex = "#FFFFFF", - BorderColorHex = "#CBD5E1", - AlternateRowBackgroundHex = "#F5F3FF", - Headers = new List { "Quadrilateral (आकृति)", "Opposite Sides", "Diagonals (विकर्ण)", "Perimeter (परिमाप)", "Area (क्षेत्रफल)" }, - Rows = new List> - { - new() { "Square (वर्ग)", "Parallel & Equal", "Equal & \u22a5 (90\u00b0)", "4a", "a\u00b2" }, - new() { "Rectangle (आयत)", "Parallel & Equal", "Equal, not \u22a5", "2(l + b)", "l \u00d7 b" }, - new() { "Parallelogram (समांतर)", "Parallel & Equal", "Bisect each other", "2(a + b)", "b \u00d7 h" }, - new() { "Rhombus (समचतुर्भुज)", "Parallel & Equal (4)", "Unequal & \u22a5 (90\u00b0)", "4a", "\u00bd \u00d7 d\u2081 \u00d7 d\u2082" }, - new() { "Trapezoid (समलंब)", "1 Pair Parallel", "Unequal", "a + b + c + d", "\u00bd(a + b) \u00d7 h" }, - new() { "Kite (पतंग)", "Adjacent Equal", "Perpendicular (\u22a5)", "2(a + b)", "\u00bd \u00d7 d\u2081 \u00d7 d\u2082" } - } - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Education/StatesOfMatterDiagramNotesTemplate.cs b/src/PdfEditorApp/Templates/Education/StatesOfMatterDiagramNotesTemplate.cs deleted file mode 100644 index f3c37ca..0000000 --- a/src/PdfEditorApp/Templates/Education/StatesOfMatterDiagramNotesTemplate.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Services; - -namespace PdfEditorApp.Templates.Education; - -public class StatesOfMatterDiagramNotesTemplate : ITemplateDefinition -{ - public string Id => "states_of_matter_notes"; - public string Name => "Science Visual Notes (States of Matter)"; - public string Description => "Science diagram study guide featuring triangular phase change cycle, particle arrangement models, comparison tables, and conceptual Q&A"; - public string Category => "Education"; - public string IconKind => "Atom"; - public string AccentColorHex => "#0284C7"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "States_of_Matter_Visual_Notes.pdf", - Author = "Science & Physics Faculty", - Subject = "Visual Study Notes: Interconversion of States of Matter & Kinetic Particle Theory" - }; - - // ========================================================================= - // PAGE 1: Triangular Phase Change Cycle & Particle Models - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Science Visual Notes Series", - HeaderCenter = "General Science • Chemistry & Thermal Physics", - HeaderRight = "Topic: Matter & Phase Change", - FooterLeft = "Comparative Study of Three States of Matter", - FooterCenter = "Authorized Educational Study Notes", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Brand Stripe - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#0284C7", StrokeColorHex = "#00000000" - }, - - // Title Banner Box - new PdfShapeElement - { - X = 40, Y = 35, Width = 720, Height = 80, - CornerRadius = 8, FillColorHex = "#F0F9FF", - StrokeColorHex = "#BAE6FD", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 45, Width = 690, Height = 28, - Text = "Interconversion of States of Matter • ठोस, द्रव और गैस का परस्पर परिवर्तन", - FontSize = 16, IsBold = true, TextColorHex = "#0369A1", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 75, Width = 690, Height = 22, - Text = "A Comprehensive Study on Phase Transitions, Latent Heat, and Kinetic Particle Theory", - FontSize = 10.5, IsItalic = true, TextColorHex = "#0284C7", - FontFamily = "Inter" - }, - - // Section 1 Header: Triangular Cycle - new PdfShapeElement - { - X = 40, Y = 125, Width = 720, Height = 26, - CornerRadius = 4, FillColorHex = "#0F172A", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 130, Width = 700, Height = 18, - Text = "1. TRIANGULAR INTERCONVERSION DIAGRAM (त्रिभुजीय परिवर्तन चक्र)", - FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Noto Sans Devanagari" - }, - - // Triangular Phase Change Vector Diagram - new PdfSvgElement - { - X = 50, Y = 160, Width = 700, Height = 350, - SvgSource = SvgOrnamentLibrary.GetTriangularPhaseCycleSvg(), - PresetName = "Triangular Phase Cycle" - }, - - // Phase Transition Explanatory Box - new PdfShapeElement - { - X = 40, Y = 525, Width = 720, Height = 75, - CornerRadius = 6, FillColorHex = "#F8FAFC", - StrokeColorHex = "#CBD5E1", StrokeThickness = 1 - }, - new PdfTextElement - { - X = 55, Y = 533, Width = 690, Height = 60, - Text = "• Melting (गलनांक): Change of solid into liquid by heating.\n• Freezing (हिमीकरण): Liquid changing into solid upon cooling.\n• Vaporization (वाष्पीकरण): Liquid converting to vapor phase.\n• Condensation (संघनन): Gas cooling to liquid phase.\n• Sublimation (ऊर्ध्वपातन): Direct transition of solid to gas without entering liquid state (e.g. Camphor, Dry Ice).\n• Deposition (निक्षेपण): Direct change of gas into solid without becoming a liquid (e.g. Frost formation).", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Noto Sans Devanagari" - }, - - // Section 2 Header: Particle Lattice Models - new PdfShapeElement - { - X = 40, Y = 615, Width = 720, Height = 26, - CornerRadius = 4, FillColorHex = "#0F172A", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 620, Width = 700, Height = 18, - Text = "2. PARTICLE ARRANGEMENT & MOLECULAR PACKING (कणों की व्यवस्था)", - FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Noto Sans Devanagari" - }, - - // Particle Arrangement Vector Grid - new PdfSvgElement - { - X = 50, Y = 650, Width = 700, Height = 175, - SvgSource = SvgOrnamentLibrary.GetParticleArrangementGridSvg(), - PresetName = "Particle Arrangement" - }, - - // Key Takeaway Card - new PdfShapeElement - { - X = 40, Y = 840, Width = 720, Height = 65, - CornerRadius = 6, FillColorHex = "#FFFBEB", - StrokeColorHex = "#FDE68A", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 55, Y = 848, Width = 690, Height = 18, - Text = "KEY THERMODYNAMIC TAKEAWAY (मुख्य निष्कर्ष):", - FontSize = 10.5, IsBold = true, TextColorHex = "#92400E", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 870, Width = 690, Height = 25, - Text = "During phase transitions, temperature remains constant despite heat absorption due to Latent Heat (गुप्त ऊष्मा), which is consumed in breaking intermolecular bonds rather than increasing kinetic energy.", - FontSize = 9.5, TextColorHex = "#78350F", - FontFamily = "Inter" - } - } - }; - - // ========================================================================= - // PAGE 2: Comparison Table & Conceptual Q&A - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - HeaderLeft = "Science Visual Notes Series", - HeaderCenter = "Properties Comparison & Conceptual Q&A", - HeaderRight = "Topic: Matter & Phase Change", - FooterLeft = "Comparative Study of Three States of Matter", - FooterCenter = "Authorized Educational Study Notes", - FooterRight = "Page 2 of 2", - Elements = new List - { - new PdfShapeElement - { - X = 0, Y = 0, Width = 800, Height = 8, - FillColorHex = "#0284C7", StrokeColorHex = "#00000000" - }, - - // Section 3 Header: Comparison Table - new PdfShapeElement - { - X = 40, Y = 35, Width = 720, Height = 26, - CornerRadius = 4, FillColorHex = "#0F172A", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 40, Width = 700, Height = 18, - Text = "3. COMPARATIVE STUDY TABLE (पदार्थ के तीनों अवस्थाओं का तुलनात्मक अध्ययन)", - FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Noto Sans Devanagari" - }, - - // Comparison Table - new PdfTableElement - { - X = 40, Y = 70, Width = 720, Height = 340, - HeaderBackgroundHex = "#0284C7", - HeaderTextHex = "#FFFFFF", - BorderColorHex = "#CBD5E1", - AlternateRowBackgroundHex = "#F0F9FF", - Headers = new List { "Properties (गुण)", "Solid (ठोस)", "Liquid (द्रव)", "Gaseous (गैस)" }, - Rows = new List> - { - new() { "Shape (आकार)", "निश्चित आकार (Fixed Shape)", "अनिश्चित आकार (Takes container shape)", "अनिश्चित आकार (Indefinite)" }, - new() { "Volume (आयतन)", "निश्चित आयतन (Fixed Volume)", "निश्चित आयतन (Definite Volume)", "अनिश्चित आयतन (Indefinite Volume)" }, - new() { "Compressibility (संपीड्यता)", "नगण्य (Negligible)", "बहुत कम (Very Low)", "अत्यधिक उच्च (Highly Compressible)" }, - new() { "Intermolecular Force (आकर्षण बल)", "प्रबलतम (Strongest)", "मध्यम (Intermediate)", "नगण्य (Negligible / Weakest)" }, - new() { "Intermolecular Space (रिक्त स्थान)", "न्यूनतम (Minimum)", "मध्यम (Intermediate)", "अधिकतम (Maximum)" }, - new() { "Kinetic Energy (गतिज ऊर्जा)", "न्यूनतम (Minimum)", "मध्यम (Intermediate)", "अधिकतम (Maximum)" }, - new() { "Fluidity / Rigidity (प्रवाह)", "दृढ़ (Rigid, Cannot flow)", "प्रवाही (Fluid, Flows downwards)", "प्रवाही (Diffuses in all directions)" }, - new() { "Density (घनत्व)", "उच्च (High)", "मध्यम (Moderate)", "अति निम्न (Very Low)" } - } - }, - - // Section 4 Header: Conceptual Q&A - new PdfShapeElement - { - X = 40, Y = 430, Width = 720, Height = 26, - CornerRadius = 4, FillColorHex = "#0F172A", - StrokeColorHex = "#00000000" - }, - new PdfTextElement - { - X = 50, Y = 435, Width = 700, Height = 18, - Text = "4. CONCEPTUAL QUESTIONS & REASONING (अवधारणात्मक प्रश्न एवं वैज्ञानिक व्याख्या)", - FontSize = 10.5, IsBold = true, TextColorHex = "#FFFFFF", - FontFamily = "Noto Sans Devanagari" - }, - - // Q1 Card - new PdfShapeElement { X = 40, Y = 465, Width = 720, Height = 80, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 472, Width = 690, Height = 22, - Text = "Q1. Why does ice float on water despite being a solid? (ठोस होने पर भी बर्फ पानी पर क्यों तैरती है?)", - FontSize = 10.5, IsBold = true, TextColorHex = "#0369A1", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 496, Width = 690, Height = 42, - Text = "Answer: Ice has a 3D cage-like hexagonal crystal structure due to hydrogen bonding with vacant spaces trapped inside. Consequently, its density is lower than that of liquid water (0.917 g/cm³ vs 1.0 g/cm³), allowing ice to float.", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Inter" - }, - - // Q2 Card - new PdfShapeElement { X = 40, Y = 555, Width = 720, Height = 80, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 562, Width = 690, Height = 22, - Text = "Q2. Why does steam cause more severe burns than boiling water at 100\u00b0C? (100\u00b0C पर भाप अधिक जलन क्यों पैदा करती है?)", - FontSize = 10.5, IsBold = true, TextColorHex = "#0369A1", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 586, Width = 690, Height = 42, - Text = "Answer: Steam at 100\u00b0C contains an additional 22.6 \u00d7 10\u2075 J/kg of energy stored as Latent Heat of Vaporization (वाष्पीकरण की गुप्त ऊष्मा). When steam condenses upon touching the skin, it releases this extra latent energy before cooling.", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Inter" - }, - - // Q3 Card - new PdfShapeElement { X = 40, Y = 645, Width = 720, Height = 80, CornerRadius = 6, FillColorHex = "#F8FAFC", StrokeColorHex = "#E2E8F0", StrokeThickness = 1 }, - new PdfTextElement - { - X = 55, Y = 652, Width = 690, Height = 22, - Text = "Q3. Why do camphor and dry ice directly sublime without melting? (कपूर और शुष्क बर्फ सीधे वाष्प क्यों बन जाते हैं?)", - FontSize = 10.5, IsBold = true, TextColorHex = "#0369A1", - FontFamily = "Noto Sans Devanagari" - }, - new PdfTextElement - { - X = 55, Y = 676, Width = 690, Height = 42, - Text = "Answer: The vapor pressure of solid CO\u2082 (dry ice) and camphor exceeds atmospheric pressure (1 atm) at room temperature. Because their triple-point pressure is higher than 1 atm, the liquid phase cannot exist stably at normal atmospheric pressure.", - FontSize = 9.5, TextColorHex = "#334155", - FontFamily = "Inter" - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Events/CreativeTypographyShowcaseTemplate.cs b/src/PdfEditorApp/Templates/Events/CreativeTypographyShowcaseTemplate.cs deleted file mode 100644 index d9fe4e7..0000000 --- a/src/PdfEditorApp/Templates/Events/CreativeTypographyShowcaseTemplate.cs +++ /dev/null @@ -1,781 +0,0 @@ -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Events; - -/// -/// Professional Showcase Poster demonstrating Bézier curved typography, text outlines, -/// drop shadows, custom kerning/tracking, decorative wave dividers, curved connectors, -/// and Catmull-Rom smooth calligraphic ink signatures. -/// -public class CreativeTypographyShowcaseTemplate : ITemplateDefinition -{ - public string Id => "typographyshowcase"; - public string Name => "Bézier Arts & Typography Specimen"; - public string Description => "Executive creative showcase featuring Bézier wave typography, text outlines, drop shadows, decorative wave dividers, and smooth ink signatures"; - public string Category => "Design & Creative"; - public string IconKind => "FormatTextVariantOutline"; - public string AccentColorHex => "#6366F1"; - - public PdfDocumentModel Create() - { - return GenerateDocument(); - } - - public static PdfDocumentModel GenerateDocument() - { - var doc = new PdfDocumentModel - { - Title = "Bezier_Typography_Creative_Showcase.pdf", - Author = "CodeFryDev Typography Institute", - Subject = "Official Specimen & Showcase of Bézier Curve Typography & Vector Arts" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 595.28, - Height = 841.89, - BackgroundColorHex = "#0B0F19", - ShowHeaderFooter = false - }; - - // ========================================== - // 1. BACKGROUND CANVAS & AMBIENT ORGANIC ART - // ========================================== - - // Main Obsidian Card Container - page.Elements.Add(new PdfShapeElement - { - X = 0, - Y = 0, - Width = 595.28, - Height = 841.89, - ShapeType = ShapeType.Rectangle, - FillColorHex = "#0B0F19", - StrokeColorHex = "#1E293B", - StrokeThickness = 2.0, - ZIndex = 0 - }); - - // Top-Right Ambient Organic Blob (Deep Indigo Accent) - page.Elements.Add(new PdfShapeElement - { - X = 350, - Y = -20, - Width = 270, - Height = 220, - ShapeType = ShapeType.OrganicBlob, - FillColorHex = "#1E1B4B", - StrokeColorHex = "#312E81", - StrokeThickness = 1.0, - Opacity = 0.45, - ZIndex = 1 - }); - - // Bottom-Left Ambient Wave Ribbon (Midnight Teal Accent) - page.Elements.Add(new PdfShapeElement - { - X = -40, - Y = 720, - Width = 340, - Height = 140, - ShapeType = ShapeType.WaveRibbon, - FillColorHex = "#064E3B", - StrokeColorHex = "#065F46", - StrokeThickness = 1.0, - Opacity = 0.35, - ZIndex = 1 - }); - - // ========================================== - // 2. HERO HEADER & BÉZIER CURVED TYPOGRAPHY - // ========================================== - - // Top Tag Badge with Background Box & Character Tracking - page.Elements.Add(new PdfTextElement - { - Text = "✦ INTERNATIONAL TYPOGRAPHY & VECTOR ARTS SHOWCASE 2026 ✦", - FontFamily = "Montserrat", - FontSize = 8.5, - IsBold = true, - TextColorHex = "#818CF8", - Alignment = TextAlignmentMode.Center, - CharacterSpacing = 2.2, - BackgroundColorHex = "#1E1B4B", - BorderColorHex = "#4F46E5", - BorderThickness = 1.0, - CornerRadius = 12, - Padding = 6, - X = 65, - Y = 28, - Width = 465, - Height = 26, - ZIndex = 5 - }); - - // Hero Bézier Wave Curve Typography - page.Elements.Add(new PdfTextElement - { - Text = "SYMPHONY OF BÉZIER CURVES & TYPOGRAPHY", - FontFamily = "Cinzel", - FontSize = 20, - IsBold = true, - TextColorHex = "#F8FAFC", - ShapeMode = TextShapeMode.BezierCurve, - BezierPreset = BezierCurvePreset.Wave, - CharacterSpacing = 2.0, - HasShadow = true, - ShadowColorHex = "#80000000", - ShadowOffsetX = 2, - ShadowOffsetY = 3, - ShadowBlurRadius = 5, - HasStroke = true, - StrokeColorHex = "#6366F1", - StrokeWidth = 0.8, - X = 40, - Y = 62, - Width = 515, - Height = 72, - ZIndex = 6 - }); - - // Curved Bridge Arc Subtitle - page.Elements.Add(new PdfTextElement - { - Text = "EXPLORING MATHEMATICAL SPLINES, CALLIGRAPHY & ADVANCED TEXT MANIPULATION", - FontFamily = "Montserrat", - FontSize = 8.0, - IsBold = true, - TextColorHex = "#F59E0B", - ShapeMode = TextShapeMode.BezierCurve, - BezierPreset = BezierCurvePreset.Bridge, - CharacterSpacing = 2.0, - X = 50, - Y = 135, - Width = 495, - Height = 35, - ZIndex = 7 - }); - - // Calligraphic Flourish Gold Divider - page.Elements.Add(new PdfDividerElement - { - X = 50, - Y = 170, - Width = 495, - Height = 22, - Style = DividerStyle.CalligraphicFlourish, - WaveAmplitude = 6.0, - Thickness = 1.8, - ColorHex = "#D97706", - ZIndex = 8 - }); - - // ========================================== - // 3. LEFT COLUMN: TYPOGRAPHY ENGINE CARD - // ========================================== - - // Card Container - page.Elements.Add(new PdfShapeElement - { - X = 48, - Y = 198, - Width = 240, - Height = 252, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 8, - FillColorHex = "#0F172A", - StrokeColorHex = "#334155", - StrokeThickness = 1.2, - ZIndex = 10 - }); - - // Card Header Title - page.Elements.Add(new PdfTextElement - { - Text = "01. ADVANCED TYPOGRAPHY", - FontFamily = "Montserrat", - FontSize = 9.5, - IsBold = true, - TextColorHex = "#38BDF8", - CharacterSpacing = 1.5, - X = 60, - Y = 210, - Width = 216, - Height = 18, - ZIndex = 11 - }); - - // Drop Shadow Text Demonstration - page.Elements.Add(new PdfTextElement - { - Text = "ELEGANT DROP SHADOWS", - FontFamily = "Playfair Display", - FontSize = 13, - IsBold = true, - TextColorHex = "#FFFFFF", - HasShadow = true, - ShadowColorHex = "#4F46E5", - ShadowOffsetX = 2, - ShadowOffsetY = 3, - ShadowBlurRadius = 4, - X = 60, - Y = 232, - Width = 216, - Height = 24, - ZIndex = 12 - }); - - // Hollow Outline Stroke Text Demonstration - page.Elements.Add(new PdfTextElement - { - Text = "HOLLOW OUTLINE STROKE", - FontFamily = "Cinzel", - FontSize = 12, - IsBold = true, - TextColorHex = "#0F172A", - HasStroke = true, - StrokeColorHex = "#A855F7", - StrokeWidth = 1.2, - X = 60, - Y = 262, - Width = 216, - Height = 22, - ZIndex = 13 - }); - - // Paragraph Formatting, Word Wrapping & Spacing Demonstration - page.Elements.Add(new PdfTextElement - { - Text = "Full typography layout engine with dynamic tracking, word wrapping, 1.4x line multipliers, and per-glyph arc-length parameterization.", - FontFamily = "Inter", - FontSize = 8.2, - TextColorHex = "#94A3B8", - LineHeight = 1.35, - X = 60, - Y = 292, - Width = 216, - Height = 50, - ZIndex = 14 - }); - - // Kerning & Tracking Badge - page.Elements.Add(new PdfTextElement - { - Text = "TRACKING & KERNING: +2.5 PT", - FontFamily = "Fira Code", - FontSize = 7.5, - IsBold = true, - TextColorHex = "#10B981", - CharacterSpacing = 1.5, - BackgroundColorHex = "#064E3B", - BorderColorHex = "#059669", - BorderThickness = 1.0, - CornerRadius = 4, - Padding = 4, - X = 60, - Y = 352, - Width = 216, - Height = 22, - ZIndex = 15 - }); - - // Wave Sub-divider inside Left Card - page.Elements.Add(new PdfDividerElement - { - X = 60, - Y = 382, - Width = 216, - Height = 12, - Style = DividerStyle.Wave, - WaveAmplitude = 3.0, - WaveFrequency = 4.0, - Thickness = 1.0, - ColorHex = "#38BDF8", - ZIndex = 16 - }); - - // Left Card Footer - page.Elements.Add(new PdfTextElement - { - Text = "✔ 100% Vector Quality at Any PDF Scale", - FontFamily = "Inter", - FontSize = 7.5, - IsBold = true, - TextColorHex = "#38BDF8", - X = 60, - Y = 402, - Width = 216, - Height = 18, - ZIndex = 17 - }); - - // ========================================== - // 4. RIGHT COLUMN: BÉZIER VECTOR SHAPES CARD - // ========================================== - - // Card Container - page.Elements.Add(new PdfShapeElement - { - X = 307, - Y = 198, - Width = 240, - Height = 252, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 8, - FillColorHex = "#0F172A", - StrokeColorHex = "#334155", - StrokeThickness = 1.2, - ZIndex = 10 - }); - - // Card Header Title - page.Elements.Add(new PdfTextElement - { - Text = "02. BÉZIER VECTOR GRAPHICS", - FontFamily = "Montserrat", - FontSize = 9.5, - IsBold = true, - TextColorHex = "#EC4899", - CharacterSpacing = 1.5, - X = 319, - Y = 210, - Width = 216, - Height = 18, - ZIndex = 11 - }); - - // Medal Ribbon Badge Seal - page.Elements.Add(new PdfShapeElement - { - X = 318, - Y = 236, - Width = 62, - Height = 62, - ShapeType = ShapeType.MedalRibbonBadge, - FillColorHex = "#F59E0B", - SecondaryFillColorHex = "#990000", - StrokeColorHex = "#B45309", - StrokeThickness = 1.5, - Label = "GOLD 2026", - LabelFontSize = 7.5, - LabelColorHex = "#78350F", - ZIndex = 12 - }); - - // Curved Speech Callout Bubble - page.Elements.Add(new PdfShapeElement - { - X = 390, - Y = 236, - Width = 145, - Height = 58, - ShapeType = ShapeType.CurvedCallout, - FillColorHex = "#1E1B4B", - StrokeColorHex = "#6366F1", - StrokeThickness = 1.2, - Label = "Cubic Spline Math", - LabelFontSize = 8.5, - LabelColorHex = "#E0E7FF", - ZIndex = 13 - }); - - // Curved Arrow Demonstration - page.Elements.Add(new PdfShapeElement - { - X = 318, - Y = 308, - Width = 90, - Height = 36, - ShapeType = ShapeType.CurvedArrow, - StrokeColorHex = "#F43F5E", - StrokeThickness = 2.0, - EndCap = LineEndCap.Arrow, - ZIndex = 14 - }); - - // S-Curve Connector with Dashes - page.Elements.Add(new PdfShapeElement - { - X = 422, - Y = 312, - Width = 110, - Height = 30, - ShapeType = ShapeType.SCurveConnector, - StrokeColorHex = "#38BDF8", - StrokeThickness = 1.8, - DashStyle = LineDashStyle.Dashed, - ZIndex = 15 - }); - - // Calligraphic Curly Brace - page.Elements.Add(new PdfShapeElement - { - X = 318, - Y = 356, - Width = 24, - Height = 62, - ShapeType = ShapeType.CurlyBrace, - StrokeColorHex = "#FCD34D", - StrokeThickness = 1.8, - ZIndex = 16 - }); - - // Grouped Feature Description next to Brace - page.Elements.Add(new PdfTextElement - { - Text = "Smooth C¹ Tangents\nHarmonic Wave Cycles\nParametric Endcaps", - FontFamily = "Inter", - FontSize = 8.0, - TextColorHex = "#CBD5E1", - LineHeight = 1.3, - X = 348, - Y = 362, - Width = 185, - Height = 52, - ZIndex = 17 - }); - - // ========================================== - // 5. MIDDLE HARMONIC WAVE DIVIDER - // ========================================== - - page.Elements.Add(new PdfDividerElement - { - X = 50, - Y = 460, - Width = 495, - Height = 22, - Style = DividerStyle.DoubleWave, - WaveAmplitude = 5.5, - WaveFrequency = 6.0, - Thickness = 1.5, - ColorHex = "#6366F1", - ZIndex = 20 - }); - - // ========================================== - // 6. MASTERCLASSES & SCHEDULE SECTION - // ========================================== - - // Container Card - page.Elements.Add(new PdfShapeElement - { - X = 48, - Y = 490, - Width = 499, - Height = 175, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 8, - FillColorHex = "#0F172A", - StrokeColorHex = "#334155", - StrokeThickness = 1.2, - ZIndex = 21 - }); - - // Section Title - page.Elements.Add(new PdfTextElement - { - Text = "FEATURED SYMPOSIUM SESSIONS & MASTERCLASSES", - FontFamily = "Cinzel", - FontSize = 11, - IsBold = true, - TextColorHex = "#F8FAFC", - Alignment = TextAlignmentMode.Center, - CharacterSpacing = 1.2, - X = 60, - Y = 500, - Width = 475, - Height = 18, - ZIndex = 22 - }); - - // Session 1 (Left) - page.Elements.Add(new PdfTextElement - { - Text = "10:00 AM • STAGE A", - FontFamily = "Montserrat", - FontSize = 7.5, - IsBold = true, - TextColorHex = "#38BDF8", - BackgroundColorHex = "#0C4A6E", - BorderColorHex = "#0284C7", - BorderThickness = 1.0, - CornerRadius = 3, - Padding = 3, - X = 62, - Y = 526, - Width = 115, - Height = 18, - ZIndex = 23 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "Mathematical Foundations of Bézier Warping", - FontFamily = "Playfair Display", - FontSize = 10, - IsBold = true, - TextColorHex = "#FFFFFF", - X = 62, - Y = 548, - Width = 220, - Height = 18, - ZIndex = 24 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "Dr. Jane Doe — Senior Computational Typographer", - FontFamily = "Inter", - FontSize = 7.8, - TextColorHex = "#94A3B8", - X = 62, - Y = 566, - Width = 220, - Height = 18, - ZIndex = 25 - }); - - // Session 2 (Right) - page.Elements.Add(new PdfTextElement - { - Text = "02:30 PM • STAGE B", - FontFamily = "Montserrat", - FontSize = 7.5, - IsBold = true, - TextColorHex = "#F472B6", - BackgroundColorHex = "#831843", - BorderColorHex = "#DB2777", - BorderThickness = 1.0, - CornerRadius = 3, - Padding = 3, - X = 308, - Y = 526, - Width = 115, - Height = 18, - ZIndex = 23 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "Calligraphic Curves & Catmull-Rom Splines", - FontFamily = "Playfair Display", - FontSize = 10, - IsBold = true, - TextColorHex = "#FFFFFF", - X = 308, - Y = 548, - Width = 225, - Height = 18, - ZIndex = 24 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "Prof. John Doe — Director of Vector Arts Institute", - FontFamily = "Inter", - FontSize = 7.8, - TextColorHex = "#94A3B8", - X = 308, - Y = 566, - Width = 225, - Height = 18, - ZIndex = 25 - }); - - // Wave Accent Line inside Schedule Card - page.Elements.Add(new PdfShapeElement - { - X = 62, - Y = 594, - Width = 471, - Height = 12, - ShapeType = ShapeType.WaveLine, - StrokeColorHex = "#4F46E5", - StrokeThickness = 1.0, - ZIndex = 26 - }); - - // Footer Note in Schedule Card - page.Elements.Add(new PdfTextElement - { - Text = "✦ All sessions include live hands-on Bézier text and curve manipulation workshops ✦", - FontFamily = "Inter", - FontSize = 7.8, - IsItalic = true, - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center, - X = 62, - Y = 614, - Width = 471, - Height = 18, - ZIndex = 27 - }); - - // ========================================== - // 7. BOTTOM VERIFICATION & FREEHAND INK - // ========================================== - - // Arch Gold Rule - page.Elements.Add(new PdfDividerElement - { - X = 50, - Y = 675, - Width = 495, - Height = 16, - Style = DividerStyle.Arch, - WaveAmplitude = 5.0, - Thickness = 1.5, - ColorHex = "#D97706", - ZIndex = 30 - }); - - // QR Code Verification - page.Elements.Add(new PdfQrCodeElement - { - X = 60, - Y = 698, - Width = 68, - Height = 68, - Content = "https://codefrydev.in/showcase/bezier-typography-2026", - DarkColorHex = "#F8FAFC", - LightColorHex = "#0B0F19", - ZIndex = 31 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "SCAN TO VERIFY\nVECTOR ASSETS", - FontFamily = "Montserrat", - FontSize = 6.0, - IsBold = true, - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 770, - Width = 88, - Height = 22, - ZIndex = 32 - }); - - // Jury Header - page.Elements.Add(new PdfTextElement - { - Text = "EXECUTIVE CURATORS & TYPOGRAPHY JURY CERTIFICATION", - FontFamily = "Montserrat", - FontSize = 7.8, - IsBold = true, - TextColorHex = "#94A3B8", - CharacterSpacing = 1.2, - X = 175, - Y = 698, - Width = 370, - Height = 18, - ZIndex = 33 - }); - - // Signer 1: Smooth Ink Signature (Jane Doe) - page.Elements.Add(new PdfInkElement - { - X = 175, - Y = 716, - Width = 145, - Height = 40, - PointsData = "175,745 195,725 215,750 235,722 255,746 280,728 305,755", - StrokeColorHex = "#38BDF8", - StrokeThickness = 2.2, - Opacity = 0.95, - IsSmoothSpline = true, - ZIndex = 34 - }); - - page.Elements.Add(new PdfDividerElement - { - X = 175, - Y = 758, - Width = 145, - Height = 4, - Style = DividerStyle.Straight, - Thickness = 1.0, - ColorHex = "#475569", - ZIndex = 35 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "Dr. Jane Doe\nLead Typographer & Jury Chair", - FontFamily = "Inter", - FontSize = 7.0, - TextColorHex = "#94A3B8", - X = 175, - Y = 766, - Width = 145, - Height = 22, - ZIndex = 36 - }); - - // Signer 2: Smooth Ink Signature (John Doe) - page.Elements.Add(new PdfInkElement - { - X = 355, - Y = 716, - Width = 145, - Height = 40, - PointsData = "355,748 375,726 395,752 420,724 445,750 475,732", - StrokeColorHex = "#F472B6", - StrokeThickness = 2.2, - Opacity = 0.95, - IsSmoothSpline = true, - ZIndex = 34 - }); - - page.Elements.Add(new PdfDividerElement - { - X = 355, - Y = 758, - Width = 145, - Height = 4, - Style = DividerStyle.Straight, - Thickness = 1.0, - ColorHex = "#475569", - ZIndex = 35 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "Prof. John Doe\nDirector of Vector Arts Institute", - FontFamily = "Inter", - FontSize = 7.0, - TextColorHex = "#94A3B8", - X = 355, - Y = 766, - Width = 145, - Height = 22, - ZIndex = 36 - }); - - // Final Bottom Subtle Brand Footnote - page.Elements.Add(new PdfTextElement - { - Text = "DESIGNED WITH ADVANCED BÉZIER VECTOR & TYPOGRAPHY ENGINE • PURE VECTOR PDF EXPORT", - FontFamily = "Montserrat", - FontSize = 6.2, - IsBold = true, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Center, - CharacterSpacing = 1.5, - X = 40, - Y = 812, - Width = 515, - Height = 18, - ZIndex = 40 - }); - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Events/GalaInvitationTemplate.cs b/src/PdfEditorApp/Templates/Events/GalaInvitationTemplate.cs deleted file mode 100644 index 0b564be..0000000 --- a/src/PdfEditorApp/Templates/Events/GalaInvitationTemplate.cs +++ /dev/null @@ -1,264 +0,0 @@ -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Services; - -namespace PdfEditorApp.Templates.Events; - -/// -/// High-end Corporate Gala & Charity Award Night Invitation with Gold Art Deco styling. -/// -public class GalaInvitationTemplate : ITemplateDefinition -{ - public string Id => "galainvitation"; - public string Name => "Charity Gala & Award Night Invitation"; - public string Description => "Black-tie executive event with gold Art Deco geometric frame, schedule, and VIP RSVP QR Code"; - public string Category => "Events & Invitations"; - public string IconKind => "TicketConfirmationOutline"; - public string AccentColorHex => "#F59E0B"; - - public PdfDocumentModel Create() - { - return GenerateDocument(); - } - - public static PdfDocumentModel GenerateDocument() - { - var doc = new PdfDocumentModel - { - Title = "Annual_Charity_Gala_Invitation.pdf", - Author = "CodeFryDev Foundation", - Subject = "Annual Charity Gala & Award Night" - }; - - var page = new PdfPageModel - { - Width = 600, - Height = 850, - Orientation = PageOrientation.Portrait, - ShowHeaderFooter = false - }; - - // 1. Sleek Midnight Obsidian Background - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.Rectangle, - X = 0, - Y = 0, - Width = 600, - Height = 850, - FillColorHex = "#0B0F19", // Deep obsidian navy - StrokeColorHex = "#D97706", - StrokeThickness = 2.0, - ZIndex = 0 - }); - - // 2. Art Deco Gold Frame - page.Elements.Add(new PdfSvgElement - { - PresetName = "ArtDecoFrame", - SvgSource = SvgOrnamentLibrary.GetArtDecoFrameSvg("#D97706"), - X = 20, - Y = 20, - Width = 560, - Height = 810, - ZIndex = 1 - }); - - // 3. Organization Host - page.Elements.Add(new PdfTextElement - { - Text = "THE CODEFRYDEV FOUNDATION CORDIALLY INVITES YOU TO THE", - FontFamily = "Montserrat", - FontSize = 9.5, - IsBold = true, - TextColorHex = "#9CA3AF", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 80, - Width = 500, - Height = 22, - ZIndex = 5 - }); - - // 4. Main Event Title - page.Elements.Add(new PdfTextElement - { - Text = "ANNUAL CHARITY\nGALA & AWARDS", - FontFamily = "Cinzel", - FontSize = 32, - IsBold = true, - TextColorHex = "#F59E0B", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 110, - Width = 500, - Height = 90, - ZIndex = 6 - }); - - // 5. Year & Theme Subtitle - page.Elements.Add(new PdfTextElement - { - Text = "— NIGHT OF ILLUMINATION 2026 —", - FontFamily = "Montserrat", - FontSize = 11, - IsBold = true, - TextColorHex = "#E5E7EB", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 205, - Width = 500, - Height = 22, - ZIndex = 6 - }); - - // 6. Gold Laurel Seal Emblem - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.LaurelWreathSeal, - X = 260, - Y = 240, - Width = 80, - Height = 80, - FillColorHex = "#F59E0B", - StrokeColorHex = "#78350F", - StrokeThickness = 2.0, - ZIndex = 6 - }); - - // 7. Event Highlights & Keynote - page.Elements.Add(new PdfTextElement - { - Text = "AN EXCLUSIVE EVENING CELEBRATING GLOBAL HUMANITARIAN LEADERSHIP,\nFEATURING LIVE PHILHARMONIC ORCHESTRA & CHARITY AUCTION", - FontFamily = "Montserrat", - FontSize = 10, - IsBold = false, - TextColorHex = "#D1D5DB", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 340, - Width = 500, - Height = 44, - ZIndex = 6 - }); - - // 8. Schedule & Location Card - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.RoundedRectangle, - X = 60, - Y = 400, - Width = 480, - Height = 180, - FillColorHex = "#111827", - StrokeColorHex = "#D97706", - StrokeThickness = 1.0, - CornerRadius = 8, - ZIndex = 5 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "SATURDAY, NOVEMBER 14, 2026", - FontFamily = "Cinzel", - FontSize = 15, - IsBold = true, - TextColorHex = "#F59E0B", - Alignment = TextAlignmentMode.Center, - X = 70, - Y = 415, - Width = 460, - Height = 26, - ZIndex = 6 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "6:00 PM Champagne Reception | 7:30 PM Three-Course Banquet & Awards\n9:30 PM Charity Auction & Live Jazz", - FontFamily = "Montserrat", - FontSize = 10, - IsBold = false, - TextColorHex = "#E5E7EB", - Alignment = TextAlignmentMode.Center, - X = 70, - Y = 445, - Width = 460, - Height = 40, - ZIndex = 6 - }); - - page.Elements.Add(new PdfDividerElement - { - Thickness = 1, - ColorHex = "#374151", - X = 100, - Y = 490, - Width = 400, - Height = 1, - ZIndex = 6 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "THE METROPOLITAN OPERA HALL\n150 LINCOLN CENTER PLAZA, NEW YORK, NY", - FontFamily = "Cinzel", - FontSize = 12, - IsBold = true, - TextColorHex = "#F3F4F6", - Alignment = TextAlignmentMode.Center, - X = 70, - Y = 505, - Width = 460, - Height = 40, - ZIndex = 6 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "DRESS CODE: BLACK TIE & EVENING GOWN", - FontFamily = "Montserrat", - FontSize = 10, - IsBold = true, - TextColorHex = "#F59E0B", - Alignment = TextAlignmentMode.Center, - X = 70, - Y = 548, - Width = 460, - Height = 22, - ZIndex = 6 - }); - - // 9. RSVP QR Code - page.Elements.Add(new PdfQrCodeElement - { - Content = "https://gala.codefrydev.in/rsvp/vip", - Label = "SCAN FOR VIP TABLE RESERVATION", - DarkColorHex = "#0B0F19", - LightColorHex = "#F59E0B", - X = 240, - Y = 610, - Width = 120, - Height = 140, - ZIndex = 7 - }); - - // 10. Footer Note - page.Elements.Add(new PdfTextElement - { - Text = "RSVP BY OCTOBER 15, 2026 • PROCEEDS BENEFIT GLOBAL CLEAN WATER INITIATIVE", - FontFamily = "Montserrat", - FontSize = 8.5, - IsBold = true, - TextColorHex = "#9CA3AF", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 770, - Width = 500, - Height = 22, - ZIndex = 6 - }); - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Events/WeddingInvitationRoyalFloralTemplate.cs b/src/PdfEditorApp/Templates/Events/WeddingInvitationRoyalFloralTemplate.cs deleted file mode 100644 index baac4ab..0000000 --- a/src/PdfEditorApp/Templates/Events/WeddingInvitationRoyalFloralTemplate.cs +++ /dev/null @@ -1,266 +0,0 @@ -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Services; - -namespace PdfEditorApp.Templates.Events; - -/// -/// Luxury Royal Botanical Floral Wedding Invitation with gold leaf wreath, cursive script, and RSVP block. -/// -public class WeddingInvitationRoyalFloralTemplate : ITemplateDefinition -{ - public string Id => "weddingroyalfloral"; - public string Name => "Royal Botanical Wedding Invitation"; - public string Description => "Luxury gold foil botanical laurel wreath, calligraphy couple script, and RSVP QR Code"; - public string Category => "Events & Invitations"; - public string IconKind => "FlowerTulipOutline"; - public string AccentColorHex => "#D97706"; - - public PdfDocumentModel Create() - { - return GenerateDocument(); - } - - public static PdfDocumentModel GenerateDocument() - { - var doc = new PdfDocumentModel - { - Title = "Wedding_Invitation_Royal_Floral.pdf", - Author = "CodeFryDev Events", - Subject = "Luxury Floral Wedding Invitation Card" - }; - - var page = new PdfPageModel - { - Width = 600, - Height = 850, - Orientation = PageOrientation.Portrait, - ShowHeaderFooter = false - }; - - // 1. Soft Linen White Background - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.Rectangle, - X = 0, - Y = 0, - Width = 600, - Height = 850, - FillColorHex = "#FAFAFA", - StrokeColorHex = "#D97706", - StrokeThickness = 2.0, - ZIndex = 0 - }); - - // 2. Inner Double Gold Border - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.Rectangle, - X = 18, - Y = 18, - Width = 564, - Height = 814, - FillColorHex = "#00000000", - StrokeColorHex = "#D97706", - StrokeThickness = 1.0, - ZIndex = 1 - }); - - // 3. Botanical Floral Wreath Monogram Crest at Top - page.Elements.Add(new PdfSvgElement - { - PresetName = "BotanicalWreath", - SvgSource = SvgOrnamentLibrary.GetBotanicalWreathSvg("#D97706", "#059669"), - X = 230, - Y = 40, - Width = 140, - Height = 140, - ZIndex = 4 - }); - - // Monogram Initial inside wreath - page.Elements.Add(new PdfTextElement - { - Text = "J & J", - FontFamily = "Great Vibes", - FontSize = 32, - IsBold = true, - TextColorHex = "#D97706", - Alignment = TextAlignmentMode.Center, - X = 230, - Y = 90, - Width = 140, - Height = 44, - ZIndex = 5 - }); - - // 4. Header Text - page.Elements.Add(new PdfTextElement - { - Text = "TOGETHER WITH THEIR FAMILIES", - FontFamily = "Montserrat", - FontSize = 11, - IsBold = true, - TextColorHex = "#6B7280", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 200, - Width = 500, - Height = 22, - ZIndex = 5 - }); - - // 5. Couple Names in Great Vibes Cursive Calligraphy - page.Elements.Add(new PdfTextElement - { - Text = "Jane Doe", - FontFamily = "Great Vibes", - FontSize = 48, - IsBold = true, - TextColorHex = "#1F2937", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 235, - Width = 500, - Height = 65, - ZIndex = 6 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "and", - FontFamily = "Cinzel", - FontSize = 14, - IsItalic = true, - TextColorHex = "#D97706", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 300, - Width = 500, - Height = 24, - ZIndex = 6 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "John Doe", - FontFamily = "Great Vibes", - FontSize = 48, - IsBold = true, - TextColorHex = "#1F2937", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 328, - Width = 500, - Height = 65, - ZIndex = 6 - }); - - // 6. Center Calligraphic Flourish - page.Elements.Add(new PdfSvgElement - { - PresetName = "CalligraphicFlourish", - SvgSource = SvgOrnamentLibrary.GetCalligraphicFlourishSvg("#D97706"), - X = 175, - Y = 395, - Width = 250, - Height = 35, - ZIndex = 6 - }); - - // 7. Request Line - page.Elements.Add(new PdfTextElement - { - Text = "REQUEST THE PLEASURE OF YOUR COMPANY\nAT THE CELEBRATION OF THEIR MARRIAGE", - FontFamily = "Montserrat", - FontSize = 11, - IsBold = true, - TextColorHex = "#4B5563", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 440, - Width = 500, - Height = 42, - ZIndex = 6 - }); - - // 8. Date & Time - page.Elements.Add(new PdfTextElement - { - Text = "SATURDAY, SEPTEMBER 19, 2026", - FontFamily = "Cinzel", - FontSize = 16, - IsBold = true, - TextColorHex = "#D97706", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 495, - Width = 500, - Height = 26, - ZIndex = 6 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "AT FOUR O'CLOCK IN THE AFTERNOON", - FontFamily = "Montserrat", - FontSize = 11, - IsBold = false, - TextColorHex = "#374151", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 525, - Width = 500, - Height = 22, - ZIndex = 6 - }); - - // 9. Venue & Location - page.Elements.Add(new PdfTextElement - { - Text = "THE GRAND ORANGERIE ESTATE\n1042 HIGHLAND VALLEY ROAD\nNAPA VALLEY, CALIFORNIA", - FontFamily = "Cinzel", - FontSize = 12.5, - IsBold = true, - TextColorHex = "#1F2937", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 560, - Width = 500, - Height = 64, - ZIndex = 6 - }); - - // 10. Reception to follow & RSVP Block - page.Elements.Add(new PdfTextElement - { - Text = "RECEPTION, DINNER & DANCING TO FOLLOW", - FontFamily = "Montserrat", - FontSize = 11, - IsBold = true, - TextColorHex = "#D97706", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 640, - Width = 500, - Height = 22, - ZIndex = 6 - }); - - page.Elements.Add(new PdfQrCodeElement - { - Content = "https://wedding.codefrydev.in/jane-and-john", - Label = "SCAN TO RSVP ONLINE", - DarkColorHex = "#D97706", - LightColorHex = "#FAFAFA", - X = 245, - Y = 675, - Width = 110, - Height = 125, - ZIndex = 7 - }); - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Events/WeddingInvitationTraditionalTemplate.cs b/src/PdfEditorApp/Templates/Events/WeddingInvitationTraditionalTemplate.cs deleted file mode 100644 index 4e29529..0000000 --- a/src/PdfEditorApp/Templates/Events/WeddingInvitationTraditionalTemplate.cs +++ /dev/null @@ -1,429 +0,0 @@ -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Services; - -namespace PdfEditorApp.Templates.Events; - -/// -/// Authentic Traditional Indian Wedding Invitation Card template featuring Marigold Torans, -/// Lord Ganesha sacred crest, calligraphic couple names, dual Muhurtham & Reception schedule columns, -/// brass deepams, and ceremonial plantain tree accents. -/// -public class WeddingInvitationTraditionalTemplate : ITemplateDefinition -{ - public string Id => "weddingtraditional"; - public string Name => "Traditional Indian Wedding Invitation"; - public string Description => "Festive marigold toran, sacred Ganesha crest, dual Muhurtham & Reception schedule, and brass deepams"; - public string Category => "Events & Invitations"; - public string IconKind => "CardsHeartOutline"; - public string AccentColorHex => "#8B0000"; - - public PdfDocumentModel Create() - { - return GenerateDocument(); - } - - public static PdfDocumentModel GenerateDocument() - { - var doc = new PdfDocumentModel - { - Title = "Wedding_Invitation_Aarav_and_Ananya.pdf", - Author = "CodeFryDev Events", - Subject = "Traditional Indian Wedding Invitation Card" - }; - - // Standard Portrait Invitation (600 x 900 pt) - var page = new PdfPageModel - { - Width = 600, - Height = 900, - Orientation = PageOrientation.Portrait, - ShowHeaderFooter = false - }; - - // 1. Warm Luxury Parchment Cream Background - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.Rectangle, - X = 0, - Y = 0, - Width = 600, - Height = 900, - FillColorHex = "#FFFDF7", // Warm parchment ivory - StrokeColorHex = "#F59E0B", - StrokeThickness = 2.0, - ZIndex = 0 - }); - - // 2. Inner Golden Delicate Border - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.Rectangle, - X = 14, - Y = 14, - Width = 572, - Height = 872, - FillColorHex = "#00000000", - StrokeColorHex = "#D97706", - StrokeThickness = 1.0, - CornerRadius = 4, - ZIndex = 1 - }); - - // 3. Top Decorative Marigold Floral Toran Garland & Hanging Lamps - page.Elements.Add(new PdfSvgElement - { - PresetName = "MarigoldToran", - SvgSource = SvgOrnamentLibrary.GetMarigoldToranSvg(), - X = 15, - Y = 15, - Width = 570, - Height = 85, - ZIndex = 5 - }); - - // 4. Auspicious Sacred Lord Ganesha Vector Crest - page.Elements.Add(new PdfSvgElement - { - PresetName = "GaneshaCrest", - SvgSource = SvgOrnamentLibrary.GetGaneshaCrestSvg("#8B0000", "#D97706"), - X = 260, - Y = 100, - Width = 80, - Height = 85, - ZIndex = 6 - }); - - // 5. Sanskrit Sacred Invocation - page.Elements.Add(new PdfTextElement - { - Text = "|| Shree Ganeshay Namah ||", - FontFamily = "Cinzel", - FontSize = 13, - IsBold = true, - TextColorHex = "#8B0000", // Auspicious Crimson Maroon - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 190, - Width = 500, - Height = 24, - ZIndex = 7 - }); - - // 6. Invitation Announcement Header - page.Elements.Add(new PdfTextElement - { - Text = "Together with their families, cordially invite you to celebrate the wedding of", - FontFamily = "Cinzel", - FontSize = 12.5, - IsBold = false, - TextColorHex = "#451A03", // Deep rich walnut - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 220, - Width = 500, - Height = 46, - ZIndex = 7 - }); - - // 7. Groom & Bride Names in Calligraphic Typography with Ampersand - // Groom Name - page.Elements.Add(new PdfTextElement - { - Text = "Aarav", - FontFamily = "Great Vibes", - FontSize = 42, - IsBold = true, - TextColorHex = "#78350F", // Royal Amber Bronze - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 270, - Width = 220, - Height = 56, - ZIndex = 8 - }); - - // Groom Parentage - page.Elements.Add(new PdfTextElement - { - Text = "Son of\nMrs. Sunita & Mr. Rajesh Sharma\nGrandson of Late Smt. Kamala Sharma", - FontFamily = "Montserrat", - FontSize = 9.5, - IsBold = true, - TextColorHex = "#451A03", - Alignment = TextAlignmentMode.Center, - X = 35, - Y = 328, - Width = 250, - Height = 58, - ZIndex = 8 - }); - - // Center Calligraphic Ampersand - page.Elements.Add(new PdfTextElement - { - Text = "&", - FontFamily = "Great Vibes", - FontSize = 36, - IsBold = true, - TextColorHex = "#D97706", // Gold - Alignment = TextAlignmentMode.Center, - X = 280, - Y = 295, - Width = 40, - Height = 40, - ZIndex = 8 - }); - - // Bride Name - page.Elements.Add(new PdfTextElement - { - Text = "Ananya", - FontFamily = "Great Vibes", - FontSize = 42, - IsBold = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - X = 330, - Y = 270, - Width = 220, - Height = 56, - ZIndex = 8 - }); - - // Bride Parentage - page.Elements.Add(new PdfTextElement - { - Text = "Daughter of\nMrs. Meenakshi & Mr. Suresh Iyer\nGranddaughter of Late Shri K. Iyer", - FontFamily = "Montserrat", - FontSize = 9.5, - IsBold = true, - TextColorHex = "#451A03", - Alignment = TextAlignmentMode.Center, - X = 315, - Y = 328, - Width = 250, - Height = 58, - ZIndex = 8 - }); - - // 8. Center Dotted Floral Lotus Divider - page.Elements.Add(new PdfSvgElement - { - PresetName = "DottedFloralDivider", - SvgSource = SvgOrnamentLibrary.GetDottedFloralDividerSvg("#D97706", "#8B0000"), - X = 80, - Y = 390, - Width = 440, - Height = 24, - ZIndex = 8 - }); - - // 9. Dual Event Schedules (MUHURTHAM & RECEPTION) - // Left: MUHURTHAM - page.Elements.Add(new PdfTextElement - { - Text = "MUHURTHAM", - FontFamily = "Cinzel", - FontSize = 14, - IsBold = true, - TextColorHex = "#8B0000", - Alignment = TextAlignmentMode.Center, - X = 40, - Y = 420, - Width = 240, - Height = 24, - ZIndex = 8 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "Wednesday, 11th November 2026\n9:30 AM – 11:30 AM (Dhanu Lagna)", - FontFamily = "Montserrat", - FontSize = 10, - IsBold = true, - TextColorHex = "#451A03", - Alignment = TextAlignmentMode.Center, - X = 40, - Y = 445, - Width = 240, - Height = 40, - ZIndex = 8 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "- Venue -\nThe Leela Palace, Grand Ballroom\nMG Road, Bengaluru, Karnataka", - FontFamily = "Montserrat", - FontSize = 9.5, - IsBold = false, - TextColorHex = "#522504", - Alignment = TextAlignmentMode.Center, - X = 40, - Y = 488, - Width = 240, - Height = 52, - ZIndex = 8 - }); - - // Vertical Dotted Divider between Muhurtham and Reception - page.Elements.Add(new PdfDividerElement - { - IsVertical = true, - Thickness = 1.5, - ColorHex = "#D97706", - X = 299, - Y = 420, - Width = 2, - Height = 115, - ZIndex = 8 - }); - - // Right: RECEPTION - page.Elements.Add(new PdfTextElement - { - Text = "RECEPTION", - FontFamily = "Cinzel", - FontSize = 14, - IsBold = true, - TextColorHex = "#8B0000", - Alignment = TextAlignmentMode.Center, - X = 320, - Y = 420, - Width = 240, - Height = 24, - ZIndex = 8 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "Thursday, 12th November 2026\n7:00 PM Onwards", - FontFamily = "Montserrat", - FontSize = 10, - IsBold = true, - TextColorHex = "#451A03", - Alignment = TextAlignmentMode.Center, - X = 320, - Y = 445, - Width = 240, - Height = 40, - ZIndex = 8 - }); - - page.Elements.Add(new PdfTextElement - { - Text = "- Venue -\nThe Leela Palace, Royal Convention Lawn\nMG Road, Bengaluru, Karnataka", - FontFamily = "Montserrat", - FontSize = 9.5, - IsBold = false, - TextColorHex = "#522504", - Alignment = TextAlignmentMode.Center, - X = 320, - Y = 488, - Width = 240, - Height = 52, - ZIndex = 8 - }); - - // 10. Secondary Dotted Floral Divider - page.Elements.Add(new PdfSvgElement - { - PresetName = "DottedFloralDivider", - SvgSource = SvgOrnamentLibrary.GetDottedFloralDividerSvg("#D97706", "#8B0000"), - X = 120, - Y = 545, - Width = 360, - Height = 20, - ZIndex = 8 - }); - - // 11. RSVP / Best Compliments - page.Elements.Add(new PdfTextElement - { - Text = "With Best Compliments From: Doe Families • RSVP: +1 (555) 019-2834", - FontFamily = "Montserrat", - FontSize = 10.5, - IsBold = true, - TextColorHex = "#78350F", - Alignment = TextAlignmentMode.Center, - X = 50, - Y = 572, - Width = 500, - Height = 24, - ZIndex = 8 - }); - - // 12. Flanking Left & Right Ceremonial Banana Plantain Trees - page.Elements.Add(new PdfSvgElement - { - PresetName = "PlantainTrees", - SvgSource = SvgOrnamentLibrary.GetPlantainTreesSvg(), - X = 18, - Y = 640, - Width = 80, - Height = 160, - ZIndex = 6 - }); - - page.Elements.Add(new PdfSvgElement - { - PresetName = "PlantainTrees", - SvgSource = SvgOrnamentLibrary.GetPlantainTreesSvg(), - X = 502, - Y = 640, - Width = 80, - Height = 160, - ZIndex = 6 - }); - - // 13. Auspicious Brass Standing Deepam Lamps (Left & Right) - page.Elements.Add(new PdfSvgElement - { - PresetName = "TraditionalDeepam", - SvgSource = SvgOrnamentLibrary.GetTraditionalDeepamSvg(), - X = 85, - Y = 675, - Width = 55, - Height = 135, - ZIndex = 7 - }); - - page.Elements.Add(new PdfSvgElement - { - PresetName = "TraditionalDeepam", - SvgSource = SvgOrnamentLibrary.GetTraditionalDeepamSvg(), - X = 460, - Y = 675, - Width = 55, - Height = 135, - ZIndex = 7 - }); - - // 14. Center Vedic Kalash with Mango Leaves and Coconut - page.Elements.Add(new PdfSvgElement - { - PresetName = "AuspiciousKalash", - SvgSource = SvgOrnamentLibrary.GetAuspiciousKalashSvg(), - X = 265, - Y = 665, - Width = 70, - Height = 90, - ZIndex = 7 - }); - - // 15. Bottom Ornate Mandap Floor Border - page.Elements.Add(new PdfSvgElement - { - PresetName = "MandapArch", - SvgSource = SvgOrnamentLibrary.GetMandapArchSvg("#D97706", "#8B0000"), - X = 20, - Y = 820, - Width = 560, - Height = 55, - ZIndex = 7 - }); - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Finance/FinanceResearchPaperTemplate.cs b/src/PdfEditorApp/Templates/Finance/FinanceResearchPaperTemplate.cs deleted file mode 100644 index f27b1c6..0000000 --- a/src/PdfEditorApp/Templates/Finance/FinanceResearchPaperTemplate.cs +++ /dev/null @@ -1,579 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Finance; - -public class FinanceResearchPaperTemplate : ITemplateDefinition -{ - public string Id => "financeresearch"; - public string Name => "Quantitative Finance & Econometrics Paper"; - public string Description => "Full 2-page quantitative finance paper with stochastic jump-diffusion SDEs, econometric calibration tables, and backtest metrics"; - public string Category => "Finance"; - public string IconKind => "ChartLineVariant"; - public string AccentColorHex => "#15803D"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Quantitative_Finance_Jump_Diffusion_2026.pdf", - Author = "Dr. John Doe, CFA & Dr. Jane Doe", - Subject = "Quantitative Finance, Stochastic Volatility & High-Frequency Risk Modeling" - }; - - // ========================================================================= - // PAGE 1: Title, Abstract, SDEs, Estimation & Model Benchmark Table - // ========================================================================= - var page1 = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "JOURNAL OF FINANCIAL ECONOMETRICS • VOL. 34, ISSUE 2 • JEL: G12, G13, C58, C63", - FooterCenter = "QUANTITATIVE RESEARCH ARTICLE", - FooterRight = "Page 1 of 2", - Elements = new List - { - // Top Header Rule & Journal Metadata - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "JOURNAL OF FINANCIAL ECONOMETRICS, VOL. 34 (2026), PP. 240–288 • DOI: 10.1093/jjfinec/nbad024", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#166534", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#86EFAC" - }, - - // Main Paper Title - new PdfTextElement - { - X = 55, - Y = 58, - Width = 690, - Height = 48, - Text = "Multi-Factor Jump-Diffusion Asset Pricing and Stochastic Volatility in High-Frequency Futures Markets", - FontSize = 16, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D", - Alignment = TextAlignmentMode.Center - }, - - // Authors & Affiliation - new PdfTextElement - { - X = 55, - Y = 108, - Width = 690, - Height = 34, - Text = "John Doe, Ph.D., CFA¹ and Jane Doe, Ph.D.²\n¹Quantitative Investment Strategies, CodeFryDev Capital Management • ²Department of Finance, CodeFryDev Institute", - FontSize = 9.5, - FontFamily = "Times New Roman", - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Center - }, - - // Abstract & JEL Codes Card - new PdfShapeElement - { - X = 75, - Y = 145, - Width = 650, - Height = 104, - CornerRadius = 4, - FillColorHex = "#F0FDF4", - StrokeColorHex = "#BBF7D0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 90, - Y = 150, - Width = 620, - Height = 68, - Text = "Abstract— We develop a generalized continuous-time Bates jump-diffusion framework with state-dependent jump intensities calibrated to 100-microsecond order-book data for S&P 500 E-mini and Treasury futures. Out-of-sample hedging tests demonstrate a 32.8% reduction in pricing root-mean-square error (RMSE) during high-volatility regime shifts compared to classical local volatility surfaces.", - FontSize = 9, - FontFamily = "Times New Roman", - IsItalic = true, - LineHeight = 1.35, - TextColorHex = "#166534", - Alignment = TextAlignmentMode.Justify - }, - new PdfTextElement - { - X = 90, - Y = 222, - Width = 620, - Height = 22, - Text = "JEL Classification— G12 (Asset Pricing), G13 (Contingent Pricing), C58 (Financial Econometrics), C63 (Computational Techniques).", - FontSize = 8.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#15803D" - }, - - // Column Split Divider - new PdfDividerElement - { - X = 55, - Y = 254, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#86EFAC" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 262, - Width = 330, - Height = 22, - Text = "1. STOCHASTIC DYNAMICS SPECIFICATION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfTextElement - { - X = 55, - Y = 286, - Width = 330, - Height = 90, - Text = "Let (Ω, ℱ, {ℱ_t}, ℙ) represent a filtered probability space satisfying usual conditions. Under the physical probability measure ℙ, the spot asset price process S_t and variance process V_t follow the coupled stochastic differential equations [1, 2]:", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // SDE Equation Box - new PdfShapeElement - { - X = 55, - Y = 380, - Width = 330, - Height = 52, - CornerRadius = 3, - FillColorHex = "#F0FDF4", - StrokeColorHex = "#BBF7D0", - StrokeThickness = 0.5 - }, - new PdfTextElement - { - X = 65, - Y = 386, - Width = 310, - Height = 40, - Text = "dS_t = (μ - λ k̄) S_t dt + √V_t S_t dW_t^S + (e^J - 1) S_t dN_t\ndV_t = κ(θ - V_t) dt + σ_v √V_t dW_t^V (1)", - FontSize = 8.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D", - Alignment = TextAlignmentMode.Center - }, - - new PdfTextElement - { - X = 55, - Y = 438, - Width = 330, - Height = 110, - Text = "where d⟨W^S, W^V⟩_t = ρ dt represents the leverage correlation, N_t is a Poisson counting process with intensity λ_t = λ₀ + λ₁ V_t, and J ~ 𝒩(μ_J, σ_J²) denotes the log-jump amplitude distribution with mean percentage jump size k̄ = exp(μ_J + ½σ_J²) - 1.", - FontSize = 9, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 554, - Width = 330, - Height = 22, - Text = "2. QUASI-MAXIMUM LIKELIHOOD ESTIMATION", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfTextElement - { - X = 55, - Y = 578, - Width = 330, - Height = 130, - Text = "Parameter estimation is performed via Sequential Monte Carlo (particle filtering) combined with Fourier transform inversion of the characteristic function ϕ(u; t) = 𝔼[exp(i u ln S_t)]. The Feller condition 2κθ > σ_v² is strictly enforced across optimization iterations to ensure that the variance process V_t remains strictly positive almost surely [3].", - FontSize = 9, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 712, - Width = 330, - Height = 22, - Text = "3. EMPIRICAL VOLATILITY SURFACE SMILE", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfTextElement - { - X = 55, - Y = 736, - Width = 330, - Height = 110, - Text = "Short-maturity options exhibit pronounced implied volatility skews that cannot be replicated by diffusion-only models. Incorporating compound Poisson jump arrivals resolves the term-structure flattening problem and matches empirical kurtosis.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 262, - Width = 330, - Height = 22, - Text = "4. OUT-OF-SAMPLE MODEL BENCHMARKS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfTextElement - { - X = 415, - Y = 286, - Width = 330, - Height = 58, - Text = "Table I reports out-of-sample pricing RMSE, annualized Sharpe ratio, maximum portfolio drawdown, and 99% daily Value-at-Risk (VaR) exception rates across 10 years of tick data (2016–2026).", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Econometric Performance Table - new PdfTableElement - { - X = 415, - Y = 348, - Width = 330, - Height = 145, - Headers = new List { "Model Specification", "RMSE", "Sharpe", "Max DD", "VaR 99%" }, - Rows = new List> - { - new() { "Black-Scholes (1973)", "$4.12", "0.84", "-28.4%", "3.8%" }, - new() { "Heston SV (1993)", "$1.89", "1.42", "-16.2%", "1.6%" }, - new() { "Rough Bergomi (2019)", "$1.34", "1.78", "-11.5%", "1.1%" }, - new() { "Proposed Jump-Diff", "$0.92", "2.14", "-7.8%", "0.9%" } - }, - HeaderBackgroundHex = "#15803D", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F0FDF4", - BorderColorHex = "#CBD5E1" - }, - - new PdfTextElement - { - X = 415, - Y = 502, - Width = 330, - Height = 22, - Text = "5. RISK PARITY BACKTESTING", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfTextElement - { - X = 415, - Y = 526, - Width = 330, - Height = 125, - Text = "Dynamic delta-gamma-vega hedging strategies derived from the calibrated jump-diffusion parameters achieved superior risk-adjusted alpha during market stress periods (e.g. Flash Crash events and interest rate spikes), cutting tail drawdown risk by 72% [4].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 415, - Y = 658, - Width = 330, - Height = 22, - Text = "6. CROSS-ASSET CALIBRATION METRICS", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfTextElement - { - X = 415, - Y = 682, - Width = 330, - Height = 145, - Text = "Calibrated mean-reversion speeds κ ranged from 3.42 for S&P 500 index options to 7.85 for Crude Oil (WTI) futures, reflecting rapid information assimilation in physical commodity derivative markets.", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - } - } - }; - - // ========================================================================= - // PAGE 2: Order-Book Microstructure, Basel IV, Author Bios & References - // ========================================================================= - var page2 = new PdfPageModel - { - PageNumber = 2, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "JOURNAL OF FINANCIAL ECONOMETRICS • VOL. 34, ISSUE 2", - FooterRight = "Page 2 of 2", - Elements = new List - { - // Top Header Metadata Strip - new PdfTextElement - { - X = 55, - Y = 32, - Width = 690, - Height = 18, - Text = "VANCE & HOLMSTRÖM: MULTI-FACTOR JUMP-DIFFUSION IN FUTURES MARKETS", - FontSize = 8.5, - FontFamily = "Times New Roman", - TextColorHex = "#166534", - Alignment = TextAlignmentMode.Center - }, - new PdfDividerElement - { - X = 55, - Y = 52, - Width = 690, - Height = 1, - Thickness = 0.75, - ColorHex = "#86EFAC" - }, - - // ========================================== - // COLUMN 1 (Left: X = 55, Width = 330) - // ========================================== - new PdfTextElement - { - X = 55, - Y = 62, - Width = 330, - Height = 22, - Text = "7. ORDER-BOOK LEVEL II MICROSTRUCTURE", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfTextElement - { - X = 55, - Y = 86, - Width = 330, - Height = 135, - Text = "By linking Poisson arrival intensities λ_t directly to limit order book imbalance (OFI_t = (V_t^bid - V_t^ask) / (V_t^bid + V_t^ask)), the model captures toxic flow arrivals preceding market-maker spread widening. Empirical tests indicate that order-flow toxicity accounts for 44.2% of jump arrival variance [5].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - new PdfTextElement - { - X = 55, - Y = 228, - Width = 330, - Height = 22, - Text = "8. BASEL IV & FRTB CAPITAL ADEQUACY", - FontSize = 11, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfTextElement - { - X = 55, - Y = 252, - Width = 330, - Height = 135, - Text = "Under the Fundamental Review of the Trading Book (FRTB), banks must compute Expected Shortfall (ES) under liquidity horizons ranging from 10 to 120 days. Incorporating heavy-tailed jump dynamics prevents systematic undercapitalization during market dislocation regimes [6].", - FontSize = 9.5, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Justify - }, - - // Author Biographies - new PdfTextElement - { - X = 55, - Y = 395, - Width = 330, - Height = 22, - Text = "AUTHOR BIOGRAPHIES", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfDividerElement - { - X = 55, - Y = 415, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#86EFAC" - }, - - new PdfShapeElement - { - X = 55, - Y = 425, - Width = 36, - Height = 36, - CornerRadius = 18, - FillColorHex = "#15803D", - Label = "JV", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 100, - Y = 423, - Width = 285, - Height = 70, - Text = "John Doe, CFA, received his Ph.D. in Finance. He is a Managing Director in Quantitative Strategies at CodeFryDev Capital Management.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - new PdfShapeElement - { - X = 55, - Y = 500, - Width = 36, - Height = 36, - CornerRadius = 18, - FillColorHex = "#166534", - Label = "JD", - LabelColorHex = "#FFFFFF", - LabelFontSize = 12 - }, - new PdfTextElement - { - X = 100, - Y = 498, - Width = 285, - Height = 70, - Text = "Jane Doe is Associate Professor of Financial Econometrics at CodeFryDev Institute, specializing in continuous-time asset pricing and rough volatility surfaces.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.3, - TextColorHex = "#334155" - }, - - // ========================================== - // COLUMN 2 (Right: X = 415, Width = 330) - // ========================================== - new PdfTextElement - { - X = 415, - Y = 62, - Width = 330, - Height = 22, - Text = "REFERENCES", - FontSize = 10.5, - FontFamily = "Times New Roman", - IsBold = true, - TextColorHex = "#14532D" - }, - new PdfDividerElement - { - X = 415, - Y = 85, - Width = 330, - Height = 1, - Thickness = 0.75, - ColorHex = "#86EFAC" - }, - new PdfTextElement - { - X = 415, - Y = 90, - Width = 330, - Height = 270, - Text = "[1] R. C. Merton, \"Option pricing when underlying stock returns are discontinuous,\" J. Financ. Econ., vol. 3, pp. 125–144, 1976.\n[2] S. L. Heston, \"A closed-form solution for options with stochastic volatility,\" Rev. Financ. Stud., vol. 6, pp. 327–343, 1993.\n[3] D. S. Bates, \"Jumps and stochastic volatility: Exchange rate processes implicit in Deutsche Mark options,\" Rev. Financ. Stud., vol. 9, pp. 69–107, 1996.\n[4] R. Cont and P. Tankov, Financial Modelling with Jump Processes, Chapman & Hall/CRC, 2004.\n[5] J. Gatheral, The Volatility Surface: A Practitioner's Guide, John Wiley & Sons, 2006.\n[6] Basel Committee on Banking Supervision, Minimum Capital Requirements for Market Risk (FRTB), Bank for International Settlements, 2019.\n[7] C. Bayer, P. Friz, and J. Gatheral, \"Pricing under rough volatility,\" Quant. Finance, vol. 16, pp. 887–904, 2016.\n[8] D. Duffie, J. Pan, and K. Singleton, \"Transform analysis and asset pricing for affine jump-diffusions,\" Econometrica, vol. 68, pp. 1343–1376, 2000.", - FontSize = 8, - FontFamily = "Times New Roman", - LineHeight = 1.35, - TextColorHex = "#475569", - Alignment = TextAlignmentMode.Justify - } - } - }; - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/Finance/InvoiceTemplate.cs b/src/PdfEditorApp/Templates/Finance/InvoiceTemplate.cs deleted file mode 100644 index 9e4eda4..0000000 --- a/src/PdfEditorApp/Templates/Finance/InvoiceTemplate.cs +++ /dev/null @@ -1,388 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Finance; - -public class InvoiceTemplate : ITemplateDefinition -{ - public string Id => "invoice"; - public string Name => "Commercial Enterprise Invoice"; - public string Description => "Full-page itemized billing invoice with bank wire details, tax breakdown, and instant payment QR code"; - public string Category => "Finance"; - public string IconKind => "ReceiptTextOutline"; - public string AccentColorHex => "#0F6CBD"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Invoice_INV-2026-8492.pdf", - Author = "CodeFryDev Solutions Inc.", - Subject = "Enterprise Software & Cloud Engineering Services Invoice" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 800, - Height = 1131, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "CodeFryDev Solutions Inc. • Tax ID: US-EIN 94-3829104 • support@codefrydev.in", - FooterCenter = "Payment Terms: Net 30 Days", - FooterRight = "Invoice #INV-2026-8492 • Page 1 of 1", - Elements = new List - { - // Top Brand Accent Header Bar - new PdfShapeElement - { - X = 0, - Y = 0, - Width = 800, - Height = 8, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#00000000", - StrokeThickness = 0 - }, - - // 1. Company Logo Badge - new PdfShapeElement - { - X = 55, - Y = 38, - Width = 52, - Height = 52, - CornerRadius = 10, - FillColorHex = "#0F6CBD", - StrokeColorHex = "#0C599B", - StrokeThickness = 0, - Label = "CF", - LabelColorHex = "#FFFFFF", - LabelFontSize = 20 - }, - - // 2. Seller Company Name & Address - new PdfTextElement - { - X = 118, - Y = 38, - Width = 320, - Height = 26, - Text = "CODEFRYDEV SOLUTIONS INC.", - FontSize = 15, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F172A" - }, - new PdfTextElement - { - X = 118, - Y = 64, - Width = 320, - Height = 58, - Text = "742 Market St, Suite 1200 • Financial District\nSan Francisco, CA 94111 • United States\nPhone: +1 (555) 019-2834 • billing@codefrydev.in", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#64748B" - }, - - // 3. Right Invoice Title & Metadata Card - new PdfTextElement - { - X = 490, - Y = 36, - Width = 255, - Height = 32, - Text = "COMMERCIAL INVOICE", - FontSize = 18, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD", - Alignment = TextAlignmentMode.Right - }, - new PdfTextElement - { - X = 470, - Y = 68, - Width = 275, - Height = 68, - Text = "Invoice Number: INV-2026-8492\nIssue Date: August 29, 2026\nPayment Due: September 28, 2026\nPO Reference: PO-ENTERPRISE-9841", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#334155", - Alignment = TextAlignmentMode.Right - }, - - // Top Separator Line - new PdfDividerElement - { - X = 55, - Y = 138, - Width = 690, - Height = 2, - Thickness = 1.5, - ColorHex = "#E2E8F0" - }, - - // 4. Client Billing & Project Information - // Left: Bill To - new PdfTextElement - { - X = 55, - Y = 146, - Width = 320, - Height = 20, - Text = "BILLED TO (CLIENT):", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - new PdfTextElement - { - X = 55, - Y = 166, - Width = 320, - Height = 72, - Text = "CodeFryDev Client Enterprises Inc.\nAttn: Accounts Payable & Financial Operations\n500 Technology Square, Floor 8\nCambridge, MA 02139 • United States\nClient Tax ID: US-EIN 04-2918471", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - - // Right: Project / Contract Details - new PdfTextElement - { - X = 440, - Y = 146, - Width = 305, - Height = 20, - Text = "CONTRACT & ENGAGEMENT:", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - new PdfTextElement - { - X = 440, - Y = 166, - Width = 305, - Height = 72, - Text = "Project: Cloud Architecture & High-Performance Publishing Suite\nEngagement Period: July 01, 2026 – August 28, 2026\nPayment Terms: Net 30 Days (Direct Wire / ACH)\nCurrency: USD ($)", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.35, - TextColorHex = "#1E293B" - }, - - // 5. Itemized Billing Table - new PdfTableElement - { - X = 55, - Y = 246, - Width = 690, - Height = 310, - Headers = new List { "Deliverable / Description", "Qty / Unit", "Rate (USD)", "Discount", "Line Total" }, - Rows = new List> - { - new() { "Enterprise UI/UX Design System & Material Tokens", "60 Hours", "$150.00", "$0.00", "$9,000.00" }, - new() { "Avalonia Multiplatform Desktop Architecture & Skia Engine", "140 Hours", "$160.00", "$500.00", "$21,900.00" }, - new() { "High-Fidelity QuestPDF Vector Document Pipeline", "45 Hours", "$160.00", "$0.00", "$7,200.00" }, - new() { "Cloud Synchronization & Scalable Microservices (AKS)", "35 Hours", "$150.00", "$0.00", "$5,250.00" }, - new() { "SOC2 Security Hardening, Audit & Automated CI/CD Setup", "25 Hours", "$170.00", "$250.00", "$4,000.00" }, - new() { "Performance Profiling & Memory Leak Optimization (60 FPS)", "15 Hours", "$150.00", "$0.00", "$2,250.00" } - }, - HeaderBackgroundHex = "#0F6CBD", - HeaderTextHex = "#FFFFFF", - AlternateRowBackgroundHex = "#F8FAFC", - BorderColorHex = "#E2E8F0" - }, - - // 6. Financial Summary Box (Right Side) - new PdfShapeElement - { - X = 430, - Y = 570, - Width = 315, - Height = 160, - CornerRadius = 8, - FillColorHex = "#F8FAFC", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 445, - Y = 582, - Width = 150, - Height = 135, - Text = "Subtotal (Gross):\nPrompt Payment Discount (3%):\nNet Taxable Amount:\nState & Local Tax (8.5%):\n\nTOTAL BALANCE DUE:", - FontSize = 10, - FontFamily = "Segoe UI", - LineHeight = 1.6, - TextColorHex = "#475569" - }, - new PdfTextElement - { - X = 595, - Y = 582, - Width = 135, - Height = 135, - Text = "$49,600.00\n-$1,488.00\n$48,112.00\n$4,089.52\n\n$52,201.52", - FontSize = 10, - FontFamily = "Segoe UI", - IsBold = true, - LineHeight = 1.6, - TextColorHex = "#0F172A", - Alignment = TextAlignmentMode.Right - }, - - // 7. Payment Instructions Box (Left Side) - new PdfShapeElement - { - X = 55, - Y = 570, - Width = 360, - Height = 160, - CornerRadius = 8, - FillColorHex = "#F0F7FF", - StrokeColorHex = "#BFDBFE", - StrokeThickness = 1 - }, - new PdfTextElement - { - X = 70, - Y = 582, - Width = 330, - Height = 20, - Text = "WIRE / ACH PAYMENT INSTRUCTIONS", - FontSize = 10.5, - FontFamily = "Segoe UI", - IsBold = true, - TextColorHex = "#0F6CBD" - }, - new PdfTextElement - { - X = 70, - Y = 604, - Width = 330, - Height = 115, - Text = "Bank Name: First Commercial Bank, N.A.\nAccount Holder: CodeFryDev Solutions Inc.\nRouting / ABA (Domestic): 121000358\nAccount Number: 98402941829\nSWIFT / BIC (International): CFDEVUS6SXXX\nPayment Reference: INV-2026-8492", - FontSize = 9.5, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#1E293B" - }, - - // 8. Instant Payment QR Code & Terms - new PdfQrCodeElement - { - X = 55, - Y = 746, - Width = 110, - Height = 110, - Content = "https://pay.codefrydev.in/invoice/INV-2026-8492", - Label = "SCAN TO PAY INVOICE", - DarkColorHex = "#0F6CBD", - LightColorHex = "#FFFFFF" - }, - - // 9. Terms & Late Fee Policy - new PdfTextElement - { - X = 180, - Y = 746, - Width = 565, - Height = 110, - Text = "TERMS & CONDITIONS:\n1. Payment is due strictly within thirty (30) calendar days from the invoice issuance date.\n2. Overdue balances are subject to a late service fee of 1.5% per month or the maximum permitted by law.\n3. All deliverables are licensed upon full settlement of this invoice according to Master Services Agreement #MSA-2026-11.\n4. For billing inquiries, discrepancies, or receipt confirmations, contact finance@codefrydev.in.", - FontSize = 9, - FontFamily = "Segoe UI", - LineHeight = 1.4, - TextColorHex = "#475569" - }, - - // Bottom Divider - new PdfDividerElement - { - X = 55, - Y = 870, - Width = 690, - Height = 1, - Thickness = 1, - ColorHex = "#CBD5E1" - }, - - // 10. Authorized Corporate Signatory & Official Stamp - new PdfTextElement - { - X = 55, - Y = 885, - Width = 320, - Height = 45, - Text = "John Doe", - FontSize = 22, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#0F172A" - }, - new PdfDividerElement - { - X = 55, - Y = 932, - Width = 280, - Height = 1, - Thickness = 1, - ColorHex = "#94A3B8" - }, - new PdfTextElement - { - X = 55, - Y = 938, - Width = 280, - Height = 35, - Text = "Authorized Corporate Signatory\nChief Financial Officer • CodeFryDev Solutions Inc.", - FontSize = 9, - FontFamily = "Segoe UI", - TextColorHex = "#64748B" - }, - - // Official Verification Seal Badge (Right) - new PdfShapeElement - { - X = 580, - Y = 880, - Width = 165, - Height = 85, - CornerRadius = 8, - FillColorHex = "#F0FDF4", - StrokeColorHex = "#86EFAC", - StrokeThickness = 1, - Label = "VERIFIED INVOICE", - LabelColorHex = "#166534", - LabelFontSize = 10 - }, - new PdfTextElement - { - X = 590, - Y = 920, - Width = 145, - Height = 38, - Text = "Cryptographically signed &\nverified by CodeFryDev Trust PKI", - FontSize = 8.5, - FontFamily = "Segoe UI", - TextColorHex = "#166534", - Alignment = TextAlignmentMode.Center - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/General/BlankDocumentTemplate.cs b/src/PdfEditorApp/Templates/General/BlankDocumentTemplate.cs deleted file mode 100644 index 0c79d43..0000000 --- a/src/PdfEditorApp/Templates/General/BlankDocumentTemplate.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.General; - -public class BlankDocumentTemplate : ITemplateDefinition -{ - public string Id => "blank"; - public string Name => "Blank Page"; - public string Description => "Start fresh with a clean customizable canvas"; - public string Category => "General"; - public string IconKind => "FileOutline"; - public string AccentColorHex => "#64748B"; - - public PdfDocumentModel Create() - { - return Create(PageFormat.A4, PageOrientation.Portrait); - } - - public PdfDocumentModel Create(PageFormat format = PageFormat.A4, PageOrientation orientation = PageOrientation.Portrait) - { - var doc = new PdfDocumentModel - { - Title = "Untitled_Document.pdf", - Author = "CodeFryDev", - Subject = "New PDF Document" - }; - - var width = format switch - { - PageFormat.A4 => 800.0, - PageFormat.Letter => 800.0, - PageFormat.Legal => 800.0, - PageFormat.Poster => 2592.0, - _ => 800.0 - }; - - var height = format switch - { - PageFormat.A4 => 1131.0, - PageFormat.Letter => 1035.0, - PageFormat.Legal => 1318.0, - PageFormat.Poster => 3456.0, - _ => 1131.0 - }; - - if (orientation == PageOrientation.Landscape) - { - (width, height) = (height, width); - } - - var page = new PdfPageModel - { - PageNumber = 1, - Format = format, - Orientation = orientation, - Width = width, - Height = height, - BackgroundColorHex = "#FFFFFF", - FooterLeft = "CONFIDENTIAL", - FooterRight = "Page 1 of 1", - Elements = new List - { - new PdfTextElement - { - X = 60, - Y = 60, - Width = 680, - Height = 40, - Text = "Click to Edit Heading", - FontSize = 24, - IsBold = true, - TextColorHex = "#111827" - }, - new PdfTextElement - { - X = 60, - Y = 110, - Width = 680, - Height = 60, - Text = "Start typing your content here, or use the Ribbon toolbar above to insert text blocks, images, tables, shapes, and watermarks.", - FontSize = 13, - TextColorHex = "#4B5563" - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/General/RichTextPublishingShowcaseTemplate.cs b/src/PdfEditorApp/Templates/General/RichTextPublishingShowcaseTemplate.cs deleted file mode 100644 index fe77928..0000000 --- a/src/PdfEditorApp/Templates/General/RichTextPublishingShowcaseTemplate.cs +++ /dev/null @@ -1,643 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.General; - -/// -/// Professional Publishing Specimen Template demonstrating inline rich text typography, -/// multi-span runs, scientific notations (H₂O, x²), highlighted legal clauses, custom colors, -/// and clickable hyperlinks. -/// -public class RichTextPublishingShowcaseTemplate : ITemplateDefinition -{ - public string Id => "richtextshowcase"; - public string Name => "Rich Typography & Publishing Specimen"; - public string Description => "Executive multi-span typography showcase featuring inline bold, italics, highlights, sub/superscript notations, custom colors, and links"; - public string Category => "Design & Creative"; - public string IconKind => "FormatColorText"; - public string AccentColorHex => "#0F6CBD"; - - public PdfDocumentModel Create() - { - return GenerateDocument(); - } - - public static PdfDocumentModel GenerateDocument() - { - var doc = new PdfDocumentModel - { - Title = "Rich_Text_Publishing_Specimen.pdf", - Author = "FryPDF Publishing Studio", - Subject = "Official Specimen Demonstrating Multi-Span Inline Typography and Rich Formatting", - Keywords = "RichText, Typography, Publishing, Specimen, Markdown, Superscript, Subscript" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.A4, - Orientation = PageOrientation.Portrait, - Width = 595.28, - Height = 841.89, - BackgroundColorHex = "#F8FAFC", - ShowHeaderFooter = true, - HeaderLeft = "FRYPDF TYPOGRAPHY SPECIMEN", - HeaderRight = "VOLUME IV • ISSUE 1", - FooterLeft = "CONFIDENTIAL & PROPRIETARY", - FooterCenter = "www.frypdf.dev", - FooterRight = "PAGE 1 OF 1" - }; - - // ========================================== - // 1. TOP HERO HEADER & BRANDING BANNER - // ========================================== - - // Header Background Banner Card - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 24, - Width = 547.28, - Height = 110, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 8, - FillColorHex = "#0F172A", - StrokeColorHex = "#1E293B", - StrokeThickness = 1.5, - ZIndex = 0 - }); - - // Top Accent Stripe - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 24, - Width = 547.28, - Height = 4, - ShapeType = ShapeType.Rectangle, - FillColorHex = "#0F6CBD", - ZIndex = 1 - }); - - // Category Badge - page.Elements.Add(new PdfTextElement - { - X = 44, - Y = 40, - Width = 220, - Height = 18, - Text = "DESKTOP PUBLISHING ENGINE • V2.0", - FontFamily = "Segoe UI", - FontSize = 9.5, - IsBold = true, - TextColorHex = "#38BDF8", - CharacterSpacing = 1.2, - ZIndex = 2 - }); - - // Main Title with Multi-Span Rich Text (Mixed White and Cyan) - var heroTitle = new PdfTextElement - { - X = 44, - Y = 60, - Width = 480, - Height = 34, - FontFamily = "Segoe UI", - FontSize = 20, - TextColorHex = "#FFFFFF", - Spans = new List - { - new() { Text = "Rich Typography ", IsBold = true, TextColorHex = "#FFFFFF" }, - new() { Text = "& Inline Formatting ", IsBold = true, TextColorHex = "#38BDF8" }, - new() { Text = "Studio", IsBold = false, IsItalic = true, TextColorHex = "#94A3B8" } - }, - ZIndex = 2 - }; - heroTitle.SynchronizePlainTextFromSpans(); - page.Elements.Add(heroTitle); - - // Subtitle Description - var heroSub = new PdfTextElement - { - X = 44, - Y = 96, - Width = 500, - Height = 22, - FontFamily = "Segoe UI", - FontSize = 10, - TextColorHex = "#CBD5E1", - Spans = new List - { - new() { Text = "High-fidelity " }, - new() { Text = "multi-span runs", IsBold = true, TextColorHex = "#FFFFFF" }, - new() { Text = " with granular control over weights, italics, colors, subscripts, and links." } - }, - ZIndex = 2 - }; - heroSub.SynchronizePlainTextFromSpans(); - page.Elements.Add(heroSub); - - // ========================================== - // 2. SECTION A: EXECUTIVE SUMMARY & CALLOUT - // ========================================== - - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 146, - Width = 547.28, - Height = 90, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 146, - Width = 4, - Height = 90, - ShapeType = ShapeType.Rectangle, - FillColorHex = "#0F6CBD", - ZIndex = 1 - }); - - var execHeading = new PdfTextElement - { - X = 40, - Y = 158, - Width = 400, - Height = 20, - FontFamily = "Segoe UI", - FontSize = 12, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "1. Executive Summary & ", IsBold = true }, - new() { Text = "Inline Style Highlights", IsBold = true, TextColorHex = "#0F6CBD" } - }, - ZIndex = 2 - }; - execHeading.SynchronizePlainTextFromSpans(); - page.Elements.Add(execHeading); - - var execBody = new PdfTextElement - { - X = 40, - Y = 180, - Width = 515, - Height = 46, - FontFamily = "Segoe UI", - FontSize = 10, - LineHeight = 1.45, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "FryPDF empowers authors to craft documents with " }, - new() { Text = "extreme typographic precision", IsBold = true, TextColorHex = "#0F172A" }, - new() { Text = ". Sentences can effortlessly combine " }, - new() { Text = "bold emphasis", IsBold = true }, - new() { Text = ", " }, - new() { Text = "italic nuance", IsItalic = true }, - new() { Text = ", " }, - new() { Text = "underline anchors", IsUnderline = true, TextColorHex = "#0F6CBD" }, - new() { Text = ", and " }, - new() { Text = "strikethrough revisions", IsStrikethrough = true, TextColorHex = "#94A3B8" }, - new() { Text = " inside a single, unified text box without creating disjointed fragments." } - }, - ZIndex = 2 - }; - execBody.SynchronizePlainTextFromSpans(); - page.Elements.Add(execBody); - - // ========================================== - // 3. SECTION B: 2-COLUMN CARDS (TECHNICAL & LEGAL) - // ========================================== - - // Left Card: Scientific & Mathematical Notations - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 248, - Width = 265, - Height = 220, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - // Left Card Top Badge - page.Elements.Add(new PdfTextElement - { - X = 38, - Y = 260, - Width = 240, - Height = 18, - FontFamily = "Segoe UI", - FontSize = 11, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "🔬 Scientific & ", IsBold = true }, - new() { Text = "Math Scripts", IsBold = true, TextColorHex = "#0284C7" } - }, - ZIndex = 2 - }); - - // Chemical Formulas - var chemText = new PdfTextElement - { - X = 38, - Y = 286, - Width = 238, - Height = 50, - FontFamily = "Segoe UI", - FontSize = 9.5, - LineHeight = 1.4, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "• Chemical Reactions:\n " }, - new() { Text = "2H", IsBold = true }, - new() { Text = "2", Script = TextScriptMode.Subscript }, - new() { Text = " + O", IsBold = true }, - new() { Text = "2", Script = TextScriptMode.Subscript }, - new() { Text = " → 2H", IsBold = true }, - new() { Text = "2", Script = TextScriptMode.Subscript }, - new() { Text = "O (Combustion)\n Photosynthesis: 6CO", TextColorHex = "#0284C7" }, - new() { Text = "2", Script = TextScriptMode.Subscript, TextColorHex = "#0284C7" }, - new() { Text = " + 6H", TextColorHex = "#0284C7" }, - new() { Text = "2", Script = TextScriptMode.Subscript, TextColorHex = "#0284C7" }, - new() { Text = "O → C", TextColorHex = "#0284C7" }, - new() { Text = "6", Script = TextScriptMode.Subscript, TextColorHex = "#0284C7" }, - new() { Text = "H", TextColorHex = "#0284C7" }, - new() { Text = "12", Script = TextScriptMode.Subscript, TextColorHex = "#0284C7" }, - new() { Text = "O", TextColorHex = "#0284C7" }, - new() { Text = "6", Script = TextScriptMode.Subscript, TextColorHex = "#0284C7" } - }, - ZIndex = 2 - }; - chemText.SynchronizePlainTextFromSpans(); - page.Elements.Add(chemText); - - // Mathematical Physics Exponents - var mathText = new PdfTextElement - { - X = 38, - Y = 344, - Width = 238, - Height = 55, - FontFamily = "Segoe UI", - FontSize = 9.5, - LineHeight = 1.4, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "• Physics & Algebra:\n " }, - new() { Text = "E = mc", IsBold = true }, - new() { Text = "2", Script = TextScriptMode.Superscript, IsBold = true }, - new() { Text = " (Mass-Energy Equivalence)\n Pythagoras: " }, - new() { Text = "a", IsItalic = true }, - new() { Text = "2", Script = TextScriptMode.Superscript }, - new() { Text = " + " }, - new() { Text = "b", IsItalic = true }, - new() { Text = "2", Script = TextScriptMode.Superscript }, - new() { Text = " = " }, - new() { Text = "c", IsItalic = true }, - new() { Text = "2", Script = TextScriptMode.Superscript }, - new() { Text = "\n Ordinal Ranks: " }, - new() { Text = "1", IsBold = true }, - new() { Text = "st", Script = TextScriptMode.Superscript }, - new() { Text = ", 2", IsBold = true }, - new() { Text = "nd", Script = TextScriptMode.Superscript }, - new() { Text = ", 3", IsBold = true }, - new() { Text = "rd", Script = TextScriptMode.Superscript }, - new() { Text = ", and n", IsBold = true }, - new() { Text = "th", Script = TextScriptMode.Superscript }, - new() { Text = " degree." } - }, - ZIndex = 2 - }; - mathText.SynchronizePlainTextFromSpans(); - page.Elements.Add(mathText); - - // Right Card: Legal, Financial & Revision Highlights - page.Elements.Add(new PdfShapeElement - { - X = 306, - Y = 248, - Width = 265, - Height = 220, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - // Right Card Top Badge - page.Elements.Add(new PdfTextElement - { - X = 320, - Y = 260, - Width = 240, - Height = 18, - FontFamily = "Segoe UI", - FontSize = 11, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "⚖️ Legal & ", IsBold = true }, - new() { Text = "Contract Revisions", IsBold = true, TextColorHex = "#EA580C" } - }, - ZIndex = 2 - }); - - // Contract Clauses with Redactions & Revisions - var legalText = new PdfTextElement - { - X = 320, - Y = 286, - Width = 238, - Height = 110, - FontFamily = "Segoe UI", - FontSize = 9.5, - LineHeight = 1.42, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "• Clause 4.2 (Payment Terms):\n The total consideration of " }, - new() { Text = "$850,000.00", IsStrikethrough = true, TextColorHex = "#94A3B8" }, - new() { Text = " " }, - new() { Text = "$1,250,000.00 USD", IsBold = true, TextColorHex = "#16A34A" }, - new() { Text = " shall be payable within " }, - new() { Text = "30 calendar days", IsBold = true, IsUnderline = true }, - new() { Text = " from notice delivery.\n\n• Governing Jurisdiction:\n All arbitration shall proceed in " }, - new() { Text = "London, UK", IsBold = true, TextColorHex = "#0F6CBD" }, - new() { Text = " under LCIA rules." } - }, - ZIndex = 2 - }; - legalText.SynchronizePlainTextFromSpans(); - page.Elements.Add(legalText); - - // Hyperlink Reference - var linkText = new PdfTextElement - { - X = 320, - Y = 412, - Width = 238, - Height = 35, - FontFamily = "Segoe UI", - FontSize = 9.5, - LineHeight = 1.35, - Spans = new List - { - new() { Text = "🔗 Full Document: " }, - new() { Text = "frypdf.dev/legal/terms", IsUnderline = true, TextColorHex = "#0F6CBD", LinkUrl = "https://frypdf.dev" } - }, - ZIndex = 2 - }; - linkText.SynchronizePlainTextFromSpans(); - page.Elements.Add(linkText); - - // ========================================== - // 4. SECTION C: IN-PLACE MARKDOWN SYNTAX GUIDE - // ========================================== - - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 480, - Width = 547.28, - Height = 175, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#F1F5F9", - StrokeColorHex = "#CBD5E1", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - var guideHeading = new PdfTextElement - { - X = 40, - Y = 494, - Width = 450, - Height = 20, - FontFamily = "Segoe UI", - FontSize = 11.5, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "⌨️ Live Canvas ", IsBold = true }, - new() { Text = "Markdown & Tag Syntax Guide", IsBold = true, TextColorHex = "#0F6CBD" } - }, - ZIndex = 2 - }; - guideHeading.SynchronizePlainTextFromSpans(); - page.Elements.Add(guideHeading); - - var guideCol1 = new PdfTextElement - { - X = 40, - Y = 520, - Width = 245, - Height = 120, - FontFamily = "Consolas", - FontSize = 9.0, - LineHeight = 1.45, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "**Bold Text** → ", TextColorHex = "#64748B" }, - new() { Text = "Bold Text\n", IsBold = true }, - new() { Text = "*Italic Text* → ", TextColorHex = "#64748B" }, - new() { Text = "Italic Text\n", IsItalic = true }, - new() { Text = "Underline → ", TextColorHex = "#64748B" }, - new() { Text = "Underline\n", IsUnderline = true }, - new() { Text = "~~Strikethrough~~ → ", TextColorHex = "#64748B" }, - new() { Text = "Strikethrough", IsStrikethrough = true } - }, - ZIndex = 2 - }; - guideCol1.SynchronizePlainTextFromSpans(); - page.Elements.Add(guideCol1); - - var guideCol2 = new PdfTextElement - { - X = 300, - Y = 520, - Width = 260, - Height = 120, - FontFamily = "Consolas", - FontSize = 9.0, - LineHeight = 1.45, - TextColorHex = "#0F172A", - Spans = new List - { - new() { Text = "Formula: H~2~O → ", TextColorHex = "#64748B" }, - new() { Text = "H" }, - new() { Text = "2", Script = TextScriptMode.Subscript }, - new() { Text = "O\n" }, - new() { Text = "Exponent: E=mc^2^ → ", TextColorHex = "#64748B" }, - new() { Text = "E=mc" }, - new() { Text = "2\n", Script = TextScriptMode.Superscript }, - new() { Text = "Red → ", TextColorHex = "#64748B" }, - new() { Text = "Red\n", IsBold = true, TextColorHex = "#DC2626" }, - new() { Text = "[Link](https://...) → ", TextColorHex = "#64748B" }, - new() { Text = "Link", IsUnderline = true, TextColorHex = "#0F6CBD" } - }, - ZIndex = 2 - }; - guideCol2.SynchronizePlainTextFromSpans(); - page.Elements.Add(guideCol2); - - // ========================================== - // 5. SECTION D: KEY METRICS ROW & FOOTER - // ========================================== - - // Metric 1: 100% Lossless Roundtrip - page.Elements.Add(new PdfShapeElement - { - X = 24, - Y = 668, - Width = 175, - Height = 65, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - page.Elements.Add(new PdfTextElement - { - X = 34, - Y = 678, - Width = 155, - Height = 45, - FontFamily = "Segoe UI", - FontSize = 9.0, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "100% Lossless\n", IsBold = true, FontSize = 12.0, TextColorHex = "#0F6CBD" }, - new() { Text = "JSON & PDF roundtrip fidelity" } - }, - ZIndex = 2 - }); - - // Metric 2: QuestPDF Native - page.Elements.Add(new PdfShapeElement - { - X = 210, - Y = 668, - Width = 175, - Height = 65, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - page.Elements.Add(new PdfTextElement - { - X = 220, - Y = 678, - Width = 155, - Height = 45, - FontFamily = "Segoe UI", - FontSize = 9.0, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "QuestPDF Native\n", IsBold = true, FontSize = 12.0, TextColorHex = "#059669" }, - new() { Text = "Zero rasterization vector export" } - }, - ZIndex = 2 - }); - - // Metric 3: Deconstruction Clean - page.Elements.Add(new PdfShapeElement - { - X = 396, - Y = 668, - Width = 175, - Height = 65, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 6, - FillColorHex = "#FFFFFF", - StrokeColorHex = "#E2E8F0", - StrokeThickness = 1.0, - ZIndex = 0 - }); - - page.Elements.Add(new PdfTextElement - { - X = 406, - Y = 678, - Width = 155, - Height = 45, - FontFamily = "Segoe UI", - FontSize = 9.0, - TextColorHex = "#334155", - Spans = new List - { - new() { Text = "No Fragmenting\n", IsBold = true, FontSize = 12.0, TextColorHex = "#7C3AED" }, - new() { Text = "Merged baseline paragraphs" } - }, - ZIndex = 2 - }); - - // Bottom Decorative Wave Divider - page.Elements.Add(new PdfDividerElement - { - X = 24, - Y = 746, - Width = 547.28, - Height = 12, - Style = DividerStyle.Wave, - ColorHex = "#0F6CBD", - Thickness = 1.5, - ZIndex = 1 - }); - - // Bottom Publication Note - var pubNote = new PdfTextElement - { - X = 24, - Y = 764, - Width = 547.28, - Height = 22, - Alignment = TextAlignmentMode.Center, - FontFamily = "Segoe UI", - FontSize = 9.0, - TextColorHex = "#64748B", - Spans = new List - { - new() { Text = "Designed with " }, - new() { Text = "FryPDF Studio", IsBold = true, TextColorHex = "#0F6CBD" }, - new() { Text = " • Cross-Platform High Performance Document Suite" } - }, - ZIndex = 2 - }; - pubNote.SynchronizePlainTextFromSpans(); - page.Elements.Add(pubNote); - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/Templates/ITemplateDefinition.cs b/src/PdfEditorApp/Templates/ITemplateDefinition.cs deleted file mode 100644 index e9703fa..0000000 --- a/src/PdfEditorApp/Templates/ITemplateDefinition.cs +++ /dev/null @@ -1,10 +0,0 @@ -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.Templates; - -/// -/// Legacy interface for templates, inheriting from the plugin-extensible . -/// -public interface ITemplateDefinition : ITemplateDescriptor -{ -} diff --git a/src/PdfEditorApp/Templates/Technical/ArduinoCheatSheetTemplate.cs b/src/PdfEditorApp/Templates/Technical/ArduinoCheatSheetTemplate.cs deleted file mode 100644 index 4132ff8..0000000 --- a/src/PdfEditorApp/Templates/Technical/ArduinoCheatSheetTemplate.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; - -namespace PdfEditorApp.Templates.Technical; - -public class ArduinoCheatSheetTemplate : ITemplateDefinition -{ - public string Id => "arduino_cheatsheet"; - public string Name => "Technical Cheat Sheet (Embedded C & Arduino)"; - public string Description => "High-density developer reference poster featuring multi-column code blocks, control structures, pinout tables, and electrical ratings"; - public string Category => "Cheat Sheets"; - public string IconKind => "Chip"; - public string AccentColorHex => "#0D9488"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel - { - Title = "Arduino_Developer_Reference_CheatSheet.pdf", - Author = "Embedded Engineering Systems", - Subject = "Technical Reference Poster: Arduino Language, Hardware Architecture & Pinout Map" - }; - - // Standard Landscape Blueprint Canvas (1920 x 1080 pt widescreen or 1600 x 1131 pt) - // We use 1600 x 1131 pt for clean viewing on all desktop screens - var page = new PdfPageModel - { - PageNumber = 1, - Format = PageFormat.Poster, - Orientation = PageOrientation.Landscape, - Width = 1600, - Height = 1131, - BackgroundColorHex = "#0F172A", // Sleek dark blueprint theme - HeaderLeft = "Embedded Systems Technical Reference Series", - HeaderCenter = "Arduino & Microcontroller Architecture Reference", - HeaderRight = "Rev 2.6 • Hardware Spec", - FooterLeft = "Embedded C/C++ Reference Cheat Sheet • Open-Hardware Reference", - FooterCenter = "Compatible with ATmega328P, AVR, RP2040 & ESP32 Platforms", - FooterRight = "Page 1 of 1", - Elements = new List - { - // Top Header Brand Band - new PdfShapeElement - { - X = 0, Y = 0, Width = 1600, Height = 10, - FillColorHex = "#0D9488", StrokeColorHex = "#00000000" - }, - - // Main Header Banner - new PdfShapeElement - { - X = 30, Y = 25, Width = 1540, Height = 75, - CornerRadius = 8, FillColorHex = "#1E293B", - StrokeColorHex = "#334155", StrokeThickness = 1.5 - }, - new PdfTextElement - { - X = 50, Y = 36, Width = 1000, Height = 30, - Text = "ARDUINO / EMBEDDED C++ QUICK REFERENCE CHEAT SHEET", - FontSize = 18, IsBold = true, TextColorHex = "#2DD4BF", - FontFamily = "Inter" - }, - new PdfTextElement - { - X = 50, Y = 68, Width = 1000, Height = 22, - Text = "Language Syntax • Hardware Timers • Serial Comm • Digital/Analog I/O • Memory & Pinout Architecture", - FontSize = 11, TextColorHex = "#94A3B8", - FontFamily = "Inter" - }, - new PdfShapeElement - { - X = 1350, Y = 38, Width = 200, Height = 48, - CornerRadius = 6, FillColorHex = "#0F766E", - StrokeColorHex = "#14B8A6", StrokeThickness = 1 - }, - new PdfTextElement - { - X = 1355, Y = 52, Width = 190, Height = 20, - Text = "PLATFORM: ATmega328P", - FontSize = 11, IsBold = true, TextColorHex = "#FFFFFF", - Alignment = TextAlignmentMode.Center, - FontFamily = "Inter" - }, - - // ========================================================================= - // COLUMN 1: STRUCTURE & FLOW (X: 30, W: 365) - // ========================================================================= - // Card 1.1: Program Structure - new PdfShapeElement { X = 30, Y = 115, Width = 365, Height = 220, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 30, Y = 115, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#0D9488", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 45, Y = 122, Width = 335, Height = 20, Text = "1. Basic Program Structure", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 45, Y = 152, Width = 335, Height = 175, - Text = "void setup() {\n // Runs once at power-up or reset\n pinMode(LED_BUILTIN, OUTPUT);\n Serial.begin(115200);\n}\n\nvoid loop() {\n // Runs repeatedly and continuously\n digitalWrite(LED_BUILTIN, HIGH);\n delay(500);\n digitalWrite(LED_BUILTIN, LOW);\n delay(500);\n}", - FontSize = 10, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // Card 1.2: Control Flow & Loops - new PdfShapeElement { X = 30, Y = 345, Width = 365, Height = 290, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 30, Y = 345, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#0D9488", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 45, Y = 352, Width = 335, Height = 20, Text = "2. Control Structures & Branching", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 45, Y = 382, Width = 335, Height = 245, - Text = "if (val > 100) {\n // condition true\n} else if (val == 50) {\n // secondary branch\n} else {\n // default fallback\n}\n\nfor (int i = 0; i < 10; i++) {\n // counted loop\n}\n\nwhile (condition) {\n // while true\n}\n\ndo { ... } while (condition);\nswitch(var) { case 1: break; default: break; }", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // Card 1.3: Data Types & RAM Footprint - new PdfShapeElement { X = 30, Y = 645, Width = 365, Height = 340, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 30, Y = 645, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#0D9488", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 45, Y = 652, Width = 335, Height = 20, Text = "3. Variable Types & Memory", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 45, Y = 682, Width = 335, Height = 295, - Text = "boolean 1 bit true / false\nbyte 8 bits 0 to 255 (unsigned char)\nchar 8 bits -128 to 127 ('A')\nint 16 bits -32,768 to 32,767\nunsigned 16 bits 0 to 65,535\nword 16 bits same as unsigned int\nlong 32 bits -2,147,483,648 to 2,147,483,647\nu_long 32 bits 0 to 4,294,967,295\nfloat 32 bits 3.4028235E+38 (6-7 digits precision)\ndouble 32 bits identical to float on AVR\nconst qualifier marks variable read-only\nvolatile qualifier prevents compiler optimization", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // ========================================================================= - // COLUMN 2: DIGITAL & ANALOG I/O (X: 420, W: 365) - // ========================================================================= - // Card 2.1: Digital I/O - new PdfShapeElement { X = 420, Y = 115, Width = 365, Height = 230, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 420, Y = 115, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#2563EB", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 435, Y = 122, Width = 335, Height = 20, Text = "4. Digital I/O Operations", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 435, Y = 152, Width = 335, Height = 185, - Text = "pinMode(pin, mode);\n // mode: INPUT, OUTPUT, INPUT_PULLUP\n\ndigitalWrite(pin, value);\n // value: HIGH (5V/3.3V), LOW (0V)\n\nint state = digitalRead(pin);\n // returns HIGH or LOW\n\n// Fast direct port manipulation:\nPORTB |= (1 << PB5); // High Pin 13\nPORTB &= ~(1 << PB5); // Low Pin 13", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // Card 2.2: Analog I/O & PWM - new PdfShapeElement { X = 420, Y = 355, Width = 365, Height = 250, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 420, Y = 355, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#2563EB", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 435, Y = 362, Width = 335, Height = 20, Text = "5. Analog I/O & PWM Duty", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 435, Y = 392, Width = 335, Height = 205, - Text = "analogReference(type);\n // DEFAULT, INTERNAL, EXTERNAL\n\nint raw = analogRead(A0);\n // 10-bit ADC returns 0 to 1023 (0 to 5V)\n // Voltage = raw * (5.0 / 1023.0);\n\nanalogWrite(pin, duty);\n // 8-bit PWM (0 to 255) at 490Hz / 980Hz\n // PWM Pins: 3, 5, 6, 9, 10, 11 on Uno", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // Card 2.3: Timing & Delays - new PdfShapeElement { X = 420, Y = 615, Width = 365, Height = 175, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 420, Y = 615, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#2563EB", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 435, Y = 622, Width = 335, Height = 20, Text = "6. Timing & Scheduling", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 435, Y = 652, Width = 335, Height = 130, - Text = "unsigned long ms = millis();\n // Milliseconds since startup (overflow ~50 days)\n\nunsigned long us = micros();\n // Microseconds since startup (overflow ~70 mins)\n\ndelay(ms); // Blocking delay in milliseconds\ndelayMicroseconds(us); // Busy-wait in microseconds", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // Card 2.4: Math & Bitwise - new PdfShapeElement { X = 420, Y = 800, Width = 365, Height = 185, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 420, Y = 800, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#2563EB", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 435, Y = 807, Width = 335, Height = 20, Text = "7. Math & Mapping Utilities", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 435, Y = 837, Width = 335, Height = 140, - Text = "map(val, fromLow, fromHigh, toLow, toHigh);\nconstrain(x, a, b); // clips x between a and b\nmin(x, y); max(x, y); abs(x); sq(x); sqrt(x);\npow(base, exponent); sin(rad); cos(rad); tan(rad);\nbitRead(x, n); bitSet(x, n); bitClear(x, n);\nbitWrite(x, n, b); bit(n); // (1 << n)", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // ========================================================================= - // COLUMN 3: SERIAL & BUS PROTOCOLS (X: 810, W: 365) - // ========================================================================= - // Card 3.1: Hardware UART Serial - new PdfShapeElement { X = 810, Y = 115, Width = 365, Height = 250, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 810, Y = 115, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#D97706", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 825, Y = 122, Width = 335, Height = 20, Text = "8. Hardware UART Serial", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 825, Y = 152, Width = 335, Height = 205, - Text = "Serial.begin(baud);\n // Typical baud: 9600, 115200, 230400\n\nwhile (!Serial); // Wait for USB on Leonardo/RP2040\nSerial.print(\"Value: \");\nSerial.println(val, HEX); // BIN, DEC, HEX, OCT\n\nif (Serial.available() > 0) {\n int byteIn = Serial.read();\n String str = Serial.readStringUntil('\\n');\n}\nSerial.flush(); // Waits for transmission complete", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // Card 3.2: I2C & SPI Communication - new PdfShapeElement { X = 810, Y = 375, Width = 365, Height = 270, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 810, Y = 375, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#D97706", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 825, Y = 382, Width = 335, Height = 20, Text = "9. I2C (Wire) & SPI Protocols", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 825, Y = 412, Width = 335, Height = 225, - Text = "#include \nWire.begin(); // Join as Master\nWire.beginTransmission(0x68); // Device address\nWire.write(0x3B); // Register address\nWire.endTransmission();\nWire.requestFrom(0x68, 6);\nwhile (Wire.available()) { byte c = Wire.read(); }\n\n#include \nSPI.begin();\nSPI.beginTransaction(SPISettings(14000000, MSBFIRST, SPI_MODE0));\ndigitalWrite(SS, LOW);\nbyte response = SPI.transfer(0xAA);\ndigitalWrite(SS, HIGH);\nSPI.endTransaction();", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // Card 3.3: External Hardware Interrupts - new PdfShapeElement { X = 810, Y = 655, Width = 365, Height = 330, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 810, Y = 655, Width = 365, Height = 30, CornerRadius = 6, FillColorHex = "#D97706", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 825, Y = 662, Width = 335, Height = 20, Text = "10. External Interrupts (ISR)", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 825, Y = 692, Width = 335, Height = 285, - Text = "attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);\n\nModes:\n LOW: Trigger whenever pin is LOW\n CHANGE: Trigger on any state transition\n RISING: Trigger on LOW to HIGH\n FALLING: Trigger on HIGH to LOW\n\nISR Rules:\n - Keep execution ultra-short (no delay() or Serial)\n - All shared variables MUST be marked 'volatile'\n\nvolatile int counter = 0;\nvoid myISR() {\n counter++;\n}\n\ndetachInterrupt(digitalPinToInterrupt(pin));", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - }, - - // ========================================================================= - // COLUMN 4: PINOUT MATRIX & ELECTRICAL RATINGS (X: 1200, W: 370) - // ========================================================================= - // Card 4.1: Pinout Table - new PdfShapeElement { X = 1200, Y = 115, Width = 370, Height = 580, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 1200, Y = 115, Width = 370, Height = 30, CornerRadius = 6, FillColorHex = "#9333EA", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 1215, Y = 122, Width = 340, Height = 20, Text = "11. Hardware Pinout & Port Map", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - - new PdfTableElement - { - X = 1205, Y = 155, Width = 360, Height = 530, - HeaderBackgroundHex = "#6B21A8", - HeaderTextHex = "#FFFFFF", - BorderColorHex = "#334155", - AlternateRowBackgroundHex = "#0F172A", - Headers = new List { "Pin", "Port", "Function", "PWM", "Notes" }, - Rows = new List> - { - new() { "D0", "PD0", "RX", "-", "Hardware Serial In" }, - new() { "D1", "PD1", "TX", "-", "Hardware Serial Out" }, - new() { "D2", "PD2", "INT0", "-", "External Interrupt 0" }, - new() { "D3", "PD3", "INT1", "Yes", "Ext Int 1 / 490Hz" }, - new() { "D4", "PD4", "T0", "-", "Timer 0 ext clock" }, - new() { "D5", "PD5", "T1", "Yes", "Timer 1 / 980Hz" }, - new() { "D6", "PD6", "AIN0", "Yes", "Analog comp / 980Hz" }, - new() { "D7", "PD7", "AIN1", "-", "Analog comparator" }, - new() { "D8", "PB0", "ICP1", "-", "Timer 1 input capture" }, - new() { "D9", "PB1", "OC1A", "Yes", "Timer 1 / 490Hz" }, - new() { "D10", "PB2", "SS", "Yes", "SPI Chip Select" }, - new() { "D11", "PB3", "MOSI", "Yes", "SPI Master Out" }, - new() { "D12", "PB4", "MISO", "-", "SPI Master In" }, - new() { "D13", "PB5", "SCK", "-", "SPI Clock / Built-in LED" }, - new() { "A0", "PC0", "ADC0", "-", "Analog input 0" }, - new() { "A1", "PC1", "ADC1", "-", "Analog input 1" }, - new() { "A2", "PC2", "ADC2", "-", "Analog input 2" }, - new() { "A3", "PC3", "ADC3", "-", "Analog input 3" }, - new() { "A4", "PC4", "SDA", "-", "I2C Data line" }, - new() { "A5", "PC5", "SCL", "-", "I2C Clock line" } - } - }, - - // Card 4.2: Electrical Ratings - new PdfShapeElement { X = 1200, Y = 705, Width = 370, Height = 280, CornerRadius = 6, FillColorHex = "#1E293B", StrokeColorHex = "#334155", StrokeThickness = 1 }, - new PdfShapeElement { X = 1200, Y = 705, Width = 370, Height = 30, CornerRadius = 6, FillColorHex = "#DC2626", StrokeColorHex = "#00000000" }, - new PdfTextElement { X = 1215, Y = 712, Width = 340, Height = 20, Text = "12. Electrical Limits & Ratings", FontSize = 12, IsBold = true, TextColorHex = "#FFFFFF" }, - new PdfTextElement - { - X = 1215, Y = 742, Width = 340, Height = 235, - Text = "Operating Voltage: 5.0V\nInput Voltage (VIN): 7.0V - 12.0V (Limit: 6-20V)\nMax DC Current per I/O: 40 mA (Safe: 20 mA)\nTotal Microcontroller DC: 200 mA (Absolute Max VCC/GND)\n3.3V Pin Current Limit: 50 mA (From onboard LDO)\nFlash Memory: 32 KB (0.5 KB bootloader)\nSRAM: 2 KB (Static RAM)\nEEPROM: 1 KB (Internal non-volatile)\nClock Speed: 16 MHz Quartz Crystal\nADC Resolution: 10-bit (1024 discrete steps)", - FontSize = 9.5, TextColorHex = "#E2E8F0", - FontFamily = "RobotoMono" - } - } - }; - - doc.Pages.Add(page); - return doc; - } -} diff --git a/src/PdfEditorApp/ViewLocator.cs b/src/PdfEditorApp/ViewLocator.cs deleted file mode 100644 index 4a975f5..0000000 --- a/src/PdfEditorApp/ViewLocator.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp; - -/// -/// Given a view model, returns the corresponding view if possible. -/// -[RequiresUnreferencedCode( - "Default implementation of ViewLocator involves reflection which may be trimmed away.", - Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] -public class ViewLocator : IDataTemplate -{ - public Control? Build(object? param) - { - if (param is null) - return null; - - var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - var type = Type.GetType(name); - - if (type != null) - { - return (Control)Activator.CreateInstance(type)!; - } - - return new TextBlock { Text = "Not Found: " + name }; - } - - public bool Match(object? data) - { - return data is ViewModelBase; - } -} diff --git a/src/PdfEditorApp/ViewModels/AiAssistantViewModel.cs b/src/PdfEditorApp/ViewModels/AiAssistantViewModel.cs deleted file mode 100644 index c544269..0000000 --- a/src/PdfEditorApp/ViewModels/AiAssistantViewModel.cs +++ /dev/null @@ -1,889 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.AI; - -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels; - -/// -/// Defines the active scope targeted by the AI Studio Assistant. -/// -public enum AiTargetKind -{ - Element, - EntirePage, - PointOnPage -} - -/// -/// ViewModel controlling the interactive AI Studio Assistant dialog/panel. -/// Powers natural language prompting, in-place element modifications, live tool invocation tracking, and atomic canvas undo. -/// -public partial class AiAssistantViewModel : ViewModelBase -{ - private readonly IPdfStudioAgentService _agentService; - private readonly IUiSettingsService _uiSettingsService; - private readonly IAiService _aiService; - private CancellationTokenSource? _generationCts; - - public Func? GetCurrentPage { get; set; } - public Func? GetSelectedElement { get; set; } - public IUndoRedoService? UndoRedo { get; set; } - public Action? RequestOpenSettings { get; set; } - public Action? RequestReturnToThumbnails { get; set; } - - [ObservableProperty] - private bool _isOpen; - - [ObservableProperty] - private string _promptText = string.Empty; - - [ObservableProperty] - private bool _isGenerating; - - [ObservableProperty] - private string _statusMessage = "Ready to generate document elements"; - - [ObservableProperty] - private string _lastGenerationSummary = string.Empty; - - [ObservableProperty] - private bool _canUndoLastGeneration; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasTargetElement))] - [NotifyPropertyChangedFor(nameof(HasTargetPoint))] - [NotifyPropertyChangedFor(nameof(IsPointTargetMode))] - [NotifyPropertyChangedFor(nameof(IsPageTargetMode))] - [NotifyPropertyChangedFor(nameof(TargetHeaderBadge))] - [NotifyPropertyChangedFor(nameof(TargetDescription))] - private AiTargetKind _targetKind = AiTargetKind.EntirePage; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(TargetPointX))] - [NotifyPropertyChangedFor(nameof(TargetPointY))] - [NotifyPropertyChangedFor(nameof(HasTargetPoint))] - [NotifyPropertyChangedFor(nameof(TargetPointDisplay))] - [NotifyPropertyChangedFor(nameof(TargetHeaderBadge))] - [NotifyPropertyChangedFor(nameof(TargetDescription))] - private (double X, double Y)? _targetPoint; - - public double TargetPointX => TargetPoint?.X ?? 0; - public double TargetPointY => TargetPoint?.Y ?? 0; - public bool HasTargetPoint => TargetPoint != null; - public bool IsPointTargetMode => TargetKind == AiTargetKind.PointOnPage; - public bool IsPageTargetMode => TargetKind == AiTargetKind.EntirePage; - public string TargetPointDisplay => TargetPoint.HasValue ? $"X: {TargetPoint.Value.X:0}, Y: {TargetPoint.Value.Y:0}" : ""; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(TargetPageNumber))] - [NotifyPropertyChangedFor(nameof(TargetPageDimensions))] - [NotifyPropertyChangedFor(nameof(TargetHeaderBadge))] - [NotifyPropertyChangedFor(nameof(TargetDescription))] - private PageViewModel? _targetPage; - - public int TargetPageNumber => TargetPage?.PageNumber ?? GetCurrentPage?.Invoke()?.PageNumber ?? 1; - public string TargetPageDimensions - { - get - { - var p = TargetPage ?? GetCurrentPage?.Invoke(); - return p != null ? $"{p.Width:0} × {p.Height:0} pt" : "800 × 1131 pt"; - } - } - - public string TargetHeaderBadge => TargetKind switch - { - AiTargetKind.Element when TargetElement != null => TargetElementKindBadge, - AiTargetKind.PointOnPage when TargetPoint.HasValue => $"Point ({TargetPointDisplay})", - _ => $"Page {TargetPageNumber}" - }; - - public string TargetDescription => TargetKind switch - { - AiTargetKind.Element when TargetElement != null => TargetElementTitle, - AiTargetKind.PointOnPage when TargetPoint.HasValue => $"Point at ({TargetPointDisplay}) on Page {TargetPageNumber}", - _ => $"Page {TargetPageNumber} ({TargetPageDimensions})" - }; - - public string TargetIcon => TargetKind switch - { - AiTargetKind.Element => TargetElementKindIcon, - AiTargetKind.PointOnPage => "Target", - _ => "FileDocumentOutline" - }; - - [ObservableProperty] - private ElementViewModelBase? _targetElement; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasTargetElement))] - private bool _isModifyMode; - - [ObservableProperty] - private string _targetElementTitle = string.Empty; - - [ObservableProperty] - private string _targetElementKindBadge = string.Empty; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(TargetIcon))] - private string _targetElementKindIcon = "AutoFixHigh"; - - public bool HasTargetElement => TargetElement != null; - - [ObservableProperty] - private string _activeModelDisplayName = "Llama 3.2"; - - [ObservableProperty] - private string _activeModelTierBadge = "Free / Local"; - - [ObservableProperty] - private AiModelTier _activeModelTier = AiModelTier.FreeLocal; - - [ObservableProperty] - private bool _isActiveModelFree = true; - - public bool IsActiveModelFreeLocal => ActiveModelTier == AiModelTier.FreeLocal; - public bool IsActiveModelFreeCloud => ActiveModelTier == AiModelTier.FreeCloud; - public bool IsActiveModelPaidCloud => ActiveModelTier == AiModelTier.PaidCloud; - - public ObservableCollection Sessions { get; } = new(); - - [ObservableProperty] - private AiChatSession? _currentSession; - - [ObservableProperty] - private bool _isHistoryDrawerOpen; - - public ObservableCollection ActivityLogs { get; } = new(); - public ObservableCollection SuggestedPrompts { get; } = new(); - - public AiAssistantViewModel( - IPdfStudioAgentService agentService, - IUiSettingsService uiSettingsService, - IAiService aiService) - { - _agentService = agentService ?? throw new ArgumentNullException(nameof(agentService)); - _uiSettingsService = uiSettingsService ?? throw new ArgumentNullException(nameof(uiSettingsService)); - _aiService = aiService ?? throw new ArgumentNullException(nameof(aiService)); - - EnsureActiveSession(); - PopulateDefaultSuggestions(); - UpdateActiveModelDisplay(); - - _uiSettingsService.SettingsChanged += _ => UpdateActiveModelDisplay(); - } - - public void PopulateDefaultSuggestions() - { - PopulateEntirePageSuggestions(); - } - - public void PopulateEntirePageSuggestions() - { - SuggestedPrompts.Clear(); - SuggestedPrompts.Add("Add a modern corporate invoice header with company name Acme Corp, invoice #1001, date today, and a blue accent bar"); - SuggestedPrompts.Add("Design a modern corporate invoice layout with header, 4-row itemized table, and payment summary"); - SuggestedPrompts.Add("Format this entire page as an elegant certificate with Georgia serif title and gold border"); - SuggestedPrompts.Add("Create a balanced two-column report page with executive summary callout card and statistics table"); - SuggestedPrompts.Add("Add a branded header with logo badge, full-width accent divider, and confidentiality footer"); - SuggestedPrompts.Add("Clean up page margins and organize all sections with consistent 20pt vertical spacing"); - } - - public void PopulatePointSuggestions() - { - SuggestedPrompts.Clear(); - SuggestedPrompts.Add("Insert a modern section heading and subheader here"); - SuggestedPrompts.Add("Add a 4-column financial data table starting at this point"); - SuggestedPrompts.Add("Insert an amber callout card with important notice title and description here"); - SuggestedPrompts.Add("Add a signature block with sign line, date, and verified badge here"); - SuggestedPrompts.Add("Insert a scannable QR code and payment pill badge here"); - SuggestedPrompts.Add("Add an elegant floral divider ornament at this position"); - } - - public void UpdateActiveModelDisplay() - { - var settings = _uiSettingsService.Settings.AiSettings; - var catalog = _aiService.GetUnifiedModelCatalog(settings); - var match = catalog.FirstOrDefault(m => m.Id.Equals(settings.SelectedModelId, StringComparison.OrdinalIgnoreCase)) - ?? catalog.FirstOrDefault(); - - if (match != null) - { - ActiveModelDisplayName = match.DisplayName; - ActiveModelTier = match.Tier; - ActiveModelTierBadge = match.TierBadgeText; - IsActiveModelFree = match.IsFree; - } - else - { - ActiveModelDisplayName = settings.SelectedModelId; - bool isOllama = settings.SelectedProvider == AiProviderType.OllamaLocal; - bool isRemote = isOllama && settings.IsOllamaRemote; - ActiveModelTier = isOllama ? (isRemote ? AiModelTier.FreeCloud : AiModelTier.FreeLocal) : AiModelTier.PaidCloud; - ActiveModelTierBadge = ActiveModelTier switch - { - AiModelTier.FreeLocal => "Free / Local", - AiModelTier.FreeCloud => "Free / Cloud", - _ => "Paid / Cloud" - }; - IsActiveModelFree = isOllama; - } - - OnPropertyChanged(nameof(IsActiveModelFreeLocal)); - OnPropertyChanged(nameof(IsActiveModelFreeCloud)); - OnPropertyChanged(nameof(IsActiveModelPaidCloud)); - } - - [RelayCommand] - public void Open() - { - UpdateActiveModelDisplay(); - var selected = GetSelectedElement?.Invoke(); - if (selected != null) - { - OpenForElement(selected); - return; - } - - if (TargetPoint.HasValue) - { - TargetKind = AiTargetKind.PointOnPage; - PopulatePointSuggestions(); - } - else - { - TargetEntirePage(GetCurrentPage?.Invoke()); - } - IsOpen = true; - } - - public void OpenForElement(ElementViewModelBase element) - { - UpdateActiveModelDisplay(); - TargetElement = element; - TargetPoint = null; - TargetKind = AiTargetKind.Element; - IsModifyMode = true; - TargetElementTitle = GetElementTitle(element); - TargetElementKindBadge = element.Kind.ToString(); - TargetElementKindIcon = GetElementIcon(element); - PopulateElementSuggestions(element); - IsOpen = true; - } - - public void UpdateTargetElement(ElementViewModelBase? element) - { - if (element != null) - { - TargetElement = element; - TargetPoint = null; - TargetKind = AiTargetKind.Element; - IsModifyMode = true; - TargetElementTitle = GetElementTitle(element); - TargetElementKindBadge = element.Kind.ToString(); - TargetElementKindIcon = GetElementIcon(element); - PopulateElementSuggestions(element); - } - else - { - TargetElement = null; - IsModifyMode = false; - if (TargetKind == AiTargetKind.Element) - { - if (TargetPoint.HasValue) - { - TargetKind = AiTargetKind.PointOnPage; - PopulatePointSuggestions(); - } - else - { - TargetEntirePage(GetCurrentPage?.Invoke()); - } - } - } - } - - [RelayCommand] - public void TargetEntirePage(PageViewModel? page = null) - { - TargetPage = page ?? GetCurrentPage?.Invoke(); - TargetPoint = null; - TargetElement = null; - IsModifyMode = false; - TargetKind = AiTargetKind.EntirePage; - PopulateEntirePageSuggestions(); - StatusMessage = $"Targeted Entire Page {TargetPageNumber} ({TargetPageDimensions})"; - } - - public void TargetPointOnPage(double x, double y, PageViewModel? page = null) - { - TargetPage = page ?? GetCurrentPage?.Invoke(); - TargetPoint = (Math.Max(0, x), Math.Max(0, y)); - TargetElement = null; - IsModifyMode = false; - TargetKind = AiTargetKind.PointOnPage; - PopulatePointSuggestions(); - StatusMessage = $"Targeted Point at ({TargetPointDisplay}) on Page {TargetPageNumber}"; - IsOpen = true; - } - - [RelayCommand] - public void ClearTargetPoint() - { - TargetEntirePage(); - } - - [RelayCommand] - public void SwitchToEntirePageMode() - { - TargetEntirePage(); - } - - [RelayCommand] - public void SwitchToPointTargetMode() - { - var page = TargetPage ?? GetCurrentPage?.Invoke(); - if (!TargetPoint.HasValue) - { - double cx = (page?.Width ?? 800) / 2.0; - double cy = 250.0; - TargetPointOnPage(cx, cy, page); - } - else - { - TargetKind = AiTargetKind.PointOnPage; - PopulatePointSuggestions(); - } - } - - [RelayCommand] - public void SwitchToModifyMode() - { - if (TargetElement != null) - { - TargetKind = AiTargetKind.Element; - IsModifyMode = true; - PopulateElementSuggestions(TargetElement); - } - } - - [RelayCommand] - public void SwitchToCreateMode() - { - IsModifyMode = false; - if (TargetPoint.HasValue) - { - TargetKind = AiTargetKind.PointOnPage; - PopulatePointSuggestions(); - } - else - { - TargetKind = AiTargetKind.EntirePage; - PopulateDefaultSuggestions(); - } - } - - [RelayCommand] - public void DeselectElementTarget() - { - TargetElement = null; - SwitchToCreateMode(); - } - - private static string GetElementTitle(ElementViewModelBase element) - { - if (element is ChartElementViewModel chart) - { - return string.IsNullOrWhiteSpace(chart.Title) ? "Untitled Chart" : chart.Title; - } - if (element is TextElementViewModel text) - { - string t = text.Text?.Replace("\r", " ").Replace("\n", " ").Trim() ?? ""; - return string.IsNullOrWhiteSpace(t) ? "Text Element" : (t.Length <= 35 ? t : t[..35] + "..."); - } - if (element is TableElementViewModel table) - { - return $"Table ({table.Rows.Count} rows, {table.Headers.Count} cols)"; - } - if (element is ShapeElementViewModel shape) - { - return $"{shape.ShapeType} Shape"; - } - if (element is ImageElementViewModel img) - { - return string.IsNullOrWhiteSpace(img.AltText) ? "Image Asset" : img.AltText; - } - if (element is MathElementViewModel math) - { - if (!string.IsNullOrWhiteSpace(math.PresetName)) - { - return math.PresetName; - } - string f = math.Formula?.Trim() ?? ""; - return string.IsNullOrWhiteSpace(f) ? "Math Formula" : (f.Length <= 35 ? f : f[..35] + "..."); - } - if (element is SvgElementViewModel) - { - return "Vector SVG Ornament"; - } - if (element is DividerElementViewModel) - { - return "Divider Line"; - } - if (element is FormFieldElementViewModel form) - { - return string.IsNullOrWhiteSpace(form.Label) ? "Form Field" : $"Field: {form.Label}"; - } - if (element is QrCodeElementViewModel qr) - { - return string.IsNullOrWhiteSpace(qr.Content) ? "QR Code" : $"QR: {qr.Content}"; - } - if (element is BarcodeElementViewModel barcode) - { - return string.IsNullOrWhiteSpace(barcode.CodeValue) ? "Barcode" : $"Barcode: {barcode.CodeValue}"; - } - if (element is StickyNoteElementViewModel note) - { - string nt = note.NoteText?.Trim() ?? ""; - return string.IsNullOrWhiteSpace(nt) ? "Sticky Note" : (nt.Length <= 30 ? nt : nt[..30] + "..."); - } - return $"{element.Kind} Element"; - } - - private static string GetElementIcon(ElementViewModelBase element) - { - return element switch - { - ChartElementViewModel => "ChartBoxOutline", - TextElementViewModel => "FormatText", - TableElementViewModel => "Table", - ShapeElementViewModel => "ShapeOutline", - ImageElementViewModel => "ImageOutline", - MathElementViewModel => "Sigma", - SvgElementViewModel => "VectorSquare", - DividerElementViewModel => "Minus", - FormFieldElementViewModel => "TextBoxOutline", - QrCodeElementViewModel => "Qrcode", - BarcodeElementViewModel => "Barcode", - StickyNoteElementViewModel => "NoteTextOutline", - _ => "AutoFixHigh" - }; - } - - private void PopulateElementSuggestions(ElementViewModelBase element) - { - SuggestedPrompts.Clear(); - - if (element is ChartElementViewModel) - { - SuggestedPrompts.Add("Switch chart type to a smooth Line chart"); - SuggestedPrompts.Add("Use Emerald Green / Mint color palette"); - SuggestedPrompts.Add("Add Q1 2027 Projections with +15% revenue growth"); - SuggestedPrompts.Add("Change to Donut Pie chart showing category share"); - SuggestedPrompts.Add("Sort categories in ascending order of values"); - SuggestedPrompts.Add("Convert to Horizontal Bar chart"); - } - else if (element is TextElementViewModel) - { - SuggestedPrompts.Add("Polish tone to be executive, authoritative, and concise"); - SuggestedPrompts.Add("Format with modern styled checkmarks (✔)"); - SuggestedPrompts.Add("Convert into a clean numbered list (1., 2., 3.)"); - SuggestedPrompts.Add("Summarize into 3 key bullet points"); - SuggestedPrompts.Add("Make title bold, 20pt, with #0F6CBD accent color"); - SuggestedPrompts.Add("Fix grammar, punctuation, and improve phrasing"); - SuggestedPrompts.Add("Translate content into Spanish"); - } - else if (element is TableElementViewModel) - { - SuggestedPrompts.Add("Add a Total summary row with computed column sums"); - SuggestedPrompts.Add("Format monetary columns with currency ($) and commas"); - SuggestedPrompts.Add("Change header theme to Emerald Green (#047857)"); - SuggestedPrompts.Add("Apply alternating row zebra shading"); - } - else if (element is ShapeElementViewModel) - { - SuggestedPrompts.Add("Change to soft rounded card with 12pt corner radius and subtle shadow"); - SuggestedPrompts.Add("Set fill to light blue tint (#EFF6FF) with blue stroke (#2563EB)"); - SuggestedPrompts.Add("Make shape a circle with emerald green theme"); - } - else if (element is MathElementViewModel) - { - SuggestedPrompts.Add("Add equation number (1.1) and right align"); - SuggestedPrompts.Add("Enlarge formula font to 18pt"); - SuggestedPrompts.Add("Set formula color to modern blue (#0F6CBD)"); - SuggestedPrompts.Add("Change to Pythagorean theorem: a^2 + b^2 = c^2"); - SuggestedPrompts.Add("Change to Quadratic formula: x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"); - } - else if (element is ImageElementViewModel) - { - SuggestedPrompts.Add("Set opacity to 80% for clean watermark presentation"); - SuggestedPrompts.Add("Make image semi-transparent (50% opacity)"); - SuggestedPrompts.Add("Add rounded border (8pt) with blue accent"); - } - else if (element is SvgElementViewModel) - { - SuggestedPrompts.Add("Set tint color to Luxury Gold (#D97706)"); - SuggestedPrompts.Add("Set tint color to Modern Blue (#0F6CBD)"); - SuggestedPrompts.Add("Set tint color to Emerald Forest (#047857)"); - } - else if (element is DividerElementViewModel) - { - SuggestedPrompts.Add("Make divider line thicker (2.5pt)"); - SuggestedPrompts.Add("Set divider color to Modern Blue (#0F6CBD)"); - SuggestedPrompts.Add("Set divider color to Soft Slate (#CBD5E1)"); - } - else if (element is QrCodeElementViewModel) - { - SuggestedPrompts.Add("Update encoded URL to https://example.com"); - SuggestedPrompts.Add("Set dark color to Corporate Navy (#1E293B)"); - } - else if (element is BarcodeElementViewModel) - { - SuggestedPrompts.Add("Update barcode code value"); - SuggestedPrompts.Add("Set bar color to Corporate Navy (#1E293B)"); - } - else if (element is FormFieldElementViewModel) - { - SuggestedPrompts.Add("Make this a required field"); - SuggestedPrompts.Add("Set border color to Modern Blue (#0F6CBD)"); - } - else if (element is StickyNoteElementViewModel) - { - SuggestedPrompts.Add("Update status to Approved"); - SuggestedPrompts.Add("Change note color to Soft Amber (#FEF3C7)"); - SuggestedPrompts.Add("Change note color to Mint Green (#DCFCE7)"); - } - else - { - SuggestedPrompts.Add($"Enhance styling and appearance of this {element.Kind}"); - SuggestedPrompts.Add($"Resize and adjust alignment of this {element.Kind}"); - } - } - - [RelayCommand] - public void Close() - { - if (IsGenerating) - { - CancelGeneration(); - } - IsOpen = false; - RequestReturnToThumbnails?.Invoke(); - } - - public AiChatSession EnsureActiveSession() - { - if (CurrentSession != null) return CurrentSession; - - if (Sessions.Count > 0) - { - CurrentSession = Sessions[0]; - return CurrentSession; - } - - return CreateNewSession(); - } - - public AiChatSession CreateNewSession() - { - var session = new AiChatSession - { - Title = "New Chat" - }; - Sessions.Insert(0, session); - CurrentSession = session; - return session; - } - - [RelayCommand] - public void NewChat() - { - CreateNewSession(); - IsHistoryDrawerOpen = false; - ActivityLogs.Clear(); - CanUndoLastGeneration = false; - LastGenerationSummary = string.Empty; - StatusMessage = "Ready to generate document elements"; - } - - [RelayCommand] - public void SwitchSession(AiChatSession? session) - { - if (session != null && Sessions.Contains(session)) - { - CurrentSession = session; - IsHistoryDrawerOpen = false; - } - } - - [RelayCommand] - public void DeleteSession(AiChatSession? session) - { - if (session == null) return; - Sessions.Remove(session); - if (CurrentSession == session) - { - CurrentSession = Sessions.FirstOrDefault() ?? CreateNewSession(); - } - } - - [RelayCommand] - public void ClearCurrentChat() - { - CurrentSession?.Messages.Clear(); - ActivityLogs.Clear(); - CanUndoLastGeneration = false; - LastGenerationSummary = string.Empty; - } - - [RelayCommand] - public void ToggleHistoryDrawer() - { - IsHistoryDrawerOpen = !IsHistoryDrawerOpen; - } - - [RelayCommand] - public async Task SendSuggestedPromptAsync(string? prompt) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - PromptText = prompt; - await GenerateAsync(); - } - } - - [RelayCommand] - public void UsePrompt(string? prompt) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - PromptText = prompt; - } - } - - [RelayCommand] - public async Task GenerateAsync() - { - if (string.IsNullOrWhiteSpace(PromptText) || IsGenerating) return; - - var session = EnsureActiveSession(); - string prompt = PromptText.Trim(); - PromptText = string.Empty; - - if (session.Title == "New Chat") - { - session.Title = prompt.Length <= 28 ? prompt : prompt[..28] + "..."; - } - session.LastModified = DateTime.Now; - - var userMsg = new AiChatMessage - { - Role = AiChatRole.User, - Content = prompt, - TargetElementTitle = TargetKind switch - { - AiTargetKind.Element => TargetElementTitle, - AiTargetKind.PointOnPage => $"Point ({TargetPointDisplay})", - _ => $"Entire Page {TargetPageNumber}" - }, - TargetElementKind = TargetKind switch - { - AiTargetKind.Element => TargetElementKindBadge, - AiTargetKind.PointOnPage => "Point Target", - _ => "Entire Page" - } - }; - session.Messages.Add(userMsg); - - var assistantMsg = new AiChatMessage - { - Role = AiChatRole.Assistant, - IsGenerating = true, - Content = "Thinking & invoking agent tools..." - }; - session.Messages.Add(assistantMsg); - - IsGenerating = true; - CanUndoLastGeneration = false; - ActivityLogs.Clear(); - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] Prompt: \"{prompt}\""); - - _generationCts = new CancellationTokenSource(); - - try - { - var settings = _uiSettingsService.Settings.AiSettings; - - if (IsModifyMode && TargetElement != null && TargetKind == AiTargetKind.Element) - { - StatusMessage = $"Modifying {TargetElementKindBadge}..."; - var result = await _agentService.ModifyElementAsync( - TargetElement, - prompt, - settings, - msg => - { - StatusMessage = msg; - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] {msg}"); - assistantMsg.ToolCalls.Add($"[{DateTime.Now:HH:mm:ss}] {msg}"); - }, - _generationCts.Token); - - assistantMsg.IsGenerating = false; - assistantMsg.Duration = result.Duration; - assistantMsg.IsSuccess = result.Success; - assistantMsg.Content = result.Message; - - if (result.Success) - { - LastGenerationSummary = result.Message; - StatusMessage = $"Completed! {result.Message}"; - CanUndoLastGeneration = true; - assistantMsg.CanUndo = true; - assistantMsg.UndoAction = () => UndoGeneration(); - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] Finished successfully in {result.Duration.TotalSeconds:0.1}s"); - TriggerToast($"✨ AI updated {TargetElementKindBadge}!", ToastNotificationType.Success, "AutoFixHigh"); - TargetElementTitle = GetElementTitle(TargetElement); - } - else - { - LastGenerationSummary = result.Message; - StatusMessage = "Modification finished with notice."; - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] {result.Message}"); - TriggerToast(result.Message, ToastNotificationType.Warning, "InformationOutline"); - } - } - else - { - var targetPage = GetCurrentPage?.Invoke(); - if (targetPage == null) - { - assistantMsg.IsGenerating = false; - assistantMsg.IsSuccess = false; - assistantMsg.Content = "No active document page found. Please open or create a page."; - TriggerToast("No active document page found. Please open or create a page.", ToastNotificationType.Warning, "AlertOutline"); - return; - } - - var targetPt = TargetKind == AiTargetKind.PointOnPage ? TargetPoint : null; - StatusMessage = targetPt.HasValue - ? $"Generating elements at ({TargetPointDisplay})..." - : "Initializing AI agent on page..."; - - var result = await _agentService.ExecutePromptAsync( - prompt, - targetPage, - settings, - msg => - { - StatusMessage = msg; - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] {msg}"); - assistantMsg.ToolCalls.Add($"[{DateTime.Now:HH:mm:ss}] {msg}"); - }, - _generationCts.Token, - targetPt); - - assistantMsg.IsGenerating = false; - assistantMsg.Duration = result.Duration; - assistantMsg.IsSuccess = result.Success; - assistantMsg.Content = result.Message; - - if (result.Success) - { - LastGenerationSummary = result.Message; - StatusMessage = $"Completed! {result.ElementsCreatedCount} elements added."; - CanUndoLastGeneration = true; - assistantMsg.CanUndo = true; - assistantMsg.UndoAction = () => UndoGeneration(); - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] Finished successfully in {result.Duration.TotalSeconds:0.1}s"); - string successToast = targetPt.HasValue - ? $"AI generated {result.ElementsCreatedCount} elements at ({TargetPointDisplay})!" - : $"AI Studio generated {result.ElementsCreatedCount} elements on Page {targetPage.PageNumber}!"; - TriggerToast(successToast, ToastNotificationType.Success, "AutoFixHigh"); - } - else - { - LastGenerationSummary = result.Message; - StatusMessage = "Generation finished with notice."; - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] {result.Message}"); - TriggerToast(result.Message, ToastNotificationType.Warning, "InformationOutline"); - } - } - } - catch (OperationCanceledException) - { - assistantMsg.IsGenerating = false; - assistantMsg.IsSuccess = false; - assistantMsg.Content = "Generation cancelled by user."; - StatusMessage = "Generation cancelled."; - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] Generation was cancelled by user."); - TriggerToast("AI Generation cancelled", ToastNotificationType.General, "CloseCircleOutline"); - } - catch (Exception ex) - { - assistantMsg.IsGenerating = false; - assistantMsg.IsSuccess = false; - assistantMsg.Content = $"Error: {ex.Message}"; - StatusMessage = $"Error: {ex.Message}"; - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] Error: {ex.Message}"); - TriggerToast($"AI Error: {ex.Message}", ToastNotificationType.Danger, "AlertOctagonOutline"); - } - finally - { - IsGenerating = false; - _generationCts?.Dispose(); - _generationCts = null; - session.LastModified = DateTime.Now; - } - } - - [RelayCommand] - public void CancelGeneration() - { - _generationCts?.Cancel(); - } - - [RelayCommand] - public void UndoGeneration() - { - if (UndoRedo != null && UndoRedo.CanUndo) - { - string? undone = UndoRedo.Undo(); - CanUndoLastGeneration = false; - StatusMessage = "AI elements reverted."; - ActivityLogs.Add($"[{DateTime.Now:HH:mm:ss}] Reverted action: {undone}"); - TriggerToast("Reverted AI-generated elements", ToastNotificationType.Primary, "Undo"); - - var lastAssistant = CurrentSession?.Messages.LastOrDefault(m => m.IsAssistant && m.CanUndo); - if (lastAssistant != null) - { - lastAssistant.CanUndo = false; - } - } - } - - [RelayCommand] - public void OpenSettings() - { - IsOpen = false; - RequestOpenSettings?.Invoke(); - } - - private void TriggerToast(string message, ToastNotificationType type, string icon) - { - WeakReferenceMessenger.Default.Send(new ShowToastMessage(message, type, icon)); - } -} diff --git a/src/PdfEditorApp/ViewModels/AiSettingsViewModel.cs b/src/PdfEditorApp/ViewModels/AiSettingsViewModel.cs deleted file mode 100644 index b072db1..0000000 --- a/src/PdfEditorApp/ViewModels/AiSettingsViewModel.cs +++ /dev/null @@ -1,691 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.AI; - -namespace PdfEditorApp.ViewModels; - -/// -/// ViewModel managing AI provider configuration, Ollama local model discovery, and model selection. -/// -public partial class AiSettingsViewModel : ViewModelBase -{ - private readonly IUiSettingsService _uiSettingsService; - private readonly IAiService _aiService; - - [ObservableProperty] - private AiProviderType _selectedProvider = AiProviderType.OllamaLocal; - - [ObservableProperty] - private string _ollamaEndpoint = "http://localhost:11434"; - - [ObservableProperty] - private string _ollamaApiKey = string.Empty; - - [ObservableProperty] - private string _selectedModelId = "llama3.2"; - - [ObservableProperty] - private string _openAiApiKey = string.Empty; - - [ObservableProperty] - private string _customBaseUrl = string.Empty; - - [ObservableProperty] - private string _customModelName = string.Empty; - - [ObservableProperty] - private float _temperature = 0.7f; - - [ObservableProperty] - private string _systemInstructions = string.Empty; - - [ObservableProperty] - private bool _isDetectingModels; - - [ObservableProperty] - private bool _isFetchingLibrary; - - [ObservableProperty] - private string _ollamaLibrarySearchQuery = string.Empty; - - [ObservableProperty] - private bool _isTestingConnection; - - [ObservableProperty] - private string _testConnectionStatus = string.Empty; - - [ObservableProperty] - private bool _testConnectionSuccess; - - [ObservableProperty] - private bool _hasTestedConnection; - - [ObservableProperty] - private AiModelInfo? _selectedModel; - - public ObservableCollection AvailableModels { get; } = new(); - - public ObservableCollection PopularModelSuggestions { get; } = new(); - - public ObservableCollection CustomModelHistory { get; } = new(); - - public bool IsOllamaProvider => SelectedProvider == AiProviderType.OllamaLocal; - public bool IsOpenAiProvider => SelectedProvider == AiProviderType.OpenAiCloud; - public bool IsCustomProvider => SelectedProvider == AiProviderType.CustomOpenAiCompatible; - - /// - /// Guaranteed non-null metadata for the currently active model (whether picked from library or custom typed). - /// - public AiModelInfo ActiveModelInfo - { - get - { - if (SelectedModel != null && string.Equals(SelectedModel.Id, SelectedModelId, StringComparison.OrdinalIgnoreCase)) - { - return SelectedModel; - } - - var match = AvailableModels.FirstOrDefault(m => m.Id.Equals(SelectedModelId, StringComparison.OrdinalIgnoreCase)); - if (match != null) - { - return match; - } - - bool isRemote = SelectedProvider switch - { - AiProviderType.OllamaLocal => !string.IsNullOrWhiteSpace(OllamaEndpoint) && - !OllamaEndpoint.Contains("localhost", StringComparison.OrdinalIgnoreCase) && - !OllamaEndpoint.Contains("127.0.0.1"), - AiProviderType.CustomOpenAiCompatible => !string.IsNullOrWhiteSpace(CustomBaseUrl) && - !CustomBaseUrl.Contains("localhost", StringComparison.OrdinalIgnoreCase) && - !CustomBaseUrl.Contains("127.0.0.1"), - _ => true - }; - - string? endpoint = SelectedProvider == AiProviderType.CustomOpenAiCompatible ? CustomBaseUrl : OllamaEndpoint; - return AiModelInfo.CreateForCustomId(SelectedModelId, SelectedProvider, isRemote, endpoint); - } - } - - /// - /// Contextual placeholder text for the active model input field. - /// - public string ModelInputPlaceholder => SelectedProvider switch - { - AiProviderType.OllamaLocal => "Type model name (e.g. llama3.2, mistral, deepseek-r1, qwen2.5)...", - AiProviderType.OpenAiCloud => "Type model name (e.g. gpt-4o, gpt-4o-mini, o3-mini)...", - _ => "Type ANY model name (e.g. openai/gpt-oss-120b, llama-3.3-70b-versatile, qwen/qwen3.6-27b)..." - }; - - /// - /// Subtitle description for Section C tailored to the active AI provider. - /// - public string ModelSectionSubtitle => SelectedProvider switch - { - AiProviderType.OllamaLocal => "Pick from installed models or enter any model tag from the Ollama library.", - AiProviderType.OpenAiCloud => "Select an official OpenAI model or enter an enterprise/fine-tuned model ID.", - _ => "Enter any custom model identifier supported by your endpoint. Future models are saved dynamically." - }; - - public AiSettingsViewModel() : this(new UiSettingsService(), new AiService()) - { - } - - public AiSettingsViewModel(IUiSettingsService uiSettingsService, IAiService aiService) - { - _uiSettingsService = uiSettingsService ?? throw new ArgumentNullException(nameof(uiSettingsService)); - _aiService = aiService ?? throw new ArgumentNullException(nameof(aiService)); - - LoadFromSettings(_uiSettingsService.Settings.AiSettings); - _uiSettingsService.SettingsChanged += s => LoadFromSettings(s.AiSettings); - - // Auto-discover models from Ollama daemon on startup (silent) - _ = DiscoverModelsCoreAsync(showToast: false); - } - - private bool _isSuppressingSave; - private bool _isSyncingModelProperties; - - private void LoadFromSettings(AiSettingsModel s) - { - _isSuppressingSave = true; - try - { - SelectedProvider = s.SelectedProvider; - OllamaEndpoint = s.OllamaEndpoint; - OllamaApiKey = s.OllamaApiKey; - SelectedModelId = s.SelectedModelId; - OpenAiApiKey = s.OpenAiApiKey; - CustomBaseUrl = s.CustomBaseUrl; - CustomModelName = s.CustomModelName; - Temperature = s.Temperature; - SystemInstructions = s.SystemInstructions; - - CustomModelHistory.Clear(); - if (s.CustomModelHistory != null && s.CustomModelHistory.Count > 0) - { - foreach (var m in s.CustomModelHistory) - { - if (!string.IsNullOrWhiteSpace(m) && !CustomModelHistory.Contains(m.Trim(), StringComparer.OrdinalIgnoreCase)) - { - CustomModelHistory.Add(m.Trim()); - } - } - } - - RefreshModelCatalog(s); - } - finally - { - _isSuppressingSave = false; - } - } - - public void RememberCustomModel(string modelId) - { - if (string.IsNullOrWhiteSpace(modelId)) return; - string clean = modelId.Trim(); - if (!CustomModelHistory.Contains(clean, StringComparer.OrdinalIgnoreCase)) - { - CustomModelHistory.Insert(0, clean); - if (!_isSuppressingSave) - { - SaveSettings(); - } - } - } - - [RelayCommand] - public void ApplyPreset(string? preset) - { - if (string.IsNullOrWhiteSpace(preset)) return; - - switch (preset.ToLowerInvariant()) - { - case "groq": - CustomBaseUrl = "https://api.groq.com/openai/v1"; - SelectedModelId = "openai/gpt-oss-120b"; - CustomModelName = "openai/gpt-oss-120b"; - RememberCustomModel("openai/gpt-oss-120b"); - TriggerToast("Preset applied: Groq (https://api.groq.com/openai/v1)", ToastNotificationType.Primary, "Flash"); - break; - - case "openrouter": - CustomBaseUrl = "https://openrouter.ai/api/v1"; - SelectedModelId = "meta-llama/llama-3.2-3b-instruct:free"; - CustomModelName = "meta-llama/llama-3.2-3b-instruct:free"; - RememberCustomModel("meta-llama/llama-3.2-3b-instruct:free"); - TriggerToast("Preset applied: OpenRouter (https://openrouter.ai/api/v1)", ToastNotificationType.Primary, "CloudOutline"); - break; - - case "together": - CustomBaseUrl = "https://api.together.xyz/v1"; - SelectedModelId = "meta-llama/Llama-3.3-70B-Instruct-Turbo"; - CustomModelName = "meta-llama/Llama-3.3-70B-Instruct-Turbo"; - RememberCustomModel("meta-llama/Llama-3.3-70B-Instruct-Turbo"); - TriggerToast("Preset applied: Together AI", ToastNotificationType.Primary, "Api"); - break; - - case "lmstudio": - CustomBaseUrl = "http://localhost:1234/v1"; - SelectedModelId = "local-model"; - CustomModelName = "local-model"; - TriggerToast("Preset applied: LM Studio (http://localhost:1234/v1)", ToastNotificationType.Primary, "Laptop"); - break; - } - - RefreshModelCatalog(); - } - - public void RefreshModelCatalog(AiSettingsModel? currentSettings = null) - { - var settings = currentSettings ?? BuildSettingsModel(); - var catalog = _aiService.GetUnifiedModelCatalog(settings); - - AvailableModels.Clear(); - PopularModelSuggestions.Clear(); - - if (IsOllamaProvider) - { - // Only Ollama local/cloud models - foreach (var m in catalog.Where(x => x.Provider == AiProviderType.OllamaLocal)) - { - AvailableModels.Add(m); - } - - foreach (var m in AvailableModels.Take(8)) - { - if (!string.IsNullOrEmpty(m?.Id)) - { - PopularModelSuggestions.Add(m.Id); - } - } - } - else if (IsOpenAiProvider) - { - // OpenAI models + cloud models - foreach (var m in catalog.Where(x => x.Provider != AiProviderType.OllamaLocal)) - { - AvailableModels.Add(m); - } - - PopularModelSuggestions.Add("gpt-4o-mini"); - PopularModelSuggestions.Add("gpt-4o"); - PopularModelSuggestions.Add("o1-mini"); - PopularModelSuggestions.Add("o3-mini"); - } - else // IsCustomProvider - { - var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); - - // 1. User's saved custom models from history (dynamic - never need to touch codebase) - foreach (var customM in CustomModelHistory.ToList()) - { - if (seenIds.Add(customM)) - { - AvailableModels.Add(AiModelInfo.CreateForCustomId(customM, AiProviderType.CustomOpenAiCompatible, isEndpointRemote: true)); - } - } - - // 2. Cloud-compatible models from catalog (Groq, OpenRouter) - NO Ollama models - foreach (var m in catalog.Where(x => x.Provider != AiProviderType.OllamaLocal)) - { - if (seenIds.Add(m.Id)) - { - AvailableModels.Add(m); - } - } - - // 3. Contextual Quick Picks based on entered endpoint - string baseUrl = (CustomBaseUrl ?? string.Empty).ToLowerInvariant(); - if (baseUrl.Contains("openrouter")) - { - PopularModelSuggestions.Add("meta-llama/llama-3.2-3b-instruct:free"); - PopularModelSuggestions.Add("deepseek/deepseek-r1"); - PopularModelSuggestions.Add("google/gemini-2.0-flash-exp:free"); - PopularModelSuggestions.Add("mistralai/mistral-7b-instruct:free"); - } - else // Default to Groq / high-speed LPU suggestions - { - PopularModelSuggestions.Add("openai/gpt-oss-120b"); - PopularModelSuggestions.Add("llama-3.3-70b-versatile"); - PopularModelSuggestions.Add("qwen/qwen-2.5-coder-32b"); - PopularModelSuggestions.Add("deepseek-r1-distill-llama-70b"); - PopularModelSuggestions.Add("llama-3.1-8b-instant"); - } - - // Append any user custom models to quick picks - var existingSuggestions = new HashSet(PopularModelSuggestions, StringComparer.OrdinalIgnoreCase); - foreach (var customM in CustomModelHistory.ToList().Take(4)) - { - if (existingSuggestions.Add(customM)) - { - PopularModelSuggestions.Add(customM); - } - } - } - - // Match selected model from catalog if present - var match = AvailableModels.FirstOrDefault(m => m != null && string.Equals(m.Id, SelectedModelId, StringComparison.OrdinalIgnoreCase)); - if (match != null) - { - SelectedModel = match; - } - else if (SelectedModel == null && !IsCustomProvider) - { - SelectedModel = AvailableModels.FirstOrDefault(); - if (SelectedModel != null && (string.IsNullOrWhiteSpace(SelectedModelId) || SelectedModelId == "llama3.2")) - { - SelectedModelId = SelectedModel.Id; - } - } - else if (match == null) - { - SelectedModel = null; - } - - OnPropertyChanged(nameof(ActiveModelInfo)); - } - - partial void OnSelectedProviderChanged(AiProviderType value) - { - OnPropertyChanged(nameof(IsOllamaProvider)); - OnPropertyChanged(nameof(IsOpenAiProvider)); - OnPropertyChanged(nameof(IsCustomProvider)); - OnPropertyChanged(nameof(ModelInputPlaceholder)); - OnPropertyChanged(nameof(ModelSectionSubtitle)); - OnPropertyChanged(nameof(ActiveModelInfo)); - if (!_isSuppressingSave) - { - SaveSettings(); - RefreshModelCatalog(); - } - } - - partial void OnOllamaEndpointChanged(string value) - { - if (!_isSuppressingSave) SaveSettings(); - } - - partial void OnOllamaApiKeyChanged(string value) - { - if (!_isSuppressingSave) SaveSettings(); - } - - partial void OnOpenAiApiKeyChanged(string value) - { - if (!_isSuppressingSave) SaveSettings(); - } - - partial void OnCustomBaseUrlChanged(string value) - { - OnPropertyChanged(nameof(ActiveModelInfo)); - if (!_isSuppressingSave) - { - SaveSettings(); - RefreshModelCatalog(); - } - } - - partial void OnCustomModelNameChanged(string value) - { - if (_isSuppressingSave || _isSyncingModelProperties) return; - - try - { - _isSyncingModelProperties = true; - if (IsCustomProvider && !string.IsNullOrWhiteSpace(value) && !string.Equals(SelectedModelId, value, StringComparison.OrdinalIgnoreCase)) - { - SelectedModelId = value.Trim(); - SelectedModel = AvailableModels.FirstOrDefault(m => m.Id.Equals(SelectedModelId, StringComparison.OrdinalIgnoreCase)); - OnPropertyChanged(nameof(ActiveModelInfo)); - } - } - finally - { - _isSyncingModelProperties = false; - } - - SaveSettings(); - } - - partial void OnTemperatureChanged(float value) - { - if (!_isSuppressingSave) SaveSettings(); - } - - partial void OnSystemInstructionsChanged(string value) - { - if (!_isSuppressingSave) SaveSettings(); - } - - partial void OnSelectedModelIdChanged(string value) - { - if (!string.IsNullOrWhiteSpace(value)) - { - var match = AvailableModels.FirstOrDefault(m => m.Id.Equals(value.Trim(), StringComparison.OrdinalIgnoreCase)); - if (!ReferenceEquals(SelectedModel, match)) - { - _isSuppressingSave = true; - SelectedModel = match; - _isSuppressingSave = false; - } - - if (!_isSyncingModelProperties && IsCustomProvider && !string.Equals(CustomModelName, value, StringComparison.OrdinalIgnoreCase)) - { - try - { - _isSyncingModelProperties = true; - CustomModelName = value.Trim(); - } - finally - { - _isSyncingModelProperties = false; - } - } - } - - OnPropertyChanged(nameof(ActiveModelInfo)); - if (!_isSuppressingSave) SaveSettings(); - } - - partial void OnSelectedModelChanged(AiModelInfo? value) - { - if (value != null && !_isSuppressingSave) - { - if (!string.Equals(SelectedModelId, value.Id, StringComparison.OrdinalIgnoreCase)) - { - SelectedModelId = value.Id; - } - OnPropertyChanged(nameof(ActiveModelInfo)); - SaveSettings(); - } - } - - [RelayCommand] - public void SelectModel(object? param) - { - if (param is AiModelInfo model) - { - SelectedModel = model; - SelectedModelId = model.Id; - } - else if (param is string modelId && !string.IsNullOrWhiteSpace(modelId)) - { - SelectedModelId = modelId.Trim(); - var match = AvailableModels.FirstOrDefault(m => m.Id.Equals(SelectedModelId, StringComparison.OrdinalIgnoreCase)); - if (match != null) - { - SelectedModel = match; - } - } - - if (IsCustomProvider && !string.IsNullOrWhiteSpace(SelectedModelId)) - { - RememberCustomModel(SelectedModelId); - } - } - - [RelayCommand] - public void SaveCustomModel(string? modelId) - { - var target = string.IsNullOrWhiteSpace(modelId) ? SelectedModelId : modelId; - if (!string.IsNullOrWhiteSpace(target)) - { - RememberCustomModel(target); - RefreshModelCatalog(); - TriggerToast($"Model '{target.Trim()}' saved to history!", ToastNotificationType.Success, "BookmarkCheckOutline"); - } - } - - [RelayCommand] - public void SetProvider(object? param) - { - AiProviderType provider; - if (param is AiProviderType p) provider = p; - else if (param is string s && Enum.TryParse(s, true, out var parsed)) provider = parsed; - else return; - - SelectedProvider = provider; - TriggerToast($"AI provider switched to {GetProviderTitle(provider)}", ToastNotificationType.Primary, "RobotOutline"); - } - - [RelayCommand] - public Task DiscoverModelsAsync() => DiscoverModelsCoreAsync(showToast: true); - - private async Task DiscoverModelsCoreAsync(bool showToast) - { - IsDetectingModels = true; - try - { - var discovered = await _aiService.DiscoverOllamaModelsAsync(OllamaEndpoint); - - if (discovered.Count > 0) - { - _uiSettingsService.UpdateSettings(s => - { - s.AiSettings.DiscoveredOllamaModels.Clear(); - s.AiSettings.DiscoveredOllamaModels.AddRange(discovered); - if (s.AiSettings.SelectedProvider == AiProviderType.OllamaLocal && - (string.IsNullOrWhiteSpace(s.AiSettings.SelectedModelId) || s.AiSettings.SelectedModelId == "llama3.2")) - { - s.AiSettings.SelectedModelId = discovered[0].Id; - } - }); - - RefreshModelCatalog(_uiSettingsService.Settings.AiSettings); - if (showToast) - { - TriggerToast($"Found {discovered.Count} local Ollama models!", ToastNotificationType.Success, "CheckCircleOutline"); - } - } - else if (showToast) - { - TriggerToast("No Ollama models detected. Ensure Ollama is running (`ollama serve`).", ToastNotificationType.Warning, "AlertOutline"); - } - } - catch (Exception ex) - { - if (showToast) - { - TriggerToast($"Ollama discovery error: {ex.Message}", ToastNotificationType.Danger, "AlertOctagonOutline"); - } - } - finally - { - IsDetectingModels = false; - } - } - - [RelayCommand] - public async Task FetchOnlineLibraryAsync() - { - IsFetchingLibrary = true; - try - { - var onlineModels = await _aiService.FetchOllamaOnlineLibraryAsync(OllamaLibrarySearchQuery); - if (onlineModels.Count > 0) - { - _uiSettingsService.UpdateSettings(s => - { - s.AiSettings.OllamaLibraryCache.Clear(); - s.AiSettings.OllamaLibraryCache.AddRange(onlineModels); - }); - - RefreshModelCatalog(_uiSettingsService.Settings.AiSettings); - TriggerToast($"Discovered {onlineModels.Count} models directly from Ollama library!", ToastNotificationType.Success, "CloudDownloadOutline"); - } - else - { - TriggerToast("No online models retrieved. Check internet connection.", ToastNotificationType.Warning, "AlertOutline"); - } - } - catch (Exception ex) - { - TriggerToast($"Error fetching Ollama library: {ex.Message}", ToastNotificationType.Danger, "AlertOctagonOutline"); - } - finally - { - IsFetchingLibrary = false; - } - } - - [RelayCommand] - public async Task TestConnectionAsync() - { - IsTestingConnection = true; - HasTestedConnection = false; - TestConnectionStatus = "Connecting..."; - - try - { - var settings = BuildSettingsModel(); - var (success, msg, latency) = await _aiService.TestConnectionAsync(settings); - - HasTestedConnection = true; - TestConnectionSuccess = success; - TestConnectionStatus = success - ? $"Connected ({latency.TotalMilliseconds:0}ms): {msg}" - : $"Failed ({latency.TotalMilliseconds:0}ms): {msg}"; - - if (success && IsCustomProvider && !string.IsNullOrWhiteSpace(SelectedModelId)) - { - RememberCustomModel(SelectedModelId); - } - - TriggerToast( - success ? "AI model connection verified!" : "Connection test failed.", - success ? ToastNotificationType.Success : ToastNotificationType.Danger, - success ? "CheckCircleOutline" : "AlertOctagonOutline"); - } - catch (Exception ex) - { - HasTestedConnection = true; - TestConnectionSuccess = false; - TestConnectionStatus = $"Error: {ex.Message}"; - TriggerToast($"Test error: {ex.Message}", ToastNotificationType.Danger, "AlertOctagonOutline"); - } - finally - { - IsTestingConnection = false; - } - } - - private AiSettingsModel BuildSettingsModel() - { - return new AiSettingsModel - { - SelectedProvider = SelectedProvider, - OllamaEndpoint = OllamaEndpoint, - OllamaApiKey = OllamaApiKey, - SelectedModelId = SelectedModelId, - OpenAiApiKey = OpenAiApiKey, - CustomBaseUrl = AiService.NormalizeCustomOpenAiBaseUrl(CustomBaseUrl), - CustomModelName = CustomModelName, - CustomModelHistory = CustomModelHistory.ToList(), - Temperature = Temperature, - SystemInstructions = SystemInstructions, - DiscoveredOllamaModels = _uiSettingsService.Settings.AiSettings.DiscoveredOllamaModels - }; - } - - private void SaveSettings() - { - _uiSettingsService.UpdateSettings(s => - { - s.AiSettings.SelectedProvider = SelectedProvider; - s.AiSettings.OllamaEndpoint = OllamaEndpoint; - s.AiSettings.OllamaApiKey = OllamaApiKey; - s.AiSettings.SelectedModelId = SelectedModelId; - s.AiSettings.OpenAiApiKey = OpenAiApiKey; - s.AiSettings.CustomBaseUrl = AiService.NormalizeCustomOpenAiBaseUrl(CustomBaseUrl); - s.AiSettings.CustomModelName = CustomModelName; - s.AiSettings.CustomModelHistory = CustomModelHistory.ToList(); - s.AiSettings.Temperature = Temperature; - s.AiSettings.SystemInstructions = SystemInstructions; - }); - } - - private void TriggerToast(string message, ToastNotificationType type, string icon) - { - WeakReferenceMessenger.Default.Send(new ShowToastMessage(message, type, icon)); - } - - private static string GetProviderTitle(AiProviderType provider) => provider switch - { - AiProviderType.OllamaLocal => "Local Ollama", - AiProviderType.OpenAiCloud => "OpenAI Cloud", - AiProviderType.CustomOpenAiCompatible => "Custom OpenAI-Compatible", - _ => "AI Provider" - }; -} diff --git a/src/PdfEditorApp/ViewModels/BatchGeneration/BatchGenerationViewModel.cs b/src/PdfEditorApp/ViewModels/BatchGeneration/BatchGenerationViewModel.cs deleted file mode 100644 index e8a35fc..0000000 --- a/src/PdfEditorApp/ViewModels/BatchGeneration/BatchGenerationViewModel.cs +++ /dev/null @@ -1,891 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Media.Imaging; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Data; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels.DataStudio; - -namespace PdfEditorApp.ViewModels.BatchGeneration; - -public partial class FieldMappingItemViewModel : ObservableObject -{ - [ObservableProperty] - private string _placeholderTag = string.Empty; - - [ObservableProperty] - private string _selectedColumnName = string.Empty; - - [ObservableProperty] - private FieldTransformType _transform = FieldTransformType.None; - - [ObservableProperty] - private string _customFormat = string.Empty; - - [ObservableProperty] - private string _defaultValue = string.Empty; - - [ObservableProperty] - private string _sampleValue = string.Empty; - - public ObservableCollection AvailableColumns { get; } = new(); - - public FieldMappingItem ToModel() - { - return new FieldMappingItem - { - PlaceholderTag = PlaceholderTag, - DataColumnName = SelectedColumnName, - Transform = Transform, - CustomFormat = CustomFormat, - DefaultValue = DefaultValue, - SampleValue = SampleValue - }; - } -} - -public class BuiltInTemplateOption -{ - public string Id { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public string Category { get; set; } = string.Empty; - public string IconKind { get; set; } = "FileDocumentOutline"; -} - -public partial class BatchGenerationViewModel : ViewModelBase -{ - private readonly IDataSourceService _dataSourceService; - private readonly IDataMergeEngine _mergeEngine; - private readonly IBatchPdfGenerator _batchGenerator; - private readonly ITemplateService _templateService; - - public IStorageProvider? StorageProvider { get; set; } - private CancellationTokenSource? _generationCts; - - [ObservableProperty] - private bool _isOpen; - - [ObservableProperty] - private string _dialogTitle = "Batch Mail Merge & Mass PDF Generator"; - - // 0: Data & Template, 1: Field Mapping, 2: Live Preview, 3: Output & Execution - [ObservableProperty] - private int _selectedStepIndex = 0; - - // --- Template Configuration --- - [ObservableProperty] - private string _selectedTemplateMode = "CurrentDocument"; // "CurrentDocument", "BuiltInTemplate" - - public ObservableCollection BuiltInTemplates { get; } = new(); - - [ObservableProperty] - private BuiltInTemplateOption? _selectedBuiltInTemplate; - - public PdfDocumentModel ActiveTemplateModel { get; private set; } = new(); - public ObservableCollection DetectedPlaceholders { get; } = new(); - - // --- Data Source Configuration --- - [ObservableProperty] - private int _selectedDataSourceTab = 0; // 0 = Excel/CSV, 1 = REST API, 2 = Manual/Paste - - [ObservableProperty] - private string _selectedFilePath = string.Empty; - - [ObservableProperty] - private string _selectedFileName = string.Empty; - - public ObservableCollection ExcelSheets { get; } = new(); - - [ObservableProperty] - private string? _selectedExcelSheet; - - [ObservableProperty] - private bool _hasMultipleSheets; - - [ObservableProperty] - private bool _firstRowIsHeader = true; - - [ObservableProperty] - private string _selectedDelimiter = ","; - - // --- REST API State --- - [ObservableProperty] - private string _apiUrl = "https://dummyjson.com/users?limit=8"; - - [ObservableProperty] - private string _apiAuthBearer = string.Empty; - - [ObservableProperty] - private string _apiKeyHeader = string.Empty; - - [ObservableProperty] - private string _apiKeyValue = string.Empty; - - [ObservableProperty] - private string _apiJsonPath = "users"; - - [ObservableProperty] - private bool _isFetchingApi; - - [ObservableProperty] - private string _apiStatusMessage = "Ready to connect"; - - [ObservableProperty] - private bool _apiHasError; - - // --- Manual / Raw Input State --- - [ObservableProperty] - private string _rawTextInput = ""; - - // --- Active Matrix & Grid --- - public DataMatrix CurrentMatrix { get; private set; } = new(); - public ObservableCollection MatrixHeaders { get; } = new(); - public ObservableCollection GridRows { get; } = new(); - - [ObservableProperty] - private string _matrixSummary = "0 rows, 0 columns loaded"; - - // --- Field Mappings --- - public ObservableCollection FieldMappings { get; } = new(); - - // --- Live Record Preview --- - [ObservableProperty] - private int _currentPreviewIndex = 0; - - [ObservableProperty] - private int _totalRecordsCount = 0; - - [ObservableProperty] - private string _currentRecordSummary = "Record 0 of 0"; - - [ObservableProperty] - private Bitmap? _previewBitmap; - - // --- Output Configuration --- - [ObservableProperty] - private BatchOutputMode _outputMode = BatchOutputMode.SeparateFiles; - - [ObservableProperty] - private string _outputDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "FryPDF_Batch_Export"); - - [ObservableProperty] - private string _outputFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "FryPDF_Batch_Export", "All_Batch_Documents.pdf"); - - [ObservableProperty] - private string _filenamePattern = "Payslip_{{EmployeeId}}_{{EmployeeName}}_{{PayPeriod}}.pdf"; - - [ObservableProperty] - private bool _skipEmptyRows = true; - - // --- Execution & Progress State --- - [ObservableProperty] - private bool _isGenerating; - - [ObservableProperty] - private bool _isCompleted; - - [ObservableProperty] - private double _progressPercentage; - - [ObservableProperty] - private string _progressMessage = "Ready to generate"; - - [ObservableProperty] - private string _currentItemName = ""; - - [ObservableProperty] - private int _succeededCount; - - [ObservableProperty] - private int _failedCount; - - [ObservableProperty] - private string _generationSummary = ""; - - public BatchGenerationResult? LastResult { get; private set; } - - public BatchGenerationViewModel( - IDataSourceService dataSourceService, - IDataMergeEngine mergeEngine, - IBatchPdfGenerator batchGenerator, - ITemplateService templateService) - { - _dataSourceService = dataSourceService ?? throw new ArgumentNullException(nameof(dataSourceService)); - _mergeEngine = mergeEngine ?? throw new ArgumentNullException(nameof(mergeEngine)); - _batchGenerator = batchGenerator ?? throw new ArgumentNullException(nameof(batchGenerator)); - _templateService = templateService ?? throw new ArgumentNullException(nameof(templateService)); - - InitializeBuiltInTemplates(); - LoadDefaultSamplePayslipDataset(); - } - - private void InitializeBuiltInTemplates() - { - BuiltInTemplates.Clear(); - BuiltInTemplates.Add(new BuiltInTemplateOption { Id = "payslip", Name = "Employee Monthly Payslip", Category = "Corporate", IconKind = "CashMultiple" }); - BuiltInTemplates.Add(new BuiltInTemplateOption { Id = "certificate", Name = "Achievement Certificate", Category = "Certificates", IconKind = "CertificateOutline" }); - BuiltInTemplates.Add(new BuiltInTemplateOption { Id = "invoice", Name = "Commercial Invoice", Category = "Finance", IconKind = "ReceiptTextOutline" }); - BuiltInTemplates.Add(new BuiltInTemplateOption { Id = "annualreport", Name = "Executive Report", Category = "Corporate", IconKind = "FileChartOutline" }); - BuiltInTemplates.Add(new BuiltInTemplateOption { Id = "interactive_executive_deck", Name = "Executive Strategy Deck (Landscape)", Category = "Corporate", IconKind = "PresentationPlay" }); - - SelectedBuiltInTemplate = BuiltInTemplates.FirstOrDefault(); - } - - public void OpenWithDocument(PdfDocumentModel currentDoc, DataMatrix? preloadedMatrix = null) - { - ActiveTemplateModel = currentDoc?.Clone() ?? _templateService.CreateEmployeePayslipTemplate(); - SelectedTemplateMode = "CurrentDocument"; - - if (preloadedMatrix != null && preloadedMatrix.RowCount > 0) - { - SetMatrix(preloadedMatrix); - } - else if (CurrentMatrix.RowCount == 0) - { - LoadDefaultSamplePayslipDataset(); - } - - RefreshTemplatePlaceholders(); - AutoMapFields(); - - SelectedStepIndex = 0; - IsGenerating = false; - IsCompleted = false; - IsOpen = true; - - UpdateLiveRecordPreview(); - } - - [RelayCommand] - public void SelectStep(string stepIndexStr) - { - if (int.TryParse(stepIndexStr, out int idx)) - { - SelectedStepIndex = Math.Clamp(idx, 0, 3); - if (SelectedStepIndex == 2) - { - UpdateLiveRecordPreview(); - } - } - } - - [RelayCommand] - public void SelectDataSourceTab(string tabIndexStr) - { - if (int.TryParse(tabIndexStr, out int idx)) - { - SelectedDataSourceTab = Math.Clamp(idx, 0, 2); - } - } - - [RelayCommand] - public void NextStep() - { - SelectedStepIndex = Math.Clamp(SelectedStepIndex + 1, 0, 3); - if (SelectedStepIndex == 2) - { - UpdateLiveRecordPreview(); - } - } - - [RelayCommand] - public void PreviousStep() - { - SelectedStepIndex = Math.Clamp(SelectedStepIndex - 1, 0, 3); - } - - [RelayCommand] - public void Close() - { - if (IsGenerating && _generationCts != null) - { - _generationCts.Cancel(); - } - IsOpen = false; - } - - public void SetTemplate(PdfDocumentModel doc) - { - ActiveTemplateModel = doc.Clone(); - RefreshTemplatePlaceholders(); - AutoMapFields(); - UpdateLiveRecordPreview(); - } - - partial void OnSelectedBuiltInTemplateChanged(BuiltInTemplateOption? value) - { - if (value != null && SelectedTemplateMode == "BuiltInTemplate") - { - var templateDoc = _templateService.CreateTemplate(value.Id); - SetTemplate(templateDoc); - } - } - - partial void OnSelectedTemplateModeChanged(string value) - { - if (value == "BuiltInTemplate" && SelectedBuiltInTemplate != null) - { - var templateDoc = _templateService.CreateTemplate(SelectedBuiltInTemplate.Id); - SetTemplate(templateDoc); - } - } - - public void RefreshTemplatePlaceholders() - { - DetectedPlaceholders.Clear(); - var tags = _mergeEngine.DetectPlaceholders(ActiveTemplateModel); - foreach (var t in tags) - { - DetectedPlaceholders.Add(t); - } - - // Adjust suggested filename pattern if placeholders detected - if (DetectedPlaceholders.Contains("EmployeeId") && DetectedPlaceholders.Contains("EmployeeName")) - { - FilenamePattern = "Payslip_{{EmployeeId}}_{{EmployeeName}}_{{PayPeriod}}.pdf"; - } - else if (DetectedPlaceholders.Count > 0) - { - FilenamePattern = $"Doc_{{{{Index}}}}_{{{{{DetectedPlaceholders[0]}}}}}.pdf"; - } - } - - public void SetMatrix(DataMatrix matrix) - { - CurrentMatrix = matrix; - MatrixHeaders.Clear(); - foreach (var h in matrix.Headers) MatrixHeaders.Add(h); - - GridRows.Clear(); - for (int r = 0; r < matrix.RowCount; r++) - { - GridRows.Add(new DataGridRowViewModel(r + 1, matrix.Rows[r])); - } - - TotalRecordsCount = matrix.RowCount; - MatrixSummary = $"{matrix.RowCount} records × {matrix.ColumnCount} columns ready for merge"; - CurrentPreviewIndex = 0; - - AutoMapFields(); - UpdateLiveRecordPreview(); - } - - [RelayCommand] - public void AutoMapFields() - { - FieldMappings.Clear(); - - foreach (var tag in DetectedPlaceholders) - { - var mapping = new FieldMappingItemViewModel - { - PlaceholderTag = tag - }; - - foreach (var col in MatrixHeaders) - { - mapping.AvailableColumns.Add(col); - } - - // Find best matching column name - string? matchedCol = MatrixHeaders.FirstOrDefault(h => string.Equals(h, tag, StringComparison.OrdinalIgnoreCase)); - if (matchedCol == null) - { - matchedCol = MatrixHeaders.FirstOrDefault(h => - h.Replace(" ", "").Replace("_", "").Equals(tag.Replace(" ", "").Replace("_", ""), StringComparison.OrdinalIgnoreCase)); - } - - mapping.SelectedColumnName = matchedCol ?? MatrixHeaders.FirstOrDefault() ?? string.Empty; - - // Infer transform - if (tag.Contains("Salary", StringComparison.OrdinalIgnoreCase) || - tag.Contains("Earnings", StringComparison.OrdinalIgnoreCase) || - tag.Contains("Deductions", StringComparison.OrdinalIgnoreCase) || - tag.Contains("Amount", StringComparison.OrdinalIgnoreCase) || - tag.Contains("Price", StringComparison.OrdinalIgnoreCase) || - tag.Contains("Bonus", StringComparison.OrdinalIgnoreCase) || - tag.Contains("Allowance", StringComparison.OrdinalIgnoreCase) || - tag.Contains("HRA", StringComparison.OrdinalIgnoreCase) || - tag.Contains("Tax", StringComparison.OrdinalIgnoreCase)) - { - mapping.Transform = FieldTransformType.Currency; - } - else if (tag.Contains("Date", StringComparison.OrdinalIgnoreCase) || tag.Contains("Period", StringComparison.OrdinalIgnoreCase)) - { - mapping.Transform = FieldTransformType.Date; - } - - // Populate sample value from Row 0 - if (CurrentMatrix.RowCount > 0 && !string.IsNullOrEmpty(mapping.SelectedColumnName)) - { - int colIdx = CurrentMatrix.Headers.IndexOf(mapping.SelectedColumnName); - if (colIdx >= 0) - { - mapping.SampleValue = CurrentMatrix.GetCellValue(0, colIdx); - } - } - - FieldMappings.Add(mapping); - } - } - - [RelayCommand] - public void LoadDefaultSamplePayslipDataset() - { - // 1. Create Payslip Template - var payslipDoc = _templateService.CreateEmployeePayslipTemplate(); - SetTemplate(payslipDoc); - - // 2. High-fidelity CSV dataset with 6 realistic employee records - string sampleCsv = -@"EmployeeId,EmployeeName,Designation,Department,JoiningDate,BankName,AccountNumber,TaxId,WorkingDays,BasicSalary,HRA,SpecialAllowance,Bonus,MedicalAllowance,GrossEarnings,ProvidentFund,IncomeTax,ProfessionalTax,Insurance,OtherDeductions,TotalDeductions,NetSalary,NetSalaryInWords,PayPeriod,CompanyName,AuthHash -EMP-2026-0842,John Doe,Senior Software Architect,Cloud Infrastructure,2021-03-15,First National Bank,****8492,US-TAX-8429,30,8500,3400,1800,1200,600,15500,950,1850,200,350,0,3350,12150,Twelve Thousand One Hundred Fifty US Dollars,August 2026,CodeFryDev Inc.,9A4F-8201-B732 -EMP-2026-0914,Jane Doe,Principal UI/UX Designer,Product Experience,2022-06-01,Metro Commercial Bank,****3198,US-TAX-1942,30,7800,3120,1500,900,500,13820,850,1550,200,300,0,2900,10920,Ten Thousand Nine Hundred Twenty US Dollars,August 2026,CodeFryDev Inc.,7E2B-9410-C311 -EMP-2026-1052,Alex Doe,Lead DevOps & Reliability Engineer,Cloud Infrastructure,2020-11-10,Standard City Bank,****9041,US-TAX-7731,30,8200,3280,1650,1100,550,14780,900,1720,200,320,0,3140,11640,Eleven Thousand Six Hundred Forty US Dollars,August 2026,CodeFryDev Inc.,4D1C-8822-A904 -EMP-2026-1188,Sam Doe,VP of Product Management,Executive Staff,2019-08-20,Apex Commerce Bank,****4412,US-TAX-5510,30,11000,4400,2400,2000,800,20600,1200,2900,200,450,0,4750,15850,Fifteen Thousand Eight Hundred Fifty US Dollars,August 2026,CodeFryDev Inc.,8F3D-1190-E542 -EMP-2026-1240,Taylor Doe,Senior QA Automation Engineer,Quality Assurance,2023-01-16,Pacific Trust Bank,****6720,US-TAX-3398,30,6900,2760,1300,750,450,12160,780,1320,200,280,0,2580,9580,Nine Thousand Five Hundred Eighty US Dollars,August 2026,CodeFryDev Inc.,2A9E-6541-D881 -EMP-2026-1315,Jordan Doe,Director of People & Culture,Human Resources,2021-09-01,Union Federal Bank,****5593,US-TAX-6624,30,9200,3680,1900,1400,650,16830,1050,2150,200,380,0,3780,13050,Thirteen Thousand Fifty US Dollars,August 2026,CodeFryDev Inc.,5C7F-3309-F104"; - - var matrix = _dataSourceService.ParseCsv(sampleCsv, ',', true); - SetMatrix(matrix); - } - - [RelayCommand] - public async Task BrowseFileAsync() - { - if (StorageProvider == null) return; - - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Select Dataset for Batch Merge (Excel, CSV, TSV)", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("Supported Datasets (*.xlsx, *.csv, *.tsv)") - { - Patterns = new[] { "*.xlsx", "*.csv", "*.tsv", "*.txt" } - }, - new FilePickerFileType("Excel Workbooks (*.xlsx)") - { - Patterns = new[] { "*.xlsx" } - }, - new FilePickerFileType("CSV Delimited (*.csv, *.tsv)") - { - Patterns = new[] { "*.csv", "*.tsv" } - } - } - }); - - if (files != null && files.Count > 0) - { - var file = files[0]; - await LoadFileAsync(file.Path.LocalPath); - } - } - - public async Task LoadFileAsync(string filePath) - { - if (!File.Exists(filePath)) return; - - SelectedFilePath = filePath; - SelectedFileName = Path.GetFileName(filePath); - string ext = Path.GetExtension(filePath).ToLowerInvariant(); - - try - { - if (ext == ".xlsx") - { - using var fs = File.OpenRead(filePath); - var sheetNames = _dataSourceService.GetExcelSheetNames(fs); - ExcelSheets.Clear(); - foreach (var name in sheetNames) ExcelSheets.Add(name); - - HasMultipleSheets = ExcelSheets.Count > 1; - SelectedExcelSheet = ExcelSheets.FirstOrDefault(); - - fs.Position = 0; - var matrix = _dataSourceService.ParseExcel(fs, SelectedExcelSheet, FirstRowIsHeader); - SetMatrix(matrix); - } - else - { - HasMultipleSheets = false; - ExcelSheets.Clear(); - string text = await File.ReadAllTextAsync(filePath); - char? delim = ext == ".tsv" ? '\t' : (SelectedDelimiter.Length > 0 ? SelectedDelimiter[0] : (char?)null); - var matrix = _dataSourceService.ParseCsv(text, delim, FirstRowIsHeader); - SetMatrix(matrix); - } - } - catch (Exception ex) - { - ApiHasError = true; - ApiStatusMessage = $"File error: {ex.Message}"; - } - } - - [RelayCommand] - public async Task FetchApiDataAsync() - { - if (string.IsNullOrWhiteSpace(ApiUrl)) return; - - IsFetchingApi = true; - ApiHasError = false; - ApiStatusMessage = "Connecting to API endpoint..."; - - try - { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(ApiAuthBearer)) - { - headers["Authorization"] = ApiAuthBearer.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) - ? ApiAuthBearer - : $"Bearer {ApiAuthBearer}"; - } - if (!string.IsNullOrWhiteSpace(ApiKeyHeader) && !string.IsNullOrWhiteSpace(ApiKeyValue)) - { - headers[ApiKeyHeader] = ApiKeyValue; - } - - var matrix = await _dataSourceService.FetchFromRestApiAsync(ApiUrl, headers, ApiJsonPath); - if (matrix.RowCount == 0) - { - ApiHasError = true; - ApiStatusMessage = "API returned 0 records."; - } - else - { - SetMatrix(matrix); - ApiStatusMessage = $"Successfully fetched {matrix.RowCount} records from REST API!"; - } - } - catch (Exception ex) - { - ApiHasError = true; - ApiStatusMessage = $"API Error: {ex.Message}"; - } - finally - { - IsFetchingApi = false; - } - } - - [RelayCommand] - public async Task BrowseOutputDirectoryAsync() - { - if (StorageProvider == null) return; - - var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions - { - Title = "Select Output Directory for Generated PDFs", - AllowMultiple = false - }); - - if (folders != null && folders.Count > 0) - { - OutputDirectory = folders[0].Path.LocalPath; - OutputFilePath = Path.Combine(OutputDirectory, "All_Merged_Documents.pdf"); - } - } - - [RelayCommand] - public async Task BrowseOutputFilePathAsync() - { - if (StorageProvider == null) return; - - string defaultExt = OutputMode == BatchOutputMode.ZipArchive ? "zip" : "pdf"; - string filterName = OutputMode == BatchOutputMode.ZipArchive ? "ZIP Archive (*.zip)" : "PDF Document (*.pdf)"; - string defaultPattern = OutputMode == BatchOutputMode.ZipArchive ? "*.zip" : "*.pdf"; - - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Select Output Destination File", - DefaultExtension = defaultExt, - SuggestedFileName = OutputMode == BatchOutputMode.ZipArchive ? "Batch_PDFs_Archive.zip" : "All_Merged_Documents.pdf", - FileTypeChoices = new[] - { - new FilePickerFileType(filterName) { Patterns = new[] { defaultPattern } } - } - }); - - if (file != null) - { - OutputFilePath = file.Path.LocalPath; - } - } - - [RelayCommand] - public void InsertPlaceholderToFilename(string tag) - { - if (!string.IsNullOrWhiteSpace(tag)) - { - FilenamePattern += $"_{{{{{tag}}}}}"; - } - } - - // --- Live Record Navigation --- - [RelayCommand] - public void NextPreviewRecord() - { - if (CurrentMatrix.RowCount > 0) - { - CurrentPreviewIndex = (CurrentPreviewIndex + 1) % CurrentMatrix.RowCount; - UpdateLiveRecordPreview(); - } - } - - [RelayCommand] - public void PreviousPreviewRecord() - { - if (CurrentMatrix.RowCount > 0) - { - CurrentPreviewIndex = (CurrentPreviewIndex - 1 + CurrentMatrix.RowCount) % CurrentMatrix.RowCount; - UpdateLiveRecordPreview(); - } - } - - public void UpdateLiveRecordPreview() - { - if (CurrentMatrix.RowCount == 0 || ActiveTemplateModel == null) - { - PreviewBitmap = null; - CurrentRecordSummary = "No records available"; - return; - } - - CurrentPreviewIndex = Math.Clamp(CurrentPreviewIndex, 0, CurrentMatrix.RowCount - 1); - var mappings = FieldMappings.Select(m => m.ToModel()).ToList(); - var record = BatchPdfGeneratorService.BuildRecordDictionary(CurrentMatrix, CurrentPreviewIndex, mappings); - - // Build brief summary for UI display - string primaryName = record.TryGetValue("EmployeeName", out var name) ? name : - (record.TryGetValue("Name", out var n) ? n : - (CurrentMatrix.ColumnCount > 1 ? CurrentMatrix.GetCellValue(CurrentPreviewIndex, 1) : "")); - - string primaryId = record.TryGetValue("EmployeeId", out var id) ? id : - (record.TryGetValue("Id", out var i) ? i : - (CurrentMatrix.ColumnCount > 0 ? CurrentMatrix.GetCellValue(CurrentPreviewIndex, 0) : "")); - - CurrentRecordSummary = $"Record {CurrentPreviewIndex + 1} of {CurrentMatrix.RowCount}: {primaryName} (ID: {primaryId})"; - - try - { - // Hydrate document model - var hydratedDoc = _mergeEngine.HydrateDocument(ActiveTemplateModel, record); - - // Rasterize preview using Skia renderer - if (hydratedDoc.Pages.Count > 0) - { - var firstPage = hydratedDoc.Pages[0]; - float w = (float)firstPage.Width; - float h = (float)firstPage.Height; - - using var surface = SkiaSharp.SKSurface.Create(new SkiaSharp.SKImageInfo((int)w, (int)h)); - var canvas = surface.Canvas; - canvas.Clear(SkiaSharp.SKColors.White); - - // Render basic elements preview - foreach (var el in firstPage.Elements.OrderBy(e => e.ZIndex)) - { - if (el is PdfShapeElement shape) - { - using var paint = new SkiaSharp.SKPaint - { - Color = SkiaSharp.SKColor.TryParse(shape.FillColorHex, out var c) ? c : SkiaSharp.SKColors.LightGray, - Style = SkiaSharp.SKPaintStyle.Fill, - IsAntialias = true - }; - canvas.DrawRoundRect(new SkiaSharp.SKRect((float)shape.X, (float)shape.Y, (float)(shape.X + shape.Width), (float)(shape.Y + shape.Height)), (float)shape.CornerRadius, (float)shape.CornerRadius, paint); - } - else if (el is PdfTextElement text) - { - using var font = new SkiaSharp.SKFont(SkiaSharp.SKTypeface.Default, (float)Math.Max(8, text.FontSize)); - font.Embolden = text.IsBold; - using var textPaint = new SkiaSharp.SKPaint - { - Color = SkiaSharp.SKColor.TryParse(text.TextColorHex, out var tc) ? tc : SkiaSharp.SKColors.Black, - IsAntialias = true - }; - float yOffset = (float)text.Y + (float)text.FontSize; - string[] lines = text.Text.Split('\n'); - foreach (var l in lines) - { - canvas.DrawText(l, (float)text.X, yOffset, SkiaSharp.SKTextAlign.Left, font, textPaint); - yOffset += (float)(text.FontSize * text.LineHeight); - } - } - else if (el is PdfTableElement table) - { - using var headerBg = new SkiaSharp.SKPaint { Color = SkiaSharp.SKColor.Parse(table.HeaderBackgroundHex), Style = SkiaSharp.SKPaintStyle.Fill }; - canvas.DrawRect(new SkiaSharp.SKRect((float)table.X, (float)table.Y, (float)(table.X + table.Width), (float)(table.Y + 24)), headerBg); - - using var fontHeader = new SkiaSharp.SKFont(SkiaSharp.SKTypeface.Default, 10); - fontHeader.Embolden = true; - using var textPaint = new SkiaSharp.SKPaint { Color = SkiaSharp.SKColors.White, IsAntialias = true }; - canvas.DrawText(table.Headers.FirstOrDefault() ?? "TABLE", (float)table.X + 8, (float)table.Y + 16, SkiaSharp.SKTextAlign.Left, fontHeader, textPaint); - - float rowY = (float)table.Y + 24; - using var fontCell = new SkiaSharp.SKFont(SkiaSharp.SKTypeface.Default, 9); - using var cellPaint = new SkiaSharp.SKPaint { Color = SkiaSharp.SKColors.Black, IsAntialias = true }; - - for (int r = 0; r < Math.Min(6, table.Rows.Count); r++) - { - using var rowBg = new SkiaSharp.SKPaint { Color = r % 2 == 0 ? SkiaSharp.SKColors.White : SkiaSharp.SKColor.Parse("#F8FAFC"), Style = SkiaSharp.SKPaintStyle.Fill }; - canvas.DrawRect(new SkiaSharp.SKRect((float)table.X, rowY, (float)(table.X + table.Width), rowY + 20), rowBg); - - string cellText = string.Join(" | ", table.Rows[r]); - canvas.DrawText(cellText, (float)table.X + 8, rowY + 14, SkiaSharp.SKTextAlign.Left, fontCell, cellPaint); - rowY += 20; - } - } - else if (el is PdfQrCodeElement qr) - { - using var qrPaint = new SkiaSharp.SKPaint { Color = SkiaSharp.SKColor.Parse(qr.DarkColorHex), Style = SkiaSharp.SKPaintStyle.Fill }; - canvas.DrawRect(new SkiaSharp.SKRect((float)qr.X, (float)qr.Y, (float)(qr.X + qr.Width), (float)(qr.Y + qr.Height)), qrPaint); - - using var fontQr = new SkiaSharp.SKFont(SkiaSharp.SKTypeface.Default, 9); - using var tPaint = new SkiaSharp.SKPaint { Color = SkiaSharp.SKColors.White, IsAntialias = true }; - canvas.DrawText("QR CODE", (float)qr.X + 10, (float)qr.Y + (float)qr.Height / 2, SkiaSharp.SKTextAlign.Left, fontQr, tPaint); - } - } - - using var image = surface.Snapshot(); - using var data = image.Encode(SkiaSharp.SKEncodedImageFormat.Png, 90); - using var ms = new MemoryStream(data.ToArray()); - PreviewBitmap = new Bitmap(ms); - } - } - catch - { - PreviewBitmap = null; - } - } - - // --- Execution --- - [RelayCommand] - public async Task StartGenerationAsync() - { - if (CurrentMatrix.RowCount == 0) - { - ProgressMessage = "No data rows loaded to generate."; - return; - } - - IsGenerating = true; - IsCompleted = false; - ProgressPercentage = 0; - SucceededCount = 0; - FailedCount = 0; - ProgressMessage = "Initializing batch generation engine..."; - - _generationCts = new CancellationTokenSource(); - - var config = new BatchGenerationConfig - { - OutputMode = OutputMode, - OutputDirectory = OutputDirectory, - OutputFilePath = OutputFilePath, - FilenamePattern = FilenamePattern, - SkipEmptyRows = SkipEmptyRows - }; - - var mappings = FieldMappings.Select(m => m.ToModel()).ToList(); - - var progress = new Progress(report => - { - ProgressPercentage = report.Percentage; - SucceededCount = report.SucceededCount; - FailedCount = report.FailedCount; - CurrentItemName = report.CurrentItemName; - ProgressMessage = report.StatusMessage; - }); - - try - { - LastResult = await _batchGenerator.GenerateBatchAsync( - ActiveTemplateModel, - CurrentMatrix, - mappings, - config, - progress, - _generationCts.Token); - - IsCompleted = true; - ProgressPercentage = 100; - ProgressMessage = $"Batch generation complete! {LastResult.SuccessfulCount} documents created successfully."; - GenerationSummary = $"Successfully created {LastResult.SuccessfulCount} PDFs in {LastResult.ElapsedTime.TotalSeconds:F2} seconds. Output: {(OutputMode == BatchOutputMode.SeparateFiles ? OutputDirectory : OutputFilePath)}"; - } - catch (OperationCanceledException) - { - ProgressMessage = "Batch generation was cancelled by user."; - } - catch (Exception ex) - { - ProgressMessage = $"Error during batch generation: {ex.Message}"; - } - finally - { - IsGenerating = false; - } - } - - [RelayCommand] - public void CancelGeneration() - { - _generationCts?.Cancel(); - } - - [RelayCommand] - public void OpenOutputFolder() - { - try - { - string folder = OutputMode == BatchOutputMode.SeparateFiles - ? OutputDirectory - : (Path.GetDirectoryName(OutputFilePath) ?? OutputDirectory); - - if (Directory.Exists(folder)) - { - Process.Start(new ProcessStartInfo - { - FileName = folder, - UseShellExecute = true - }); - } - } - catch { } - } - - [RelayCommand] - public void OpenOutputFile() - { - try - { - string target = OutputMode == BatchOutputMode.SeparateFiles - ? LastResult?.GeneratedFiles.FirstOrDefault() ?? OutputDirectory - : OutputFilePath; - - if (File.Exists(target) || Directory.Exists(target)) - { - Process.Start(new ProcessStartInfo - { - FileName = target, - UseShellExecute = true - }); - } - } - catch { } - } -} diff --git a/src/PdfEditorApp/ViewModels/DataStudio/DataStudioViewModel.cs b/src/PdfEditorApp/ViewModels/DataStudio/DataStudioViewModel.cs deleted file mode 100644 index e16939c..0000000 --- a/src/PdfEditorApp/ViewModels/DataStudio/DataStudioViewModel.cs +++ /dev/null @@ -1,646 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia.Input.Platform; -using Avalonia.Media.Imaging; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Data; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels.DataStudio; - -public class DataColumnSelectionItem : ObservableObject -{ - public int Index { get; set; } - public string Header { get; set; } = string.Empty; - public DataColumnType ColumnType { get; set; } - - private bool _isSelected; - public bool IsSelected - { - get => _isSelected; - set => SetProperty(ref _isSelected, value); - } -} - -public class DataGridRowViewModel : ObservableObject -{ - public int RowIndex { get; set; } - public ObservableCollection Cells { get; } = new(); - - public DataGridRowViewModel(int rowIndex, IEnumerable cells) - { - RowIndex = rowIndex; - foreach (var c in cells) Cells.Add(c); - } -} - -public partial class DataStudioViewModel : ViewModelBase -{ - private readonly IDataSourceService _dataSourceService; - private readonly IDataBindingService _dataBindingService; - - public IStorageProvider? StorageProvider { get; set; } - public IUndoRedoService? UndoRedo { get; set; } - - [ObservableProperty] - private bool _isOpen; - - [ObservableProperty] - private string _dialogTitle = "Data Studio & Connector"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsChartTarget))] - [NotifyPropertyChangedFor(nameof(IsTableTarget))] - private string _targetMode = "Chart"; // "Chart", "Table", "NewChart", "NewTable" - - public bool IsChartTarget => TargetMode == "Chart" || TargetMode == "NewChart"; - public bool IsTableTarget => TargetMode == "Table" || TargetMode == "NewTable"; - - public ChartElementViewModel? TargetChart { get; private set; } - public TableElementViewModel? TargetTable { get; private set; } - public PageViewModel? TargetPage { get; private set; } - - // Active Source Tab: 0 = Excel/Files, 1 = REST API, 2 = Spreadsheet / Clipboard - [ObservableProperty] - private int _selectedTabIndex = 0; - - // --- File Import State --- - [ObservableProperty] - private string _selectedFilePath = string.Empty; - - [ObservableProperty] - private string _selectedFileName = string.Empty; - - public ObservableCollection ExcelSheets { get; } = new(); - - [ObservableProperty] - private string? _selectedExcelSheet; - - [ObservableProperty] - private bool _hasMultipleSheets; - - [ObservableProperty] - private bool _firstRowIsHeader = true; - - [ObservableProperty] - private string _selectedDelimiter = ","; - - // --- REST API State --- - [ObservableProperty] - private string _apiUrl = "https://dummyjson.com/products?limit=6"; - - [ObservableProperty] - private string _apiAuthBearer = string.Empty; - - [ObservableProperty] - private string _apiKeyHeader = string.Empty; - - [ObservableProperty] - private string _apiKeyValue = string.Empty; - - [ObservableProperty] - private string _apiJsonPath = "products"; - - [ObservableProperty] - private bool _isFetchingApi; - - [ObservableProperty] - private string _apiStatusMessage = "Ready to connect"; - - [ObservableProperty] - private bool _apiHasError; - - // --- Manual / Raw Text Input State --- - [ObservableProperty] - private string _rawTextInput = "Category\t2025 Actual\t2026 Target\nQ1\t120\t140\nQ2\t150\t175\nQ3\t180\t210\nQ4\t200\t240"; - - // --- Live Matrix & Grid State --- - public DataMatrix CurrentMatrix { get; private set; } = new(); - - public ObservableCollection MatrixHeaders { get; } = new(); - public ObservableCollection GridRows { get; } = new(); - public ObservableCollection AvailableColumns { get; } = new(); - - [ObservableProperty] - private int _selectedCategoryColumnIndex = 0; - - [ObservableProperty] - private Bitmap? _chartPreviewBitmap; - - [ObservableProperty] - private string _matrixSummary = "0 rows, 0 columns loaded"; - - public event Action? OnElementCreated; - public event Action? OpenBatchMergeRequested; - - public DataStudioViewModel(IDataSourceService dataSourceService, IDataBindingService dataBindingService) - { - _dataSourceService = dataSourceService; - _dataBindingService = dataBindingService; - - LoadDefaultSampleMatrix(); - } - - public void OpenForChart(ChartElementViewModel chart, PageViewModel? page) - { - TargetChart = chart; - TargetTable = null; - TargetPage = page; - TargetMode = "Chart"; - DialogTitle = $"Data Studio — Edit Data for '{chart.Title}'"; - - // Extract current data from chart into matrix - var model = (PdfChartElement)chart.ToModel(); - var matrix = _dataBindingService.ExtractFromChart(model); - SetMatrix(matrix); - - IsOpen = true; - } - - public void OpenForTable(TableElementViewModel table, PageViewModel? page) - { - TargetChart = null; - TargetTable = table; - TargetPage = page; - TargetMode = "Table"; - DialogTitle = $"Data Studio — Edit Data for Table"; - - var model = (PdfTableElement)table.ToModel(); - var matrix = _dataBindingService.ExtractFromTable(model); - SetMatrix(matrix); - - IsOpen = true; - } - - [RelayCommand] - public void SelectTab(string tabIndexStr) - { - if (int.TryParse(tabIndexStr, out int idx)) - { - SelectedTabIndex = idx; - } - } - - [RelayCommand] - public void SetPresetApi(string presetKey) - { - switch (presetKey.ToLowerInvariant()) - { - case "products": - case "dummyjson": - ApiUrl = "https://dummyjson.com/products?limit=8"; - ApiJsonPath = "products"; - break; - - case "crypto": - case "coingecko": - ApiUrl = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=8&page=1"; - ApiJsonPath = ""; - break; - - case "users": - case "jsonplaceholder": - ApiUrl = "https://jsonplaceholder.typicode.com/users"; - ApiJsonPath = ""; - break; - - case "sales": - ApiUrl = "https://dummyjson.com/carts/1"; - ApiJsonPath = "products"; - break; - } - - ApiStatusMessage = "Preset configured. Click 'Fetch API Data' to connect."; - ApiHasError = false; - } - - public void OpenForNew(string mode, PageViewModel? page) - { - TargetChart = null; - TargetTable = null; - TargetPage = page; - TargetMode = mode; - DialogTitle = mode == "NewChart" ? "Data Studio — Import Data to New Chart" : "Data Studio — Import Data to New Table"; - - LoadDefaultSampleMatrix(); - IsOpen = true; - } - - [RelayCommand] - public void Close() - { - IsOpen = false; - } - - private void LoadDefaultSampleMatrix() - { - var matrix = _dataSourceService.ParseTsv(RawTextInput, true); - SetMatrix(matrix); - } - - public void SetMatrix(DataMatrix matrix) - { - CurrentMatrix = matrix; - MatrixHeaders.Clear(); - foreach (var h in matrix.Headers) MatrixHeaders.Add(h); - - GridRows.Clear(); - for (int r = 0; r < matrix.RowCount; r++) - { - GridRows.Add(new DataGridRowViewModel(r + 1, matrix.Rows[r])); - } - - AvailableColumns.Clear(); - for (int c = 0; c < matrix.ColumnCount; c++) - { - var colType = matrix.InferColumnType(c); - AvailableColumns.Add(new DataColumnSelectionItem - { - Index = c, - Header = matrix.Headers[c], - ColumnType = colType, - IsSelected = c != SelectedCategoryColumnIndex - }); - } - - SelectedCategoryColumnIndex = Math.Clamp(SelectedCategoryColumnIndex, 0, Math.Max(0, matrix.ColumnCount - 1)); - MatrixSummary = $"{matrix.RowCount} rows × {matrix.ColumnCount} columns loaded"; - - UpdateChartPreview(); - } - - partial void OnSelectedCategoryColumnIndexChanged(int value) - { - UpdateChartPreview(); - } - - partial void OnFirstRowIsHeaderChanged(bool value) - { - if (!string.IsNullOrEmpty(SelectedFilePath) && File.Exists(SelectedFilePath)) - { - ReloadCurrentFile(); - } - } - - partial void OnSelectedExcelSheetChanged(string? value) - { - if (!string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(SelectedFilePath) && File.Exists(SelectedFilePath)) - { - ReloadCurrentFile(); - } - } - - [RelayCommand] - public async Task BrowseFileAsync() - { - if (StorageProvider == null) return; - - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Select Excel Workbook, CSV, or Data File", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("Supported Data Files (*.xlsx, *.csv, *.tsv, *.txt)") - { - Patterns = new[] { "*.xlsx", "*.csv", "*.tsv", "*.txt" } - }, - new FilePickerFileType("Excel Workbooks (*.xlsx)") - { - Patterns = new[] { "*.xlsx" } - }, - new FilePickerFileType("CSV / Delimited (*.csv, *.tsv)") - { - Patterns = new[] { "*.csv", "*.tsv", "*.txt" } - } - } - }); - - if (files != null && files.Count > 0) - { - var file = files[0]; - string path = file.Path.LocalPath; - await LoadFileAsync(path); - } - } - - public async Task LoadFileAsync(string filePath) - { - if (!File.Exists(filePath)) return; - - SelectedFilePath = filePath; - SelectedFileName = Path.GetFileName(filePath); - string ext = Path.GetExtension(filePath).ToLowerInvariant(); - - try - { - if (ext == ".xlsx") - { - using var fs = File.OpenRead(filePath); - var sheetNames = _dataSourceService.GetExcelSheetNames(fs); - ExcelSheets.Clear(); - foreach (var name in sheetNames) ExcelSheets.Add(name); - - HasMultipleSheets = ExcelSheets.Count > 1; - SelectedExcelSheet = ExcelSheets.FirstOrDefault(); - - fs.Position = 0; - var matrix = _dataSourceService.ParseExcel(fs, SelectedExcelSheet, FirstRowIsHeader); - SetMatrix(matrix); - } - else - { - HasMultipleSheets = false; - ExcelSheets.Clear(); - string text = await File.ReadAllTextAsync(filePath); - char? delim = ext == ".tsv" ? '\t' : (SelectedDelimiter.Length > 0 ? SelectedDelimiter[0] : (char?)null); - var matrix = _dataSourceService.ParseCsv(text, delim, FirstRowIsHeader); - SetMatrix(matrix); - } - } - catch (Exception ex) - { - ApiHasError = true; - ApiStatusMessage = $"File read error: {ex.Message}"; - } - } - - private void ReloadCurrentFile() - { - if (!string.IsNullOrEmpty(SelectedFilePath) && File.Exists(SelectedFilePath)) - { - _ = LoadFileAsync(SelectedFilePath); - } - } - - [RelayCommand] - public async Task FetchApiDataAsync() - { - if (string.IsNullOrWhiteSpace(ApiUrl)) return; - - IsFetchingApi = true; - ApiHasError = false; - ApiStatusMessage = "Connecting to API endpoint..."; - - try - { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(ApiAuthBearer)) - { - headers["Authorization"] = ApiAuthBearer.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) - ? ApiAuthBearer - : $"Bearer {ApiAuthBearer}"; - } - if (!string.IsNullOrWhiteSpace(ApiKeyHeader) && !string.IsNullOrWhiteSpace(ApiKeyValue)) - { - headers[ApiKeyHeader] = ApiKeyValue; - } - - var matrix = await _dataSourceService.FetchFromRestApiAsync(ApiUrl, headers, ApiJsonPath); - if (matrix.RowCount == 0) - { - ApiHasError = true; - ApiStatusMessage = "API returned 0 rows or empty structure."; - } - else - { - SetMatrix(matrix); - ApiStatusMessage = $"Successfully fetched {matrix.RowCount} rows from API!"; - } - } - catch (Exception ex) - { - ApiHasError = true; - ApiStatusMessage = $"API Error: {ex.Message}"; - } - finally - { - IsFetchingApi = false; - } - } - - [RelayCommand] - public async Task PasteFromClipboardAsync() - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && - desktop.MainWindow?.Clipboard != null) - { - string? text = await desktop.MainWindow.Clipboard.TryGetTextAsync(); - if (!string.IsNullOrWhiteSpace(text)) - { - RawTextInput = text; - var matrix = _dataSourceService.ParseCsv(text, null, FirstRowIsHeader); - SetMatrix(matrix); - } - } - } - - [RelayCommand] - public void ParseRawTextInput() - { - if (!string.IsNullOrWhiteSpace(RawTextInput)) - { - var matrix = _dataSourceService.ParseCsv(RawTextInput, null, FirstRowIsHeader); - SetMatrix(matrix); - } - } - - [RelayCommand] - public void AddRow() - { - var cells = Enumerable.Range(1, CurrentMatrix.ColumnCount).Select(c => $"New {GridRows.Count + 1}.{c}"); - CurrentMatrix.AddRow(cells); - SetMatrix(CurrentMatrix); - } - - [RelayCommand] - public void RemoveRow() - { - if (CurrentMatrix.RowCount > 1) - { - CurrentMatrix.RemoveRow(CurrentMatrix.RowCount - 1); - SetMatrix(CurrentMatrix); - } - } - - [RelayCommand] - public void AddColumn() - { - CurrentMatrix.AddColumn($"Col {CurrentMatrix.ColumnCount + 1}"); - SetMatrix(CurrentMatrix); - } - - [RelayCommand] - public void RemoveColumn() - { - if (CurrentMatrix.ColumnCount > 1) - { - CurrentMatrix.RemoveColumn(CurrentMatrix.ColumnCount - 1); - SetMatrix(CurrentMatrix); - } - } - - private void UpdateChartPreview() - { - if (CurrentMatrix.RowCount == 0 || CurrentMatrix.ColumnCount == 0) - { - ChartPreviewBitmap = null; - return; - } - - try - { - var previewChart = new PdfChartElement - { - Width = 360, - Height = 200, - Title = TargetChart?.Title ?? "Data Preview", - ChartType = TargetChart?.ChartType ?? ChartType.BarColumn, - Palette = TargetChart?.Palette ?? ChartPalette.CorporateBlue, - LegendPosition = ChartLegendPosition.Top - }; - - int catCol = SelectedCategoryColumnIndex; - var valCols = AvailableColumns.Where(c => c.IsSelected && c.Index != catCol).Select(c => c.Index).ToList(); - if (valCols.Count == 0) - { - valCols.Add(catCol == 0 ? Math.Min(1, CurrentMatrix.ColumnCount - 1) : 0); - } - - _dataBindingService.ApplyToChart(CurrentMatrix, previewChart, catCol, valCols); - - byte[] png = LiveChartsRenderer.RenderChartToPngBytes(previewChart, 360, 200, 2.0f); - if (png != null && png.Length > 0) - { - using var ms = new MemoryStream(png); - ChartPreviewBitmap = new Bitmap(ms); - } - } - catch - { - ChartPreviewBitmap = null; - } - } - - [RelayCommand] - public void ApplyData() - { - if (CurrentMatrix.RowCount == 0 || CurrentMatrix.ColumnCount == 0) - { - IsOpen = false; - return; - } - - int catCol = SelectedCategoryColumnIndex; - var valCols = AvailableColumns.Where(c => c.IsSelected && c.Index != catCol).Select(c => c.Index).ToList(); - if (valCols.Count == 0) - { - valCols.Add(catCol == 0 ? Math.Min(1, CurrentMatrix.ColumnCount - 1) : 0); - } - - if (TargetMode == "Chart" && TargetChart != null) - { - var oldModel = (PdfChartElement)TargetChart.ToModel(); - var newModel = (PdfChartElement)oldModel.Clone(); - - _dataBindingService.ApplyToChart(CurrentMatrix, newModel, catCol, valCols); - - TargetChart.LoadFromModel(newModel); - UndoRedo?.RecordAction( - "Update Chart Data", - () => TargetChart.LoadFromModel(oldModel), - () => TargetChart.LoadFromModel(newModel) - ); - } - else if (TargetMode == "Table" && TargetTable != null) - { - var oldModel = (PdfTableElement)TargetTable.ToModel(); - var newModel = (PdfTableElement)oldModel.Clone(); - - _dataBindingService.ApplyToTable(CurrentMatrix, newModel); - - TargetTable.LoadFromModel(newModel); - UndoRedo?.RecordAction( - "Update Table Data", - () => TargetTable.LoadFromModel(oldModel), - () => TargetTable.LoadFromModel(newModel) - ); - } - else if (TargetMode == "NewChart" && TargetPage != null) - { - var newChartModel = new PdfChartElement - { - X = 50, - Y = 150, - Width = 420, - Height = 240, - Title = "Imported Data Analysis", - ChartType = ChartType.BarColumn, - Palette = ChartPalette.CorporateBlue - }; - _dataBindingService.ApplyToChart(CurrentMatrix, newChartModel, catCol, valCols); - - var chartVm = new ChartElementViewModel(); - chartVm.LoadFromModel(newChartModel); - OnElementCreated?.Invoke(chartVm, "Imported Chart Data"); - } - else if (TargetMode == "NewTable" && TargetPage != null) - { - var newTableModel = new PdfTableElement - { - X = 50, - Y = 150, - Width = 450, - Height = 35 + (CurrentMatrix.RowCount * 26) - }; - _dataBindingService.ApplyToTable(CurrentMatrix, newTableModel); - - var tableVm = new TableElementViewModel(); - tableVm.LoadFromModel(newTableModel); - OnElementCreated?.Invoke(tableVm, "Imported Table Data"); - } - - IsOpen = false; - } - - [RelayCommand] - public async Task ExportToCsvAsync() - { - if (StorageProvider == null || CurrentMatrix.RowCount == 0) return; - - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Export Data as CSV", - DefaultExtension = "csv", - SuggestedFileName = "dataset_export.csv", - FileTypeChoices = new[] - { - new FilePickerFileType("CSV (Comma Delimited) (*.csv)") { Patterns = new[] { "*.csv" } } - } - }); - - if (file != null) - { - string csvContent = _dataSourceService.FormatAsCsv(CurrentMatrix); - await File.WriteAllTextAsync(file.Path.LocalPath, csvContent); - } - } - - [RelayCommand] - public void OpenBatchMerge() - { - IsOpen = false; - OpenBatchMergeRequested?.Invoke(CurrentMatrix); - } -} diff --git a/src/PdfEditorApp/ViewModels/DynamicInspectorSectionViewModel.cs b/src/PdfEditorApp/ViewModels/DynamicInspectorSectionViewModel.cs deleted file mode 100644 index a1eeeb2..0000000 --- a/src/PdfEditorApp/ViewModels/DynamicInspectorSectionViewModel.cs +++ /dev/null @@ -1,27 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace PdfEditorApp.ViewModels; - -/// -/// Presentation model for a dynamic inspector property panel section contributed by a plugin. -/// -public partial class DynamicInspectorSectionViewModel : ViewModelBase -{ - [ObservableProperty] - private string _sectionId = ""; - - [ObservableProperty] - private string _title = ""; - - [ObservableProperty] - private string _iconKind = "Tune"; - - [ObservableProperty] - private int _order = 100; - - [ObservableProperty] - private object? _content; - - [ObservableProperty] - private bool _isExpanded = true; -} diff --git a/src/PdfEditorApp/ViewModels/DynamicRibbonGroupViewModel.cs b/src/PdfEditorApp/ViewModels/DynamicRibbonGroupViewModel.cs deleted file mode 100644 index 9ae2ed2..0000000 --- a/src/PdfEditorApp/ViewModels/DynamicRibbonGroupViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.ObjectModel; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.ViewModels; - -/// -/// Presentation model for a dynamic ribbon group containing contributed actions. -/// -public sealed class DynamicRibbonGroupViewModel -{ - public required string Id { get; init; } - public required string TabId { get; init; } - public required string Title { get; init; } - public ObservableCollection Actions { get; } = new(); -} diff --git a/src/PdfEditorApp/ViewModels/DynamicRibbonTabViewModel.cs b/src/PdfEditorApp/ViewModels/DynamicRibbonTabViewModel.cs deleted file mode 100644 index 7c74461..0000000 --- a/src/PdfEditorApp/ViewModels/DynamicRibbonTabViewModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.ViewModels; - -/// -/// Presentation wrapper for dynamic plugin ribbon tabs. -/// -public sealed partial class DynamicRibbonTabViewModel : ViewModelBase -{ - public required RibbonTabDescriptor Descriptor { get; init; } - public string Id => Descriptor.Id; - public string Title => Descriptor.Title; - - [ObservableProperty] - private bool _isActive; -} diff --git a/src/PdfEditorApp/ViewModels/DynamicSidebarTabViewModel.cs b/src/PdfEditorApp/ViewModels/DynamicSidebarTabViewModel.cs deleted file mode 100644 index 83ea888..0000000 --- a/src/PdfEditorApp/ViewModels/DynamicSidebarTabViewModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.ViewModels; - -/// -/// Presentation wrapper for dynamic plugin sidebar tabs. -/// -public sealed partial class DynamicSidebarTabViewModel : ViewModelBase -{ - public required SidebarTabDescriptor Descriptor { get; init; } - public string Id => Descriptor.Id; - public string Title => Descriptor.Title; - public string IconKind => Descriptor.IconKind; - public string Tooltip => Descriptor.Tooltip; - - [ObservableProperty] - private bool _isActive; -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/BarcodeElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/BarcodeElementViewModel.cs deleted file mode 100644 index 51c4678..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/BarcodeElementViewModel.cs +++ /dev/null @@ -1,83 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class BarcodeElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _codeValue = "DOC-2026-984210"; - - [ObservableProperty] - private string _barcodeFormat = "Code128"; - - [ObservableProperty] - private string _barColorHex = "#0F172A"; - - [ObservableProperty] - private string _backgroundColorHex = "#FFFFFF"; - - [ObservableProperty] - private bool _showText = true; - - [RelayCommand] - public void SetFormat(string formatStr) - { - BarcodeFormat = formatStr; - } - - public override ElementKind Kind => ElementKind.Barcode; - public override string DisplayName => $"Barcode ({CodeValue})"; - - public BarcodeElementViewModel() - { - Width = 220; - Height = 70; - } - - public override PdfElementBase ToModel() - { - return new PdfBarcodeElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - CodeValue = CodeValue, - BarcodeFormat = BarcodeFormat, - BarColorHex = BarColorHex, - BackgroundColorHex = BackgroundColorHex, - ShowText = ShowText - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfBarcodeElement barcode) - { - Id = barcode.Id; - X = barcode.X; - Y = barcode.Y; - Width = barcode.Width; - Height = barcode.Height; - ZIndex = barcode.ZIndex; - Rotation = barcode.Rotation; - Opacity = barcode.Opacity; - IsLocked = barcode.IsLocked; - - CodeValue = barcode.CodeValue; - BarcodeFormat = barcode.BarcodeFormat; - BarColorHex = barcode.BarColorHex; - BackgroundColorHex = barcode.BackgroundColorHex; - ShowText = barcode.ShowText; - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/ChartElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/ChartElementViewModel.cs deleted file mode 100644 index 46df4b3..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/ChartElementViewModel.cs +++ /dev/null @@ -1,710 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using LiveChartsCore; -using LiveChartsCore.Defaults; -using LiveChartsCore.Drawing; -using LiveChartsCore.Measure; -using LiveChartsCore.SkiaSharpView; -using LiveChartsCore.SkiaSharpView.Painting; -using LiveChartsCore.SkiaSharpView.VisualElements; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using SkiaSharp; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public class ChartBarItem : ObservableObject -{ - private string _category = "Q1"; - public string Category - { - get => _category; - set => SetProperty(ref _category, value); - } - - private double _value = 1.0; - public double Value - { - get => _value; - set - { - if (SetProperty(ref _value, value)) - { - OnPropertyChanged(nameof(BarHeightPx)); - } - } - } - - private string _valueLabel = "$1.0B"; - public string ValueLabel - { - get => _valueLabel; - set => SetProperty(ref _valueLabel, value); - } - - private string _colorHex = "#0F6CBD"; - public string ColorHex - { - get => _colorHex; - set => SetProperty(ref _colorHex, value); - } - - public double BarHeightPx => Math.Max(10, Value * 35); -} - -public partial class ChartElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _title = "Revenue Growth (Q1-Q4)"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsBarColumn))] - [NotifyPropertyChangedFor(nameof(IsHorizontalBar))] - [NotifyPropertyChangedFor(nameof(IsLine))] - [NotifyPropertyChangedFor(nameof(IsSmoothLine))] - [NotifyPropertyChangedFor(nameof(IsArea))] - [NotifyPropertyChangedFor(nameof(IsDonutPie))] - [NotifyPropertyChangedFor(nameof(IsStackedBar))] - [NotifyPropertyChangedFor(nameof(IsStackedHorizontalBar))] - [NotifyPropertyChangedFor(nameof(IsRadar))] - [NotifyPropertyChangedFor(nameof(IsPolarArea))] - [NotifyPropertyChangedFor(nameof(IsFunnel))] - [NotifyPropertyChangedFor(nameof(IsWaterfall))] - [NotifyPropertyChangedFor(nameof(IsGaugeProgress))] - [NotifyPropertyChangedFor(nameof(IsStepLine))] - [NotifyPropertyChangedFor(nameof(IsPyramid))] - [NotifyPropertyChangedFor(nameof(IsScatterPlot))] - [NotifyPropertyChangedFor(nameof(IsCandlestick))] - [NotifyPropertyChangedFor(nameof(IsCartesianChart))] - [NotifyPropertyChangedFor(nameof(IsPieChart))] - [NotifyPropertyChangedFor(nameof(IsPolarChart))] - [NotifyPropertyChangedFor(nameof(ChartTypeDescription))] - private ChartType _chartType = ChartType.BarColumn; - - public bool IsBarColumn => ChartType == ChartType.BarColumn; - public bool IsHorizontalBar => ChartType == ChartType.HorizontalBar; - public bool IsLine => ChartType == ChartType.Line; - public bool IsSmoothLine => ChartType == ChartType.SmoothLine; - public bool IsArea => ChartType == ChartType.Area; - public bool IsDonutPie => ChartType == ChartType.DonutPie; - public bool IsStackedBar => ChartType == ChartType.StackedBar; - public bool IsStackedHorizontalBar => ChartType == ChartType.StackedHorizontalBar; - public bool IsRadar => ChartType == ChartType.Radar; - public bool IsPolarArea => ChartType == ChartType.PolarArea; - public bool IsFunnel => ChartType == ChartType.Funnel; - public bool IsWaterfall => ChartType == ChartType.Waterfall; - public bool IsGaugeProgress => ChartType == ChartType.GaugeProgress; - public bool IsStepLine => ChartType == ChartType.StepLine; - public bool IsPyramid => ChartType == ChartType.Pyramid; - public bool IsScatterPlot => ChartType == ChartType.ScatterPlot; - public bool IsCandlestick => ChartType == ChartType.Candlestick; - - public bool IsPieChart => ChartType == ChartType.DonutPie || ChartType == ChartType.GaugeProgress; - public bool IsPolarChart => ChartType == ChartType.Radar || ChartType == ChartType.PolarArea; - public bool IsCartesianChart => !IsPieChart && !IsPolarChart; - - public string ChartTypeDescription => ChartType.ToString(); - - [ObservableProperty] - private ChartPalette _palette = ChartPalette.CorporateBlue; - - [ObservableProperty] - private ChartLegendPosition _legendPosition = ChartLegendPosition.Top; - - [ObservableProperty] - private bool _showDataLabels = true; - - [ObservableProperty] - private bool _showGridlines = true; - - [ObservableProperty] - private double _donutHoleRatio = 0.6; - - [ObservableProperty] - private double _curveSmoothness = 0.65; - - [ObservableProperty] - private double _strokeThickness = 3.0; - - [ObservableProperty] - private string _backgroundColorHex = "#FAFAFA"; - - [ObservableProperty] - private string _borderColorHex = "#E2E8F0"; - - public ObservableCollection Bars { get; } = new(); - - // LiveCharts2 Series & Axes - [ObservableProperty] - private ISeries[] _cartesianSeries = Array.Empty(); - - [ObservableProperty] - private ISeries[] _pieSeries = Array.Empty(); - - [ObservableProperty] - private ISeries[] _polarSeries = Array.Empty(); - - [ObservableProperty] - private Axis[] _xAxes = Array.Empty(); - - [ObservableProperty] - private Axis[] _yAxes = Array.Empty(); - - [ObservableProperty] - private PolarAxis[] _angleAxes = Array.Empty(); - - [ObservableProperty] - private PolarAxis[] _radiusAxes = Array.Empty(); - - [ObservableProperty] - private LiveChartsCore.Measure.LegendPosition _liveLegendPosition = LiveChartsCore.Measure.LegendPosition.Top; - - [ObservableProperty] - private double _pieMaxAngle = 360; - - [ObservableProperty] - private double _pieInitialRotation = -90; - - [ObservableProperty] - private Avalonia.Media.Imaging.Bitmap? _chartBitmap; - - private Avalonia.Media.Imaging.Bitmap? _previousChartBitmap; - - /// Disposes the outgoing bitmap whenever a new one is rasterized — - /// is re-rasterized on nearly every property change with no other lifecycle owner, so without this - /// the old native bitmap leaks. - partial void OnChartBitmapChanged(Avalonia.Media.Imaging.Bitmap? value) - { - if (_previousChartBitmap != null && _previousChartBitmap != value) - { - _previousChartBitmap.Dispose(); - } - _previousChartBitmap = value; - } - - public override ElementKind Kind => ElementKind.Chart; - public override string DisplayName => Title; - - public ChartElementViewModel() - { - LiveChartsRenderer.EnsureConfigured(); - - // Default sample bars - Bars.Add(new ChartBarItem { Category = "Q1", Value = 1.2, ValueLabel = "$1.2B", ColorHex = "#93C5FD" }); - Bars.Add(new ChartBarItem { Category = "Q2", Value = 1.8, ValueLabel = "$1.8B", ColorHex = "#60A5FA" }); - Bars.Add(new ChartBarItem { Category = "Q3", Value = 2.5, ValueLabel = "$2.5B", ColorHex = "#3B82F6" }); - Bars.Add(new ChartBarItem { Category = "Q4", Value = 3.1, ValueLabel = "$3.1B", ColorHex = "#0F6CBD" }); - - Bars.CollectionChanged += (s, e) => { if (!_suppressChartUpdate) UpdateLiveChart(); }; - PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(Width) || e.PropertyName == nameof(Height)) - { - // Width/Height fire on every pointer-move during an interactive resize drag — - // rasterizing a fresh chart bitmap on every one of those would make resize feel - // like it's dragging through mud. Coalesce to roughly one rasterize per settle. - RequestChartUpdate(); - } - }; - UpdateLiveChart(); - } - - private bool _suppressChartUpdate; - private CancellationTokenSource? _chartUpdateDebounceCts; - - private void RequestChartUpdate() - { - _chartUpdateDebounceCts?.Cancel(); - _chartUpdateDebounceCts = new CancellationTokenSource(); - var token = _chartUpdateDebounceCts.Token; - - _ = Task.Run(async () => - { - try - { - await Task.Delay(120, token); - } - catch (TaskCanceledException) - { - return; - } - if (token.IsCancellationRequested) return; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - if (!token.IsCancellationRequested) - { - UpdateLiveChart(); - } - }); - }, token); - } - - partial void OnChartTypeChanged(ChartType value) => UpdateLiveChart(); - partial void OnPaletteChanged(ChartPalette value) => UpdateLiveChart(); - partial void OnLegendPositionChanged(ChartLegendPosition value) => UpdateLiveChart(); - partial void OnShowDataLabelsChanged(bool value) => UpdateLiveChart(); - partial void OnShowGridlinesChanged(bool value) => UpdateLiveChart(); - partial void OnDonutHoleRatioChanged(double value) => UpdateLiveChart(); - partial void OnCurveSmoothnessChanged(double value) => UpdateLiveChart(); - partial void OnStrokeThicknessChanged(double value) => UpdateLiveChart(); - partial void OnTitleChanged(string value) => UpdateLiveChart(); - - [RelayCommand] - public void SetChartType(string typeStr) - { - if (Enum.TryParse(typeStr, true, out var type)) - { - ChartType = type; - } - } - - [RelayCommand] - public void SetPalette(string paletteStr) - { - if (Enum.TryParse(paletteStr, true, out var pal)) - { - Palette = pal; - } - } - - [RelayCommand] - public void SetLegendPosition(string posStr) - { - if (Enum.TryParse(posStr, true, out var pos)) - { - LegendPosition = pos; - } - } - - [RelayCommand] - public void AddDataPoint() - { - int nextNum = Bars.Count + 1; - var palette = LiveChartsRenderer.GetPaletteHexColors(Palette); - string color = palette[(nextNum - 1) % palette.Count]; - double val = Math.Round(1.0 + (nextNum * 0.5), 1); - - _suppressChartUpdate = true; - Bars.Add(new ChartBarItem - { - Category = $"Q{nextNum}", - Value = val, - ValueLabel = $"${val:F1}B", - ColorHex = color - }); - _suppressChartUpdate = false; - OnPropertyChanged(nameof(Bars)); - UpdateLiveChart(); - } - - [RelayCommand] - public void RemoveDataPoint() - { - if (Bars.Count > 1) - { - _suppressChartUpdate = true; - Bars.RemoveAt(Bars.Count - 1); - _suppressChartUpdate = false; - OnPropertyChanged(nameof(Bars)); - UpdateLiveChart(); - } - } - - public void UpdateLiveChart() - { - var palette = LiveChartsRenderer.GetPaletteHexColors(Palette); - LiveLegendPosition = LegendPosition switch - { - ChartLegendPosition.Top => LiveChartsCore.Measure.LegendPosition.Top, - ChartLegendPosition.Bottom => LiveChartsCore.Measure.LegendPosition.Bottom, - ChartLegendPosition.Left => LiveChartsCore.Measure.LegendPosition.Left, - ChartLegendPosition.Right => LiveChartsCore.Measure.LegendPosition.Right, - _ => LiveChartsCore.Measure.LegendPosition.Hidden - }; - - var categories = Bars.Select(b => b.Category).ToArray(); - var values = Bars.Select(b => b.Value).ToArray(); - - // 1. Cartesian Series - var cSeries = new List(); - switch (ChartType) - { - case ChartType.HorizontalBar: - { - var color = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - cSeries.Add(new RowSeries - { - Name = Title, - Values = values, - Fill = new SolidColorPaint(color), - Stroke = null, - DataLabelsPaint = ShowDataLabels ? new SolidColorPaint(SKColors.DarkSlateGray) : null - }); - break; - } - - case ChartType.StackedBar: - { - for (int i = 0; i < values.Length; i++) - { - var color = LiveChartsRenderer.ParseColor(i < Bars.Count ? Bars[i].ColorHex : palette[i % palette.Count], SKColors.RoyalBlue); - cSeries.Add(new StackedColumnSeries - { - Name = i < categories.Length ? categories[i] : $"Segment {i + 1}", - Values = new double[] { values[i] }, - Fill = new SolidColorPaint(color), - Stroke = null - }); - } - break; - } - - case ChartType.StackedHorizontalBar: - { - var color = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - cSeries.Add(new StackedRowSeries - { - Name = Title, - Values = values, - Fill = new SolidColorPaint(color), - Stroke = null - }); - break; - } - - case ChartType.Line: - { - var color = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - cSeries.Add(new LineSeries - { - Name = Title, - Values = values, - Stroke = new SolidColorPaint(color, (float)StrokeThickness), - Fill = null, - GeometrySize = 7, - GeometryStroke = new SolidColorPaint(color, 2), - GeometryFill = new SolidColorPaint(SKColors.White), - LineSmoothness = 0.0 - }); - break; - } - - case ChartType.SmoothLine: - { - var color = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - cSeries.Add(new LineSeries - { - Name = Title, - Values = values, - Stroke = new SolidColorPaint(color, (float)StrokeThickness), - Fill = null, - GeometrySize = 8, - GeometryStroke = new SolidColorPaint(color, 2), - GeometryFill = new SolidColorPaint(SKColors.White), - LineSmoothness = (float)CurveSmoothness - }); - break; - } - - case ChartType.Area: - { - var color = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - var fillColor = color.WithAlpha(65); - cSeries.Add(new LineSeries - { - Name = Title, - Values = values, - Stroke = new SolidColorPaint(color, (float)StrokeThickness), - Fill = new SolidColorPaint(fillColor), - GeometrySize = 7, - GeometryStroke = new SolidColorPaint(color, 2), - GeometryFill = new SolidColorPaint(SKColors.White), - LineSmoothness = (float)CurveSmoothness - }); - break; - } - - case ChartType.StepLine: - { - var color = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - cSeries.Add(new StepLineSeries - { - Name = Title, - Values = values, - Stroke = new SolidColorPaint(color, (float)StrokeThickness), - Fill = null, - GeometrySize = 7, - GeometryStroke = new SolidColorPaint(color, 2), - GeometryFill = new SolidColorPaint(SKColors.White) - }); - break; - } - - case ChartType.ScatterPlot: - { - var color = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - var points = new List(); - for (int i = 0; i < values.Length; i++) - { - points.Add(new ObservablePoint(i + 1, values[i])); - } - cSeries.Add(new ScatterSeries - { - Name = Title, - Values = points, - Stroke = new SolidColorPaint(color, 2), - Fill = new SolidColorPaint(color.WithAlpha(180)), - GeometrySize = 12 - }); - break; - } - - case ChartType.Candlestick: - { - var financialPoints = new List(); - for (int i = 0; i < values.Length; i++) - { - double close = values[i]; - double open = i > 0 ? values[i - 1] : close * 0.95; - double high = Math.Max(open, close) * 1.08; - double low = Math.Min(open, close) * 0.92; - financialPoints.Add(new FinancialPoint(new DateTime(2026, 1, 1).AddDays(i), high, open, close, low)); - } - cSeries.Add(new CandlesticksSeries - { - Name = Title, - Values = financialPoints, - UpStroke = new SolidColorPaint(SKColors.ForestGreen, 2), - UpFill = new SolidColorPaint(SKColors.ForestGreen.WithAlpha(200)), - DownStroke = new SolidColorPaint(SKColors.IndianRed, 2), - DownFill = new SolidColorPaint(SKColors.IndianRed.WithAlpha(200)) - }); - break; - } - - case ChartType.Waterfall: - case ChartType.Funnel: - case ChartType.Pyramid: - case ChartType.BarColumn: - default: - { - var color = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - cSeries.Add(new ColumnSeries - { - Name = Title, - Values = values, - Fill = new SolidColorPaint(color), - Stroke = null, - Rx = 4, - Ry = 4, - DataLabelsPaint = ShowDataLabels ? new SolidColorPaint(SKColors.DarkSlateGray) : null - }); - break; - } - } - CartesianSeries = cSeries.ToArray(); - - // Cartesian Axes - XAxes = new Axis[] - { - new Axis - { - Labels = categories, - LabelsPaint = new SolidColorPaint(SKColors.DarkSlateGray), - TextSize = 11, - SeparatorsPaint = ShowGridlines ? new SolidColorPaint(SKColors.LightGray.WithAlpha(110)) { StrokeThickness = 1 } : null - } - }; - - YAxes = new Axis[] - { - new Axis - { - LabelsPaint = new SolidColorPaint(SKColors.DarkSlateGray), - TextSize = 11, - SeparatorsPaint = ShowGridlines ? new SolidColorPaint(SKColors.LightGray.WithAlpha(110)) { StrokeThickness = 1 } : null - } - }; - - // 2. Pie & Donut Series - var pSeries = new List(); - double innerRadius = ChartType == ChartType.DonutPie ? Math.Max(0.1, DonutHoleRatio) : 0.0; - PieMaxAngle = ChartType == ChartType.GaugeProgress ? 180 : 360; - PieInitialRotation = ChartType == ChartType.GaugeProgress ? -180 : -90; - - for (int i = 0; i < values.Length; i++) - { - var color = LiveChartsRenderer.ParseColor(i < Bars.Count ? Bars[i].ColorHex : palette[i % palette.Count], SKColors.RoyalBlue); - string name = i < categories.Length ? categories[i] : $"Segment {i + 1}"; - string valLabel = i < Bars.Count && !string.IsNullOrEmpty(Bars[i].ValueLabel) ? Bars[i].ValueLabel : values[i].ToString("G"); - - pSeries.Add(new PieSeries - { - Name = $"{name} ({valLabel})", - Values = new double[] { values[i] }, - Fill = new SolidColorPaint(color), - Stroke = new SolidColorPaint(SKColors.White, 2), - InnerRadius = innerRadius, - DataLabelsPaint = ShowDataLabels ? new SolidColorPaint(SKColors.White) : null - }); - } - PieSeries = pSeries.ToArray(); - - // 3. Polar & Radar Series - var polSeries = new List(); - var polColor = LiveChartsRenderer.ParseColor(Bars.FirstOrDefault()?.ColorHex ?? palette[0], SKColors.RoyalBlue); - bool isArea = ChartType == ChartType.PolarArea; - - polSeries.Add(new PolarLineSeries - { - Name = Title, - Values = values, - Stroke = new SolidColorPaint(polColor, (float)StrokeThickness), - Fill = isArea ? new SolidColorPaint(polColor.WithAlpha(90)) : null, - GeometrySize = 7, - GeometryStroke = new SolidColorPaint(polColor, 2), - GeometryFill = new SolidColorPaint(SKColors.White), - IsClosed = true - }); - PolarSeries = polSeries.ToArray(); - - AngleAxes = new PolarAxis[] - { - new PolarAxis - { - Labels = categories, - LabelsPaint = new SolidColorPaint(SKColors.DarkSlateGray), - TextSize = 10, - SeparatorsPaint = ShowGridlines ? new SolidColorPaint(SKColors.LightGray.WithAlpha(120)) { StrokeThickness = 1 } : null - } - }; - - RadiusAxes = new PolarAxis[] - { - new PolarAxis - { - LabelsPaint = new SolidColorPaint(SKColors.Gray), - TextSize = 9, - SeparatorsPaint = ShowGridlines ? new SolidColorPaint(SKColors.LightGray.WithAlpha(100)) { StrokeThickness = 1 } : null - } - }; - - // Render high-DPI live bitmap for native Avalonia canvas - try - { - var model = (PdfChartElement)ToModel(); - int renderW = Math.Max(200, (int)Width); - int renderH = Math.Max(120, (int)Height); - byte[] pngBytes = LiveChartsRenderer.RenderChartToPngBytes(model, renderW, renderH, 2.0f); - if (pngBytes != null && pngBytes.Length > 0) - { - using var ms = new System.IO.MemoryStream(pngBytes); - ChartBitmap = new Avalonia.Media.Imaging.Bitmap(ms); - } - } - catch - { - // Fallback for edge cases - } - } - - public List MultiSeries { get; set; } = new(); - - public override PdfElementBase ToModel() - { - var model = new PdfChartElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - Title = Title, - ChartType = ChartType, - Palette = Palette, - LegendPosition = LegendPosition, - ShowDataLabels = ShowDataLabels, - ShowGridlines = ShowGridlines, - DonutHoleRatio = DonutHoleRatio, - CurveSmoothness = CurveSmoothness, - StrokeThickness = StrokeThickness, - BackgroundColorHex = BackgroundColorHex, - BorderColorHex = BorderColorHex, - Categories = new List(), - Values = new List(), - ValueLabels = new List(), - BarColorsHex = new List(), - MultiSeries = MultiSeries.Select(s => s.Clone()).ToList() - }; - - foreach (var bar in Bars) - { - model.Categories.Add(bar.Category); - model.Values.Add(bar.Value); - model.ValueLabels.Add(bar.ValueLabel); - model.BarColorsHex.Add(bar.ColorHex); - } - - return model; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfChartElement chart) - { - Id = chart.Id; - X = chart.X; - Y = chart.Y; - Width = chart.Width; - Height = chart.Height; - ZIndex = chart.ZIndex; - Rotation = chart.Rotation; - Opacity = chart.Opacity; - IsLocked = chart.IsLocked; - - Title = chart.Title; - ChartType = chart.ChartType; - Palette = chart.Palette; - LegendPosition = chart.LegendPosition; - ShowDataLabels = chart.ShowDataLabels; - ShowGridlines = chart.ShowGridlines; - DonutHoleRatio = chart.DonutHoleRatio; - CurveSmoothness = chart.CurveSmoothness; - StrokeThickness = chart.StrokeThickness; - BackgroundColorHex = chart.BackgroundColorHex; - BorderColorHex = chart.BorderColorHex; - MultiSeries = chart.MultiSeries?.Select(s => s.Clone()).ToList() ?? new(); - - // Adding N bars one at a time would otherwise fire UpdateLiveChart() (and a full - // chart rasterization) once per bar — suppress until the whole set has loaded, - // then rasterize exactly once. - _suppressChartUpdate = true; - Bars.Clear(); - for (int i = 0; i < chart.Categories.Count; i++) - { - Bars.Add(new ChartBarItem - { - Category = chart.Categories[i], - Value = i < chart.Values.Count ? chart.Values[i] : 1.0, - ValueLabel = i < chart.ValueLabels.Count ? chart.ValueLabels[i] : "", - ColorHex = i < chart.BarColorsHex.Count ? chart.BarColorsHex[i] : "#0F6CBD" - }); - } - _suppressChartUpdate = false; - UpdateLiveChart(); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/DividerElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/DividerElementViewModel.cs deleted file mode 100644 index d8dc449..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/DividerElementViewModel.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.ComponentModel; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class DividerElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _colorHex = "#0F6CBD"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - [NotifyPropertyChangedFor(nameof(DashArrayString))] - private double _thickness = 2; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private bool _isVertical; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private DividerStyle _style = DividerStyle.Straight; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _waveAmplitude = 6.0; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _waveFrequency = 4.0; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(DashArrayString))] - private LineDashStyle _dashStyle = LineDashStyle.Solid; - - public override ElementKind Kind => ElementKind.Divider; - public override string DisplayName => $"Divider ({Style})"; - - public string PathData => SvgShapeHelper.GenerateDividerSvgPath(Style, Width, Height > 0 ? Height : 16, WaveAmplitude, WaveFrequency, IsVertical); - public string? DashArrayString => SvgShapeHelper.GetDashArray(DashStyle, Thickness); - - protected override void OnPropertyChanged(PropertyChangedEventArgs e) - { - base.OnPropertyChanged(e); - if (e.PropertyName is nameof(Width) or nameof(Height) or nameof(Thickness) or nameof(Style) or nameof(WaveAmplitude) or nameof(WaveFrequency) or nameof(IsVertical) or nameof(DashStyle)) - { - OnPropertyChanged(nameof(PathData)); - OnPropertyChanged(nameof(DashArrayString)); - } - } - - public override PdfElementBase ToModel() - { - return new PdfDividerElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - ColorHex = ColorHex, - Thickness = Thickness, - IsVertical = IsVertical, - Style = Style, - WaveAmplitude = WaveAmplitude, - WaveFrequency = WaveFrequency, - DashStyle = DashStyle - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfDividerElement div) - { - Id = div.Id; - X = div.X; - Y = div.Y; - Width = div.Width; - Height = div.Height; - ZIndex = div.ZIndex; - Rotation = div.Rotation; - Opacity = div.Opacity; - IsLocked = div.IsLocked; - - ColorHex = div.ColorHex; - Thickness = div.Thickness; - IsVertical = div.IsVertical; - Style = div.Style; - WaveAmplitude = div.WaveAmplitude; - WaveFrequency = div.WaveFrequency; - DashStyle = div.DashStyle; - } - } -} - diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/ElementViewModelBase.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/ElementViewModelBase.cs deleted file mode 100644 index 97838f9..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/ElementViewModelBase.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public abstract partial class ElementViewModelBase : ViewModelBase -{ - [ObservableProperty] - private string _id = Guid.NewGuid().ToString("N"); - - [ObservableProperty] - private double _x; - - [ObservableProperty] - private double _y; - - [ObservableProperty] - private double _width = 200; - - [ObservableProperty] - private double _height = 100; - - [ObservableProperty] - private int _zIndex = 0; - - [ObservableProperty] - private double _rotation = 0; - - [ObservableProperty] - private double _opacity = 1.0; - - [ObservableProperty] - private bool _isSelected; - - [ObservableProperty] - private bool _isInEditMode; - - partial void OnIsInEditModeChanged(bool value) - { - OnEditModeChanged(value); - } - - protected virtual void OnEditModeChanged(bool isInEditMode) - { - } - - [ObservableProperty] - private bool _isLocked; - - [ObservableProperty] - private string? _groupId; - - public abstract ElementKind Kind { get; } - public abstract string DisplayName { get; } - - public abstract PdfElementBase ToModel(); - public abstract void LoadFromModel(PdfElementBase model); - - protected void CopyBasePropertiesTo(PdfElementBase model) - { - model.Id = Id; - model.X = X; - model.Y = Y; - model.Width = Width; - model.Height = Height; - model.ZIndex = ZIndex; - model.Rotation = Rotation; - model.Opacity = Opacity; - model.IsLocked = IsLocked; - model.GroupId = GroupId; - } - - protected void PopulateBaseProperties(PdfElementBase model) - { - Id = model.Id; - X = model.X; - Y = model.Y; - Width = model.Width; - Height = model.Height; - ZIndex = model.ZIndex; - Rotation = model.Rotation; - Opacity = model.Opacity; - IsLocked = model.IsLocked; - GroupId = model.GroupId; - } - - public virtual void MoveBy(double deltaX, double deltaY, double canvasWidth, double canvasHeight) - { - if (IsLocked) return; - - double newX = Math.Max(0, Math.Min(canvasWidth - Width, X + deltaX)); - double newY = Math.Max(0, Math.Min(canvasHeight - Height, Y + deltaY)); - - X = Math.Round(newX, 1); - Y = Math.Round(newY, 1); - } - - public virtual void Resize(string handle, double deltaX, double deltaY, double minW = 30, double minH = 20) - { - if (IsLocked) return; - - switch (handle.ToLowerInvariant()) - { - case "topleft": - double newW = Math.Max(minW, Width - deltaX); - double newH = Math.Max(minH, Height - deltaY); - X += Width - newW; - Y += Height - newH; - Width = newW; - Height = newH; - break; - - case "top": - double newTopH = Math.Max(minH, Height - deltaY); - Y += Height - newTopH; - Height = newTopH; - break; - - case "topright": - Width = Math.Max(minW, Width + deltaX); - double newTRH = Math.Max(minH, Height - deltaY); - Y += Height - newTRH; - Height = newTRH; - break; - - case "left": - double newLeftW = Math.Max(minW, Width - deltaX); - X += Width - newLeftW; - Width = newLeftW; - break; - - case "right": - Width = Math.Max(minW, Width + deltaX); - break; - - case "bottomleft": - double newBLW = Math.Max(minW, Width - deltaX); - X += Width - newBLW; - Width = newBLW; - Height = Math.Max(minH, Height + deltaY); - break; - - case "bottom": - Height = Math.Max(minH, Height + deltaY); - break; - - case "bottomright": - Width = Math.Max(minW, Width + deltaX); - Height = Math.Max(minH, Height + deltaY); - break; - } - - X = Math.Round(X, 1); - Y = Math.Round(Y, 1); - Width = Math.Round(Width, 1); - Height = Math.Round(Height, 1); - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/FormFieldElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/FormFieldElementViewModel.cs deleted file mode 100644 index cda5255..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/FormFieldElementViewModel.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class FormFieldElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private FormFieldType _fieldType = FormFieldType.Text; - - [ObservableProperty] - private string _fieldName = "form_field_1"; - - [ObservableProperty] - private string _label = "Full Legal Name:"; - - [ObservableProperty] - private string _placeholder = "Type text here..."; - - [ObservableProperty] - private string _value = ""; - - [ObservableProperty] - private string _defaultValue = ""; - - [ObservableProperty] - private string _tooltip = ""; - - [ObservableProperty] - private bool _isRequired = true; - - [ObservableProperty] - private bool _isReadOnly = false; - - [ObservableProperty] - private bool _isChecked; - - [ObservableProperty] - private FormValidationType _validationType = FormValidationType.None; - - [ObservableProperty] - private string _customValidationRegex = ""; - - [ObservableProperty] - private string _borderColorHex = "#0F6CBD"; - - [ObservableProperty] - private string _backgroundColorHex = "#F8FAFC"; - - [ObservableProperty] - private double _fontSize = 12; - - // Acrobat Form Calculations & Action Buttons - [ObservableProperty] - private CalculationFormula _calculationFormula = CalculationFormula.None; - - [ObservableProperty] - private string _calculationSourceFields = ""; - - [ObservableProperty] - private FormButtonAction _buttonAction = FormButtonAction.SubmitForm; - - [ObservableProperty] - private string _actionTarget = ""; - - public ObservableCollection Options { get; } = new(); - - public override ElementKind Kind => ElementKind.FormField; - public override string DisplayName => $"Form: {Label} ({FieldType})"; - - public FormFieldElementViewModel() - { - Options.Add("Approved"); - Options.Add("Under Review"); - Options.Add("Declined"); - } - - [RelayCommand] - public void ToggleChecked() - { - IsChecked = !IsChecked; - } - - [RelayCommand] - public void AddOption(string? optionText) - { - string text = string.IsNullOrWhiteSpace(optionText) ? $"Option {Options.Count + 1}" : optionText.Trim(); - Options.Add(text); - } - - [RelayCommand] - public void RemoveOption(string? optionText) - { - if (optionText != null && Options.Contains(optionText)) - { - Options.Remove(optionText); - } - } - - public override PdfElementBase ToModel() - { - var model = new PdfFormFieldElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - FieldType = FieldType, - FieldName = FieldName, - Label = Label, - Placeholder = Placeholder, - Value = Value, - DefaultValue = DefaultValue, - Tooltip = Tooltip, - IsRequired = IsRequired, - IsReadOnly = IsReadOnly, - IsChecked = IsChecked, - ValidationType = ValidationType, - CustomValidationRegex = CustomValidationRegex, - BorderColorHex = BorderColorHex, - BackgroundColorHex = BackgroundColorHex, - FontSize = FontSize, - CalculationFormula = CalculationFormula, - CalculationSourceFields = CalculationSourceFields, - ButtonAction = ButtonAction, - ActionTarget = ActionTarget, - Options = new System.Collections.Generic.List(Options) - }; - return model; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfFormFieldElement form) - { - Id = form.Id; - X = form.X; - Y = form.Y; - Width = form.Width; - Height = form.Height; - ZIndex = form.ZIndex; - Rotation = form.Rotation; - Opacity = form.Opacity; - IsLocked = form.IsLocked; - - FieldType = form.FieldType; - FieldName = form.FieldName; - Label = form.Label; - Placeholder = form.Placeholder; - Value = form.Value; - DefaultValue = form.DefaultValue; - Tooltip = form.Tooltip; - IsRequired = form.IsRequired; - IsReadOnly = form.IsReadOnly; - IsChecked = form.IsChecked; - ValidationType = form.ValidationType; - CustomValidationRegex = form.CustomValidationRegex; - BorderColorHex = form.BorderColorHex; - BackgroundColorHex = form.BackgroundColorHex; - FontSize = form.FontSize; - CalculationFormula = form.CalculationFormula; - CalculationSourceFields = form.CalculationSourceFields; - ButtonAction = form.ButtonAction; - ActionTarget = form.ActionTarget; - - Options.Clear(); - foreach (var opt in form.Options) Options.Add(opt); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/ImageElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/ImageElementViewModel.cs deleted file mode 100644 index 969f3fa..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/ImageElementViewModel.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using System.IO; -using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class ImageElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string? _imagePath; - - [ObservableProperty] - private byte[]? _imageData; - - [ObservableProperty] - private string? _base64Data; - - [ObservableProperty] - private Bitmap? _previewBitmap; - - private Bitmap? _previousPreviewBitmap; - - /// Disposes the outgoing bitmap whenever a new one is decoded — - /// is re-decoded on every path/data change with no other lifecycle owner, so without this the old - /// native bitmap leaks. - partial void OnPreviewBitmapChanged(Bitmap? value) - { - if (_previousPreviewBitmap != null && _previousPreviewBitmap != value) - { - _previousPreviewBitmap.Dispose(); - } - _previousPreviewBitmap = value; - } - - [ObservableProperty] - private bool _keepAspectRatio = true; - - [ObservableProperty] - private double _cornerRadius = 4; - - [ObservableProperty] - private string _borderColorHex = "#E1DFDD"; - - [ObservableProperty] - private double _borderThickness = 1; - - [ObservableProperty] - private string _altText = "Image Placeholder"; - - public override ElementKind Kind => ElementKind.Image; - public override string DisplayName => !string.IsNullOrEmpty(ImagePath) - ? Path.GetFileName(ImagePath) - : (!string.IsNullOrEmpty(AltText) ? AltText : "Image"); - - partial void OnImagePathChanged(string? value) - { - UpdateBitmap(); - } - - partial void OnImageDataChanged(byte[]? value) - { - UpdateBitmap(); - } - - partial void OnBase64DataChanged(string? value) - { - UpdateBitmap(); - } - - public void UpdateBitmap() - { - if (!string.IsNullOrEmpty(ImagePath) && File.Exists(ImagePath)) - { - try - { - PreviewBitmap = new Bitmap(ImagePath); - return; - } - catch - { - PreviewBitmap = null; - } - } - - if (ImageData != null && ImageData.Length > 0) - { - try - { - using var ms = new MemoryStream(ImageData); - PreviewBitmap = new Bitmap(ms); - return; - } - catch - { - PreviewBitmap = null; - } - } - - if (!string.IsNullOrEmpty(Base64Data)) - { - try - { - byte[] bytes = Convert.FromBase64String(Base64Data); - using var ms = new MemoryStream(bytes); - PreviewBitmap = new Bitmap(ms); - return; - } - catch - { - PreviewBitmap = null; - } - } - - PreviewBitmap = null; - } - - public override PdfElementBase ToModel() - { - var model = new PdfImageElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - ImagePath = ImagePath, - KeepAspectRatio = KeepAspectRatio, - CornerRadius = CornerRadius, - BorderColorHex = BorderColorHex, - BorderThickness = BorderThickness, - AltText = AltText - }; - - if (ImageData != null) - { - model.ImageData = ImageData; - } - if (!string.IsNullOrEmpty(Base64Data)) - { - model.Base64Data = Base64Data; - } - - return model; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfImageElement img) - { - Id = img.Id; - X = img.X; - Y = img.Y; - Width = img.Width; - Height = img.Height; - ZIndex = img.ZIndex; - Rotation = img.Rotation; - Opacity = img.Opacity; - IsLocked = img.IsLocked; - - ImagePath = img.ImagePath; - ImageData = img.ImageData; - Base64Data = img.Base64Data; - KeepAspectRatio = img.KeepAspectRatio; - CornerRadius = img.CornerRadius; - BorderColorHex = img.BorderColorHex; - BorderThickness = img.BorderThickness; - AltText = img.AltText; - - UpdateBitmap(); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/InkElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/InkElementViewModel.cs deleted file mode 100644 index d50adc2..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/InkElementViewModel.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.ComponentModel; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class InkElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private string _pointsData = "10,10 50,40 100,20 180,60 220,30"; - - [ObservableProperty] - private string _strokeColorHex = "#0F6CBD"; - - [ObservableProperty] - private double _strokeThickness = 3.0; - - [ObservableProperty] - private bool _isHighlighter; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private bool _isSmoothSpline = true; - - public override ElementKind Kind => ElementKind.Ink; - public override string DisplayName => IsHighlighter ? "Highlighter Path" : "Freehand Ink Drawing"; - - public string PathData => SvgShapeHelper.GenerateSmoothInkSvgPath(PointsData, IsSmoothSpline); - - public InkElementViewModel() - { - Width = 240; - Height = 80; - } - - protected override void OnPropertyChanged(PropertyChangedEventArgs e) - { - base.OnPropertyChanged(e); - if (e.PropertyName is nameof(PointsData) or nameof(IsSmoothSpline)) - { - OnPropertyChanged(nameof(PathData)); - } - } - - public override PdfElementBase ToModel() - { - return new PdfInkElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - PointsData = PointsData, - StrokeColorHex = StrokeColorHex, - StrokeThickness = StrokeThickness, - IsHighlighter = IsHighlighter, - IsSmoothSpline = IsSmoothSpline - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfInkElement ink) - { - Id = ink.Id; - X = ink.X; - Y = ink.Y; - Width = ink.Width; - Height = ink.Height; - ZIndex = ink.ZIndex; - Rotation = ink.Rotation; - Opacity = ink.Opacity; - IsLocked = ink.IsLocked; - - PointsData = ink.PointsData; - StrokeColorHex = ink.StrokeColorHex; - StrokeThickness = ink.StrokeThickness; - IsHighlighter = ink.IsHighlighter; - IsSmoothSpline = ink.IsSmoothSpline; - } - } -} - diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/MathElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/MathElementViewModel.cs deleted file mode 100644 index ce63c39..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/MathElementViewModel.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services.MathEngine; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class MathElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _formula = @"\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}"; - - [ObservableProperty] - private double _fontSize = 16; - - [ObservableProperty] - private string _textColorHex = "#0F172A"; - - [ObservableProperty] - private string _backgroundColorHex = "#00000000"; - - [ObservableProperty] - private string _borderColorHex = "#00000000"; - - [ObservableProperty] - private double _borderThickness = 0; - - [ObservableProperty] - private double _cornerRadius = 4; - - [ObservableProperty] - private double _padding = 8; - - [ObservableProperty] - private bool _showBackground; - - [ObservableProperty] - private bool _showBorder; - - [ObservableProperty] - private bool _showEquationNumber; - - [ObservableProperty] - private string _equationNumber = "(1)"; - - [ObservableProperty] - private TextAlignmentMode _alignment = TextAlignmentMode.Center; - - [ObservableProperty] - private MathDisplayStyle _displayStyle = MathDisplayStyle.DisplayBlock; - - [ObservableProperty] - private MathCategory _category = MathCategory.Calculus; - - [ObservableProperty] - private string? _presetName = "Gaussian Integral"; - - [ObservableProperty] - private string? _description = "Euler-Poisson Gaussian Integral"; - - [ObservableProperty] - private string _svgSource = ""; - - [ObservableProperty] - private string _pathGeometryData = ""; - - [ObservableProperty] - private bool _hasError; - - [ObservableProperty] - private string? _errorMessage; - - public override ElementKind Kind => ElementKind.Math; - public override string DisplayName => !string.IsNullOrWhiteSpace(PresetName) ? $"Formula ({PresetName})" : "Math Equation"; - - public ObservableCollection AvailablePresets { get; } = new(MathPresetsLibrary.AllPresets); - - public MathElementViewModel() - { - Width = 280; - Height = 60; - RenderSvg(); - } - - partial void OnFormulaChanged(string value) => RenderSvg(); - partial void OnFontSizeChanged(double value) => RenderSvg(); - partial void OnTextColorHexChanged(string value) => RenderSvg(); - partial void OnBackgroundColorHexChanged(string value) => RenderSvg(); - partial void OnBorderColorHexChanged(string value) => RenderSvg(); - partial void OnBorderThicknessChanged(double value) => RenderSvg(); - partial void OnCornerRadiusChanged(double value) => RenderSvg(); - partial void OnPaddingChanged(double value) => RenderSvg(); - partial void OnShowBackgroundChanged(bool value) => RenderSvg(); - partial void OnShowBorderChanged(bool value) => RenderSvg(); - partial void OnShowEquationNumberChanged(bool value) => RenderSvg(); - partial void OnEquationNumberChanged(string value) => RenderSvg(); - partial void OnAlignmentChanged(TextAlignmentMode value) => RenderSvg(); - partial void OnDisplayStyleChanged(MathDisplayStyle value) => RenderSvg(); - - public void RenderSvg() - { - var options = new MathRenderOptions( - FontSize: FontSize, - TextColorHex: TextColorHex, - BackgroundColorHex: BackgroundColorHex, - BorderColorHex: BorderColorHex, - BorderThickness: BorderThickness, - CornerRadius: CornerRadius, - Padding: Padding, - ShowBackground: ShowBackground, - ShowBorder: ShowBorder, - ShowEquationNumber: ShowEquationNumber, - EquationNumber: EquationNumber, - Alignment: Alignment, - DisplayStyle: DisplayStyle, - TargetWidth: Width, - TargetHeight: Height - ); - - var result = MathLayoutEngine.RenderToSvg(Formula, options); - SvgSource = result.SvgXml; - PathGeometryData = result.PathGeometryData; - HasError = !result.IsSuccess; - ErrorMessage = result.ErrorMessage; - } - - [RelayCommand] - public void ApplyPreset(string? presetId) - { - if (string.IsNullOrEmpty(presetId)) return; - - var preset = MathPresetsLibrary.FindById(presetId) ?? MathPresetsLibrary.FindByName(presetId); - if (preset != null) - { - Formula = preset.Formula; - PresetName = preset.Name; - Description = preset.Description; - Category = preset.Category; - EquationNumber = preset.DefaultEquationNumber; - Width = Math.Max(Width, preset.DefaultWidth); - Height = Math.Max(Height, preset.DefaultHeight); - RenderSvg(); - } - } - - [RelayCommand] - public void InsertSymbol(string snippet) - { - if (string.IsNullOrEmpty(snippet)) return; - string resolved = MathPresetsLibrary.ResolveSnippet(snippet); - Formula = string.IsNullOrEmpty(Formula) ? resolved : $"{Formula} {resolved}"; - } - - public override PdfElementBase ToModel() - { - return new PdfMathElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - Formula = Formula, - FontSize = FontSize, - TextColorHex = TextColorHex, - BackgroundColorHex = BackgroundColorHex, - BorderColorHex = BorderColorHex, - BorderThickness = BorderThickness, - CornerRadius = CornerRadius, - Padding = Padding, - ShowBackground = ShowBackground, - ShowBorder = ShowBorder, - ShowEquationNumber = ShowEquationNumber, - EquationNumber = EquationNumber, - Alignment = Alignment, - DisplayStyle = DisplayStyle, - Category = Category, - PresetName = PresetName, - Description = Description - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfMathElement math) - { - Id = math.Id; - X = math.X; - Y = math.Y; - Width = math.Width; - Height = math.Height; - ZIndex = math.ZIndex; - Rotation = math.Rotation; - Opacity = math.Opacity; - IsLocked = math.IsLocked; - - Formula = math.Formula; - FontSize = math.FontSize; - TextColorHex = math.TextColorHex; - BackgroundColorHex = math.BackgroundColorHex; - BorderColorHex = math.BorderColorHex; - BorderThickness = math.BorderThickness; - CornerRadius = math.CornerRadius; - Padding = math.Padding; - ShowBackground = math.ShowBackground; - ShowBorder = math.ShowBorder; - ShowEquationNumber = math.ShowEquationNumber; - EquationNumber = math.EquationNumber; - Alignment = math.Alignment; - DisplayStyle = math.DisplayStyle; - Category = math.Category; - PresetName = math.PresetName; - Description = math.Description; - - RenderSvg(); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/MeasurementElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/MeasurementElementViewModel.cs deleted file mode 100644 index 1fbf52c..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/MeasurementElementViewModel.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class MeasurementElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private RulerUnit _unit = RulerUnit.Points; - - [ObservableProperty] - private double _scaleFactor = 1.0; - - [ObservableProperty] - private string _customLabel = ""; - - [ObservableProperty] - private string _strokeColorHex = "#DC2626"; - - [ObservableProperty] - private double _strokeThickness = 1.5; - - [ObservableProperty] - private double _arrowSize = 6.0; - - [ObservableProperty] - private double _extensionLineLength = 10.0; - - [ObservableProperty] - private double _fontSize = 10.0; - - public override ElementKind Kind => ElementKind.Measurement; - public override string DisplayName => "Measurement Dimension"; - - public string FormattedDistance => GetFormattedDistance(); - - partial void OnUnitChanged(RulerUnit value) => OnPropertyChanged(nameof(FormattedDistance)); - partial void OnCustomLabelChanged(string value) => OnPropertyChanged(nameof(FormattedDistance)); - - public double CalculateDistance() - { - return Width; - } - - public string GetFormattedDistance() - { - if (!string.IsNullOrWhiteSpace(CustomLabel)) - return CustomLabel; - - double distancePts = CalculateDistance(); - return Unit switch - { - RulerUnit.Inches => $"{distancePts / 72.0:F2} in", - RulerUnit.Millimeters => $"{distancePts * 25.4 / 72.0:F1} mm", - _ => $"{distancePts:F1} pt" - }; - } - - public override PdfElementBase ToModel() - { - return new PdfMeasurementElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - Unit = Unit, - ScaleFactor = ScaleFactor, - CustomLabel = CustomLabel, - StrokeColorHex = StrokeColorHex, - StrokeThickness = StrokeThickness, - ArrowSize = ArrowSize, - ExtensionLineLength = ExtensionLineLength, - FontSize = FontSize - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfMeasurementElement m) - { - Id = m.Id; - X = m.X; - Y = m.Y; - Width = m.Width; - Height = m.Height; - ZIndex = m.ZIndex; - Rotation = m.Rotation; - Opacity = m.Opacity; - IsLocked = m.IsLocked; - - Unit = m.Unit; - ScaleFactor = m.ScaleFactor; - CustomLabel = m.CustomLabel; - StrokeColorHex = m.StrokeColorHex; - StrokeThickness = m.StrokeThickness; - ArrowSize = m.ArrowSize; - ExtensionLineLength = m.ExtensionLineLength; - FontSize = m.FontSize; - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/QrCodeElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/QrCodeElementViewModel.cs deleted file mode 100644 index d3966e0..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/QrCodeElementViewModel.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.IO; -using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class QrCodeElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _content = "https://github.com/PrashantUnity/PDFCreator"; - - [ObservableProperty] - private string _darkColorHex = "#0F172A"; - - [ObservableProperty] - private string _lightColorHex = "#FFFFFF"; - - [ObservableProperty] - private string _label = "SCAN TO VERIFY CREDENTIAL"; - - [ObservableProperty] - private QrCodeEccLevel _eccLevel = QrCodeEccLevel.M; - - [ObservableProperty] - private bool _drawQuietZones = true; - - [ObservableProperty] - private byte[]? _qrPngBytes; - - [ObservableProperty] - private Bitmap? _qrBitmap; - - partial void OnContentChanged(string value) => RefreshQrBitmap(); - partial void OnDarkColorHexChanged(string value) => RefreshQrBitmap(); - partial void OnLightColorHexChanged(string value) => RefreshQrBitmap(); - partial void OnEccLevelChanged(QrCodeEccLevel value) => RefreshQrBitmap(); - partial void OnDrawQuietZonesChanged(bool value) => RefreshQrBitmap(); - - public void RefreshQrBitmap() - { - QrPngBytes = QrCodeHelper.GeneratePngBytes(Content, DarkColorHex, LightColorHex, EccLevel, pixelsPerModule: 8, drawQuietZones: DrawQuietZones); - try - { - if (QrPngBytes != null && QrPngBytes.Length > 0) - { - using var ms = new MemoryStream(QrPngBytes); - QrBitmap = new Bitmap(ms); - } - else - { - QrBitmap = null; - } - } - catch - { - // In headless/mock test runner without Skia rendering context - QrBitmap = null; - } - } - - [RelayCommand] - public void ApplyPresetType(string presetStr) - { - if (Enum.TryParse(presetStr, true, out var kind)) - { - switch (kind) - { - case QrCodePresetKind.Url: - Content = "https://github.com/PrashantUnity/PDFCreator"; - Label = "SCAN TO VISIT WEBSITE"; - break; - case QrCodePresetKind.Wifi: - Content = "WIFI:S:Enterprise-Secure;T:WPA2;P:Passcode2026;;"; - Label = "SCAN TO CONNECT WI-FI"; - break; - case QrCodePresetKind.VCard: - Content = "BEGIN:VCARD\nVERSION:3.0\nN:Doe;John\nORG:CodeFryDev\nTEL:+1-555-0149\nEMAIL:john.doe@codefrydev.in\nEND:VCARD"; - Label = "SCAN FOR DIGITAL VCARD"; - break; - case QrCodePresetKind.Email: - Content = "mailto:legal@codefrydev.in?subject=Document%20Verification"; - Label = "SCAN TO SEND EMAIL"; - break; - case QrCodePresetKind.PhoneCall: - Content = "tel:+18005550199"; - Label = "SCAN TO DIAL DIRECT"; - break; - case QrCodePresetKind.Sms: - Content = "SMSTO:+18005550199:Verified Document Request"; - Label = "SCAN TO SEND SMS"; - break; - case QrCodePresetKind.GeoLocation: - Content = "geo:37.7749,-122.4194"; - Label = "SCAN FOR GPS LOCATION"; - break; - case QrCodePresetKind.CryptoAddress: - Content = "bitcoin:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh?amount=0.005"; - Label = "SCAN TO PAY CRYPTO"; - break; - case QrCodePresetKind.EventCalendar: - Content = "BEGIN:VEVENT\nSUMMARY:Global Tech Summit 2026\nLOCATION:Moscone Center, SF\nDESCRIPTION:Official PDF Session\nEND:VEVENT"; - Label = "SCAN TO ADD CALENDAR EVENT"; - break; - case QrCodePresetKind.PlainText: - Content = "AUTHENTICATED-DOCUMENT-HASH-892401924"; - Label = "SCAN TO VERIFY TOKEN"; - break; - } - } - } - - public override ElementKind Kind => ElementKind.QrCode; - public override string DisplayName => $"QR Code ({Content})"; - - public QrCodeElementViewModel() - { - Width = 140; - Height = 160; - RefreshQrBitmap(); - } - - public override PdfElementBase ToModel() - { - return new PdfQrCodeElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - Content = Content, - DarkColorHex = DarkColorHex, - LightColorHex = LightColorHex, - Label = Label, - EccLevel = EccLevel, - DrawQuietZones = DrawQuietZones - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfQrCodeElement qr) - { - Id = qr.Id; - X = qr.X; - Y = qr.Y; - Width = qr.Width; - Height = qr.Height; - ZIndex = qr.ZIndex; - Rotation = qr.Rotation; - Opacity = qr.Opacity; - IsLocked = qr.IsLocked; - - Content = qr.Content; - DarkColorHex = qr.DarkColorHex; - LightColorHex = qr.LightColorHex; - Label = qr.Label; - EccLevel = qr.EccLevel; - DrawQuietZones = qr.DrawQuietZones; - - RefreshQrBitmap(); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/RedactionElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/RedactionElementViewModel.cs deleted file mode 100644 index 61bcf38..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/RedactionElementViewModel.cs +++ /dev/null @@ -1,86 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class RedactionElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private RedactionMode _mode = RedactionMode.Blackout; - - [ObservableProperty] - private string _exemptionCode = "[REDACTED - (b)(4) PRIVILEGED]"; - - [ObservableProperty] - private string _fillColorHex = "#0F172A"; - - [ObservableProperty] - private string _textColorHex = "#FFFFFF"; - - [ObservableProperty] - private string _borderColorHex = "#DC2626"; - - [ObservableProperty] - private double _borderThickness = 1.5; - - [ObservableProperty] - private bool _showOverlayText = true; - - public override ElementKind Kind => ElementKind.Redaction; - public override string DisplayName => $"Redaction ({ExemptionCode})"; - - public RedactionElementViewModel() - { - Width = 240; - Height = 40; - } - - public override PdfElementBase ToModel() - { - return new PdfRedactionElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - Mode = Mode, - ExemptionCode = ExemptionCode, - FillColorHex = FillColorHex, - TextColorHex = TextColorHex, - BorderColorHex = BorderColorHex, - BorderThickness = BorderThickness, - ShowOverlayText = ShowOverlayText - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfRedactionElement r) - { - Id = r.Id; - X = r.X; - Y = r.Y; - Width = r.Width; - Height = r.Height; - ZIndex = r.ZIndex; - Rotation = r.Rotation; - Opacity = r.Opacity; - IsLocked = r.IsLocked; - - Mode = r.Mode; - ExemptionCode = r.ExemptionCode; - FillColorHex = r.FillColorHex; - TextColorHex = r.TextColorHex; - BorderColorHex = r.BorderColorHex; - BorderThickness = r.BorderThickness; - ShowOverlayText = r.ShowOverlayText; - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/ShapeElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/ShapeElementViewModel.cs deleted file mode 100644 index 8f1b6d7..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/ShapeElementViewModel.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.ComponentModel; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class ShapeElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - [NotifyPropertyChangedFor(nameof(DisplayName))] - private ShapeType _shapeType = ShapeType.Rectangle; - - [ObservableProperty] - private string _fillColorHex = "#F8F9FA"; - - [ObservableProperty] - private string _strokeColorHex = "#0F6CBD"; - - [ObservableProperty] - private double _strokeThickness = 1.5; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _cornerRadius = 4; - - [ObservableProperty] - private string? _label; - - [ObservableProperty] - private string? _labelColorHex = "#201F1E"; - - [ObservableProperty] - private double _labelFontSize = 12; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private string? _customPathData; - - [ObservableProperty] - private string? _secondaryFillColorHex; - - [ObservableProperty] - private string? _secondaryStrokeColorHex; - - // Bézier Curves, Connectors & Line Caps - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _bezierP0X = 0.0; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _bezierP0Y = 0.5; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _bezierP1X = 0.33; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _bezierP1Y = 0.10; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _bezierP2X = 0.67; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _bezierP2Y = 0.90; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _bezierP3X = 1.0; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _bezierP3Y = 0.5; - - [ObservableProperty] - private LineEndCap _startCap = LineEndCap.None; - - [ObservableProperty] - private LineEndCap _endCap = LineEndCap.None; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(DashArrayString))] - private LineDashStyle _dashStyle = LineDashStyle.Solid; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _waveFrequency = 2.0; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PathData))] - private double _curvatureDepth = 40.0; - - public override ElementKind Kind => ElementKind.Shape; - public override string DisplayName => string.IsNullOrEmpty(Label) ? $"Shape ({ShapeType})" : Label; - - public bool IsLineOrCurve => ShapeType is ShapeType.Line or ShapeType.Arrow or ShapeType.BezierCurve or ShapeType.CurvedArrow or ShapeType.SCurveConnector or ShapeType.WaveLine or ShapeType.ArcLine or ShapeType.CurlyBrace; - - public string PathData => SvgShapeHelper.GetVectorPath(ShapeType, Width, Height, CornerRadius, CustomPathData); - public string? DashArrayString => SvgShapeHelper.GetDashArray(DashStyle, StrokeThickness); - - protected override void OnPropertyChanged(PropertyChangedEventArgs e) - { - base.OnPropertyChanged(e); - if (e.PropertyName is nameof(Width) or nameof(Height) or nameof(CornerRadius) or nameof(ShapeType) or nameof(CustomPathData) or nameof(StrokeThickness) or nameof(DashStyle)) - { - OnPropertyChanged(nameof(PathData)); - OnPropertyChanged(nameof(DashArrayString)); - } - } - - public override PdfElementBase ToModel() - { - return new PdfShapeElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - GroupId = GroupId, - ShapeType = ShapeType, - FillColorHex = FillColorHex, - StrokeColorHex = StrokeColorHex, - StrokeThickness = StrokeThickness, - CornerRadius = CornerRadius, - Label = Label, - LabelColorHex = LabelColorHex, - LabelFontSize = LabelFontSize, - CustomPathData = CustomPathData, - SecondaryFillColorHex = SecondaryFillColorHex, - SecondaryStrokeColorHex = SecondaryStrokeColorHex, - BezierP0X = BezierP0X, - BezierP0Y = BezierP0Y, - BezierP1X = BezierP1X, - BezierP1Y = BezierP1Y, - BezierP2X = BezierP2X, - BezierP2Y = BezierP2Y, - BezierP3X = BezierP3X, - BezierP3Y = BezierP3Y, - StartCap = StartCap, - EndCap = EndCap, - DashStyle = DashStyle, - WaveFrequency = WaveFrequency, - CurvatureDepth = CurvatureDepth - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfShapeElement shape) - { - Id = shape.Id; - X = shape.X; - Y = shape.Y; - Width = shape.Width; - Height = shape.Height; - ZIndex = shape.ZIndex; - Rotation = shape.Rotation; - Opacity = shape.Opacity; - IsLocked = shape.IsLocked; - GroupId = shape.GroupId; - - ShapeType = shape.ShapeType; - FillColorHex = shape.FillColorHex; - StrokeColorHex = shape.StrokeColorHex; - StrokeThickness = shape.StrokeThickness; - CornerRadius = shape.CornerRadius; - Label = shape.Label; - LabelColorHex = shape.LabelColorHex; - LabelFontSize = shape.LabelFontSize; - CustomPathData = shape.CustomPathData; - SecondaryFillColorHex = shape.SecondaryFillColorHex; - SecondaryStrokeColorHex = shape.SecondaryStrokeColorHex; - BezierP0X = shape.BezierP0X; - BezierP0Y = shape.BezierP0Y; - BezierP1X = shape.BezierP1X; - BezierP1Y = shape.BezierP1Y; - BezierP2X = shape.BezierP2X; - BezierP2Y = shape.BezierP2Y; - BezierP3X = shape.BezierP3X; - BezierP3Y = shape.BezierP3Y; - StartCap = shape.StartCap; - EndCap = shape.EndCap; - DashStyle = shape.DashStyle; - WaveFrequency = shape.WaveFrequency; - CurvatureDepth = shape.CurvatureDepth; - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/StickyNoteElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/StickyNoteElementViewModel.cs deleted file mode 100644 index 9340a7a..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/StickyNoteElementViewModel.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class StickyNoteElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _author = "Reviewer"; - - [ObservableProperty] - private string _timestamp = DateTime.Now.ToString("MMM dd, yyyy HH:mm"); - - [ObservableProperty] - private string _noteText = "Please verify the audit figures with the legal compliance team prior to final PDF release."; - - [ObservableProperty] - private string _status = "Pending Review"; - - [ObservableProperty] - private string _colorHex = "#FEF3C7"; - - [ObservableProperty] - private string _borderColorHex = "#F59E0B"; - - [ObservableProperty] - private bool _isExpanded = true; - - public override ElementKind Kind => ElementKind.StickyNote; - public override string DisplayName => $"Sticky Note ({Author})"; - - public StickyNoteElementViewModel() - { - Width = 200; - Height = 150; - } - - [RelayCommand] - public void ToggleExpanded() - { - IsExpanded = !IsExpanded; - Height = IsExpanded ? 150 : 36; - } - - public override PdfElementBase ToModel() - { - return new PdfStickyNoteElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - Author = Author, - Timestamp = Timestamp, - NoteText = NoteText, - Status = Status, - ColorHex = ColorHex, - BorderColorHex = BorderColorHex, - IsExpanded = IsExpanded - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfStickyNoteElement note) - { - Id = note.Id; - X = note.X; - Y = note.Y; - Width = note.Width; - Height = note.Height; - ZIndex = note.ZIndex; - Rotation = note.Rotation; - Opacity = note.Opacity; - IsLocked = note.IsLocked; - - Author = note.Author; - Timestamp = note.Timestamp; - NoteText = note.NoteText; - Status = note.Status; - ColorHex = note.ColorHex; - BorderColorHex = note.BorderColorHex; - IsExpanded = note.IsExpanded; - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/SvgElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/SvgElementViewModel.cs deleted file mode 100644 index e6b715c..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/SvgElementViewModel.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.IO; -using System.Text.RegularExpressions; -using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class SvgElementViewModel : ElementViewModelBase, IDisposable -{ - [ObservableProperty] - private string _svgSource = SvgOrnamentLibrary.GetGaneshaCrestSvg(); - - [ObservableProperty] - private string? _filePath; - - [ObservableProperty] - private string? _tintColorHex; - - [ObservableProperty] - private string? _presetName = "GaneshaCrest"; - - [ObservableProperty] - private bool _keepAspectRatio = true; - - [ObservableProperty] - private double _cornerRadius = 0; - - [ObservableProperty] - private string? _borderColorHex; - - [ObservableProperty] - private double _borderThickness = 0; - - [ObservableProperty] - private string _pathGeometryData = ""; - - [ObservableProperty] - private Bitmap? _previewBitmap; - - private Bitmap? _previousPreviewBitmap; - - /// - /// Disposes the outgoing native bitmap whenever a new SVG is rasterized, - /// preventing unmanaged Skia memory leaks per AGENTS.md Section 4.E. - /// - partial void OnPreviewBitmapChanged(Bitmap? value) - { - if (_previousPreviewBitmap != null && _previousPreviewBitmap != value) - { - _previousPreviewBitmap.Dispose(); - } - _previousPreviewBitmap = value; - } - - public override ElementKind Kind => ElementKind.Svg; - public override string DisplayName => !string.IsNullOrEmpty(PresetName) ? $"SVG ({PresetName})" : (!string.IsNullOrEmpty(FilePath) ? Path.GetFileName(FilePath) : "Vector SVG"); - - public SvgElementViewModel() - { - Width = 160; - Height = 160; - RefreshSvgPreview(); - } - - partial void OnSvgSourceChanged(string value) => RefreshSvgPreview(); - partial void OnPresetNameChanged(string? value) => RefreshSvgPreview(); - partial void OnTintColorHexChanged(string? value) => RefreshSvgPreview(); - - public void RefreshSvgPreview() - { - UpdatePathGeometry(); - - if (string.IsNullOrWhiteSpace(SvgSource)) - { - PreviewBitmap = null; - return; - } - - try - { - string svgData = SvgSource; - if (!string.IsNullOrWhiteSpace(TintColorHex)) - { - svgData = svgData.Replace("currentColor", TintColorHex); - } - - var bmp = PdfPageRenderer.RenderSvgToBitmap(svgData, Width, Height); - if (bmp != null) - { - PreviewBitmap = bmp; - } - } - catch - { - // Retain existing or fallback - } - } - - public void UpdatePathGeometry() - { - if (string.IsNullOrWhiteSpace(SvgSource)) - { - PathGeometryData = ""; - return; - } - - // Extract the primary or combined path 'd' attributes from the SVG markup for Avalonia Vector rendering - var match = Regex.Match(SvgSource, @"]*\sd=[""']([^""']+)[""']", RegexOptions.IgnoreCase); - if (match.Success) - { - PathGeometryData = match.Groups[1].Value; - } - else - { - PathGeometryData = "M 10,10 L 90,10 L 90,90 L 10,90 Z"; - } - } - - [RelayCommand] - public void ApplyPreset(string preset) - { - PresetName = preset; - SvgSource = SvgOrnamentLibrary.GetSvg(preset, TintColorHex); - FilePath = null; - RefreshSvgPreview(); - } - - public void LoadFromFile(string path) - { - if (File.Exists(path)) - { - FilePath = path; - SvgSource = File.ReadAllText(path); - PresetName = Path.GetFileNameWithoutExtension(path); - RefreshSvgPreview(); - } - } - - public override PdfElementBase ToModel() - { - return new PdfSvgElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - SvgSource = SvgSource, - FilePath = FilePath, - TintColorHex = TintColorHex, - PresetName = PresetName, - KeepAspectRatio = KeepAspectRatio, - CornerRadius = CornerRadius, - BorderColorHex = BorderColorHex, - BorderThickness = BorderThickness - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfSvgElement svg) - { - Id = svg.Id; - X = svg.X; - Y = svg.Y; - Width = svg.Width; - Height = svg.Height; - ZIndex = svg.ZIndex; - Rotation = svg.Rotation; - Opacity = svg.Opacity; - IsLocked = svg.IsLocked; - - SvgSource = !string.IsNullOrWhiteSpace(svg.SvgSource) - ? svg.SvgSource - : (!string.IsNullOrWhiteSpace(svg.PresetName) - ? SvgOrnamentLibrary.GetSvg(svg.PresetName, svg.TintColorHex) - : SvgOrnamentLibrary.GetGaneshaCrestSvg()); - FilePath = svg.FilePath; - TintColorHex = svg.TintColorHex; - PresetName = svg.PresetName; - KeepAspectRatio = svg.KeepAspectRatio; - CornerRadius = svg.CornerRadius; - BorderColorHex = svg.BorderColorHex; - BorderThickness = svg.BorderThickness; - - RefreshSvgPreview(); - } - } - - public void Dispose() - { - _previousPreviewBitmap?.Dispose(); - _previousPreviewBitmap = null; - PreviewBitmap?.Dispose(); - PreviewBitmap = null; - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/TableElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/TableElementViewModel.cs deleted file mode 100644 index 6593278..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/TableElementViewModel.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class TableHeaderItem : ObservableObject -{ - [ObservableProperty] - private string _text = ""; - - public TableHeaderItem(string text = "") - { - _text = text; - } - - public override string ToString() => Text; -} - -public partial class TableCellItem : ObservableObject -{ - [ObservableProperty] - private string _text = ""; - - public TableCellItem(string text = "") - { - _text = text; - } - - public override string ToString() => Text; -} - -public class TableRowItem : ObservableObject -{ - public ObservableCollection Cells { get; } = new(); - - public TableRowItem(IEnumerable cells) - { - foreach (var c in cells) Cells.Add(new TableCellItem(c)); - } - - public TableRowItem(IEnumerable cells) - { - foreach (var c in cells) Cells.Add(c); - } -} - -public partial class TableElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _headerBackgroundHex = "#0F6CBD"; - - [ObservableProperty] - private string _headerTextHex = "#FFFFFF"; - - [ObservableProperty] - private string _alternateRowBackgroundHex = "#F8F9FA"; - - [ObservableProperty] - private string _borderColorHex = "#E1DFDD"; - - public ObservableCollection Headers { get; } = new(); - public ObservableCollection Rows { get; } = new(); - - public override ElementKind Kind => ElementKind.Table; - public override string DisplayName => $"Table ({Headers.Count} cols, {Rows.Count} rows)"; - - public TableElementViewModel() - { - Headers.Add(new TableHeaderItem("Item Description")); - Headers.Add(new TableHeaderItem("Qty")); - Headers.Add(new TableHeaderItem("Rate")); - Headers.Add(new TableHeaderItem("Amount")); - - Rows.Add(new TableRowItem(new[] { "Cloud Architecture Consulting", "40 hrs", "$150.00", "$6,000.00" })); - Rows.Add(new TableRowItem(new[] { "Avalonia Desktop UI Engineering", "60 hrs", "$140.00", "$8,400.00" })); - Rows.Add(new TableRowItem(new[] { "QuestPDF Engine Integration", "25 hrs", "$160.00", "$4,000.00" })); - } - - [RelayCommand] - public void AddRow() - { - var cells = new List(); - for (int i = 0; i < Headers.Count; i++) - { - cells.Add($"Data {Rows.Count + 1}.{i + 1}"); - } - Rows.Add(new TableRowItem(cells)); - Height = System.Math.Max(Height, 40 + (Rows.Count * 28)); - OnPropertyChanged(nameof(DisplayName)); - } - - [RelayCommand] - public void RemoveRow() - { - if (Rows.Count > 1) - { - Rows.RemoveAt(Rows.Count - 1); - OnPropertyChanged(nameof(DisplayName)); - } - } - - [RelayCommand] - public void AddColumn() - { - string headerName = $"Col {Headers.Count + 1}"; - Headers.Add(new TableHeaderItem(headerName)); - foreach (var r in Rows) - { - r.Cells.Add(new TableCellItem("---")); - } - OnPropertyChanged(nameof(DisplayName)); - } - - [RelayCommand] - public void RemoveColumn() - { - if (Headers.Count > 1) - { - int colIdx = Headers.Count - 1; - Headers.RemoveAt(colIdx); - foreach (var r in Rows) - { - if (r.Cells.Count > colIdx) - { - r.Cells.RemoveAt(colIdx); - } - } - OnPropertyChanged(nameof(DisplayName)); - } - } - - [RelayCommand] - public void ApplyPresetStyle(string styleStr) - { - if (Enum.TryParse(styleStr, true, out var style)) - { - switch (style) - { - case TablePresetStyle.ModernMinimal: - HeaderBackgroundHex = "#F1F5F9"; - HeaderTextHex = "#0F172A"; - AlternateRowBackgroundHex = "#FFFFFF"; - BorderColorHex = "#E2E8F0"; - break; - case TablePresetStyle.EnterpriseBlue: - HeaderBackgroundHex = "#0F6CBD"; - HeaderTextHex = "#FFFFFF"; - AlternateRowBackgroundHex = "#F0F7FD"; - BorderColorHex = "#CBD5E1"; - break; - case TablePresetStyle.DarkModeSlate: - HeaderBackgroundHex = "#1E293B"; - HeaderTextHex = "#FFFFFF"; - AlternateRowBackgroundHex = "#F8FAFC"; - BorderColorHex = "#334155"; - break; - case TablePresetStyle.ZebraStriped: - HeaderBackgroundHex = "#475569"; - HeaderTextHex = "#FFFFFF"; - AlternateRowBackgroundHex = "#F1F5F9"; - BorderColorHex = "#CBD5E1"; - break; - case TablePresetStyle.EmeraldGreen: - HeaderBackgroundHex = "#047857"; - HeaderTextHex = "#FFFFFF"; - AlternateRowBackgroundHex = "#ECFDF5"; - BorderColorHex = "#A7F3D0"; - break; - case TablePresetStyle.AmberAccent: - HeaderBackgroundHex = "#D97706"; - HeaderTextHex = "#FFFFFF"; - AlternateRowBackgroundHex = "#FFFBEB"; - BorderColorHex = "#FDE68A"; - break; - case TablePresetStyle.FinancialBordered: - HeaderBackgroundHex = "#0F172A"; - HeaderTextHex = "#FFFFFF"; - AlternateRowBackgroundHex = "#FFFFFF"; - BorderColorHex = "#0F172A"; - break; - case TablePresetStyle.CompactClean: - HeaderBackgroundHex = "#E2E8F0"; - HeaderTextHex = "#1E293B"; - AlternateRowBackgroundHex = "#FAFAFA"; - BorderColorHex = "#E2E8F0"; - break; - } - } - } - - public override PdfElementBase ToModel() - { - var model = new PdfTableElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - HeaderBackgroundHex = HeaderBackgroundHex, - HeaderTextHex = HeaderTextHex, - AlternateRowBackgroundHex = AlternateRowBackgroundHex, - BorderColorHex = BorderColorHex, - Headers = Headers.Select(h => h.Text).ToList(), - Rows = Rows.Select(r => r.Cells.Select(c => c.Text).ToList()).ToList() - }; - - return model; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfTableElement table) - { - Id = table.Id; - X = table.X; - Y = table.Y; - Width = table.Width; - Height = table.Height; - ZIndex = table.ZIndex; - Rotation = table.Rotation; - Opacity = table.Opacity; - IsLocked = table.IsLocked; - - HeaderBackgroundHex = table.HeaderBackgroundHex; - HeaderTextHex = table.HeaderTextHex; - AlternateRowBackgroundHex = table.AlternateRowBackgroundHex; - BorderColorHex = table.BorderColorHex; - - Headers.Clear(); - foreach (var h in table.Headers) Headers.Add(new TableHeaderItem(h)); - - Rows.Clear(); - foreach (var r in table.Rows) Rows.Add(new TableRowItem(r)); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/TextElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/TextElementViewModel.cs deleted file mode 100644 index 148d8cf..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/TextElementViewModel.cs +++ /dev/null @@ -1,798 +0,0 @@ -using System; -using System.Linq; -using Avalonia.Media; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Typography; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class TextElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _text = "Enter text here"; - - [ObservableProperty] - private System.Collections.Generic.List? _spans; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(AvaloniaFontFamily))] - private string _fontFamily = "Arial"; - - public FontFamily AvaloniaFontFamily => FontHelper.CreateFontFamily(FontFamily); - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ComputedLineHeight))] - private double _fontSize = 14; - - [ObservableProperty] - private bool _isBold; - - [ObservableProperty] - private bool _isItalic; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(TextDecorations))] - private bool _isUnderline; - - [ObservableProperty] - private bool _isDoubleUnderline; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(TextDecorations))] - private bool _isStrikethrough; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(TextBrush))] - private string _textColorHex = "#201F1E"; - - public IBrush TextBrush => HexToBrush(TextColorHex, "#201F1E"); - - [ObservableProperty] - private double _textOpacity = 1.0; - - [ObservableProperty] - private TextAlignmentMode _alignment = TextAlignmentMode.Left; - - [ObservableProperty] - private TextVerticalAlignment _verticalAlignment = TextVerticalAlignment.Top; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ComputedLineHeight))] - private double _lineHeight = 1.4; - - [ObservableProperty] - private double _characterSpacing = 0; - - [ObservableProperty] - private double _wordSpacing = 0; - - [ObservableProperty] - private double _paragraphSpacing = 0; - - [ObservableProperty] - private bool _textWrap = true; - - // Stroke / Outline - [ObservableProperty] - private bool _hasStroke; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(StrokeBrush))] - private string _strokeColorHex = "#000000"; - - public IBrush StrokeBrush => HexToBrush(StrokeColorHex, "#000000"); - - [ObservableProperty] - private double _strokeWidth = 1.0; - - // Shadow & Glow - [ObservableProperty] - private bool _hasShadow; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ShadowBrush))] - private string _shadowColorHex = "#80000000"; - - public IBrush ShadowBrush => HexToBrush(ShadowColorHex, "#80000000"); - - [ObservableProperty] - private double _shadowOffsetX = 2.0; - - [ObservableProperty] - private double _shadowOffsetY = 2.0; - - [ObservableProperty] - private double _shadowBlurRadius = 4.0; - - [ObservableProperty] - private double _shadowOpacity = 0.5; - - // Box Background, Border & Padding - [ObservableProperty] - private double _padding = 0; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(BackgroundBrush))] - private string _backgroundColorHex = "#00000000"; - - public IBrush BackgroundBrush => HexToBrush(BackgroundColorHex, "#00000000"); - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(BorderBrush))] - private string _borderColorHex = "#00000000"; - - public IBrush BorderBrush => HexToBrush(BorderColorHex, "#00000000"); - - [ObservableProperty] - private double _borderThickness = 0; - - [ObservableProperty] - private double _cornerRadius = 0; - - // Curved & Circular Typography - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsNormalMode))] - [NotifyPropertyChangedFor(nameof(IsCurvedMode))] - [NotifyPropertyChangedFor(nameof(IsCircularMode))] - private TextShapeMode _shapeMode = TextShapeMode.Normal; - - public bool IsNormalMode => ShapeMode == TextShapeMode.Normal; - public bool IsCurvedMode => ShapeMode == TextShapeMode.Curved; - public bool IsCircularMode => ShapeMode == TextShapeMode.Circular; - public bool IsBezierMode => ShapeMode == TextShapeMode.BezierCurve; - - [ObservableProperty] - private double _curveRadius = 120; - - [ObservableProperty] - private double _curveArcAngle = 180; - - [ObservableProperty] - private double _curveStartAngle = 0; - - [ObservableProperty] - private bool _curveClockwise = true; - - [ObservableProperty] - private bool _curveInvert; - - [ObservableProperty] - private CircularTextPlacement _circularPlacement = CircularTextPlacement.TopArc; - - // Per-Character Transforms & Baseline Offsets - [ObservableProperty] - private double _baselineShift = 0; - - [ObservableProperty] - private double _characterRotation = 0; - - [ObservableProperty] - private double _scaleX = 1.0; - - [ObservableProperty] - private double _scaleY = 1.0; - - [ObservableProperty] - private bool _flipX; - - [ObservableProperty] - private bool _flipY; - - // Bézier Curve Typography (Normalized 0.0 to 1.0) - [ObservableProperty] - private BezierCurvePreset _bezierPreset = BezierCurvePreset.Wave; - - [ObservableProperty] - private double _bezierP0X = 0.0; - - [ObservableProperty] - private double _bezierP0Y = 0.5; - - [ObservableProperty] - private double _bezierP1X = 0.33; - - [ObservableProperty] - private double _bezierP1Y = 0.10; - - [ObservableProperty] - private double _bezierP2X = 0.67; - - [ObservableProperty] - private double _bezierP2Y = 0.90; - - [ObservableProperty] - private double _bezierP3X = 1.0; - - [ObservableProperty] - private double _bezierP3Y = 0.5; - - public TextDecorationCollection? TextDecorations - { - get - { - if (IsUnderline && IsStrikethrough) - { - var decs = new TextDecorationCollection(); - foreach (var d in Avalonia.Media.TextDecorations.Underline) decs.Add(d); - foreach (var d in Avalonia.Media.TextDecorations.Strikethrough) decs.Add(d); - return decs; - } - if (IsUnderline) return Avalonia.Media.TextDecorations.Underline; - if (IsStrikethrough) return Avalonia.Media.TextDecorations.Strikethrough; - return null; - } - } - - public double ComputedLineHeight => FontSize * (LineHeight > 0.1 ? LineHeight : 1.2); - - public override ElementKind Kind => ElementKind.Text; - public override string DisplayName => Text.Length > 20 ? Text.Substring(0, 17) + "..." : (string.IsNullOrWhiteSpace(Text) ? "Text Box" : Text); - - public TextElementViewModel(PdfTextElement? element = null) - { - if (element != null) - { - LoadFromModel(element); - } - } - - private static IBrush HexToBrush(string? hex, string fallbackHex = "#00000000") - { - try - { - string targetHex = !string.IsNullOrWhiteSpace(hex) ? hex : fallbackHex; - if (targetHex.Equals("Transparent", StringComparison.OrdinalIgnoreCase) || targetHex == "#00000000") - { - return Brushes.Transparent; - } - if (Color.TryParse(targetHex, out var color)) - { - return new SolidColorBrush(color); - } - } - catch { } - return Brushes.Transparent; - } - - public void TransformUppercase() - { - if (!string.IsNullOrEmpty(Text)) Text = Text.ToUpperInvariant(); - } - - public void TransformLowercase() - { - if (!string.IsNullOrEmpty(Text)) Text = Text.ToLowerInvariant(); - } - - public void TransformTitleCase() - { - if (!string.IsNullOrEmpty(Text)) - { - var ti = System.Globalization.CultureInfo.CurrentCulture.TextInfo; - Text = ti.ToTitleCase(Text.ToLower()); - } - } - - public void TransformCapitalize() - { - if (!string.IsNullOrEmpty(Text)) - { - var sentences = Text.Split('.'); - for (int i = 0; i < sentences.Length; i++) - { - var s = sentences[i].TrimStart(); - if (s.Length > 0) - { - sentences[i] = char.ToUpperInvariant(s[0]) + (s.Length > 1 ? s.Substring(1) : ""); - } - } - Text = string.Join(". ", sentences); - } - } - - public void ToggleBulletList() - { - if (string.IsNullOrEmpty(Text)) return; - var lines = Text.Split('\n'); - bool allBulleted = lines.All(l => l.TrimStart().StartsWith("• ")); - if (allBulleted) - { - Text = string.Join('\n', lines.Select(l => l.TrimStart().StartsWith("• ") ? l.TrimStart().Substring(2) : l)); - } - else - { - Text = string.Join('\n', lines.Select(l => string.IsNullOrWhiteSpace(l) ? l : (l.TrimStart().StartsWith("• ") ? l : $"• {l}"))); - } - } - - public void ToggleNumberedList() - { - if (string.IsNullOrEmpty(Text)) return; - var lines = Text.Split('\n'); - int count = 1; - Text = string.Join('\n', lines.Select(l => - { - if (string.IsNullOrWhiteSpace(l)) return l; - var trimmed = l.TrimStart(); - int dotIdx = trimmed.IndexOf('.'); - if (dotIdx > 0 && int.TryParse(trimmed.Substring(0, dotIdx), out _)) - { - return trimmed.Substring(dotIdx + 1).TrimStart(); - } - return $"{count++}. {trimmed}"; - })); - } - - public void ApplyTypographyPreset(string presetName) - { - switch (presetName.ToLowerInvariant()) - { - case "archup": - case "arch-up": - ShapeMode = TextShapeMode.Curved; - CurveClockwise = true; - CurveRadius = Math.Max(60, Width * 0.6); - CurveArcAngle = 140; - CurveStartAngle = 0; - CurveInvert = false; - break; - - case "archdown": - case "arch-down": - ShapeMode = TextShapeMode.Curved; - CurveClockwise = false; - CurveRadius = Math.Max(60, Width * 0.6); - CurveArcAngle = 140; - CurveStartAngle = 0; - CurveInvert = false; - break; - - case "circlebadge": - case "circle-badge": - ShapeMode = TextShapeMode.Circular; - CircularPlacement = CircularTextPlacement.FullCircle; - CurveRadius = Math.Max(40, Math.Min(Width, Height) / 2.0 - FontSize); - CurveArcAngle = 360; - CurveClockwise = true; - CurveInvert = false; - break; - - case "toparc": - case "top-arc": - ShapeMode = TextShapeMode.Circular; - CircularPlacement = CircularTextPlacement.TopArc; - CurveRadius = Math.Max(40, Math.Min(Width, Height) / 2.0 - FontSize); - CurveArcAngle = 180; - CurveClockwise = true; - CurveInvert = false; - break; - - case "bottomarc": - case "bottom-arc": - ShapeMode = TextShapeMode.Circular; - CircularPlacement = CircularTextPlacement.BottomArc; - CurveRadius = Math.Max(40, Math.Min(Width, Height) / 2.0 - FontSize); - CurveArcAngle = 180; - CurveClockwise = true; - CurveInvert = false; - break; - - case "outlined": - HasStroke = true; - StrokeColorHex = "#0F6CBD"; - StrokeWidth = 1.5; - TextColorHex = "#FFFFFF"; - break; - - case "shadowheading": - case "shadow-heading": - HasShadow = true; - ShadowColorHex = "#60000000"; - ShadowOffsetX = 3.0; - ShadowOffsetY = 3.0; - ShadowBlurRadius = 6.0; - ShadowOpacity = 0.6; - break; - - case "wave": - ShapeMode = TextShapeMode.BezierCurve; - ApplyBezierPreset(BezierCurvePreset.Wave); - break; - - case "scurve": - case "s-curve": - ShapeMode = TextShapeMode.BezierCurve; - ApplyBezierPreset(BezierCurvePreset.SCurve); - break; - - case "bridge": - ShapeMode = TextShapeMode.BezierCurve; - ApplyBezierPreset(BezierCurvePreset.Bridge); - break; - - case "valley": - ShapeMode = TextShapeMode.BezierCurve; - ApplyBezierPreset(BezierCurvePreset.Valley); - break; - - case "rise": - ShapeMode = TextShapeMode.BezierCurve; - ApplyBezierPreset(BezierCurvePreset.Rise); - break; - - case "neonglow": - case "neon-glow": - HasShadow = true; - ShadowColorHex = "#00F0FF"; - ShadowOffsetX = 0.0; - ShadowOffsetY = 0.0; - ShadowBlurRadius = 12.0; - ShadowOpacity = 0.85; - TextColorHex = "#FFFFFF"; - break; - - case "reset": - case "normal": - ShapeMode = TextShapeMode.Normal; - HasStroke = false; - HasShadow = false; - ScaleX = 1.0; - ScaleY = 1.0; - FlipX = false; - FlipY = false; - BaselineShift = 0; - CharacterRotation = 0; - break; - } - } - - public void ApplyBezierPreset(BezierCurvePreset preset) - { - BezierPreset = preset; - var pts = TextLayoutEngine.GetPresetBezierControlPoints(preset); - BezierP0X = pts.P0.X; - BezierP0Y = pts.P0.Y; - BezierP1X = pts.P1.X; - BezierP1Y = pts.P1.Y; - BezierP2X = pts.P2.X; - BezierP2Y = pts.P2.Y; - BezierP3X = pts.P3.X; - BezierP3Y = pts.P3.Y; - } - - public void ResetTransforms() - { - ScaleX = 1.0; - ScaleY = 1.0; - FlipX = false; - FlipY = false; - BaselineShift = 0; - CharacterRotation = 0; - } - - public double CalculateRequiredHeight() - { - var model = (PdfTextElement)ToModel(); - var dims = TextLayoutEngine.CalculateRequiredDimensions(model); - return dims.Height; - } - - public double CalculateRequiredWidth() - { - var model = (PdfTextElement)ToModel(); - var dims = TextLayoutEngine.CalculateRequiredDimensions(model); - return dims.Width; - } - - [RelayCommand] - public void AutoFitHeight() - { - double req = CalculateRequiredHeight(); - if (req > 0) - { - Height = Math.Ceiling(req); - } - } - - [RelayCommand] - public void AutoFitWidth() - { - double req = CalculateRequiredWidth(); - if (req > 0) - { - Width = Math.Ceiling(req); - } - } - - [RelayCommand] - public void AutoFitBoth() - { - var model = (PdfTextElement)ToModel(); - var dims = TextLayoutEngine.CalculateRequiredDimensions(model); - Width = Math.Ceiling(dims.Width); - Height = Math.Ceiling(dims.Height); - } - - public void SetMarkdownText(string markdown) - { - var model = (PdfTextElement)ToModel(); - var parsedSpans = RichTextHelper.ParseMarkdownToSpans(markdown, model); - if (parsedSpans.Count > 1 || parsedSpans.Any(s => s.IsBold == true || s.IsItalic == true || s.IsUnderline == true || s.IsStrikethrough == true || !string.IsNullOrEmpty(s.TextColorHex) || s.Script != TextScriptMode.Normal)) - { - Spans = parsedSpans; - Text = RichTextHelper.SpansToPlainText(parsedSpans); - } - else - { - Spans = null; - Text = RichTextHelper.SpansToPlainText(parsedSpans); - } - } - - public string GetMarkdownText() - { - if (Spans != null && Spans.Count > 0) - { - var model = (PdfTextElement)ToModel(); - return RichTextHelper.SpansToMarkdown(Spans, model); - } - return Text ?? string.Empty; - } - - public void ClearSpans() - { - Spans = null; - } - - [ObservableProperty] - private int _activeSelectionStart; - - [ObservableProperty] - private int _activeSelectionLength; - - [ObservableProperty] - private string _activeSelectedText = string.Empty; - - public bool HasTextSelection => !string.IsNullOrEmpty(ActiveSelectedText) && ActiveSelectionLength > 0; - - public void UpdateTextSelection(int start, int length, string selectedText) - { - ActiveSelectionStart = start; - ActiveSelectionLength = length; - ActiveSelectedText = selectedText ?? string.Empty; - OnPropertyChanged(nameof(HasTextSelection)); - } - - public void ClearTextSelection() - { - ActiveSelectionStart = 0; - ActiveSelectionLength = 0; - ActiveSelectedText = string.Empty; - OnPropertyChanged(nameof(HasTextSelection)); - } - - public bool ApplyInlineFormatting(InlineFormatType formatType, string? argument = null) - { - if (!HasTextSelection || string.IsNullOrEmpty(Text)) return false; - - var result = RichTextHelper.ToggleInlineFormatting( - Text, - ActiveSelectionStart, - ActiveSelectionLength, - formatType, - argument); - - if (result.NewText == Text) return false; - - Text = result.NewText; - ActiveSelectionStart = result.NewSelectionStart; - ActiveSelectionLength = result.NewSelectionLength; - ActiveSelectedText = result.NewSelectionLength > 0 && result.NewSelectionStart + result.NewSelectionLength <= Text.Length - ? Text.Substring(result.NewSelectionStart, result.NewSelectionLength) - : string.Empty; - OnPropertyChanged(nameof(HasTextSelection)); - - // Update parsed spans in real-time - var model = (PdfTextElement)ToModel(); - var parsed = RichTextHelper.ParseMarkdownToSpans(Text, model); - Spans = (parsed.Count > 1 || parsed.Any(s => s.IsBold == true || s.IsItalic == true || s.IsUnderline == true || s.IsStrikethrough == true || !string.IsNullOrEmpty(s.TextColorHex) || s.Script != TextScriptMode.Normal)) - ? parsed - : null; - - if (!IsInEditMode && Spans != null && Spans.Count > 0) - { - Text = RichTextHelper.SpansToPlainText(Spans); - } - - return true; - } - - protected override void OnEditModeChanged(bool isInEditMode) - { - if (isInEditMode) - { - if (Spans != null && Spans.Count > 0) - { - Text = GetMarkdownText(); - } - } - else - { - ClearTextSelection(); - if (!string.IsNullOrEmpty(Text)) - { - SetMarkdownText(Text); - } - } - } - - public override PdfElementBase ToModel() - { - var el = new PdfTextElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - GroupId = GroupId, - Text = Text, - FontFamily = FontFamily, - FontSize = FontSize, - IsBold = IsBold, - IsItalic = IsItalic, - IsUnderline = IsUnderline, - IsDoubleUnderline = IsDoubleUnderline, - IsStrikethrough = IsStrikethrough, - TextColorHex = TextColorHex, - TextOpacity = TextOpacity, - Alignment = Alignment, - VerticalAlignment = VerticalAlignment, - LineHeight = LineHeight, - CharacterSpacing = CharacterSpacing, - WordSpacing = WordSpacing, - ParagraphSpacing = ParagraphSpacing, - TextWrap = TextWrap, - HasStroke = HasStroke, - StrokeColorHex = StrokeColorHex, - StrokeWidth = StrokeWidth, - HasShadow = HasShadow, - ShadowColorHex = ShadowColorHex, - ShadowOffsetX = ShadowOffsetX, - ShadowOffsetY = ShadowOffsetY, - ShadowBlurRadius = ShadowBlurRadius, - ShadowOpacity = ShadowOpacity, - Padding = Padding, - BackgroundColorHex = BackgroundColorHex, - BorderColorHex = BorderColorHex, - BorderThickness = BorderThickness, - CornerRadius = CornerRadius, - ShapeMode = ShapeMode, - CurveRadius = CurveRadius, - CurveArcAngle = CurveArcAngle, - CurveStartAngle = CurveStartAngle, - CurveClockwise = CurveClockwise, - CurveInvert = CurveInvert, - CircularPlacement = CircularPlacement, - BaselineShift = BaselineShift, - CharacterRotation = CharacterRotation, - ScaleX = ScaleX, - ScaleY = ScaleY, - FlipX = FlipX, - FlipY = FlipY, - BezierPreset = BezierPreset, - BezierP0X = BezierP0X, - BezierP0Y = BezierP0Y, - BezierP1X = BezierP1X, - BezierP1Y = BezierP1Y, - BezierP2X = BezierP2X, - BezierP2Y = BezierP2Y, - BezierP3X = BezierP3X, - BezierP3Y = BezierP3Y - }; - - if (Spans != null) - { - el.Spans = new System.Collections.Generic.List(Spans.Count); - foreach (var span in Spans) - { - el.Spans.Add(span.Clone()); - } - } - - return el; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfTextElement textModel) - { - Id = textModel.Id; - X = textModel.X; - Y = textModel.Y; - Width = textModel.Width; - Height = textModel.Height; - ZIndex = textModel.ZIndex; - Rotation = textModel.Rotation; - Opacity = textModel.Opacity; - IsLocked = textModel.IsLocked; - GroupId = textModel.GroupId; - - Text = textModel.Text; - FontFamily = textModel.FontFamily; - FontSize = textModel.FontSize; - IsBold = textModel.IsBold; - IsItalic = textModel.IsItalic; - IsUnderline = textModel.IsUnderline; - IsDoubleUnderline = textModel.IsDoubleUnderline; - IsStrikethrough = textModel.IsStrikethrough; - TextColorHex = textModel.TextColorHex; - TextOpacity = textModel.TextOpacity; - Alignment = textModel.Alignment; - VerticalAlignment = textModel.VerticalAlignment; - LineHeight = textModel.LineHeight; - CharacterSpacing = textModel.CharacterSpacing; - WordSpacing = textModel.WordSpacing; - ParagraphSpacing = textModel.ParagraphSpacing; - TextWrap = textModel.TextWrap; - HasStroke = textModel.HasStroke; - StrokeColorHex = textModel.StrokeColorHex; - StrokeWidth = textModel.StrokeWidth; - HasShadow = textModel.HasShadow; - ShadowColorHex = textModel.ShadowColorHex; - ShadowOffsetX = textModel.ShadowOffsetX; - ShadowOffsetY = textModel.ShadowOffsetY; - ShadowBlurRadius = textModel.ShadowBlurRadius; - ShadowOpacity = textModel.ShadowOpacity; - Padding = textModel.Padding; - BackgroundColorHex = textModel.BackgroundColorHex; - BorderColorHex = textModel.BorderColorHex; - BorderThickness = textModel.BorderThickness; - CornerRadius = textModel.CornerRadius; - ShapeMode = textModel.ShapeMode; - CurveRadius = textModel.CurveRadius; - CurveArcAngle = textModel.CurveArcAngle; - CurveStartAngle = textModel.CurveStartAngle; - CurveClockwise = textModel.CurveClockwise; - CurveInvert = textModel.CurveInvert; - CircularPlacement = textModel.CircularPlacement; - BaselineShift = textModel.BaselineShift; - CharacterRotation = textModel.CharacterRotation; - ScaleX = textModel.ScaleX; - ScaleY = textModel.ScaleY; - FlipX = textModel.FlipX; - FlipY = textModel.FlipY; - BezierPreset = textModel.BezierPreset; - BezierP0X = textModel.BezierP0X; - BezierP0Y = textModel.BezierP0Y; - BezierP1X = textModel.BezierP1X; - BezierP1Y = textModel.BezierP1Y; - BezierP2X = textModel.BezierP2X; - BezierP2Y = textModel.BezierP2Y; - BezierP3X = textModel.BezierP3X; - BezierP3Y = textModel.BezierP3Y; - - if (textModel.Spans != null) - { - Spans = new System.Collections.Generic.List(textModel.Spans.Count); - foreach (var span in textModel.Spans) - { - Spans.Add(span.Clone()); - } - } - else - { - Spans = null; - } - } - } -} diff --git a/src/PdfEditorApp/ViewModels/ElementViewModels/WatermarkElementViewModel.cs b/src/PdfEditorApp/ViewModels/ElementViewModels/WatermarkElementViewModel.cs deleted file mode 100644 index 20a12f7..0000000 --- a/src/PdfEditorApp/ViewModels/ElementViewModels/WatermarkElementViewModel.cs +++ /dev/null @@ -1,72 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels.ElementViewModels; - -public partial class WatermarkElementViewModel : ElementViewModelBase -{ - [ObservableProperty] - private string _text = "CONFIDENTIAL"; - - [ObservableProperty] - private double _fontSize = 48; - - [ObservableProperty] - private string _colorHex = "#CC0000"; - - [ObservableProperty] - private double _angle = -35; - - public override ElementKind Kind => ElementKind.Watermark; - public override string DisplayName => $"Watermark ({Text})"; - - public WatermarkElementViewModel() - { - Opacity = 0.15; - Width = 600; - Height = 300; - } - - public override PdfElementBase ToModel() - { - return new PdfWatermarkElement - { - Id = Id, - X = X, - Y = Y, - Width = Width, - Height = Height, - ZIndex = ZIndex, - Rotation = Rotation, - Opacity = Opacity, - IsLocked = IsLocked, - Text = Text, - FontSize = FontSize, - ColorHex = ColorHex, - Angle = Angle - }; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfWatermarkElement wm) - { - Id = wm.Id; - X = wm.X; - Y = wm.Y; - Width = wm.Width; - Height = wm.Height; - ZIndex = wm.ZIndex; - Rotation = wm.Rotation; - Opacity = wm.Opacity; - IsLocked = wm.IsLocked; - - Text = wm.Text; - FontSize = wm.FontSize; - ColorHex = wm.ColorHex; - Angle = wm.Angle; - } - } -} diff --git a/src/PdfEditorApp/ViewModels/FontManagerViewModel.cs b/src/PdfEditorApp/ViewModels/FontManagerViewModel.cs deleted file mode 100644 index ed06da3..0000000 --- a/src/PdfEditorApp/ViewModels/FontManagerViewModel.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Services; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels; - -/// -/// ViewModel representing an individual downloadable language / font package card. -/// -public partial class FontPackageItemViewModel : ObservableObject -{ - private readonly IFontPackageService _fontService; - - public FontPackageInfo Model { get; } - - public string Id => Model.Id; - public string Name => Model.Name; - public string NativeName => Model.NativeName; - public string FlagEmoji => Model.FlagEmoji; - public string Region => Model.Region; - public FontPackageCategory Category => Model.Category; - public string Description => Model.Description; - public string SampleText => Model.SampleText; - public string FormattedSize => Model.FormattedSize; - public IReadOnlyList SupportedLanguages => Model.SupportedLanguages; - public IReadOnlyList IncludedFontFamilies => Model.IncludedFontFamilies; - - [ObservableProperty] - private bool _isInstalled; - - [ObservableProperty] - private bool _isDownloading; - - [ObservableProperty] - private double _downloadProgress; - - [ObservableProperty] - private string _statusText = "Available"; - - [ObservableProperty] - private string _progressText = ""; - - public FontPackageItemViewModel(FontPackageInfo model, IFontPackageService fontService) - { - Model = model; - _fontService = fontService; - RefreshStatus(); - } - - public void RefreshStatus() - { - IsInstalled = _fontService.IsPackageInstalled(Model); - if (IsInstalled) - { - StatusText = "Installed"; - DownloadProgress = 1.0; - } - else - { - StatusText = $"Available ({FormattedSize})"; - DownloadProgress = 0.0; - } - } - - [RelayCommand] - public async Task DownloadAsync() - { - if (IsDownloading || IsInstalled) return; - - IsDownloading = true; - StatusText = "Connecting..."; - ProgressText = "0%"; - DownloadProgress = 0.0; - - var progress = new Progress(p => - { - DownloadProgress = p; - ProgressText = $"{p * 100:F0}%"; - StatusText = $"Downloading {ProgressText} ({FormattedSize})"; - }); - - bool success = await _fontService.DownloadPackageAsync(Model, progress, status => - { - StatusText = status; - }); - - IsDownloading = false; - IsInstalled = success; - - if (success) - { - StatusText = "Installed & Active"; - DownloadProgress = 1.0; - } - else - { - StatusText = "Download Failed (Click to retry)"; - } - } - - [RelayCommand] - public async Task DeleteAsync() - { - if (IsDownloading) return; - - bool deleted = await _fontService.DeletePackageAsync(Model); - if (deleted) - { - IsInstalled = false; - StatusText = $"Available ({FormattedSize})"; - DownloadProgress = 0.0; - } - } -} - -/// -/// Main ViewModel for the Font & Language Pack Management Studio. -/// Empowers users to download, inspect, and delete international font packs to keep the app ultra-lightweight. -/// -public partial class FontManagerViewModel : ViewModelBase -{ - private readonly IFontPackageService _fontService; - - [ObservableProperty] - private string _searchQuery = ""; - - [ObservableProperty] - private string _selectedCategory = "All"; - - [ObservableProperty] - private string _totalCacheSizeFormatted = "0 MB"; - - [ObservableProperty] - private int _installedCount; - - [ObservableProperty] - private int _totalPackagesCount; - - [ObservableProperty] - private bool _isDownloadingAll; - - [ObservableProperty] - private string _globalStatusMessage = ""; - - public string UserFontDirectory => _fontService.GetUserFontDirectory(); - - public ObservableCollection AllPackages { get; } = new(); - public ObservableCollection FilteredPackages { get; } = new(); - - public FontManagerViewModel() : this(new FontPackageService()) { } - - public FontManagerViewModel(IFontPackageService fontService) - { - _fontService = fontService; - _fontService.FontLibraryChanged += OnFontLibraryChanged; - - InitializePackages(); - _ = RefreshStatsAsync(); - } - - private void InitializePackages() - { - AllPackages.Clear(); - foreach (var pack in _fontService.GetAllPackages()) - { - AllPackages.Add(new FontPackageItemViewModel(pack, _fontService)); - } - - TotalPackagesCount = AllPackages.Count; - ApplyFilter(); - } - - private void OnFontLibraryChanged() - { - foreach (var item in AllPackages) - { - item.RefreshStatus(); - } - _ = RefreshStatsAsync(); - ApplyFilter(); - } - - public async Task RefreshStatsAsync() - { - long bytes = await _fontService.GetTotalCacheSizeBytesAsync(); - TotalCacheSizeFormatted = FontPackageInfo.FormatBytes(bytes); - InstalledCount = AllPackages.Count(p => p.IsInstalled); - } - - partial void OnSearchQueryChanged(string value) => ApplyFilter(); - partial void OnSelectedCategoryChanged(string value) => ApplyFilter(); - - [RelayCommand] - public void SelectCategory(string category) - { - SelectedCategory = category; - } - - private void ApplyFilter() - { - FilteredPackages.Clear(); - string query = SearchQuery.Trim().ToLowerInvariant(); - - foreach (var pack in AllPackages) - { - // Category filter - bool matchesCategory = SelectedCategory switch - { - "All" => true, - "Installed" => pack.IsInstalled, - "EastAsia" => pack.Category == FontPackageCategory.EastAsia, - "SouthAsia" => pack.Category == FontPackageCategory.SouthAsia, - "MiddleEast" => pack.Category == FontPackageCategory.MiddleEast, - "SoutheastAsia" => pack.Category == FontPackageCategory.SoutheastAsia, - "EuropeAndEurasia" => pack.Category == FontPackageCategory.EuropeAndEurasia, - "DesignAndTypography" => pack.Category == FontPackageCategory.DesignAndTypography, - _ => true - }; - - if (!matchesCategory) continue; - - // Search query - if (!string.IsNullOrEmpty(query)) - { - bool matchesSearch = - pack.Name.ToLowerInvariant().Contains(query) || - pack.NativeName.ToLowerInvariant().Contains(query) || - pack.Region.ToLowerInvariant().Contains(query) || - pack.Description.ToLowerInvariant().Contains(query) || - pack.SupportedLanguages.Any(l => l.ToLowerInvariant().Contains(query)) || - pack.IncludedFontFamilies.Any(f => f.ToLowerInvariant().Contains(query)); - - if (!matchesSearch) continue; - } - - FilteredPackages.Add(pack); - } - } - - [RelayCommand] - public async Task DownloadAllAsync() - { - if (IsDownloadingAll) return; - - IsDownloadingAll = true; - GlobalStatusMessage = "Downloading all language packs..."; - - var uninstalled = AllPackages.Where(p => !p.IsInstalled).ToList(); - int completed = 0; - - foreach (var pack in uninstalled) - { - GlobalStatusMessage = $"Downloading {pack.Name} ({completed + 1}/{uninstalled.Count})..."; - await pack.DownloadAsync(); - completed++; - } - - IsDownloadingAll = false; - GlobalStatusMessage = "All packs downloaded successfully!"; - await RefreshStatsAsync(); - } - - [RelayCommand] - public async Task ClearAllCacheAsync() - { - await _fontService.ClearAllCacheAsync(); - foreach (var pack in AllPackages) - { - pack.RefreshStatus(); - } - await RefreshStatsAsync(); - ApplyFilter(); - GlobalStatusMessage = "Font cache cleared. All local font downloads removed."; - } - - [RelayCommand] - public void OpenFontFolder() - { - try - { - string dir = UserFontDirectory; - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - if (OperatingSystem.IsMacOS()) - { - Process.Start("open", dir); - } - else if (OperatingSystem.IsWindows()) - { - Process.Start("explorer.exe", dir); - } - else - { - Process.Start("xdg-open", dir); - } - } - catch { } - } -} diff --git a/src/PdfEditorApp/ViewModels/FryPdfViewer/FryPdfPageViewerViewModel.cs b/src/PdfEditorApp/ViewModels/FryPdfViewer/FryPdfPageViewerViewModel.cs deleted file mode 100644 index 73853e8..0000000 --- a/src/PdfEditorApp/ViewModels/FryPdfViewer/FryPdfPageViewerViewModel.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels.FryPdfViewer; - -/// -/// Page ViewModel specifically designed for read-only presentation and interactive document viewing. -/// Separates standard visual elements from rich interactive elements (tables, charts, living forms). -/// -public partial class FryPdfPageViewerViewModel : ObservableObject -{ - [ObservableProperty] - private string _id = Guid.NewGuid().ToString("N"); - - [ObservableProperty] - private int _pageNumber = 1; - - [ObservableProperty] - private PageFormat _format = PageFormat.A4; - - [ObservableProperty] - private PageOrientation _orientation = PageOrientation.Portrait; - - [ObservableProperty] - private int _rotationAngle = 0; - - [ObservableProperty] - private double _width = 800; - - [ObservableProperty] - private double _height = 1131; - - [ObservableProperty] - private string _backgroundColorHex = "#FFFFFF"; - - [ObservableProperty] - private bool _showHeaderFooter = true; - - [ObservableProperty] - private string? _headerLeft; - - [ObservableProperty] - private string? _headerCenter; - - [ObservableProperty] - private string? _headerRight; - - [ObservableProperty] - private string? _footerLeft; - - [ObservableProperty] - private string? _footerCenter; - - [ObservableProperty] - private string? _footerRight; - - /// Unified collection of all presentation elements on this page in natural Z-index order. - public ObservableCollection Elements { get; } = new(); - - /// Interactive living data tables with real-time row search, sorting, and CSV export. - public IEnumerable InteractiveTables => Elements.OfType(); - - /// Interactive animated charts with hover tooltips and dynamic animations. - public IEnumerable InteractiveCharts => Elements.OfType(); - - /// Standard visual elements (text, vector shapes, images, math formulas, svgs, barcodes, stamps). - public IEnumerable StandardElements => Elements.Where(e => e is not InteractiveTableViewModel && e is not InteractiveChartViewModel); - - public FryPdfPageViewerViewModel() - { - } - - public static FryPdfPageViewerViewModel FromPageModel(PdfPageModel pageModel) - { - var vm = new FryPdfPageViewerViewModel - { - Id = pageModel.Id, - PageNumber = pageModel.PageNumber, - Format = pageModel.Format, - Orientation = pageModel.Orientation, - RotationAngle = pageModel.RotationAngle, - Width = pageModel.Width, - Height = pageModel.Height, - BackgroundColorHex = pageModel.BackgroundColorHex, - ShowHeaderFooter = pageModel.ShowHeaderFooter, - HeaderLeft = pageModel.HeaderLeft, - HeaderCenter = pageModel.HeaderCenter, - HeaderRight = pageModel.HeaderRight, - FooterLeft = pageModel.FooterLeft, - FooterCenter = pageModel.FooterCenter, - FooterRight = pageModel.FooterRight - }; - - foreach (var el in pageModel.Elements) - { - if (el is PdfTableElement tableModel) - { - vm.Elements.Add(new InteractiveTableViewModel(tableModel)); - } - else if (el is PdfChartElement chartModel) - { - vm.Elements.Add(new InteractiveChartViewModel(chartModel)); - } - else - { - // Create standard view model using default service - var standardVm = PageViewModel.DefaultElementService.CreateViewModel(el); - // Ensure edit mode is false in viewer - standardVm.IsSelected = false; - standardVm.IsInEditMode = false; - vm.Elements.Add(standardVm); - } - } - - return vm; - } - - public static FryPdfPageViewerViewModel FromPageViewModel(PageViewModel pageVm) - { - var vm = new FryPdfPageViewerViewModel - { - Id = pageVm.Id, - PageNumber = pageVm.PageNumber, - Format = pageVm.Format, - Orientation = pageVm.Orientation, - RotationAngle = pageVm.RotationAngle, - Width = pageVm.Width, - Height = pageVm.Height, - BackgroundColorHex = pageVm.BackgroundColorHex, - ShowHeaderFooter = pageVm.ShowHeaderFooter, - HeaderLeft = pageVm.HeaderLeft, - HeaderCenter = pageVm.HeaderCenter, - HeaderRight = pageVm.HeaderRight, - FooterLeft = pageVm.FooterLeft, - FooterCenter = pageVm.FooterCenter, - FooterRight = pageVm.FooterRight - }; - - foreach (var el in pageVm.Elements) - { - if (el is TableElementViewModel tableVm) - { - vm.Elements.Add(new InteractiveTableViewModel(tableVm)); - } - else if (el is ChartElementViewModel chartVm) - { - vm.Elements.Add(new InteractiveChartViewModel(chartVm)); - } - else - { - // Ensure selection / edit mode is disabled in read-only presentation - el.IsSelected = false; - el.IsInEditMode = false; - vm.Elements.Add(el); - } - } - - return vm; - } - - /// - /// Replays entrance animations on all interactive charts on this page. - /// - public void ReplayChartAnimations() - { - foreach (var chart in InteractiveCharts) - { - chart.ReplayAnimation(); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/FryPdfViewer/FryPdfViewerViewModel.cs b/src/PdfEditorApp/ViewModels/FryPdfViewer/FryPdfViewerViewModel.cs deleted file mode 100644 index f96262c..0000000 --- a/src/PdfEditorApp/ViewModels/FryPdfViewer/FryPdfViewerViewModel.cs +++ /dev/null @@ -1,463 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Controls.Primitives; -using Avalonia.Media; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.FryPdfViewer; - -/// -/// Result item for text searches across pages in a .frypdf document. -/// -public record FryPdfSearchResult(int PageIndex, int PageNumber, string Snippet, string ElementId); - -/// -/// Master ViewModel for the dedicated .frypdf Interactive Document Viewer & Presentation Reader. -/// Provides a distraction-free, read-only presentation experience with dynamic capabilities -/// impossible in static binary PDFs (scrolling data tables, animated charts, live forms, slide show mode). -/// -public partial class FryPdfViewerViewModel : ViewModelBase -{ - private readonly IProjectPersistenceService _persistenceService; - private readonly IPdfExportService _exportService; - - public IStorageProvider? StorageProvider { get; set; } - - [ObservableProperty] - private string _documentTitle = "Document.frypdf"; - - [ObservableProperty] - private string _filePath = ""; - - [ObservableProperty] - private bool _isLoading = false; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(TotalPagesCount))] - [NotifyPropertyChangedFor(nameof(HasPages))] - [NotifyPropertyChangedFor(nameof(ShowHeaderFooterInView))] - private FryPdfPageViewerViewModel? _currentPage; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(CurrentPageNumber))] - [NotifyPropertyChangedFor(nameof(CanGoNextPage))] - [NotifyPropertyChangedFor(nameof(CanGoPreviousPage))] - [NotifyCanExecuteChangedFor(nameof(NextPageCommand))] - [NotifyCanExecuteChangedFor(nameof(PreviousPageCommand))] - private int _currentPageIndex = 0; - - [ObservableProperty] - private double _zoomLevel = 1.0; - - [ObservableProperty] - private bool _isContinuousScroll = false; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(CanGoNextPage))] - [NotifyPropertyChangedFor(nameof(CanGoPreviousPage))] - [NotifyPropertyChangedFor(nameof(IsSidebarVisibleInView))] - [NotifyPropertyChangedFor(nameof(ViewportBackgroundBrush))] - [NotifyPropertyChangedFor(nameof(ViewportPadding))] - [NotifyPropertyChangedFor(nameof(ViewportScrollVisibility))] - [NotifyPropertyChangedFor(nameof(PageBorderThickness))] - [NotifyPropertyChangedFor(nameof(PageCornerRadius))] - [NotifyPropertyChangedFor(nameof(PageBoxShadow))] - [NotifyPropertyChangedFor(nameof(ShowHeaderFooterInView))] - [NotifyCanExecuteChangedFor(nameof(NextPageCommand))] - [NotifyCanExecuteChangedFor(nameof(PreviousPageCommand))] - private bool _isPresentationMode = false; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsSidebarVisibleInView))] - private bool _isThumbnailSidebarOpen = true; - - [ObservableProperty] - private PdfReaderTheme _readingTheme = PdfReaderTheme.Default; - - [ObservableProperty] - private string _searchQuery = ""; - - [ObservableProperty] - private int _activeSearchResultIndex = 0; - - public ObservableCollection Pages { get; } = new(); - public ObservableCollection SearchResults { get; } = new(); - - public int TotalPagesCount => Pages.Count; - public bool HasPages => Pages.Count > 0; - public int CurrentPageNumber => CurrentPageIndex + 1; - public bool CanGoNextPage => IsPresentationMode ? Pages.Count > 1 : (Pages.Count > 0 && CurrentPageIndex < Pages.Count - 1); - public bool CanGoPreviousPage => IsPresentationMode ? Pages.Count > 1 : (Pages.Count > 0 && CurrentPageIndex > 0); - - public bool IsSidebarVisibleInView => IsThumbnailSidebarOpen && !IsPresentationMode; - public string ViewportBackgroundBrush => IsPresentationMode ? "#000000" : "Transparent"; - public Thickness ViewportPadding => IsPresentationMode ? new Thickness(0) : new Thickness(36); - public ScrollBarVisibility ViewportScrollVisibility => IsPresentationMode ? ScrollBarVisibility.Disabled : ScrollBarVisibility.Auto; - public double PageBorderThickness => IsPresentationMode ? 0 : 1; - public CornerRadius PageCornerRadius => IsPresentationMode ? new CornerRadius(0) : new CornerRadius(4); - public BoxShadows PageBoxShadow => IsPresentationMode ? new BoxShadows(new BoxShadow()) : BoxShadows.Parse("0 8 32 #18000000"); - public bool ShowHeaderFooterInView => CurrentPage != null && CurrentPage.ShowHeaderFooter && !IsPresentationMode; - - public PdfDocumentModel? CurrentDocumentModel { get; private set; } - - public FryPdfViewerViewModel( - IProjectPersistenceService? persistenceService = null, - IPdfExportService? exportService = null) - { - _persistenceService = persistenceService ?? new ProjectPersistenceService(); - _exportService = exportService ?? new PdfExportService(); - } - - /// - /// Loads a .frypdf document file from disk into the interactive viewer. - /// - public async Task LoadDocumentAsync(string filePath) - { - if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) - return; - - IsLoading = true; - FilePath = filePath; - DocumentTitle = Path.GetFileName(filePath); - - try - { - var model = await _persistenceService.LoadProjectAsync(filePath); - if (model != null) - { - LoadFromModel(model, filePath); - } - } - finally - { - IsLoading = false; - } - } - - /// - /// Loads an in-memory PdfDocumentModel into the viewer. - /// - public void LoadFromModel(PdfDocumentModel model, string? filePath = null) - { - CurrentDocumentModel = model; - FilePath = filePath ?? string.Empty; - DocumentTitle = !string.IsNullOrWhiteSpace(filePath) - ? Path.GetFileName(filePath) - : (!string.IsNullOrWhiteSpace(model.Title) - ? (model.Title.EndsWith(".frypdf", StringComparison.OrdinalIgnoreCase) - ? model.Title - : $"{Path.GetFileNameWithoutExtension(model.Title)}.frypdf") - : "Document.frypdf"); - - Pages.Clear(); - foreach (var pageModel in model.Pages) - { - var pageVm = FryPdfPageViewerViewModel.FromPageModel(pageModel); - Pages.Add(pageVm); - } - - CurrentPageIndex = 0; - CurrentPage = Pages.FirstOrDefault(); - - // Trigger chart entry animations on first page - CurrentPage?.ReplayChartAnimations(); - - OnPropertyChanged(nameof(TotalPagesCount)); - OnPropertyChanged(nameof(HasPages)); - OnPropertyChanged(nameof(CurrentPageNumber)); - OnPropertyChanged(nameof(CanGoNextPage)); - OnPropertyChanged(nameof(CanGoPreviousPage)); - NextPageCommand.NotifyCanExecuteChanged(); - PreviousPageCommand.NotifyCanExecuteChanged(); - } - - partial void OnCurrentPageIndexChanged(int value) - { - if (value >= 0 && value < Pages.Count) - { - CurrentPage = Pages[value]; - CurrentPage.ReplayChartAnimations(); - } - } - - // --- PAGE NAVIGATION COMMANDS --- - - [RelayCommand(CanExecute = nameof(CanGoNextPage))] - public void NextPage() - { - if (Pages.Count <= 1) return; - - if (CurrentPageIndex < Pages.Count - 1) - { - CurrentPageIndex++; - } - else if (IsPresentationMode) - { - // Smooth wrap-around in presentation mode - CurrentPageIndex = 0; - } - } - - [RelayCommand(CanExecute = nameof(CanGoPreviousPage))] - public void PreviousPage() - { - if (Pages.Count <= 1) return; - - if (CurrentPageIndex > 0) - { - CurrentPageIndex--; - } - else if (IsPresentationMode) - { - // Smooth wrap-around in presentation mode - CurrentPageIndex = Pages.Count - 1; - } - } - - [RelayCommand] - public void FirstPage() - { - if (Pages.Count > 0) - { - CurrentPageIndex = 0; - } - } - - [RelayCommand] - public void LastPage() - { - if (Pages.Count > 0) - { - CurrentPageIndex = Pages.Count - 1; - } - } - - [RelayCommand] - public void GoToPage(int pageNumber) - { - int targetIdx = pageNumber - 1; - if (targetIdx >= 0 && targetIdx < Pages.Count) - { - CurrentPageIndex = targetIdx; - } - } - - // --- ZOOM COMMANDS --- - - [RelayCommand] - public void ZoomIn() - { - ZoomLevel = Math.Min(5.0, Math.Round(ZoomLevel + 0.15, 2)); - } - - [RelayCommand] - public void ZoomOut() - { - ZoomLevel = Math.Max(0.25, Math.Round(ZoomLevel - 0.15, 2)); - } - - [RelayCommand] - public void ResetZoom() - { - ZoomLevel = 1.0; - } - - [RelayCommand] - public void FitToWidth() - { - ZoomLevel = 1.25; - } - - public void FitToViewport(double viewportWidth, double viewportHeight) - { - if (CurrentPage == null || CurrentPage.Width <= 0 || CurrentPage.Height <= 0) - return; - - double availWidth = Math.Max(100, viewportWidth); - double availHeight = Math.Max(100, viewportHeight); - - double scaleX = availWidth / CurrentPage.Width; - double scaleY = availHeight / CurrentPage.Height; - double fitScale = Math.Min(scaleX, scaleY); - - if (fitScale > 0.1 && fitScale < 10.0) - { - ZoomLevel = Math.Round(fitScale, 3); - } - } - - [RelayCommand] - public void FitToPage() - { - ZoomLevel = 0.85; - } - - // --- INTERACTIVE FEATURES COMMANDS --- - - [RelayCommand] - public void TogglePresentationMode() - { - IsPresentationMode = !IsPresentationMode; - } - - [RelayCommand] - public void ToggleThumbnailSidebar() - { - IsThumbnailSidebarOpen = !IsThumbnailSidebarOpen; - } - - [RelayCommand] - public void ReplayAnimations() - { - CurrentPage?.ReplayChartAnimations(); - } - - [RelayCommand] - public void SetReadingTheme(PdfReaderTheme theme) - { - ReadingTheme = theme; - } - - // --- SEARCH COMMANDS --- - - partial void OnSearchQueryChanged(string value) - { - PerformSearch(); - } - - private void PerformSearch() - { - SearchResults.Clear(); - if (string.IsNullOrWhiteSpace(SearchQuery)) - { - ActiveSearchResultIndex = 0; - return; - } - - string q = SearchQuery.Trim(); - for (int p = 0; p < Pages.Count; p++) - { - var page = Pages[p]; - foreach (var el in page.Elements) - { - if (el is ElementViewModels.TextElementViewModel textEl && - !string.IsNullOrEmpty(textEl.Text) && - textEl.Text.Contains(q, StringComparison.OrdinalIgnoreCase)) - { - string snippet = textEl.Text.Length > 60 ? textEl.Text.Substring(0, 57) + "..." : textEl.Text; - SearchResults.Add(new FryPdfSearchResult(p, p + 1, snippet, textEl.Id)); - } - } - - foreach (var table in page.InteractiveTables) - { - if (table.AllRows.Any(r => r.MatchesFilter(q))) - { - SearchResults.Add(new FryPdfSearchResult(p, p + 1, $"Table match on page {p + 1}", string.Empty)); - } - } - } - - if (SearchResults.Count > 0) - { - ActiveSearchResultIndex = 0; - GoToSearchResult(0); - } - } - - [RelayCommand] - public void NextSearchResult() - { - if (SearchResults.Count == 0) return; - ActiveSearchResultIndex = (ActiveSearchResultIndex + 1) % SearchResults.Count; - GoToSearchResult(ActiveSearchResultIndex); - } - - [RelayCommand] - public void PreviousSearchResult() - { - if (SearchResults.Count == 0) return; - ActiveSearchResultIndex = (ActiveSearchResultIndex - 1 + SearchResults.Count) % SearchResults.Count; - GoToSearchResult(ActiveSearchResultIndex); - } - - private void GoToSearchResult(int index) - { - if (index >= 0 && index < SearchResults.Count) - { - CurrentPageIndex = SearchResults[index].PageIndex; - } - } - - // --- APPLICATION WORKFLOW COMMANDS --- - - /// - /// Navigates back to the Home Dashboard. - /// - [RelayCommand] - public void BackToHome() - { - WeakReferenceMessenger.Default.Send(new NavigateToHomeMessage()); - } - - /// - /// Switches from read-only Interactive Viewer into full Studio Editor for the active document. - /// - [RelayCommand] - public void OpenInEditor() - { - if (!string.IsNullOrWhiteSpace(FilePath)) - { - WeakReferenceMessenger.Default.Send(new OpenInEditorMessage(FilePath)); - } - } - - /// - /// Exports the current document to standard binary PDF via QuestPDF. - /// - [RelayCommand] - public async Task ExportPdfAsync() - { - if (CurrentDocumentModel == null) return; - - try - { - if (StorageProvider != null) - { - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Export .frypdf to Vector PDF", - SuggestedFileName = Path.ChangeExtension(DocumentTitle, ".pdf"), - DefaultExtension = "pdf", - FileTypeChoices = new[] - { - new FilePickerFileType("PDF Document (*.pdf)") { Patterns = new[] { "*.pdf" } } - } - }); - - if (file != null) - { - string targetPath = file.Path.LocalPath; - await _exportService.ExportToFileAsync(CurrentDocumentModel, targetPath); - WeakReferenceMessenger.Default.Send(new ShowToastMessage($"Exported PDF: {Path.GetFileName(targetPath)}")); - } - } - } - catch (Exception ex) - { - WeakReferenceMessenger.Default.Send(new ShowToastMessage($"Export error: {ex.Message}", ToastNotificationType.Danger)); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/FryPdfViewer/InteractiveChartViewModel.cs b/src/PdfEditorApp/ViewModels/FryPdfViewer/InteractiveChartViewModel.cs deleted file mode 100644 index d471a2b..0000000 --- a/src/PdfEditorApp/ViewModels/FryPdfViewer/InteractiveChartViewModel.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels.FryPdfViewer; - -/// -/// Individual interactive bar/data item with animation interpolation and hover details. -/// -public partial class InteractiveChartItem : ObservableObject -{ - [ObservableProperty] - private string _category = ""; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(AnimatedValue))] - [NotifyPropertyChangedFor(nameof(AnimatedHeightPx))] - private double _targetValue; - - [ObservableProperty] - private string _valueLabel = ""; - - [ObservableProperty] - private string _colorHex = "#0F6CBD"; - - [ObservableProperty] - private double _percentage; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(AnimatedValue))] - [NotifyPropertyChangedFor(nameof(AnimatedHeightPx))] - [NotifyPropertyChangedFor(nameof(AnimatedWidthPx))] - private double _animationProgress = 1.0; - - public double AnimatedValue => TargetValue * AnimationProgress; - - public double MaxPlotHeight { get; set; } = 150.0; - public double MaxPlotWidth { get; set; } = 260.0; - public double MaxValue { get; set; } = 1.0; - - public double AnimatedHeightPx - { - get - { - if (MaxValue <= 0) return 6; - double ratio = Math.Clamp(AnimatedValue / MaxValue, 0.0, 1.0); - return Math.Max(6, ratio * MaxPlotHeight); - } - } - - public double AnimatedWidthPx - { - get - { - if (MaxValue <= 0) return 8; - double ratio = Math.Clamp(AnimatedValue / MaxValue, 0.0, 1.0); - return Math.Max(8, ratio * MaxPlotWidth); - } - } - - public string ProgressPercentageString => $"{Percentage:F1}%"; - public string TooltipText => $"{Category}: {ValueLabel} ({Percentage:F1}%)"; -} - -/// -/// Interactive ViewModel wrapping a chart in the .frypdf document. -/// Provides dynamic capabilities impossible in static binary PDFs: -/// - Smooth animated chart transitions & entry animations -/// - Interactive hover tooltips showing category, value, and share % -/// - Multi-chart support: Column Bar, Horizontal Bar, Donut/Pie, Line/Area, and Gauge -/// - Instant toggle between visual chart and underlying data table -/// - Replay animation trigger for presentations -/// -public partial class InteractiveChartViewModel : ElementViewModelBase -{ - private DispatcherTimer? _animationTimer; - private int _animationStep = 0; - private const int TotalAnimationSteps = 24; - private PdfChartElement? _sourceModel; - - public override ElementKind Kind => ElementKind.Chart; - public override string DisplayName => "Interactive Chart"; - - [ObservableProperty] - private string _title = "Chart Overview"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ChartTypeDescription))] - [NotifyPropertyChangedFor(nameof(IsBarColumn))] - [NotifyPropertyChangedFor(nameof(IsHorizontalBar))] - [NotifyPropertyChangedFor(nameof(IsDonutPie))] - [NotifyPropertyChangedFor(nameof(IsLineOrArea))] - [NotifyPropertyChangedFor(nameof(IsGaugeProgress))] - private ChartType _chartType = ChartType.BarColumn; - - [ObservableProperty] - private string _backgroundColorHex = "#FFFFFF"; - - [ObservableProperty] - private string _borderColorHex = "#E2E8F0"; - - [ObservableProperty] - private bool _isShowingDataTable = false; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(CenterSummaryValue))] - [NotifyPropertyChangedFor(nameof(CenterSummaryLabel))] - private InteractiveChartItem? _hoveredItem; - - [ObservableProperty] - private double _animationProgress = 1.0; - - [ObservableProperty] - private Avalonia.Media.Imaging.Bitmap? _chartBitmap; - - public ObservableCollection Items { get; } = new(); - - public string ChartTypeDescription => ChartType.ToString(); - public bool IsBarColumn => ChartType == ChartType.BarColumn; - public bool IsHorizontalBar => ChartType == ChartType.HorizontalBar; - public bool IsDonutPie => ChartType == ChartType.DonutPie; - public bool IsLineOrArea => ChartType == ChartType.Line || ChartType == ChartType.Area || ChartType == ChartType.SmoothLine; - public bool IsGaugeProgress => ChartType == ChartType.GaugeProgress; - - public double TotalSum => Items.Sum(i => i.TargetValue); - public double MaxItemValue => Items.Count > 0 ? Math.Max(1.0, Items.Max(i => i.TargetValue)) : 1.0; - - public string CenterSummaryValue => HoveredItem != null - ? HoveredItem.ValueLabel - : (TotalSum >= 1000 ? $"${TotalSum / 1000.0:F1}B" : (TotalSum > 0 ? $"${TotalSum:F1}M" : "100%")); - - public string CenterSummaryLabel => HoveredItem != null - ? HoveredItem.Category - : (IsDonutPie ? "Total ARR" : (IsGaugeProgress ? "Attainment" : "Portfolio")); - - public InteractiveChartViewModel() - { - Width = 460; - Height = 260; - ZIndex = 400; - } - - public InteractiveChartViewModel(ChartElementViewModel chartVm) - { - Id = chartVm.Id; - X = chartVm.X; - Y = chartVm.Y; - Width = chartVm.Width; - Height = chartVm.Height; - ZIndex = chartVm.ZIndex; - Rotation = chartVm.Rotation; - Opacity = chartVm.Opacity; - Title = chartVm.Title; - ChartType = chartVm.ChartType; - BackgroundColorHex = chartVm.BackgroundColorHex; - BorderColorHex = chartVm.BorderColorHex; - ChartBitmap = chartVm.ChartBitmap; - - PopulateFromBars(chartVm.Bars.Select(b => (b.Category, b.Value, b.ValueLabel, b.ColorHex))); - } - - public InteractiveChartViewModel(PdfChartElement chartModel) - { - _sourceModel = chartModel; - PopulateBaseProperties(chartModel); - LoadFromChartModel(chartModel); - } - - private void LoadFromChartModel(PdfChartElement chartModel) - { - Title = chartModel.Title; - ChartType = chartModel.ChartType; - BackgroundColorHex = chartModel.BackgroundColorHex; - BorderColorHex = chartModel.BorderColorHex; - - var palette = LiveChartsRenderer.GetPaletteHexColors(chartModel.Palette); - var barData = new List<(string Category, double Value, string ValueLabel, string ColorHex)>(); - - var categories = chartModel.Categories.Count > 0 ? chartModel.Categories : new List { "A", "B", "C", "D" }; - - for (int i = 0; i < categories.Count; i++) - { - double val = (i < chartModel.Values.Count) ? chartModel.Values[i] : 0.0; - string label = (i < chartModel.ValueLabels.Count && !string.IsNullOrWhiteSpace(chartModel.ValueLabels[i])) - ? chartModel.ValueLabels[i] - : val.ToString("N0"); - string color = (i < chartModel.BarColorsHex.Count && !string.IsNullOrWhiteSpace(chartModel.BarColorsHex[i])) - ? chartModel.BarColorsHex[i] - : palette[i % palette.Count]; - barData.Add((categories[i], val, label, color)); - } - - PopulateFromBars(barData); - - if (IsLineOrArea || ChartType == ChartType.Radar || ChartType == ChartType.PolarArea || ChartType == ChartType.ScatterPlot) - { - try - { - var pngBytes = LiveChartsRenderer.RenderChartToPngBytes(chartModel, (int)Width, (int)Height, 2.0f); - if (pngBytes.Length > 0) - { - using var ms = new System.IO.MemoryStream(pngBytes); - var oldBmp = ChartBitmap; - ChartBitmap = new Avalonia.Media.Imaging.Bitmap(ms); - oldBmp?.Dispose(); - } - } - catch - { - // Fallback gracefully in headless test environments - } - } - } - - public override PdfElementBase ToModel() - { - var model = _sourceModel ?? new PdfChartElement(); - CopyBasePropertiesTo(model); - model.Title = Title; - model.ChartType = ChartType; - model.BackgroundColorHex = BackgroundColorHex; - model.BorderColorHex = BorderColorHex; - model.Categories = Items.Select(i => i.Category).ToList(); - model.Values = Items.Select(i => i.TargetValue).ToList(); - model.ValueLabels = Items.Select(i => i.ValueLabel).ToList(); - model.BarColorsHex = Items.Select(i => i.ColorHex).ToList(); - return model; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfChartElement chart) - { - _sourceModel = chart; - PopulateBaseProperties(chart); - LoadFromChartModel(chart); - } - } - - private void PopulateFromBars(IEnumerable<(string Category, double Value, string ValueLabel, string ColorHex)> bars) - { - Items.Clear(); - var barList = bars.ToList(); - double sum = barList.Sum(b => b.Value); - double max = barList.Count > 0 ? Math.Max(1.0, barList.Max(b => b.Value)) : 1.0; - double plotHeight = Math.Max(60, Height - 80); - double plotWidth = Math.Max(80, Width - 190); - - foreach (var (cat, val, label, color) in barList) - { - double pct = sum > 0 ? (val / sum) * 100.0 : 0.0; - Items.Add(new InteractiveChartItem - { - Category = cat, - TargetValue = val, - ValueLabel = string.IsNullOrWhiteSpace(label) ? val.ToString("N0") : label, - ColorHex = color, - Percentage = pct, - MaxPlotHeight = plotHeight, - MaxPlotWidth = plotWidth, - MaxValue = max, - AnimationProgress = 1.0 - }); - } - } - - /// - /// Smoothly triggers or replays the chart entry animation. - /// - [RelayCommand] - public void ReplayAnimation() - { - _animationTimer?.Stop(); - _animationStep = 0; - AnimationProgress = 0.0; - - foreach (var item in Items) - { - item.AnimationProgress = 0.0; - } - - _animationTimer = new DispatcherTimer - { - Interval = TimeSpan.FromMilliseconds(16) // ~60 FPS - }; - - _animationTimer.Tick += (s, e) => - { - _animationStep++; - double t = (double)_animationStep / TotalAnimationSteps; - - // Ease-out cubic: 1 - (1 - t)^3 - double eased = 1.0 - Math.Pow(1.0 - t, 3.0); - AnimationProgress = Math.Clamp(eased, 0.0, 1.0); - - foreach (var item in Items) - { - item.AnimationProgress = AnimationProgress; - } - - if (_animationStep >= TotalAnimationSteps) - { - _animationTimer.Stop(); - AnimationProgress = 1.0; - foreach (var item in Items) - { - item.AnimationProgress = 1.0; - } - } - }; - - _animationTimer.Start(); - } - - /// - /// Toggles between visual chart and underlying tabular data view. - /// - [RelayCommand] - public void ToggleDataTable() - { - IsShowingDataTable = !IsShowingDataTable; - } - - [RelayCommand] - public void SetHoveredItem(InteractiveChartItem? item) - { - HoveredItem = item; - } -} diff --git a/src/PdfEditorApp/ViewModels/FryPdfViewer/InteractiveTableViewModel.cs b/src/PdfEditorApp/ViewModels/FryPdfViewer/InteractiveTableViewModel.cs deleted file mode 100644 index 5d51ec7..0000000 --- a/src/PdfEditorApp/ViewModels/FryPdfViewer/InteractiveTableViewModel.cs +++ /dev/null @@ -1,307 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Input.Platform; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels.FryPdfViewer; - -/// -/// Represents a row in an interactive data table with cell lookup and formatted display. -/// -public class InteractiveTableRowItem : ObservableObject -{ - public List Cells { get; } - - public InteractiveTableRowItem(IEnumerable cells) - { - Cells = cells.ToList(); - } - - public string GetCell(int index) => (index >= 0 && index < Cells.Count) ? Cells[index] : string.Empty; - - public bool MatchesFilter(string query) - { - if (string.IsNullOrWhiteSpace(query)) return true; - for (int i = 0; i < Cells.Count; i++) - { - if (Cells[i].Contains(query, StringComparison.OrdinalIgnoreCase)) - return true; - } - return false; - } -} - -/// -/// Interactive ViewModel wrapping a data table in the .frypdf document. -/// Provides capabilities impossible in static binary PDFs: -/// - Real-time in-table row search & filtering -/// - Clickable column headers with automatic ascending/descending sorting -/// - Sticky header and smooth virtualized scrolling -/// - One-click "Copy Table as CSV" to clipboard -/// - Alternating row styling & row selection highlighting -/// -public partial class InteractiveTableViewModel : ElementViewModelBase -{ - private PdfTableElement? _sourceModel; - - public override ElementKind Kind => ElementKind.Table; - public override string DisplayName => "Interactive Table"; - - [ObservableProperty] - private string _headerBackgroundHex = "#0F6CBD"; - - [ObservableProperty] - private string _headerTextHex = "#FFFFFF"; - - [ObservableProperty] - private string _alternateRowBackgroundHex = "#F8FAFC"; - - [ObservableProperty] - private string _borderColorHex = "#E2E8F0"; - - [ObservableProperty] - private string _filterQuery = ""; - - [ObservableProperty] - private int? _sortedColumnIndex = null; - - [ObservableProperty] - private bool _sortAscending = true; - - [ObservableProperty] - private InteractiveTableRowItem? _selectedRow; - - public ObservableCollection Headers { get; } = new(); - public List AllRows { get; } = new(); - public ObservableCollection DisplayedRows { get; } = new(); - - public int TotalRowCount => AllRows.Count; - public int FilteredRowCount => DisplayedRows.Count; - - public string RowCountSummary => string.IsNullOrWhiteSpace(FilterQuery) - ? $"{TotalRowCount} rows" - : $"{FilteredRowCount} of {TotalRowCount} rows match"; - - public InteractiveTableViewModel() - { - Width = 500; - Height = 240; - ZIndex = 500; - } - - public InteractiveTableViewModel(TableElementViewModel tableVm) - { - Id = tableVm.Id; - X = tableVm.X; - Y = tableVm.Y; - Width = tableVm.Width; - Height = tableVm.Height; - ZIndex = tableVm.ZIndex; - Rotation = tableVm.Rotation; - Opacity = tableVm.Opacity; - HeaderBackgroundHex = tableVm.HeaderBackgroundHex; - HeaderTextHex = tableVm.HeaderTextHex; - AlternateRowBackgroundHex = tableVm.AlternateRowBackgroundHex; - BorderColorHex = tableVm.BorderColorHex; - - foreach (var h in tableVm.Headers) - { - Headers.Add(h.Text); - } - - foreach (var r in tableVm.Rows) - { - var cellValues = r.Cells.Select(c => c.Text).ToList(); - var rowItem = new InteractiveTableRowItem(cellValues); - AllRows.Add(rowItem); - DisplayedRows.Add(rowItem); - } - } - - public InteractiveTableViewModel(PdfTableElement tableModel) - { - _sourceModel = tableModel; - PopulateBaseProperties(tableModel); - LoadFromTableModel(tableModel); - } - - private void LoadFromTableModel(PdfTableElement tableModel) - { - HeaderBackgroundHex = tableModel.HeaderBackgroundHex; - HeaderTextHex = tableModel.HeaderTextHex; - AlternateRowBackgroundHex = tableModel.AlternateRowBackgroundHex; - BorderColorHex = tableModel.BorderColorHex; - - Headers.Clear(); - foreach (var h in tableModel.Headers) - { - Headers.Add(h); - } - - AllRows.Clear(); - DisplayedRows.Clear(); - foreach (var r in tableModel.Rows) - { - var rowItem = new InteractiveTableRowItem(r); - AllRows.Add(rowItem); - DisplayedRows.Add(rowItem); - } - } - - public override PdfElementBase ToModel() - { - var model = _sourceModel ?? new PdfTableElement(); - CopyBasePropertiesTo(model); - model.HeaderBackgroundHex = HeaderBackgroundHex; - model.HeaderTextHex = HeaderTextHex; - model.AlternateRowBackgroundHex = AlternateRowBackgroundHex; - model.BorderColorHex = BorderColorHex; - model.Headers = Headers.ToList(); - model.Rows = AllRows.Select(r => r.Cells.ToList()).ToList(); - return model; - } - - public override void LoadFromModel(PdfElementBase model) - { - if (model is PdfTableElement table) - { - _sourceModel = table; - PopulateBaseProperties(table); - LoadFromTableModel(table); - } - } - - partial void OnFilterQueryChanged(string value) - { - ApplyFilterAndSort(); - } - - /// - /// Sorts the table rows by the specified column index. - /// Toggles between ascending and descending if the same column is clicked repeatedly. - /// - [RelayCommand] - public void SortByColumn(int columnIndex) - { - if (columnIndex < 0 || columnIndex >= Headers.Count) return; - - if (SortedColumnIndex == columnIndex) - { - SortAscending = !SortAscending; - } - else - { - SortedColumnIndex = columnIndex; - SortAscending = true; - } - - ApplyFilterAndSort(); - } - - /// - /// Clears the current table filter query. - /// - [RelayCommand] - public void ClearFilter() - { - FilterQuery = string.Empty; - } - - /// - /// Copies the currently filtered table data as CSV to the system clipboard. - /// - [RelayCommand] - public async Task CopyToClipboardAsync() - { - var csv = GenerateCsv(); - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(csv); - } - } - - /// - /// Generates a standardized comma-separated CSV string of headers and currently filtered rows. - /// - public string GenerateCsv() - { - var sb = new StringBuilder(); - - // Headers - sb.AppendLine(string.Join(",", Headers.Select(EscapeCsvValue))); - - // Rows - foreach (var row in DisplayedRows) - { - sb.AppendLine(string.Join(",", row.Cells.Select(EscapeCsvValue))); - } - - return sb.ToString(); - } - - private static string EscapeCsvValue(string? value) - { - if (string.IsNullOrEmpty(value)) return "\"\""; - if (value.Contains(',') || value.Contains('\"') || value.Contains('\n') || value.Contains('\r')) - { - return $"\"{value.Replace("\"", "\"\"")}\""; - } - return value; - } - - private void ApplyFilterAndSort() - { - IEnumerable rows = AllRows; - - if (!string.IsNullOrWhiteSpace(FilterQuery)) - { - rows = rows.Where(r => r.MatchesFilter(FilterQuery)); - } - - if (SortedColumnIndex.HasValue && SortedColumnIndex.Value < Headers.Count) - { - int colIdx = SortedColumnIndex.Value; - bool isNumeric = rows.All(r => - { - var val = r.GetCell(colIdx).Trim('$', '€', '£', '%', ' ', ','); - return double.TryParse(val, out _); - }); - - if (isNumeric) - { - rows = SortAscending - ? rows.OrderBy(r => ParseNumericCell(r.GetCell(colIdx))) - : rows.OrderByDescending(r => ParseNumericCell(r.GetCell(colIdx))); - } - else - { - rows = SortAscending - ? rows.OrderBy(r => r.GetCell(colIdx), StringComparer.CurrentCultureIgnoreCase) - : rows.OrderByDescending(r => r.GetCell(colIdx), StringComparer.CurrentCultureIgnoreCase); - } - } - - DisplayedRows.Clear(); - foreach (var r in rows) - { - DisplayedRows.Add(r); - } - - OnPropertyChanged(nameof(RowCountSummary)); - OnPropertyChanged(nameof(FilteredRowCount)); - } - - private static double ParseNumericCell(string text) - { - string cleaned = text.Trim('$', '€', '£', '%', ' ', ','); - return double.TryParse(cleaned, out var num) ? num : 0.0; - } -} diff --git a/src/PdfEditorApp/ViewModels/HelpGuideViewModel.cs b/src/PdfEditorApp/ViewModels/HelpGuideViewModel.cs deleted file mode 100644 index df0ec9d..0000000 --- a/src/PdfEditorApp/ViewModels/HelpGuideViewModel.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels; - -/// -/// ViewModel for the interactive Help & Guides Page in FryPDF. -/// Provides live multi-term search, category filtering, detailed step-by-step walkthroughs, pro tips, and direct tool launching. -/// -public partial class HelpGuideViewModel : ViewModelBase -{ - private readonly IHelpGuideService _helpService; - - // --- Events --- - public event Action? ToolLaunchRequested; - - // --- Observable State --- - - [ObservableProperty] - private string _searchQuery = ""; - - [ObservableProperty] - private string _selectedCategory = "All Guides"; - - [ObservableProperty] - private HelpGuideItem? _selectedGuide; - - [ObservableProperty] - private bool _isDetailViewActive; - - public ObservableCollection AllGuides { get; } = new(); - public ObservableCollection FilteredGuides { get; } = new(); - public ObservableCollection FeaturedGuides { get; } = new(); - public ObservableCollection Categories { get; } = new(); - - public int MatchingGuidesCount => FilteredGuides.Count; - public bool HasNoMatchingGuides => FilteredGuides.Count == 0; - public bool HasActiveSearch => !string.IsNullOrWhiteSpace(SearchQuery); - - public HelpGuideViewModel() : this(new HelpGuideService()) { } - - public HelpGuideViewModel(IHelpGuideService helpService) - { - _helpService = helpService ?? new HelpGuideService(); - InitializeGuides(); - } - - private void InitializeGuides() - { - AllGuides.Clear(); - FeaturedGuides.Clear(); - Categories.Clear(); - - var categories = _helpService.GetAllCategories(); - foreach (var cat in categories) - { - Categories.Add(cat); - } - - var all = _helpService.GetAllGuides(); - foreach (var item in all) - { - AllGuides.Add(item); - if (item.IsFeatured) - { - FeaturedGuides.Add(item); - } - } - - UpdateFilteredGuides(); - } - - // --- Search & Filter Synchronization --- - - partial void OnSearchQueryChanged(string value) - { - OnPropertyChanged(nameof(HasActiveSearch)); - UpdateFilteredGuides(); - } - - partial void OnSelectedCategoryChanged(string value) - { - UpdateFilteredGuides(); - } - - partial void OnSelectedGuideChanged(HelpGuideItem? value) - { - IsDetailViewActive = value != null; - } - - private void UpdateFilteredGuides() - { - FilteredGuides.Clear(); - - string query = SearchQuery.Trim(); - bool hasQuery = !string.IsNullOrWhiteSpace(query); - string category = SelectedCategory; - bool isAllCategory = string.IsNullOrWhiteSpace(category) || - category.Equals("All Guides", StringComparison.OrdinalIgnoreCase) || - category.Equals("All", StringComparison.OrdinalIgnoreCase); - - foreach (var guide in AllGuides) - { - bool matchesCategory = isAllCategory || guide.Category.Equals(category, StringComparison.OrdinalIgnoreCase); - bool matchesQuery = !hasQuery || MatchesSearch(guide, query); - - if (matchesCategory && matchesQuery) - { - FilteredGuides.Add(guide); - } - } - - OnPropertyChanged(nameof(MatchingGuidesCount)); - OnPropertyChanged(nameof(HasNoMatchingGuides)); - } - - private bool MatchesSearch(HelpGuideItem guide, string query) - { - var terms = query.Split(new[] { ' ', ',', '+', '&' }, StringSplitOptions.RemoveEmptyEntries); - if (terms.Length == 0) return true; - - string aggregate = $"{guide.Title} {guide.Summary} {guide.Description} {guide.Category} {guide.Keywords} {guide.Badge} {guide.SupportedFormats} {string.Join(" ", guide.Steps)} {string.Join(" ", guide.KeyFeatures)} {string.Join(" ", guide.ProTips)}"; - - return terms.All(term => aggregate.Contains(term, StringComparison.OrdinalIgnoreCase)); - } - - // --- Commands --- - - [RelayCommand] - public void SetCategory(string category) - { - SelectedCategory = category; - IsDetailViewActive = false; - SelectedGuide = null; - } - - [RelayCommand] - public void SelectGuide(HelpGuideItem? guide) - { - if (guide == null) return; - SelectedGuide = guide; - IsDetailViewActive = true; - } - - [RelayCommand] - public void BackToGrid() - { - SelectedGuide = null; - IsDetailViewActive = false; - } - - [RelayCommand] - public void ClearSearch() - { - SearchQuery = ""; - } - - [RelayCommand] - public void LaunchTool(HelpGuideItem? guide) - { - var target = guide ?? SelectedGuide; - if (target?.RelatedToolId.HasValue == true) - { - ToolLaunchRequested?.Invoke(target.RelatedToolId.Value); - } - } - - [RelayCommand] - public void OpenTopicById(string topicId) - { - var match = _helpService.GetGuideById(topicId); - if (match != null) - { - SelectedGuide = match; - IsDetailViewActive = true; - } - } - - [RelayCommand] - public void OpenGuideForTool(PdfToolId toolId) - { - var match = _helpService.GetGuideByToolId(toolId); - if (match != null) - { - SelectedGuide = match; - IsDetailViewActive = true; - } - } - - [RelayCommand] - public void OpenExternalLink(string? url) - { - if (string.IsNullOrWhiteSpace(url)) return; - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Launcher != null) - { - _ = desktop.MainWindow.Launcher.LaunchUriAsync(new Uri(url)); - } - } - catch { } - } -} diff --git a/src/PdfEditorApp/ViewModels/HomeViewModel.cs b/src/PdfEditorApp/ViewModels/HomeViewModel.cs deleted file mode 100644 index 9567ccf..0000000 --- a/src/PdfEditorApp/ViewModels/HomeViewModel.cs +++ /dev/null @@ -1,1925 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.Core.Plugins; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia.Input.Platform; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools; -using PdfEditorApp.Templates; -using PdfEditorApp.Templates.General; -using PdfEditorApp.ViewModels.Tools; - -namespace PdfEditorApp.ViewModels; - -/// -/// ViewModel for the Google Docs, Canva, and Adobe Acrobat inspired Home / Tools Dashboard. -/// Provides a comprehensive PDF Tools Studio (all 32 tools), expandable template gallery, and recent document management. -/// -public partial class HomeViewModel : ViewModelBase, IServiceProvider -{ - private readonly IRecentDocumentsService _recentService; - private readonly ITemplateService _templateService; - private readonly IProjectPersistenceService _persistenceService; - private readonly IPdfToolRegistry _toolRegistry = null!; - private readonly IPdfToolViewModelFactory? _toolViewModelFactory; - private readonly IThemeService? _themeService; - private readonly IUiSettingsService? _uiSettingsService; - private readonly PdfEditorApp.Core.Plugins.Descriptors.INavigationRegistry? _navigationRegistry; - private readonly IServiceProvider? _serviceProvider; - - public ObservableCollection DynamicNavigationItems { get; } = new(); - - /// - /// Fully plugin-driven, grouped navigation items rendered in the sidebar. - /// Populated from by . - /// - public ObservableCollection NavGroups { get; } = new(); - - [ObservableProperty] - private object? _dynamicPageView; - - private readonly Dictionary _dynamicViewCache = new(StringComparer.OrdinalIgnoreCase); - - /// True when navigation is fully driven by the plugin registry (NavGroups is populated). - public bool IsPluginDrivenNavigation => NavGroups.Count > 0; - - // --- Events to tell the shell what to do --- - - public event Action? OpenTemplateRequested; // templateName (null = blank) - public event Action? OpenFileRequested; - public event Action? OpenWorkflowBuilderRequested; - public event Action? OpenBatchGenerationRequested; - public event Action? DocumentRenamed; // oldPath, newPath - public event Action? DocumentDeleted; // deletedPath - - // --- Observable State --- - - // Rename Dialog State - [ObservableProperty] - private bool _isRenameDialogOpen; - - [ObservableProperty] - private string _renameTargetFilePath = ""; - - [ObservableProperty] - private string _renameNewName = ""; - - [ObservableProperty] - private string _renameExtension = ""; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasRenameError))] - private string _renameErrorMessage = ""; - - public bool HasRenameError => !string.IsNullOrWhiteSpace(RenameErrorMessage); - - // Delete Confirmation Dialog State - [ObservableProperty] - private bool _isDeleteConfirmDialogOpen; - - [ObservableProperty] - private string _deleteTargetFilePath = ""; - - [ObservableProperty] - private string _deleteTargetFileName = ""; - - [ObservableProperty] - private bool _isDarkMode; - - [ObservableProperty] - private string _searchQuery = ""; - - [ObservableProperty] - private string _selectedToolCategory = "All"; - - [ObservableProperty] - private string _selectedTemplateCategory = "All"; - - [ObservableProperty] - private string _selectedLicenseCategory = "All"; - - [ObservableProperty] - private HomeNavSection _selectedNavSection = HomeNavSection.Home; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsPluginsWorkspaceActive))] - [NotifyPropertyChangedFor(nameof(IsStandardScrollableContentActive))] - private bool _isToolPageActive; - - [ObservableProperty] - private PdfToolCardViewModel? _activeToolCard; - - [ObservableProperty] - private PdfToolViewModelBase? _activeToolViewModel; - - [ObservableProperty] - private bool _isTemplateGalleryExpanded; - - public ObservableCollection AllTools { get; } = new(); - public ObservableCollection QuickTools { get; } = new(); - public ObservableCollection FilteredTools { get; } = new(); - public ObservableCollection StarredTools { get; } = new(); - public ObservableCollection AllTemplates { get; } = new(); - public ObservableCollection FilteredTemplates { get; } = new(); - public ObservableCollection FeaturedTemplates { get; } = new(); - public ObservableCollection RecentDocuments { get; } = new(); - public ObservableCollection AllLicenses { get; } = new(); - public ObservableCollection FilteredLicenses { get; } = new(); - - public PdfToolCardViewModel? WorkflowBannerCard { get; private set; } - - public PdfToolRunnerViewModel? ToolRunner { get; } - - public bool IsHomeSection => SelectedNavSection == HomeNavSection.Home; - public bool IsNewDocumentSection => SelectedNavSection == HomeNavSection.NewDocument; - public bool IsPdfReaderSection => SelectedNavSection == HomeNavSection.PdfReader; - public bool IsToolsSection => SelectedNavSection is >= HomeNavSection.AllTools and <= HomeNavSection.AiAndAutomation; - public bool IsStarredSection => SelectedNavSection == HomeNavSection.Starred; - public bool IsTrashSection => SelectedNavSection == HomeNavSection.Trash; - public bool IsLicensingSection => SelectedNavSection == HomeNavSection.Licensing; - public bool IsFontPackagesSection => SelectedNavSection == HomeNavSection.FontPackages; - public bool IsTesseractDataSection => SelectedNavSection == HomeNavSection.TesseractData; - public bool IsHelpSection => SelectedNavSection == HomeNavSection.Help; - public bool IsSettingsSection => SelectedNavSection == HomeNavSection.Settings; - public bool IsPluginsSection => SelectedNavSection == HomeNavSection.Plugins; - public bool IsPluginsWorkspaceActive => SelectedNavSection == HomeNavSection.Plugins && !IsToolPageActive; - public bool IsStandardScrollableContentActive => !IsToolPageActive && !IsPluginsWorkspaceActive; - - public FontManagerViewModel FontManager { get; } = new(); - public TesseractManagerViewModel TesseractManager { get; } = new(); - public HelpGuideViewModel HelpGuide { get; } = new(); - public SettingsViewModel Settings { get; } - public PluginsManagerViewModel PluginsManager { get; } - - public void TriggerToast(string msg, ToastNotificationType type = ToastNotificationType.Primary, string? icon = null) - { - WeakReferenceMessenger.Default.Send(new ShowToastMessage(msg, type, icon)); - } - - public int MatchingToolsCount => FilteredTools.Count; - public bool HasNoMatchingTools => FilteredTools.Count == 0; - public int MatchingTemplatesCount => FilteredTemplates.Count; - public bool HasNoMatchingTemplates => FilteredTemplates.Count == 0; - public int MatchingLicensesCount => FilteredLicenses.Count; - public bool HasNoMatchingLicenses => FilteredLicenses.Count == 0; - public bool HasRecentDocuments => RecentDocuments.Count > 0; - public bool HasStarredTools => StarredTools.Count > 0; - - // --- Constructor --- - - public HomeViewModel() : this(new RecentDocumentsService(), new TemplateService(), new ProjectPersistenceService(), new PdfToolRegistry(), null, null, new ThemeService(), new UiSettingsService(), null, null) { } - - public HomeViewModel( - IRecentDocumentsService recentService, - ITemplateService templateService, - IProjectPersistenceService persistenceService, - IPdfToolRegistry toolRegistry, - PdfToolRunnerViewModel? toolRunner = null, - IPdfToolViewModelFactory? toolViewModelFactory = null, - IThemeService? themeService = null, - IUiSettingsService? uiSettingsService = null, - PdfEditorApp.Core.Plugins.Descriptors.INavigationRegistry? navigationRegistry = null, - IServiceProvider? serviceProvider = null) - { - _recentService = recentService; - _templateService = templateService; - _persistenceService = persistenceService; - _toolRegistry = toolRegistry; - _toolViewModelFactory = toolViewModelFactory; - _themeService = themeService; - _uiSettingsService = uiSettingsService; - _navigationRegistry = navigationRegistry; - _serviceProvider = serviceProvider; - ToolRunner = toolRunner; - - Settings = new SettingsViewModel(_uiSettingsService ?? new UiSettingsService(), _themeService); - PluginsManager = (serviceProvider?.GetService(typeof(PluginsManagerViewModel)) as PluginsManagerViewModel) - ?? new PluginsManagerViewModel( - serviceProvider?.GetService(typeof(PluginHost)) as PluginHost, - serviceProvider?.GetService(typeof(PdfEditorApp.Core.Plugins.Marketplace.IPluginMarketplaceService)) as PdfEditorApp.Core.Plugins.Marketplace.IPluginMarketplaceService, - _toolRegistry); - PluginsManager.ShowToastCallback = msg => TriggerToast(msg); - - if (_themeService != null) - { - IsDarkMode = _themeService.IsDarkMode; - _themeService.ThemeChanged += (mode) => - { - IsDarkMode = _themeService.IsDarkMode; - }; - } - - if (ToolRunner != null) - { - ToolRunner.BackRequested += BackToTools; - } - - HelpGuide.ToolLaunchRequested += (id) => OpenToolPage(id); - - if (_navigationRegistry != null) - { - _navigationRegistry.RegistryChanged += RefreshDynamicNavigationItems; - RefreshDynamicNavigationItems(); - } - - if (_toolRegistry != null) - { - _toolRegistry.RegistryChanged += InitializeTools; - } - - InitializeTools(); - InitializeTemplates(); - InitializeLicenses(); - RefreshRecent(); - } - - - [RelayCommand] - public void ToggleTheme() - { - if (_themeService != null) - { - _themeService.ToggleTheme(); - IsDarkMode = _themeService.IsDarkMode; - } - else - { - IsDarkMode = !IsDarkMode; - } - } - - // --- Tools Initialization --- - - private void InitializeTools() - { - AllTools.Clear(); - var toolDefs = _toolRegistry.GetAllTools(); - - foreach (var def in toolDefs) - { - var cardVm = new PdfToolCardViewModel(def); - cardVm.ToolSelected += (id) => - { - if (id == PdfToolId.WorkflowBuilder) - { - WeakReferenceMessenger.Default.Send(new OpenWorkflowStudioMessage()); - OpenWorkflowBuilderRequested?.Invoke(); - } - else - { - OpenToolPage(id); - } - }; - cardVm.StarToggled += (id) => - { - RefreshStarredTools(); - }; - - if (def.IsWorkflowBanner) - { - WorkflowBannerCard = cardVm; - } - else - { - AllTools.Add(cardVm); - } - } - - UpdateFilteredTools(); - - QuickTools.Clear(); - var quickIds = new[] { PdfToolId.MergePdf, PdfToolId.CompressPdf, PdfToolId.PdfToWord, PdfToolId.SignPdf, PdfToolId.OcrPdf, PdfToolId.AiSummarizer }; - foreach (var qid in quickIds) - { - var match = AllTools.FirstOrDefault(t => t.Id == qid); - if (match != null) QuickTools.Add(match); - } - } - - // --- Template Initialization --- - - private void InitializeTemplates() - { - AllTemplates.Clear(); - - // 1. Blank Canvas - var blankDoc = _templateService.CreateBlankDocument(); - var blankPageVm = new PageViewModel(); - blankPageVm.LoadFromModel(blankDoc.Pages[0]); - AllTemplates.Add(new TemplateCardViewModel - { - Id = "", - Name = "Blank Canvas", - Category = "General", - Subtitle = "Clean Canvas", - Description = "Start fresh with a clean customizable canvas", - Badge = "Blank", - AccentColorHex = "#0F6CBD", - IconKind = "FilePlusOutline", - PagePreview = blankPageVm, - IsBlank = true, - IsFeatured = true, - IsLandscape = false - }); - - // 2. Templates from ITemplateService - var templateDefs = _templateService.GetAllTemplates(); - foreach (var def in templateDefs) - { - if (def is BlankDocumentTemplate) continue; - - try - { - var doc = def.Create(); - var page = doc.Pages.FirstOrDefault() ?? new PdfPageModel(); - var pageVm = new PageViewModel(); - pageVm.LoadFromModel(page); - - bool isFeatured = def.Id is "interactive_executive_deck" or "annualreport" or "invoice" or "resume" or "resumemodern" or "academic" or "mathresearch" or "physicsresearch" or "certificate" or "typographyshowcase" - or "bodmas_worksheet" or "bilingual_exam_paper" or "states_of_matter_notes" or "arduino_cheatsheet" or "factorization_worksheet"; - string subtitle = def.Id switch - { - "interactive_executive_deck" => "Interactive Landscape Deck", - "annualreport" => "Executive Summary & Charts", - "invoice" => "Itemized Billing & Terms", - "resume" => "Executive CV with Live QR", - "resumemodern" => "Two-Column Tech & Product", - "resumecreative" => "Creative Director & Portfolio", - "resumeacademic" => "Faculty Dossier & Grants", - "academic" => "2-Column Systems Research", - "mathresearch" => "Discrete Hodge Theory", - "physicsresearch" => "Cavity QED & Quantum Spin", - "historyresearch" => "Mediterranean Trade Ledgers", - "financeresearch" => "Jump-Diffusion Econometrics", - "certificate" => "Crimson & Gold Award", - "certificatenavygold" => "Navy & Gold Crest", - "diploma" => "Collegiate Degree", - "weddingtraditional" => "Marigold & Ganesha Crest", - "weddingroyalfloral" => "Botanical Laurel Wreath", - "galainvitation" => "Black-Tie Art Deco", - "typographyshowcase" => "Bézier Waves & Ink Specimen", - "bodmas_worksheet" => "BODMAS & Student Header", - "factorization_worksheet" => "150 Questions & Identities", - "bilingual_exam_paper" => "Hindi & English Exam Paper", - "states_of_matter_notes" => "Phase Cycle & Particle Models", - "quadrilaterals_guide" => "Geometric Properties & Formulas", - "arduino_cheatsheet" => "Embedded C & Pinout Map", - _ => def.Category - }; - - string badge = def.Id switch - { - "interactive_executive_deck" => "Live Deck", - "annualreport" => "Popular", - "invoice" => "Finance", - "resume" => "Executive", - "resumemodern" => "Tech", - "resumecreative" => "Design", - "resumeacademic" => "Academic", - "academic" => "Systems", - "mathresearch" => "Pure Math", - "physicsresearch" => "Quantum", - "historyresearch" => "History", - "financeresearch" => "Quant", - "certificate" => "Award", - "certificatenavygold" => "Executive", - "diploma" => "Degree", - "weddingtraditional" => "Festive", - "weddingroyalfloral" => "Luxury", - "galainvitation" => "Art Deco", - "typographyshowcase" => "Featured", - "bodmas_worksheet" => "Worksheet", - "factorization_worksheet" => "Algebra", - "bilingual_exam_paper" => "Bilingual", - "states_of_matter_notes" => "Science", - "quadrilaterals_guide" => "Geometry", - "arduino_cheatsheet" => "Cheat Sheet", - _ => def.Category - }; - - AllTemplates.Add(new TemplateCardViewModel - { - Id = def.Id, - Name = def.Name, - Category = def.Category, - Subtitle = subtitle, - Description = def.Description, - Badge = badge, - AccentColorHex = def.AccentColorHex, - IconKind = def.IconKind, - PagePreview = pageVm, - IsBlank = false, - IsFeatured = isFeatured, - IsLandscape = page.Orientation == PageOrientation.Landscape || page.Width > page.Height - }); - } - catch - { - // Fallback for safe startup - } - } - - UpdateFilteredTemplates(); - } - - // --- Search & Filter Synchronization --- - - partial void OnSearchQueryChanged(string value) - { - UpdateFilteredTools(); - UpdateFilteredTemplates(); - UpdateFilteredLicenses(); - } - - partial void OnSelectedToolCategoryChanged(string value) => UpdateFilteredTools(); - partial void OnSelectedTemplateCategoryChanged(string value) => UpdateFilteredTemplates(); - partial void OnSelectedLicenseCategoryChanged(string value) => UpdateFilteredLicenses(); - - partial void OnSelectedNavSectionChanged(HomeNavSection value) - { - OnPropertyChanged(nameof(IsHomeSection)); - OnPropertyChanged(nameof(IsNewDocumentSection)); - OnPropertyChanged(nameof(IsPdfReaderSection)); - OnPropertyChanged(nameof(IsToolsSection)); - OnPropertyChanged(nameof(IsStarredSection)); - OnPropertyChanged(nameof(IsTrashSection)); - OnPropertyChanged(nameof(IsLicensingSection)); - OnPropertyChanged(nameof(IsFontPackagesSection)); - OnPropertyChanged(nameof(IsTesseractDataSection)); - OnPropertyChanged(nameof(IsHelpSection)); - OnPropertyChanged(nameof(IsSettingsSection)); - OnPropertyChanged(nameof(IsPluginsSection)); - OnPropertyChanged(nameof(IsPluginsWorkspaceActive)); - OnPropertyChanged(nameof(IsStandardScrollableContentActive)); - - if (value == HomeNavSection.TesseractData) - { - _ = TesseractManager.RefreshStatsAsync(); - } - } - - [RelayCommand] - public void OpenHelpGuide(string? topicId = null) - { - SelectedNavSection = HomeNavSection.Help; - IsToolPageActive = false; - DynamicPageView = null; - if (!string.IsNullOrWhiteSpace(topicId)) - { - HelpGuide.OpenTopicById(topicId); - } - else - { - HelpGuide.BackToGrid(); - } - } - - [RelayCommand] - public void OpenHelpForTool(PdfToolId toolId) - { - SelectedNavSection = HomeNavSection.Help; - IsToolPageActive = false; - DynamicPageView = null; - HelpGuide.OpenGuideForTool(toolId); - } - - private void UpdateFilteredTools() - { - FilteredTools.Clear(); - foreach (var tool in AllTools) - { - if (MatchesToolCategory(tool) && MatchesToolSearch(tool)) - { - FilteredTools.Add(tool); - } - } - - OnPropertyChanged(nameof(MatchingToolsCount)); - OnPropertyChanged(nameof(HasNoMatchingTools)); - } - - private bool MatchesToolCategory(PdfToolCardViewModel card) - { - if (SelectedToolCategory == "All") return true; - - return SelectedToolCategory switch - { - "Organize & Page" => card.Category == PdfToolCategory.OrganizeAndPage, - "Optimize & Security" => card.Category == PdfToolCategory.OptimizeAndSecurity, - "Convert from PDF" => card.Category == PdfToolCategory.ConvertFromPdf, - "Convert to PDF" => card.Category == PdfToolCategory.ConvertToPdf, - "Edit & Forms" => card.Category == PdfToolCategory.EditAndForms, - "AI & Automation" => card.Category == PdfToolCategory.AiAndAutomation, - _ => true - }; - } - - private bool MatchesToolSearch(PdfToolCardViewModel card) - { - if (string.IsNullOrWhiteSpace(SearchQuery)) return true; - var q = SearchQuery.Trim(); - return card.Name.Contains(q, StringComparison.OrdinalIgnoreCase) - || card.Description.Contains(q, StringComparison.OrdinalIgnoreCase) - || card.CategoryDisplayName.Contains(q, StringComparison.OrdinalIgnoreCase); - } - - private void UpdateFilteredTemplates() - { - FilteredTemplates.Clear(); - FeaturedTemplates.Clear(); - - foreach (var t in AllTemplates) - { - if (MatchesTemplateFilter(t)) - { - FilteredTemplates.Add(t); - if (t.IsFeatured) - { - FeaturedTemplates.Add(t); - } - } - } - - if (FeaturedTemplates.Count == 0 && FilteredTemplates.Count > 0) - { - foreach (var t in FilteredTemplates) - { - FeaturedTemplates.Add(t); - } - } - - OnPropertyChanged(nameof(MatchingTemplatesCount)); - OnPropertyChanged(nameof(HasNoMatchingTemplates)); - } - - private bool MatchesTemplateFilter(TemplateCardViewModel card) - { - if (SelectedTemplateCategory != "All" && - !string.Equals(SelectedTemplateCategory, card.Category, StringComparison.OrdinalIgnoreCase)) - return false; - - if (string.IsNullOrWhiteSpace(SearchQuery)) return true; - - var q = SearchQuery.Trim(); - return card.Name.Contains(q, StringComparison.OrdinalIgnoreCase) - || card.Description.Contains(q, StringComparison.OrdinalIgnoreCase) - || card.Category.Contains(q, StringComparison.OrdinalIgnoreCase) - || card.Subtitle.Contains(q, StringComparison.OrdinalIgnoreCase); - } - - // --- Commands --- - - [RelayCommand] - public void SetToolCategory(string? category) - { - SelectedToolCategory = string.IsNullOrWhiteSpace(category) ? "All" : category; - } - - [RelayCommand] - public void SetTemplateCategory(string? category) - { - SelectedTemplateCategory = string.IsNullOrWhiteSpace(category) ? "All" : category; - } - - [RelayCommand] - public void ClearSearch() - { - SearchQuery = ""; - } - - [RelayCommand] - public void ToggleTemplateGallery() - { - IsTemplateGalleryExpanded = !IsTemplateGalleryExpanded; - } - - [RelayCommand] - public void CollapseTemplateGallery() - { - IsTemplateGalleryExpanded = false; - } - - [RelayCommand] - public void ClearTemplateSearch() - { - SearchQuery = ""; - SelectedTemplateCategory = "All"; - UpdateFilteredTools(); - UpdateFilteredTemplates(); - } - - // Kept for backward-compatibility with unit tests that assert on ContributedNavigationItems - public ObservableCollection ContributedNavigationItems { get; } = new(); - - public void RefreshDynamicNavigationItems() - { - if (_navigationRegistry == null) return; - - DynamicNavigationItems.Clear(); - ContributedNavigationItems.Clear(); - NavGroups.Clear(); - - var allItems = _navigationRegistry.GetAllItems(); - var currentSectionId = SelectedNavSection.ToString(); - - // Flat list (for active-state tracking) - foreach (var desc in allItems) - { - var item = new NavigationItemViewModel(desc) - { - IsActive = string.Equals(desc.Id, currentSectionId, StringComparison.OrdinalIgnoreCase) - }; - DynamicNavigationItems.Add(item); - - // Contributed = items NOT in the built-in standard set (for legacy EXTENSIONS section) - if (!StandardNavSectionIds.Contains(desc.Id)) - { - ContributedNavigationItems.Add(item); - } - } - - // Grouped list (for fully plugin-driven sidebar rendering) - var groups = allItems - .GroupBy(d => d.Group) - .OrderBy(g => allItems.First(d => d.Group == g.Key).Order) - .Select(g => - { - var items = g - .OrderBy(d => d.Order) - .Select(d => DynamicNavigationItems.First(v => v.Id == d.Id)) - .ToList(); - return new NavGroupViewModel(g.Key, items); - }) - .ToList(); - - foreach (var group in groups) - { - NavGroups.Add(group); - } - - OnPropertyChanged(nameof(IsPluginDrivenNavigation)); - } - - - private static readonly HashSet StandardNavSectionIds = new(StringComparer.OrdinalIgnoreCase) - { - "Home", "PdfReader", "NewDocument", "AllTools", "OrganizeAndPage", "OptimizeAndSecurity", - "ConvertFromPdf", "ConvertToPdf", "EditAndForms", "AiAndAutomation", "Starred", - "FontPackages", "TesseractData", "Trash", "Help", "Licensing", "Settings", "Plugins" - }; - - [RelayCommand] - public void SelectNavSection(string sectionName) - { - // Special case for Plugins: HomeView hosts a dedicated full-viewport PluginsManagerPageView - if (string.Equals(sectionName, "Plugins", StringComparison.OrdinalIgnoreCase)) - { - DynamicPageView = null; - } - else if (_navigationRegistry != null) - { - var desc = _navigationRegistry.GetItem(sectionName); - if (desc?.ViewFactory != null) - { - if (!_dynamicViewCache.TryGetValue(sectionName, out var cachedView)) - { - try - { - var sp = _serviceProvider ?? (IServiceProvider)this; - cachedView = desc.ViewFactory(sp); - _dynamicViewCache[sectionName] = cachedView; - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[HomeViewModel] Failed to create plugin view for '{sectionName}': {ex}"); - cachedView = null; - } - } - DynamicPageView = cachedView; - } - else - { - DynamicPageView = null; - } - } - else - { - DynamicPageView = null; - } - - // Update active states across all flat nav items - foreach (var item in DynamicNavigationItems) - { - item.IsActive = string.Equals(item.Id, sectionName, StringComparison.OrdinalIgnoreCase); - } - - if (IsToolPageActive) - { - BackToTools(); - } - - if (Enum.TryParse(sectionName, true, out var section)) - { - SelectedNavSection = section; - - if (section == HomeNavSection.NewDocument) - { - IsTemplateGalleryExpanded = true; - } - - // Automatically set tool category filter according to selected section - var category = section switch - { - HomeNavSection.OrganizeAndPage => "Organize & Page", - HomeNavSection.OptimizeAndSecurity => "Optimize & Security", - HomeNavSection.ConvertFromPdf => "Convert from PDF", - HomeNavSection.ConvertToPdf => "Convert to PDF", - HomeNavSection.EditAndForms => "Edit & Forms", - HomeNavSection.AiAndAutomation => "AI & Automation", - _ => "All" - }; - - if (SelectedToolCategory != category) - { - SelectedToolCategory = category; - } - else - { - UpdateFilteredTools(); - } - } - } - - - [RelayCommand] - public void OpenToolPage(PdfToolId toolId) - { - OpenToolPage(toolId, null); - } - - public void OpenToolPage(PdfToolId toolId, string? initialFilePath = null) - { - if (toolId == PdfToolId.BatchMailMerge) - { - WeakReferenceMessenger.Default.Send(new OpenBatchGenerationMessage()); - OpenBatchGenerationRequested?.Invoke(); - return; - } - - var card = AllTools.FirstOrDefault(t => t.Id == toolId); - if (card != null) - { - ActiveToolCard = card; - IsToolPageActive = true; - - if (_toolViewModelFactory != null) - { - if (ActiveToolViewModel != null) - { - ActiveToolViewModel.BackRequested -= BackToTools; - ActiveToolViewModel.NavigateToToolRequested -= OnToolNavigateToToolRequested; - ActiveToolViewModel.HelpGuideRequested -= OnToolHelpGuideRequested; - } - - ActiveToolViewModel = _toolViewModelFactory.Create(toolId); - ActiveToolViewModel.StorageProvider = MainViewModel.StorageProvider; - ActiveToolViewModel.IsToolStarred = card.IsStarred; - if (!string.IsNullOrEmpty(initialFilePath)) - { - ActiveToolViewModel.SetupInitialFiles(new[] { initialFilePath }); - } - ActiveToolViewModel.BackRequested += BackToTools; - ActiveToolViewModel.NavigateToToolRequested += OnToolNavigateToToolRequested; - ActiveToolViewModel.HelpGuideRequested += OnToolHelpGuideRequested; - ActiveToolViewModel.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(PdfToolViewModelBase.IsToolStarred) && ActiveToolCard != null && ActiveToolViewModel != null) - { - if (ActiveToolCard.IsStarred != ActiveToolViewModel.IsToolStarred) - { - ActiveToolCard.ToggleStar(); - RefreshStarredTools(); - } - } - }; - } - } - } - - private void OnToolNavigateToToolRequested(PdfToolId nextToolId, string targetFile) - { - OpenToolPage(nextToolId, targetFile); - } - - private void OnToolHelpGuideRequested(PdfToolId toolId) - { - OpenHelpForTool(toolId); - } - - [RelayCommand] - public void BackToTools() - { - IsToolPageActive = false; - ActiveToolCard = null; - if (ActiveToolViewModel != null) - { - ActiveToolViewModel.BackRequested -= BackToTools; - ActiveToolViewModel.NavigateToToolRequested -= OnToolNavigateToToolRequested; - ActiveToolViewModel.HelpGuideRequested -= OnToolHelpGuideRequested; - ActiveToolViewModel = null; - } - } - - [RelayCommand] - public void ToggleStarActiveTool() - { - if (ActiveToolCard != null) - { - ActiveToolCard.ToggleStar(); - if (ActiveToolViewModel != null) - { - ActiveToolViewModel.IsToolStarred = ActiveToolCard.IsStarred; - } - if (ToolRunner != null) - { - ToolRunner.IsToolStarred = ActiveToolCard.IsStarred; - } - RefreshStarredTools(); - } - } - - public void RefreshStarredTools() - { - StarredTools.Clear(); - foreach (var tool in AllTools.Where(t => t.IsStarred)) - { - StarredTools.Add(tool); - } - OnPropertyChanged(nameof(HasStarredTools)); - } - - [RelayCommand] - public void SelectTool(PdfToolId toolId) - { - if (toolId == PdfToolId.WorkflowBuilder) - { - WeakReferenceMessenger.Default.Send(new OpenWorkflowStudioMessage()); - OpenWorkflowBuilderRequested?.Invoke(); - } - else - { - OpenToolPage(toolId); - } - } - - [RelayCommand] - public void OpenWorkflowBuilder() - { - WeakReferenceMessenger.Default.Send(new OpenWorkflowStudioMessage()); - OpenWorkflowBuilderRequested?.Invoke(); - } - - [RelayCommand] - public void SelectTemplate(string? templateName) - { - WeakReferenceMessenger.Default.Send(new OpenTemplateMessage(templateName)); - OpenTemplateRequested?.Invoke(templateName); - } - - [RelayCommand] - public void OpenFile() - { - WeakReferenceMessenger.Default.Send(new OpenProjectFileMessage()); - OpenFileRequested?.Invoke(); - } - - [RelayCommand] - public void OpenRecent(string filePath) - { - WeakReferenceMessenger.Default.Send(new OpenInEditorMessage(filePath)); - } - - [RelayCommand] - public void OpenInViewer(string? filePath) - { - var path = filePath ?? string.Empty; - WeakReferenceMessenger.Default.Send(new OpenInViewerMessage(path)); - } - - [RelayCommand] - public void OpenInInteractiveViewer(string? filePath) - { - var path = filePath ?? string.Empty; - WeakReferenceMessenger.Default.Send(new OpenInFryPdfViewerMessage(path)); - } - - [RelayCommand] - public void LaunchInteractiveDeck() - { - WeakReferenceMessenger.Default.Send(new LaunchInteractiveDeckMessage()); - } - - [RelayCommand] - public void PromptRename(string? filePath) - { - if (string.IsNullOrWhiteSpace(filePath)) return; - RenameTargetFilePath = filePath; - RenameNewName = Path.GetFileNameWithoutExtension(filePath); - RenameExtension = Path.GetExtension(filePath); - RenameErrorMessage = ""; - IsRenameDialogOpen = true; - } - - [RelayCommand] - public void ConfirmRename() - { - if (!FileOperationHelper.RenameFile(RenameTargetFilePath, RenameNewName, out var newPath, out var error)) - { - RenameErrorMessage = error ?? "Failed to rename document."; - return; - } - - var oldPath = RenameTargetFilePath; - var finalNewPath = newPath!; - var newTitle = Path.GetFileName(finalNewPath); - - _recentService.Rename(oldPath, finalNewPath, newTitle); - RefreshRecent(); - - IsRenameDialogOpen = false; - WeakReferenceMessenger.Default.Send(new ProjectRenamedMessage(oldPath, finalNewPath)); - DocumentRenamed?.Invoke(oldPath, finalNewPath); - TriggerToast($"Renamed to {newTitle}", ToastNotificationType.Success, "PencilOutline"); - } - - [RelayCommand] - public void CancelRename() - { - IsRenameDialogOpen = false; - RenameErrorMessage = ""; - } - - [RelayCommand] - public void PromptDelete(string? filePath) - { - if (string.IsNullOrWhiteSpace(filePath)) return; - DeleteTargetFilePath = filePath; - DeleteTargetFileName = Path.GetFileName(filePath); - IsDeleteConfirmDialogOpen = true; - } - - [RelayCommand] - public void ConfirmDelete() - { - var targetPath = DeleteTargetFilePath; - var targetName = DeleteTargetFileName; - IsDeleteConfirmDialogOpen = false; - - if (!FileOperationHelper.DeleteFile(targetPath, out var error)) - { - TriggerToast($"Could not delete file: {error}", ToastNotificationType.Danger, "AlertCircleOutline"); - return; - } - - _recentService.Remove(targetPath); - RefreshRecent(); - WeakReferenceMessenger.Default.Send(new ProjectDeletedMessage(targetPath)); - DocumentDeleted?.Invoke(targetPath); - TriggerToast($"Deleted {targetName}", ToastNotificationType.Success, "TrashCanOutline"); - } - - [RelayCommand] - public void CancelDelete() - { - IsDeleteConfirmDialogOpen = false; - } - - [RelayCommand] - public void DuplicateDocument(string? filePath) - { - if (string.IsNullOrWhiteSpace(filePath)) return; - if (!FileOperationHelper.DuplicateFile(filePath, out var newPath, out var error)) - { - TriggerToast($"Could not duplicate document: {error}", ToastNotificationType.Danger, "AlertCircleOutline"); - return; - } - - var copyTitle = Path.GetFileName(newPath!); - _recentService.Add(new RecentDocumentItem - { - FilePath = newPath!, - Title = copyTitle, - LastOpened = DateTime.UtcNow - }); - RefreshRecent(); - TriggerToast($"Created duplicate: {copyTitle}", ToastNotificationType.Success, "ContentCopy"); - } - - [RelayCommand] - public void RevealInFileManager(string? filePath) - { - if (string.IsNullOrWhiteSpace(filePath)) return; - if (!FileOperationHelper.RevealInFileManager(filePath, out var error)) - { - TriggerToast($"Could not reveal file: {error}", ToastNotificationType.Danger, "AlertCircleOutline"); - return; - } - TriggerToast("Opened containing folder", ToastNotificationType.Primary, "FolderOpenOutline"); - } - - [RelayCommand] - public async Task CopyPath(string? filePath) - { - if (string.IsNullOrWhiteSpace(filePath)) return; - await CopyToClipboardAsync(filePath); - TriggerToast("Copied file path to clipboard", ToastNotificationType.Primary, "ClipboardTextOutline"); - } - - [RelayCommand] - public void RemoveRecent(string filePath) - { - _recentService.Remove(filePath); - RefreshRecent(); - TriggerToast("Removed from recent documents", ToastNotificationType.Primary, "BookmarkRemoveOutline"); - } - - [RelayCommand] - public void ClearRecent() - { - _recentService.Clear(); - RefreshRecent(); - TriggerToast("Cleared recent document history", ToastNotificationType.Primary, "TrashCanOutline"); - } - - // --- Licensing & Third-Party Open Source Initialization --- - - private void InitializeLicenses() - { - AllLicenses.Clear(); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Avalonia UI", - Version = "12.1.1", - LicenseType = "MIT License", - Category = "UI & Graphics Frameworks", - Purpose = "Core cross-platform modern XAML UI application framework providing high-performance GPU-accelerated graphics, styling, smooth vector transitions, and native window management on macOS, Windows, and Linux.", - Maintainer = "AvaloniaUI Team & .NET Foundation Community", - ProjectUrl = "https://avaloniaui.net", - IconKind = "ApplicationOutline", - AccentColorHex = "#7029E6", - LicenseText = """ -MIT License - -Copyright (c) 2014-2026 AvaloniaUI OÜ - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "QuestPDF", - Version = "2026.8.0", - LicenseType = "Community / MIT", - Category = "PDF & Document Engines", - Purpose = "High-performance vector PDF rendering and generation engine utilizing fluent layout APIs, smart pagination, table structures, vector shapes, and sub-millimeter typographical accuracy.", - Maintainer = "Marcin Ziąbek & QuestPDF Contributors", - ProjectUrl = "https://www.questpdf.com", - IconKind = "FilePdfBox", - AccentColorHex = "#FF4500", - LicenseText = """ -QuestPDF Community License / MIT License - -Copyright (c) 2020-2026 Marcin Ziąbek - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "PdfPig & Skia Rendering", - Version = "0.1.16.1", - LicenseType = "Apache 2.0", - Category = "PDF & Document Engines", - Purpose = "Pure C# low-level PDF parsing and extraction engine. Extracts structured text glyphs, font matrices, bounding boxes, vector curves, and rasterizes high-fidelity pages to Skia surfaces for the PDF viewer.", - Maintainer = "Eli White (UglyToad) & Open Source Contributors", - ProjectUrl = "https://github.com/UglyToad/PdfPig", - IconKind = "VectorCircle", - AccentColorHex = "#0284C7", - LicenseText = """ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -Copyright 2018-2026 UglyToad (Eli White) and contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "PdfSharpCore", - Version = "1.3.67", - LicenseType = "MIT License", - Category = "PDF & Document Engines", - Purpose = "Low-level PDF manipulation engine handling direct binary page extraction, PDF merging, page splitting, rotation, AES-128/256 document security encryption, metadata injection, and Bates stamping.", - Maintainer = "Stefan Lange, empira Software & Community", - ProjectUrl = "https://github.com/stefan-lange/pdfsharpcore", - IconKind = "ShieldLockOutline", - AccentColorHex = "#10B981", - LicenseText = """ -MIT License - -Copyright (c) 2005-2026 empira Software GmbH, Troisdorf (Germany) -Copyright (c) 2017-2026 Stefan Lange and PdfSharpCore contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "SkiaSharp", - Version = "3.116.1", - LicenseType = "MIT License", - Category = "UI & Graphics Frameworks", - Purpose = "Cross-platform 2D graphics API based on Google's Skia Graphics Library. Powers fluid continuous canvas zooming, anti-aliased Bézier curve evaluation, sub-pixel text rendering, and image caching.", - Maintainer = "Mono Project & .NET Foundation", - ProjectUrl = "https://github.com/mono/SkiaSharp", - IconKind = "Draw", - AccentColorHex = "#0EA5E9", - LicenseText = """ -MIT License - -Copyright (c) 2015-2026 Xamarin Inc. -Copyright (c) 2017-2026 Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Tabula Table Extractor", - Version = "1.0.1", - LicenseType = "MIT License", - Category = "Office & Data Formats", - Purpose = "Spatial table extraction engine that detects table boundaries, cell grids, columns, and data coordinates from unstructured PDF documents for CSV, JSON, and Excel tabular conversions.", - Maintainer = "Manuel Aristarán & Tabula Contributors", - ProjectUrl = "https://github.com/tabulapdf/tabula-java", - IconKind = "Table", - AccentColorHex = "#F59E0B", - LicenseText = """ -MIT License - -Copyright (c) 2014-2026 Manuel Aristarán, Mike Tigas, and Jeremy B. Merrill - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "DocumentFormat.OpenXml", - Version = "3.5.1", - LicenseType = "MIT License", - Category = "Office & Data Formats", - Purpose = "High-speed OpenXML engine for reading, parsing, and exporting structured Microsoft Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) documents from converted PDF files.", - Maintainer = "Microsoft Corporation & .NET Foundation", - ProjectUrl = "https://github.com/dotnet/Open-XML-SDK", - IconKind = "FileWordOutline", - AccentColorHex = "#2563EB", - LicenseText = """ -MIT License - -Copyright (c) Microsoft Corporation. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Material.Icons.Avalonia", - Version = "3.0.2", - LicenseType = "MIT License", - Category = "UI & Graphics Frameworks", - Purpose = "Vector iconography library supplying thousands of scalable Material Design icon glyphs across toolbars, ribbon bars, inspectors, tool cards, and dialogs.", - Maintainer = "SKFox5330 & Pictogrammers Community", - ProjectUrl = "https://github.com/SKFox5330/Material.Icons.Avalonia", - IconKind = "StarFourPointsOutline", - AccentColorHex = "#8B5CF6", - LicenseText = """ -MIT License - -Copyright (c) 2020-2026 SKFox5330 & Pictogrammers - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "CommunityToolkit.Mvvm", - Version = "8.4.2", - LicenseType = "MIT License", - Category = "Architecture & Runtime", - Purpose = "Official Microsoft .NET Community Toolkit MVVM framework providing high-performance source-generated ObservableObject, RelayCommand, and reactive property notifications.", - Maintainer = "Microsoft Corporation & Community Toolkit Team", - ProjectUrl = "https://github.com/CommunityToolkit/dotnet", - IconKind = "LightningBoltOutline", - AccentColorHex = "#6366F1", - LicenseText = """ -MIT License - -Copyright (c) .NET Foundation and Contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "QRCoder", - Version = "1.8.0", - LicenseType = "MIT License", - Category = "Office & Data Formats", - Purpose = "Pure C# QR code generating library powering dynamic vector URL, Wi-Fi, vCard, SMS, Email, Crypto, and plain text QR code elements in FryPDF document authoring.", - Maintainer = "Raffael Herrmann (codebude) & Contributors", - ProjectUrl = "https://github.com/codebude/QRCoder", - IconKind = "Qrcode", - AccentColorHex = "#EC4899", - LicenseText = """ -MIT License - -Copyright (c) 2013-2026 Raffael Herrmann - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Microsoft.Extensions.DependencyInjection", - Version = "10.0.11", - LicenseType = "MIT License", - Category = "Architecture & Runtime", - Purpose = "High-performance Inversion of Control (IoC) dependency injection container powering modular service registration, loose coupling, and decoupled testing across all 32 PDF tools.", - Maintainer = "Microsoft Corporation & .NET Runtime Team", - ProjectUrl = "https://github.com/dotnet/runtime", - IconKind = "ToyBrickOutline", - AccentColorHex = "#0078D4", - LicenseText = """ -MIT License - -Copyright (c) .NET Foundation and Contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = ".NET 10 Runtime & Base Libraries", - Version = "10.0", - LicenseType = "MIT License", - Category = "Architecture & Runtime", - Purpose = "Modern cross-platform .NET 10 runtime engine providing hardware-accelerated SIMD (AVX/NEON) vector mathematics, AES-256 cryptography primitives, Task-based asynchronous I/O, and garbage collection.", - Maintainer = "Microsoft Corporation & .NET Foundation", - ProjectUrl = "https://dotnet.microsoft.com", - IconKind = "Microsoft", - AccentColorHex = "#512BD4", - LicenseText = """ -MIT License - -Copyright (c) .NET Foundation and Contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "LiveChartsCore & LiveChartsCore.SkiaSharpView", - Version = "2.0.5", - LicenseType = "MIT License", - Category = "UI & Graphics Frameworks", - Purpose = "Modern, fluid, and hardware-accelerated charting library for .NET. Renders high-performance bar charts, line graphs, and pie/doughnut charts with SkiaSharp integration directly into PDF documents and vector canvases.", - Maintainer = "Alberto Rodriguez (beto-rodriguez) & LiveCharts Contributors", - ProjectUrl = "https://github.com/beto-rodriguez/LiveCharts2", - IconKind = "ChartBoxOutline", - AccentColorHex = "#3B82F6", - LicenseText = """ -MIT License - -Copyright (c) 2020-2026 Alberto Rodriguez - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Microsoft.Extensions.Logging.Abstractions", - Version = "10.0.1", - LicenseType = "MIT License", - Category = "Architecture & Runtime", - Purpose = "Structured logging abstractions and diagnostic interfaces for .NET. Powers logging pipelines, diagnostic tracing, and pipeline telemetry across core PDF deconstruction, OCR processing, and document layout engines.", - Maintainer = "Microsoft Corporation & .NET Foundation", - ProjectUrl = "https://github.com/dotnet/runtime", - IconKind = "FormatListBulleted", - AccentColorHex = "#2563EB", - LicenseText = """ -MIT License - -Copyright (c) .NET Foundation and Contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "SIL Open Font License (OFL 1.1) Typeface Collection", - Version = "1.1", - LicenseType = "SIL OFL 1.1", - Category = "Typography & Typefaces", - Purpose = "Comprehensive open-source multilingual font collection powering FryPDF typography, including Google Noto Sans (Devanagari, Tamil, Telugu, Bengali, Gujarati, Kannada, Malayalam, Arabic, Urdu, Hebrew, Thai, Lao, Khmer, Myanmar, Sinhala, CJK), Inter, Poppins, Montserrat, Raleway, Lato, Fira Code, Caveat, Orbitron, and Oswald.", - Maintainer = "SIL International, Google Fonts & Type Designers", - ProjectUrl = "https://scripts.sil.org/OFL", - IconKind = "FormatFont", - AccentColorHex = "#D946EF", - LicenseText = """ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. - -The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. -2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. -3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. -5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. - -TERMINATION -This license becomes null and void if any of the above conditions are not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Roboto & Roboto Mono Font Family", - Version = "2.138", - LicenseType = "Apache 2.0", - Category = "Typography & Typefaces", - Purpose = "Google's flagship sans-serif and monospaced typefaces designed by Christian Robertson. Serves as the primary document reader font, technical tables, code spans, and corporate report templates.", - Maintainer = "Google LLC & Christian Robertson", - ProjectUrl = "https://github.com/googlefonts/roboto", - IconKind = "FormatText", - AccentColorHex = "#EA4335", - LicenseText = """ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -Copyright 2011 Google Inc. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Ubuntu Font Family", - Version = "0.83", - LicenseType = "Ubuntu Font Licence 1.0", - Category = "Typography & Typefaces", - Purpose = "Distinctive contemporary humanist sans-serif typeface designed by Dalton Maag for Canonical Ltd. Included in the Creative & Design typography package for modern headings, branded certificates, and flyers.", - Maintainer = "Canonical Ltd & Dalton Maag", - ProjectUrl = "https://design.ubuntu.com/font/", - IconKind = "Alphabetical", - AccentColorHex = "#E95420", - LicenseText = """ -Ubuntu Font Licence - Version 1.0 -https://ubuntu.com/legal/font-licence - -Copyright 2010 Canonical Ltd. -Licensed under the Ubuntu Font Licence version 1.0. - -Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: - -1. Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. -2. Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. -3. No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. -4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version. -5. The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Microsoft.Extensions.AI & OpenAI", - Version = "10.9.0", - LicenseType = "MIT License", - Category = "AI & Document Intelligence", - Purpose = "Official Microsoft unified AI abstractions and OpenAI connector enabling autonomous AI studio agent workflows, generative PDF document composition from natural language prompts, text summarization, and neural translation.", - Maintainer = "Microsoft Corporation & .NET AI Team", - ProjectUrl = "https://github.com/dotnet/extensions", - IconKind = "RobotOutline", - AccentColorHex = "#6366F1", - LicenseText = """ -MIT License - -Copyright (c) .NET Foundation and Contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "OllamaSharp", - Version = "5.4.30", - LicenseType = "MIT License", - Category = "AI & Document Intelligence", - Purpose = "High-performance .NET API client for Ollama enabling 100% private, offline, and zero-telemetry local LLM inference (Llama 3.2, Mistral, Gemma, Phi-3) to create PDF documents and analyze text locally on CPU/GPU hardware.", - Maintainer = "Andreas Müller (awaescher) & Open Source Community", - ProjectUrl = "https://github.com/awaescher/OllamaSharp", - IconKind = "LaptopAccount", - AccentColorHex = "#EC4899", - LicenseText = """ -MIT License - -Copyright (c) 2023-2026 Andreas Müller - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - }); - - AllLicenses.Add(new ThirdPartyToolLicense - { - Name = "Tesseract OCR Engine", - Version = "5.4.0", - LicenseType = "Apache 2.0", - Category = "AI & Document Intelligence", - Purpose = "Industrial open-source Optical Character Recognition engine with support for multi-language traineddata models, powering local scanned document text extraction, word bounding-box discovery, and searchable PDF compilation.", - Maintainer = "Ray Smith, Google LLC & Tesseract Community", - ProjectUrl = "https://github.com/tesseract-ocr/tesseract", - IconKind = "TextRecognition", - AccentColorHex = "#0EA5E9", - LicenseText = """ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -Copyright (c) 1985-2026 Ray Smith, Google LLC, and Tesseract contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - }); - - UpdateFilteredLicenses(); - } - - private void UpdateFilteredLicenses() - { - FilteredLicenses.Clear(); - foreach (var lib in AllLicenses) - { - if (MatchesLicenseCategory(lib) && MatchesLicenseSearch(lib)) - { - FilteredLicenses.Add(lib); - } - } - - OnPropertyChanged(nameof(MatchingLicensesCount)); - OnPropertyChanged(nameof(HasNoMatchingLicenses)); - } - - private bool MatchesLicenseCategory(ThirdPartyToolLicense lib) - { - if (SelectedLicenseCategory == "All") return true; - - return string.Equals(SelectedLicenseCategory, lib.Category, StringComparison.OrdinalIgnoreCase); - } - - private bool MatchesLicenseSearch(ThirdPartyToolLicense lib) - { - if (string.IsNullOrWhiteSpace(SearchQuery)) return true; - var q = SearchQuery.Trim(); - return lib.Name.Contains(q, StringComparison.OrdinalIgnoreCase) - || lib.Category.Contains(q, StringComparison.OrdinalIgnoreCase) - || lib.LicenseType.Contains(q, StringComparison.OrdinalIgnoreCase) - || lib.Purpose.Contains(q, StringComparison.OrdinalIgnoreCase) - || lib.Maintainer.Contains(q, StringComparison.OrdinalIgnoreCase); - } - - [RelayCommand] - public void SetLicenseCategory(string? category) - { - SelectedLicenseCategory = string.IsNullOrWhiteSpace(category) ? "All" : category; - } - - [RelayCommand] - public void ClearLicenseSearch() - { - SearchQuery = ""; - SelectedLicenseCategory = "All"; - UpdateFilteredLicenses(); - } - - [RelayCommand] - public void ToggleLicenseExpand(ThirdPartyToolLicense? license) - { - if (license != null) - { - license.IsExpanded = !license.IsExpanded; - } - } - - [RelayCommand] - public async Task CopyLicenseText(ThirdPartyToolLicense? license) - { - if (license == null) return; - var textToCopy = $"--- {license.Name} ({license.Version}) ---\nLicense: {license.LicenseType}\nMaintainer: {license.Maintainer}\nProject URL: {license.ProjectUrl}\n\n{license.LicenseText}"; - await CopyToClipboardAsync(textToCopy); - } - - [RelayCommand] - public async Task CopyFullAttributions() - { - var sb = new System.Text.StringBuilder(); - sb.AppendLine("================================================================================"); - sb.AppendLine("FryPDF — Open Source & Third-Party Software Attribution Notice"); - sb.AppendLine("Copyright (c) 2026 Code Fry Dev. All rights reserved."); - sb.AppendLine("================================================================================\n"); - - foreach (var lib in AllLicenses) - { - sb.AppendLine($"--------------------------------------------------------------------------------"); - sb.AppendLine($"Package: {lib.Name} (v{lib.Version})"); - sb.AppendLine($"Category: {lib.Category}"); - sb.AppendLine($"License: {lib.LicenseType}"); - sb.AppendLine($"Maintainer: {lib.Maintainer}"); - sb.AppendLine($"URL: {lib.ProjectUrl}"); - sb.AppendLine($"Purpose: {lib.Purpose}"); - sb.AppendLine($"--------------------------------------------------------------------------------"); - sb.AppendLine(lib.LicenseText.Trim()); - sb.AppendLine(); - } - - await CopyToClipboardAsync(sb.ToString()); - } - - [RelayCommand] - public async Task OpenLibraryWebsite(string? url) - { - if (string.IsNullOrWhiteSpace(url)) return; - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Launcher != null) - { - await desktop.MainWindow.Launcher.LaunchUriAsync(new Uri(url)); - } - else - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = url, UseShellExecute = true }); - } - } - catch - { - await CopyToClipboardAsync(url); - } - } - - private static async Task CopyToClipboardAsync(string text) - { - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(text); - } - } - catch { } - } - - // --- Helpers --- - - public void RefreshRecent() - { - RecentDocuments.Clear(); - var items = _recentService.Load(); - foreach (var item in items) - { - RecentDocuments.Add(item); - } - OnPropertyChanged(nameof(HasRecentDocuments)); - - _ = LoadRecentPreviewsAsync(items); - } - - private async Task LoadRecentPreviewsAsync(List items) - { - foreach (var item in items) - { - if (string.IsNullOrWhiteSpace(item.FilePath) || !File.Exists(item.FilePath)) - continue; - - try - { - if (item.FilePath.EndsWith(".frypdf", StringComparison.OrdinalIgnoreCase) || - item.FilePath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) - { - var doc = await _persistenceService.LoadProjectAsync(item.FilePath); - if (doc != null && doc.Pages.Count > 0) - { - var pageVm = new PageViewModel(); - pageVm.LoadFromModel(doc.Pages[0]); - item.PagePreview = pageVm; - item.PageCount = doc.Pages.Count; - item.FormatDescription = $"{doc.Pages[0].Format} {doc.Pages[0].Orientation}"; - } - } - } - catch { } - } - } - - /// - /// Resolves services and child ViewModels requested by plugin-driven navigation page factories. - /// - public object? GetService(Type serviceType) - { - if (serviceType == typeof(HomeViewModel)) return this; - if (serviceType == typeof(SettingsViewModel)) return Settings; - if (serviceType == typeof(PluginsManagerViewModel)) return PluginsManager; - if (serviceType == typeof(FontManagerViewModel)) return FontManager; - if (serviceType == typeof(TesseractManagerViewModel)) return TesseractManager; - if (serviceType == typeof(HelpGuideViewModel)) return HelpGuide; - if (serviceType == typeof(PdfToolRunnerViewModel) && ToolRunner != null) return ToolRunner; - if (serviceType == typeof(IRecentDocumentsService)) return _recentService; - if (serviceType == typeof(ITemplateService)) return _templateService; - if (serviceType == typeof(IProjectPersistenceService)) return _persistenceService; - if (serviceType == typeof(IPdfToolRegistry)) return _toolRegistry; - if (serviceType == typeof(IThemeService) && _themeService != null) return _themeService; - if (serviceType == typeof(IUiSettingsService) && _uiSettingsService != null) return _uiSettingsService; - if (serviceType == typeof(PdfEditorApp.Core.Plugins.Descriptors.INavigationRegistry) && _navigationRegistry != null) return _navigationRegistry; - return _serviceProvider?.GetService(serviceType) ?? App.Services?.GetService(serviceType); - } -} - diff --git a/src/PdfEditorApp/ViewModels/InspectorViewModel.cs b/src/PdfEditorApp/ViewModels/InspectorViewModel.cs deleted file mode 100644 index 0d198c9..0000000 --- a/src/PdfEditorApp/ViewModels/InspectorViewModel.cs +++ /dev/null @@ -1,2477 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Canvas; -using PdfEditorApp.Services.Typography; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels; - -public class ColorSwatchItem -{ - public string Name { get; set; } = "Black"; - public string Hex { get; set; } = "#201F1E"; -} - -public partial class InspectorViewModel : ViewModelBase -{ - private readonly ICanvasElementService _elementService; - private readonly PdfEditorApp.Core.Plugins.Descriptors.IInspectorRegistry? _inspectorRegistry; - - public ObservableCollection DynamicSections { get; } = new(); - - public InspectorViewModel( - ICanvasElementService? elementService = null, - PdfEditorApp.Core.Plugins.Descriptors.IInspectorRegistry? inspectorRegistry = null) - { - _elementService = elementService ?? PageViewModel.DefaultElementService; - _inspectorRegistry = inspectorRegistry; - if (_inspectorRegistry != null) - { - _inspectorRegistry.RegistryChanged += () => - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => RefreshDynamicSections(SelectedElement)); - }; - } - } - - public void RefreshDynamicSections(object? element) - { - DynamicSections.Clear(); - if (_inspectorRegistry != null) - { - var sections = _inspectorRegistry.GetSectionsForTarget(element); - foreach (var sec in sections) - { - var content = sec.Factory(null!, element); - if (content is DynamicInspectorSectionViewModel dvm) - { - DynamicSections.Add(dvm); - } - else - { - DynamicSections.Add(new DynamicInspectorSectionViewModel - { - SectionId = sec.SectionId, - Title = sec.Title, - IconKind = sec.IconKind, - Order = sec.Order, - Content = content - }); - } - } - } - } - - public UndoRedoService? UndoRedo { get; set; } - - public static Action? OnActiveTextFormattingApplied { get; set; } - - [ObservableProperty] - private ElementViewModelBase? _selectedElement; - - [ObservableProperty] - private PageViewModel? _selectedPage; - - [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(DeleteSelectedElementCommand))] - [NotifyCanExecuteChangedFor(nameof(DuplicateSelectedElementCommand))] - private bool _hasSelectedElement; - - [ObservableProperty] - private bool _hasMultiSelection; - - [ObservableProperty] - private int _selectionCount; - - [ObservableProperty] - private bool _isTextElement; - - [ObservableProperty] - private bool _isShapeElement; - - [ObservableProperty] - private bool _isImageElement; - - [ObservableProperty] - private bool _isTableElement; - - [ObservableProperty] - private bool _isChartElement; - - [ObservableProperty] - private bool _isDividerElement; - - [ObservableProperty] - private bool _isWatermarkElement; - - [ObservableProperty] - private bool _isFormFieldElement; - - [ObservableProperty] - private bool _isQrCodeElement; - - [ObservableProperty] - private bool _isBarcodeElement; - - [ObservableProperty] - private bool _isRedactionElement; - - [ObservableProperty] - private bool _isStickyNoteElement; - - [ObservableProperty] - private bool _isMeasurementElement; - - [ObservableProperty] - private bool _isSvgElement; - - [ObservableProperty] - private bool _isMathElement; - - // Collapsible Inspector Sections / Accordions (Default true for instant discovery) - [ObservableProperty] - private bool _isPageSetupExpanded = true; - - [ObservableProperty] - private bool _isPresetGeometryExpanded = true; - - [ObservableProperty] - private bool _isLineStrokePatternExpanded = true; - - [ObservableProperty] - private bool _isColorsExpanded = true; - - [ObservableProperty] - private bool _isDimensionsExpanded = true; - - [ObservableProperty] - private bool _isTypographyExpanded = true; - - [ObservableProperty] - private bool _isParagraphExpanded = true; - - [ObservableProperty] - private bool _isTransformExpanded = true; - - [ObservableProperty] - private bool _isShadowExpanded = true; - - [ObservableProperty] - private bool _isImageAdjustmentsExpanded = true; - - [ObservableProperty] - private bool _isTablePropertiesExpanded = true; - - [ObservableProperty] - private bool _isMathFormulaExpanded = true; - - [RelayCommand] - public void TogglePresetGeometryExpanded() => IsPresetGeometryExpanded = !IsPresetGeometryExpanded; - - [RelayCommand] - public void ToggleLineStrokePatternExpanded() => IsLineStrokePatternExpanded = !IsLineStrokePatternExpanded; - - [RelayCommand] - public void ToggleColorsExpanded() => IsColorsExpanded = !IsColorsExpanded; - - [RelayCommand] - public void ToggleDimensionsExpanded() => IsDimensionsExpanded = !IsDimensionsExpanded; - - [RelayCommand] - public void ToggleTypographyExpanded() => IsTypographyExpanded = !IsTypographyExpanded; - - [RelayCommand] - public void ToggleParagraphExpanded() => IsParagraphExpanded = !IsParagraphExpanded; - - [RelayCommand] - public void ToggleTransformExpanded() => IsTransformExpanded = !IsTransformExpanded; - - [RelayCommand] - public void ToggleShadowExpanded() => IsShadowExpanded = !IsShadowExpanded; - - [RelayCommand] - public void TogglePageSetupExpanded() => IsPageSetupExpanded = !IsPageSetupExpanded; - - [RelayCommand] - public void ToggleImageAdjustmentsExpanded() => IsImageAdjustmentsExpanded = !IsImageAdjustmentsExpanded; - - [RelayCommand] - public void ToggleTablePropertiesExpanded() => IsTablePropertiesExpanded = !IsTablePropertiesExpanded; - - [RelayCommand] - public void ToggleMathFormulaExpanded() => IsMathFormulaExpanded = !IsMathFormulaExpanded; - - [RelayCommand] - public void ExpandAllSections() - { - IsPageSetupExpanded = true; - IsPresetGeometryExpanded = true; - IsLineStrokePatternExpanded = true; - IsColorsExpanded = true; - IsDimensionsExpanded = true; - IsTypographyExpanded = true; - IsParagraphExpanded = true; - IsTransformExpanded = true; - IsShadowExpanded = true; - IsImageAdjustmentsExpanded = true; - IsTablePropertiesExpanded = true; - IsMathFormulaExpanded = true; - } - - [RelayCommand] - public void CollapseAllSections() - { - IsPageSetupExpanded = false; - IsPresetGeometryExpanded = false; - IsLineStrokePatternExpanded = false; - IsColorsExpanded = false; - IsDimensionsExpanded = false; - IsTypographyExpanded = false; - IsParagraphExpanded = false; - IsTransformExpanded = false; - IsShadowExpanded = false; - IsImageAdjustmentsExpanded = false; - IsTablePropertiesExpanded = false; - IsMathFormulaExpanded = false; - } - - [ObservableProperty] - private string _selectedFontFamily = "Arial"; - - partial void OnSelectedFontFamilyChanged(string value) - { - if (TextElement != null && !string.IsNullOrEmpty(value)) - { - if (TryApplyInlineFormatting(InlineFormatType.Font, $"Font: {value}", value)) return; - - if (TextElement.FontFamily != value) - { - var el = TextElement; - string oldFont = el.FontFamily; - el.FontFamily = value; - UndoRedo?.RecordAction( - $"Font: {value}", - () => { el.FontFamily = oldFont; SelectedFontFamily = oldFont; }, - () => { el.FontFamily = value; SelectedFontFamily = value; } - ); - } - } - } - - [ObservableProperty] - private double _selectedFontSize = 14; - - partial void OnSelectedFontSizeChanged(double value) - { - if (TextElement != null && value > 0) - { - if (TryApplyInlineFormatting(InlineFormatType.Size, $"Font Size: {value}pt", value.ToString("0.#", CultureInfo.InvariantCulture))) return; - - if (Math.Abs(TextElement.FontSize - value) > 0.1) - { - var el = TextElement; - double oldSize = el.FontSize; - el.FontSize = value; - UndoRedo?.RecordAction( - $"Font Size: {value}pt", - () => { el.FontSize = oldSize; SelectedFontSize = oldSize; }, - () => { el.FontSize = value; SelectedFontSize = value; } - ); - } - } - } - - public ObservableCollection AvailableFontFamilies { get; } = new() - { - // ── Sans-Serif (Modern) ───────────────────────────────────────────── - "Inter", - "Roboto", - "Open Sans", - "Lato", - "Poppins", - "Nunito", - "Raleway", - "Montserrat", - "Source Sans 3", - "Ubuntu", - "Cabin", - "Josefin Sans", - "Titillium Web", - "Exo 2", - "Noto Sans", - "Oswald", - - // ── Sans-Serif (System) ───────────────────────────────────────────── - "Arial", - "Verdana", - "Segoe UI", - "Helvetica", - "Trebuchet MS", - - // ── Serif ─────────────────────────────────────────────────────────── - "Playfair Display", - "Merriweather", - "Lora", - "PT Serif", - "Noto Serif", - "Crimson Text", - "Libre Baskerville", - "Libre Franklin", - "Cinzel", - "Times New Roman", - "Georgia", - "Palatino", - - // ── Monospace ─────────────────────────────────────────────────────── - "Fira Code", - "Roboto Mono", - "Courier New", - "Consolas", - "Menlo", - - // ── Display & Decorative ──────────────────────────────────────────── - "Bebas Neue", - "Orbitron", - "Lobster", - "Pacifico", - "Impact", - - // ── Handwriting & Script ──────────────────────────────────────────── - "Dancing Script", - "Caveat", - "Great Vibes", - "Comic Neue", - - // ── Indian Scripts ────────────────────────────────────────────────── - "Noto Sans Devanagari", - "Tiro Devanagari Hindi", - "Noto Sans Tamil", - "Noto Sans Telugu", - "Noto Sans Bengali", - "Noto Sans Gujarati", - "Noto Sans Kannada", - "Noto Sans Malayalam", - "Noto Sans Sinhala", - - // ── CJK — Chinese ─────────────────────────────────────────────────── - "Noto Sans SC", // Simplified Chinese - "Noto Sans TC", // Traditional Chinese - - // ── CJK — Japanese ────────────────────────────────────────────────── - "Noto Sans JP", - "Noto Serif JP", - - // ── CJK — Korean ──────────────────────────────────────────────────── - "Noto Sans KR", - "Nanum Gothic", - - // ── Southeast Asian ────────────────────────────────────────────────── - "Noto Sans Thai", - "Sarabun", - "Noto Sans Myanmar", - "Noto Sans Khmer", - "Noto Sans Lao", - "Be Vietnam Pro", - - // ── Middle Eastern / RTL ───────────────────────────────────────────── - "Noto Sans Arabic", - "Vazirmatn", - "Noto Nastaliq Urdu", - "Noto Sans Hebrew", - "Heebo", - - // ── Eurasian ───────────────────────────────────────────────────────── - "Golos Text", - "Russo One", - "GFS Neohellenic", - "Noto Sans Georgian", - "Noto Sans Armenian", - "Noto Sans Ethiopic", - }; - - public ObservableCollection AvailableFontSizes { get; } = new() - { - 8, 9, 10, 11, 12, 13, 13.5, 14, 15, 16, 18, 20, 22, 24, 28, 32, 36, 48, 72 - }; - - public ObservableCollection Swatches { get; } = new() - { - new ColorSwatchItem { Name = "Black", Hex = "#201F1E" }, - new ColorSwatchItem { Name = "Charcoal", Hex = "#4B5563" }, - new ColorSwatchItem { Name = "Accent Blue", Hex = "#0F6CBD" }, - new ColorSwatchItem { Name = "Navy", Hex = "#1E3A8A" }, - new ColorSwatchItem { Name = "Green", Hex = "#16A34A" }, - new ColorSwatchItem { Name = "Red", Hex = "#DC2626" }, - new ColorSwatchItem { Name = "Orange", Hex = "#EA580C" }, - new ColorSwatchItem { Name = "Purple", Hex = "#7C3AED" }, - new ColorSwatchItem { Name = "White", Hex = "#FFFFFF" } - }; - - public ObservableCollection MeasurementUnits { get; } = new() - { - RulerUnit.Points, - RulerUnit.Inches, - RulerUnit.Millimeters - }; - - public TextElementViewModel? TextElement => SelectedElement as TextElementViewModel; - public ShapeElementViewModel? ShapeElement => SelectedElement as ShapeElementViewModel; - public ImageElementViewModel? ImageElement => SelectedElement as ImageElementViewModel; - public TableElementViewModel? TableElement => SelectedElement as TableElementViewModel; - public ChartElementViewModel? ChartElement => SelectedElement as ChartElementViewModel; - public DividerElementViewModel? DividerElement => SelectedElement as DividerElementViewModel; - public WatermarkElementViewModel? WatermarkElement => SelectedElement as WatermarkElementViewModel; - public FormFieldElementViewModel? FormFieldElement => SelectedElement as FormFieldElementViewModel; - public QrCodeElementViewModel? QrCodeElement => SelectedElement as QrCodeElementViewModel; - public BarcodeElementViewModel? BarcodeElement => SelectedElement as BarcodeElementViewModel; - public RedactionElementViewModel? RedactionElement => SelectedElement as RedactionElementViewModel; - public InkElementViewModel? InkElement => SelectedElement as InkElementViewModel; - public StickyNoteElementViewModel? StickyNoteElement => SelectedElement as StickyNoteElementViewModel; - public MeasurementElementViewModel? MeasurementElement => SelectedElement as MeasurementElementViewModel; - public SvgElementViewModel? SvgElement => SelectedElement as SvgElementViewModel; - public MathElementViewModel? MathElement => SelectedElement as MathElementViewModel; - - public string ActiveCategoryName => SelectedElement != null ? SelectedElement.Kind.ToString() : "Document"; - public ObservableCollection ColorSwatches => Swatches; - - public void UpdateSelection(ElementViewModelBase? element, PageViewModel? page) - { - SelectedElement = element; - SelectedPage = page; - - HasSelectedElement = element != null; - HasMultiSelection = page != null && page.SelectedElements.Count > 1; - SelectionCount = page != null ? page.SelectedElements.Count : (element != null ? 1 : 0); - - IsTextElement = element is TextElementViewModel; - IsShapeElement = element is ShapeElementViewModel; - IsImageElement = element is ImageElementViewModel; - IsTableElement = element is TableElementViewModel; - IsChartElement = element is ChartElementViewModel; - IsDividerElement = element is DividerElementViewModel; - IsWatermarkElement = element is WatermarkElementViewModel; - IsFormFieldElement = element is FormFieldElementViewModel; - IsQrCodeElement = element is QrCodeElementViewModel; - IsBarcodeElement = element is BarcodeElementViewModel; - IsRedactionElement = element is RedactionElementViewModel; - IsStickyNoteElement = element is StickyNoteElementViewModel; - IsMeasurementElement = element is MeasurementElementViewModel; - IsSvgElement = element is SvgElementViewModel; - IsMathElement = element is MathElementViewModel; - - RefreshDynamicSections(element); - - if (element is TextElementViewModel textVm) - { - SelectedFontFamily = textVm.FontFamily; - SelectedFontSize = textVm.FontSize; - - if (!AvailableFontSizes.Contains(textVm.FontSize)) - { - AvailableFontSizes.Add(textVm.FontSize); - } - } - - OnPropertyChanged(nameof(TextElement)); - OnPropertyChanged(nameof(ShapeElement)); - OnPropertyChanged(nameof(ImageElement)); - OnPropertyChanged(nameof(TableElement)); - OnPropertyChanged(nameof(ChartElement)); - OnPropertyChanged(nameof(DividerElement)); - OnPropertyChanged(nameof(WatermarkElement)); - OnPropertyChanged(nameof(FormFieldElement)); - OnPropertyChanged(nameof(QrCodeElement)); - OnPropertyChanged(nameof(BarcodeElement)); - OnPropertyChanged(nameof(RedactionElement)); - OnPropertyChanged(nameof(InkElement)); - OnPropertyChanged(nameof(StickyNoteElement)); - OnPropertyChanged(nameof(MeasurementElement)); - OnPropertyChanged(nameof(SvgElement)); - OnPropertyChanged(nameof(MathElement)); - OnPropertyChanged(nameof(ActiveCategoryName)); - } - - private bool TryApplyInlineFormatting(InlineFormatType formatType, string actionName, string? argument = null) - { - if (TextElement != null && TextElement.HasTextSelection) - { - var el = TextElement; - string oldText = el.Text; - int oldSelStart = el.ActiveSelectionStart; - int oldSelLen = el.ActiveSelectionLength; - if (el.ApplyInlineFormatting(formatType, argument)) - { - string newText = el.Text; - int newSelStart = el.ActiveSelectionStart; - int newSelLen = el.ActiveSelectionLength; - OnActiveTextFormattingApplied?.Invoke(el); - UndoRedo?.RecordAction( - actionName, - () => - { - el.Text = oldText; - int s = Math.Min(oldSelStart, el.Text.Length); - int l = Math.Min(oldSelLen, el.Text.Length - s); - el.UpdateTextSelection(s, l, el.Text.Substring(s, l)); - OnActiveTextFormattingApplied?.Invoke(el); - }, - () => - { - el.Text = newText; - int s = Math.Min(newSelStart, el.Text.Length); - int l = Math.Min(newSelLen, el.Text.Length - s); - el.UpdateTextSelection(s, l, el.Text.Substring(s, l)); - OnActiveTextFormattingApplied?.Invoke(el); - } - ); - return true; - } - } - return false; - } - - [RelayCommand] - public void SetTextColor(string hex) - { - if (string.IsNullOrEmpty(hex) || TextElement == null) return; - - if (TryApplyInlineFormatting(InlineFormatType.Color, "Format Color Selection", hex)) return; - - if (TextElement.TextColorHex != hex) - { - var el = TextElement; - string oldHex = el.TextColorHex; - el.TextColorHex = hex; - UndoRedo?.RecordAction( - "Change Text Color", - () => el.TextColorHex = oldHex, - () => el.TextColorHex = hex - ); - } - } - - [RelayCommand] - public void SetShapeFillColor(string hex) - { - if (ShapeElement != null && ShapeElement.FillColorHex != hex) - { - var el = ShapeElement; - string oldHex = el.FillColorHex; - el.FillColorHex = hex; - UndoRedo?.RecordAction( - "Change Fill Color", - () => el.FillColorHex = oldHex, - () => el.FillColorHex = hex - ); - } - } - - [RelayCommand] - public void SetShapeStrokeColor(string hex) - { - if (ShapeElement != null && ShapeElement.StrokeColorHex != hex) - { - var el = ShapeElement; - string oldHex = el.StrokeColorHex; - el.StrokeColorHex = hex; - UndoRedo?.RecordAction( - "Change Stroke Color", - () => el.StrokeColorHex = oldHex, - () => el.StrokeColorHex = hex - ); - } - } - - [RelayCommand] - public void SetAlignment(string alignmentStr) - { - if (TextElement != null && Enum.TryParse(alignmentStr, true, out var mode) && TextElement.Alignment != mode) - { - var el = TextElement; - var oldMode = el.Alignment; - el.Alignment = mode; - UndoRedo?.RecordAction( - $"Align Text {mode}", - () => el.Alignment = oldMode, - () => el.Alignment = mode - ); - } - } - - [RelayCommand] - public void ToggleBold() - { - if (TextElement != null) - { - if (TryApplyInlineFormatting(InlineFormatType.Bold, "Format Bold Selection")) return; - - var el = TextElement; - bool oldVal = el.IsBold; - bool newVal = !oldVal; - el.IsBold = newVal; - UndoRedo?.RecordAction( - newVal ? "Format Bold" : "Remove Bold", - () => el.IsBold = oldVal, - () => el.IsBold = newVal - ); - } - } - - [RelayCommand] - public void ToggleItalic() - { - if (TextElement != null) - { - if (TryApplyInlineFormatting(InlineFormatType.Italic, "Format Italic Selection")) return; - - var el = TextElement; - bool oldVal = el.IsItalic; - bool newVal = !oldVal; - el.IsItalic = newVal; - UndoRedo?.RecordAction( - newVal ? "Format Italic" : "Remove Italic", - () => el.IsItalic = oldVal, - () => el.IsItalic = newVal - ); - } - } - - [RelayCommand] - public void ToggleUnderline() - { - if (TextElement != null) - { - if (TryApplyInlineFormatting(InlineFormatType.Underline, "Format Underline Selection")) return; - - var el = TextElement; - bool oldVal = el.IsUnderline; - bool newVal = !oldVal; - el.IsUnderline = newVal; - UndoRedo?.RecordAction( - newVal ? "Format Underline" : "Remove Underline", - () => el.IsUnderline = oldVal, - () => el.IsUnderline = newVal - ); - } - } - - [RelayCommand] - public void AutoFitTextHeight() - { - if (TextElement != null) - { - var el = TextElement; - double oldH = el.Height; - double newH = el.CalculateRequiredHeight(); - if (Math.Abs(newH - oldH) > 0.5) - { - el.Height = newH; - UndoRedo?.RecordAction( - "Auto-Fit Text Height", - () => el.Height = oldH, - () => el.Height = newH - ); - } - } - } - - [RelayCommand(CanExecute = nameof(HasSelectedElement))] - public void DeleteSelectedElement() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - foreach (var el in targets) page.RemoveElement(el); - UpdateSelection(null, page); - - UndoRedo?.RecordAction( - $"Delete {targets.Count} Elements", - () => { - foreach (var el in targets) page.AddElement(el); - page.SelectElements(targets); - UpdateSelection(page.SelectedElement, page); - }, - () => { - foreach (var el in targets) page.RemoveElement(el); - UpdateSelection(null, page); - }, - onDiscarded: () => { foreach (var el in targets) ReleaseElementResources(el); } - ); - } - else if (SelectedElement != null) - { - var el = SelectedElement; - page.RemoveElement(el); - UpdateSelection(null, page); - - UndoRedo?.RecordAction( - $"Delete {el.DisplayName}", - () => { page.AddElement(el); UpdateSelection(el, page); }, - () => { page.RemoveElement(el); UpdateSelection(null, page); }, - onDiscarded: () => ReleaseElementResources(el) - ); - } - } - - /// - /// Releases the decoded bitmap of a deleted chart/image element once its undo entry is - /// discarded (capacity eviction or document teardown) — safe specifically because a - /// discarded delete-type undo entry is, by construction, not reachable from the live page - /// any more. Relies on ChartElementViewModel/ImageElementViewModel already disposing their - /// previous bitmap whenever the property is reassigned. - /// - private static void ReleaseElementResources(ElementViewModelBase element) - { - switch (element) - { - case ImageElementViewModel img: - img.PreviewBitmap = null; - break; - case ChartElementViewModel chart: - chart.ChartBitmap = null; - break; - } - } - - [RelayCommand(CanExecute = nameof(HasSelectedElement))] - public void DuplicateSelectedElement() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - var newElements = new List(); - - foreach (var el in targets) - { - var model = el.ToModel(); - var clone = model.Clone(); - clone.Id = Guid.NewGuid().ToString("N"); - clone.X += 20; - clone.Y += 20; - - ElementViewModelBase newVm = _elementService.CreateViewModel(clone); - page.AddElement(newVm); - newElements.Add(newVm); - } - - page.SelectElements(newElements); - UpdateSelection(page.SelectedElement, page); - - UndoRedo?.RecordAction( - $"Duplicate {targets.Count} Elements", - () => { - foreach (var el in newElements) page.RemoveElement(el); - page.SelectElements(targets); - UpdateSelection(page.SelectedElement, page); - }, - () => { - foreach (var el in newElements) page.AddElement(el); - page.SelectElements(newElements); - UpdateSelection(page.SelectedElement, page); - } - ); - } - else if (SelectedElement != null) - { - var model = SelectedElement.ToModel(); - var clone = model.Clone(); - clone.Id = Guid.NewGuid().ToString("N"); - clone.X += 20; - clone.Y += 20; - - ElementViewModelBase newVm = _elementService.CreateViewModel(clone); - page.AddElement(newVm); - UpdateSelection(newVm, page); - - UndoRedo?.RecordAction( - $"Duplicate {newVm.DisplayName}", - () => { page.RemoveElement(newVm); UpdateSelection(null, page); }, - () => { page.AddElement(newVm); UpdateSelection(newVm, page); } - ); - } - } - - [RelayCommand] - public void BringToFront() - { - if (SelectedElement != null && SelectedPage != null) - { - var el = SelectedElement; - var page = SelectedPage; - int oldIdx = page.Elements.IndexOf(el); - if (oldIdx < 0 || oldIdx == page.Elements.Count - 1) return; - - page.BringToFront(el); - int newIdx = page.Elements.IndexOf(el); - - UndoRedo?.RecordAction( - $"Bring {el.DisplayName} to Front", - () => { - if (page.Elements.Contains(el)) - { - int cur = page.Elements.IndexOf(el); - if (cur != oldIdx && oldIdx >= 0 && oldIdx < page.Elements.Count) page.Elements.Move(cur, oldIdx); - page.NormalizeZIndices(); - } - }, - () => { - if (page.Elements.Contains(el)) - { - int cur = page.Elements.IndexOf(el); - if (cur != newIdx && newIdx >= 0 && newIdx < page.Elements.Count) page.Elements.Move(cur, newIdx); - page.NormalizeZIndices(); - } - } - ); - } - } - - [RelayCommand] - public void SendToBack() - { - if (SelectedElement != null && SelectedPage != null) - { - var el = SelectedElement; - var page = SelectedPage; - int oldIdx = page.Elements.IndexOf(el); - if (oldIdx <= 0) return; - - page.SendToBack(el); - int newIdx = page.Elements.IndexOf(el); - - UndoRedo?.RecordAction( - $"Send {el.DisplayName} to Back", - () => { - if (page.Elements.Contains(el)) - { - int cur = page.Elements.IndexOf(el); - if (cur != oldIdx && oldIdx >= 0 && oldIdx < page.Elements.Count) page.Elements.Move(cur, oldIdx); - page.NormalizeZIndices(); - } - }, - () => { - if (page.Elements.Contains(el)) - { - int cur = page.Elements.IndexOf(el); - if (cur != newIdx && newIdx >= 0 && newIdx < page.Elements.Count) page.Elements.Move(cur, newIdx); - page.NormalizeZIndices(); - } - } - ); - } - } - - [RelayCommand] - public void BringForward() - { - if (SelectedElement != null && SelectedPage != null) - { - var el = SelectedElement; - var page = SelectedPage; - int oldIdx = page.Elements.IndexOf(el); - if (oldIdx < 0 || oldIdx >= page.Elements.Count - 1) return; - - page.BringForward(el); - int newIdx = page.Elements.IndexOf(el); - - UndoRedo?.RecordAction( - $"Bring {el.DisplayName} Forward", - () => { - if (page.Elements.Contains(el)) - { - int cur = page.Elements.IndexOf(el); - if (cur != oldIdx && oldIdx >= 0 && oldIdx < page.Elements.Count) page.Elements.Move(cur, oldIdx); - page.NormalizeZIndices(); - } - }, - () => { - if (page.Elements.Contains(el)) - { - int cur = page.Elements.IndexOf(el); - if (cur != newIdx && newIdx >= 0 && newIdx < page.Elements.Count) page.Elements.Move(cur, newIdx); - page.NormalizeZIndices(); - } - } - ); - } - } - - [RelayCommand] - public void SendBackward() - { - if (SelectedElement != null && SelectedPage != null) - { - var el = SelectedElement; - var page = SelectedPage; - int oldIdx = page.Elements.IndexOf(el); - if (oldIdx <= 0) return; - - page.SendBackward(el); - int newIdx = page.Elements.IndexOf(el); - - UndoRedo?.RecordAction( - $"Send {el.DisplayName} Backward", - () => { - if (page.Elements.Contains(el)) - { - int cur = page.Elements.IndexOf(el); - if (cur != oldIdx && oldIdx >= 0 && oldIdx < page.Elements.Count) page.Elements.Move(cur, oldIdx); - page.NormalizeZIndices(); - } - }, - () => { - if (page.Elements.Contains(el)) - { - int cur = page.Elements.IndexOf(el); - if (cur != newIdx && newIdx >= 0 && newIdx < page.Elements.Count) page.Elements.Move(cur, newIdx); - page.NormalizeZIndices(); - } - } - ); - } - } - - [RelayCommand] - public void SetShapeType(string shapeTypeStr) - { - if (ShapeElement != null && Enum.TryParse(shapeTypeStr, true, out var type) && ShapeElement.ShapeType != type) - { - var el = ShapeElement; - var oldType = el.ShapeType; - double oldRadius = el.CornerRadius; - double newRadius = type == ShapeType.Circle ? el.Width / 2 : (type == ShapeType.RoundedRectangle ? 12 : 0); - - el.ShapeType = type; - el.CornerRadius = newRadius; - - UndoRedo?.RecordAction( - $"Shape: {type}", - () => { el.ShapeType = oldType; el.CornerRadius = oldRadius; }, - () => { el.ShapeType = type; el.CornerRadius = newRadius; } - ); - } - } - - [RelayCommand] - public void SetWatermarkColor(string hex) - { - if (WatermarkElement != null && WatermarkElement.ColorHex != hex) - { - var el = WatermarkElement; - string oldHex = el.ColorHex; - el.ColorHex = hex; - UndoRedo?.RecordAction( - "Change Watermark Color", - () => el.ColorHex = oldHex, - () => el.ColorHex = hex - ); - } - } - - [RelayCommand] - public void AlignLeft() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - double minX = targets.Min(e => e.X); - var prevPositions = targets.Select(e => (Element: e, e.X, e.Y)).ToList(); - - foreach (var el in targets) el.X = minX; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Left", - () => { foreach (var item in prevPositions) item.Element.X = item.X; page.UpdateSelectionBoundingBox(); }, - () => { foreach (var el in targets) el.X = minX; page.UpdateSelectionBoundingBox(); } - ); - } - else if (SelectedElement != null) - { - var el = SelectedElement; - double oldX = el.X; - double newX = 60.0; - el.X = newX; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Left to Margin", - () => { el.X = oldX; page.UpdateSelectionBoundingBox(); }, - () => { el.X = newX; page.UpdateSelectionBoundingBox(); } - ); - } - } - - [RelayCommand] - public void AlignCenter() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - double minX = targets.Min(e => e.X); - double maxX = targets.Max(e => e.X + Math.Max(1, e.Width)); - double centerX = (minX + maxX) / 2.0; - var prevPositions = targets.Select(e => (Element: e, e.X, e.Y)).ToList(); - - foreach (var el in targets) el.X = centerX - (el.Width / 2.0); - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Center", - () => { foreach (var item in prevPositions) item.Element.X = item.X; page.UpdateSelectionBoundingBox(); }, - () => { foreach (var el in targets) el.X = centerX - (el.Width / 2.0); page.UpdateSelectionBoundingBox(); } - ); - } - else if (SelectedElement != null) - { - var el = SelectedElement; - double oldX = el.X; - double newX = Math.Max(0, (page.Width - el.Width) / 2.0); - el.X = newX; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Center on Page", - () => { el.X = oldX; page.UpdateSelectionBoundingBox(); }, - () => { el.X = newX; page.UpdateSelectionBoundingBox(); } - ); - } - } - - [RelayCommand] - public void AlignRight() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - double maxX = targets.Max(e => e.X + Math.Max(1, e.Width)); - var prevPositions = targets.Select(e => (Element: e, e.X, e.Y)).ToList(); - - foreach (var el in targets) el.X = maxX - el.Width; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Right", - () => { foreach (var item in prevPositions) item.Element.X = item.X; page.UpdateSelectionBoundingBox(); }, - () => { foreach (var el in targets) el.X = maxX - el.Width; page.UpdateSelectionBoundingBox(); } - ); - } - else if (SelectedElement != null) - { - var el = SelectedElement; - double oldX = el.X; - double newX = Math.Max(0, page.Width - el.Width - 60.0); - el.X = newX; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Right to Margin", - () => { el.X = oldX; page.UpdateSelectionBoundingBox(); }, - () => { el.X = newX; page.UpdateSelectionBoundingBox(); } - ); - } - } - - [RelayCommand] - public void AlignTop() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - double minY = targets.Min(e => e.Y); - var prevPositions = targets.Select(e => (Element: e, e.X, e.Y)).ToList(); - - foreach (var el in targets) el.Y = minY; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Top", - () => { foreach (var item in prevPositions) item.Element.Y = item.Y; page.UpdateSelectionBoundingBox(); }, - () => { foreach (var el in targets) el.Y = minY; page.UpdateSelectionBoundingBox(); } - ); - } - else if (SelectedElement != null) - { - var el = SelectedElement; - double oldY = el.Y; - double newY = 60.0; - el.Y = newY; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Top to Margin", - () => { el.Y = oldY; page.UpdateSelectionBoundingBox(); }, - () => { el.Y = newY; page.UpdateSelectionBoundingBox(); } - ); - } - } - - [RelayCommand] - public void AlignMiddle() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - double minY = targets.Min(e => e.Y); - double maxY = targets.Max(e => e.Y + Math.Max(1, e.Height)); - double centerY = (minY + maxY) / 2.0; - var prevPositions = targets.Select(e => (Element: e, e.X, e.Y)).ToList(); - - foreach (var el in targets) el.Y = centerY - (el.Height / 2.0); - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Middle", - () => { foreach (var item in prevPositions) item.Element.Y = item.Y; page.UpdateSelectionBoundingBox(); }, - () => { foreach (var el in targets) el.Y = centerY - (el.Height / 2.0); page.UpdateSelectionBoundingBox(); } - ); - } - else if (SelectedElement != null) - { - var el = SelectedElement; - double oldY = el.Y; - double newY = Math.Max(0, (page.Height - el.Height) / 2.0); - el.Y = newY; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Middle on Page", - () => { el.Y = oldY; page.UpdateSelectionBoundingBox(); }, - () => { el.Y = newY; page.UpdateSelectionBoundingBox(); } - ); - } - } - - [RelayCommand] - public void AlignBottom() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - double maxY = targets.Max(e => e.Y + Math.Max(1, e.Height)); - var prevPositions = targets.Select(e => (Element: e, e.X, e.Y)).ToList(); - - foreach (var el in targets) el.Y = maxY - el.Height; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Bottom", - () => { foreach (var item in prevPositions) item.Element.Y = item.Y; page.UpdateSelectionBoundingBox(); }, - () => { foreach (var el in targets) el.Y = maxY - el.Height; page.UpdateSelectionBoundingBox(); } - ); - } - else if (SelectedElement != null) - { - var el = SelectedElement; - double oldY = el.Y; - double newY = Math.Max(0, page.Height - el.Height - 60.0); - el.Y = newY; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Align Bottom to Margin", - () => { el.Y = oldY; page.UpdateSelectionBoundingBox(); }, - () => { el.Y = newY; page.UpdateSelectionBoundingBox(); } - ); - } - } - - [RelayCommand] - public void DistributeHorizontally() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - var targets = page.SelectedElements.Count >= 3 ? page.SelectedElements.ToList() : page.Elements.ToList(); - if (targets.Count < 3) return; - - var sorted = targets.OrderBy(e => e.X).ToList(); - double firstX = sorted.First().X; - double lastRight = sorted.Last().X + sorted.Last().Width; - double totalElementsWidth = sorted.Sum(e => e.Width); - double totalSpan = lastRight - firstX; - double totalGaps = totalSpan - totalElementsWidth; - double gap = Math.Max(0, totalGaps / (sorted.Count - 1)); - - var prevPositions = sorted.Select(e => (Element: e, e.X, e.Y)).ToList(); - double curX = firstX; - var newPositions = new List<(ElementViewModelBase Element, double X)>(); - - for (int i = 0; i < sorted.Count; i++) - { - var el = sorted[i]; - if (i == 0) - { - newPositions.Add((el, firstX)); - curX += el.Width + gap; - } - else if (i == sorted.Count - 1) - { - newPositions.Add((el, lastRight - el.Width)); - } - else - { - newPositions.Add((el, curX)); - curX += el.Width + gap; - } - } - - foreach (var p in newPositions) p.Element.X = p.X; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Distribute Horizontally", - () => { foreach (var p in prevPositions) p.Element.X = p.X; page.UpdateSelectionBoundingBox(); }, - () => { foreach (var p in newPositions) p.Element.X = p.X; page.UpdateSelectionBoundingBox(); } - ); - } - - [RelayCommand] - public void DistributeVertically() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - var targets = page.SelectedElements.Count >= 3 ? page.SelectedElements.ToList() : page.Elements.ToList(); - if (targets.Count < 3) return; - - var sorted = targets.OrderBy(e => e.Y).ToList(); - double firstY = sorted.First().Y; - double lastBottom = sorted.Last().Y + sorted.Last().Height; - double totalElementsHeight = sorted.Sum(e => e.Height); - double totalSpan = lastBottom - firstY; - double totalGaps = totalSpan - totalElementsHeight; - double gap = Math.Max(0, totalGaps / (sorted.Count - 1)); - - var prevPositions = sorted.Select(e => (Element: e, e.X, e.Y)).ToList(); - double curY = firstY; - var newPositions = new List<(ElementViewModelBase Element, double Y)>(); - - for (int i = 0; i < sorted.Count; i++) - { - var el = sorted[i]; - if (i == 0) - { - newPositions.Add((el, firstY)); - curY += el.Height + gap; - } - else if (i == sorted.Count - 1) - { - newPositions.Add((el, lastBottom - el.Height)); - } - else - { - newPositions.Add((el, curY)); - curY += el.Height + gap; - } - } - - foreach (var p in newPositions) p.Element.Y = p.Y; - page.UpdateSelectionBoundingBox(); - - UndoRedo?.RecordAction( - "Distribute Vertically", - () => { foreach (var p in prevPositions) p.Element.Y = p.Y; page.UpdateSelectionBoundingBox(); }, - () => { foreach (var p in newPositions) p.Element.Y = p.Y; page.UpdateSelectionBoundingBox(); } - ); - } - - [RelayCommand] - public void ToggleLock() - { - if (SelectedPage == null) return; - var page = SelectedPage; - - if (page.SelectedElements.Count > 1) - { - var targets = page.SelectedElements.ToList(); - bool allLocked = targets.All(e => e.IsLocked); - bool targetLock = !allLocked; - var prevStates = targets.Select(e => (Element: e, e.IsLocked)).ToList(); - - foreach (var el in targets) el.IsLocked = targetLock; - - UndoRedo?.RecordAction( - targetLock ? $"Lock {targets.Count} Elements" : $"Unlock {targets.Count} Elements", - () => { foreach (var item in prevStates) item.Element.IsLocked = item.IsLocked; }, - () => { foreach (var el in targets) el.IsLocked = targetLock; } - ); - } - else if (SelectedElement != null) - { - var el = SelectedElement; - bool oldLock = el.IsLocked; - bool newLock = !oldLock; - el.IsLocked = newLock; - UndoRedo?.RecordAction( - newLock ? "Lock Element" : "Unlock Element", - () => el.IsLocked = oldLock, - () => el.IsLocked = newLock - ); - } - } - - [RelayCommand] - public void ApplyTableStyle(string styleStr) - { - if (TableElement != null) - { - var el = TableElement; - string oldHeader = el.HeaderBackgroundHex; - string oldAlt = el.AlternateRowBackgroundHex; - string oldBorder = el.BorderColorHex; - - el.ApplyPresetStyle(styleStr); - - string newHeader = el.HeaderBackgroundHex; - string newAlt = el.AlternateRowBackgroundHex; - string newBorder = el.BorderColorHex; - - UndoRedo?.RecordAction( - $"Apply Table Style ({styleStr})", - () => { el.HeaderBackgroundHex = oldHeader; el.AlternateRowBackgroundHex = oldAlt; el.BorderColorHex = oldBorder; }, - () => { el.HeaderBackgroundHex = newHeader; el.AlternateRowBackgroundHex = newAlt; el.BorderColorHex = newBorder; } - ); - } - } - - [RelayCommand] - public void SetChartType(string typeStr) - { - if (ChartElement != null && Enum.TryParse(typeStr, true, out var parsed) && ChartElement.ChartType != parsed) - { - var el = ChartElement; - var oldType = el.ChartType; - el.SetChartType(typeStr); - UndoRedo?.RecordAction( - $"Change Chart to {parsed}", - () => el.ChartType = oldType, - () => el.ChartType = parsed - ); - } - } - - [RelayCommand] - public void SetChartPalette(string paletteStr) - { - if (ChartElement != null && Enum.TryParse(paletteStr, true, out var parsed) && ChartElement.Palette != parsed) - { - var el = ChartElement; - var oldPal = el.Palette; - el.SetPalette(paletteStr); - UndoRedo?.RecordAction( - $"Change Palette to {parsed}", - () => el.Palette = oldPal, - () => el.Palette = parsed - ); - } - } - - [RelayCommand] - public void SetChartLegendPosition(string posStr) - { - if (ChartElement != null && Enum.TryParse(posStr, true, out var parsed) && ChartElement.LegendPosition != parsed) - { - var el = ChartElement; - var oldPos = el.LegendPosition; - el.SetLegendPosition(posStr); - UndoRedo?.RecordAction( - $"Change Legend to {parsed}", - () => el.LegendPosition = oldPos, - () => el.LegendPosition = parsed - ); - } - } - - public PdfEditorApp.ViewModels.DataStudio.DataStudioViewModel? DataStudio { get; set; } - - [RelayCommand] - public void OpenDataStudioForChart() - { - if (ChartElement != null && DataStudio != null) - { - DataStudio.OpenForChart(ChartElement, SelectedPage); - } - } - - [RelayCommand] - public void OpenDataStudioForTable() - { - if (TableElement != null && DataStudio != null) - { - DataStudio.OpenForTable(TableElement, SelectedPage); - } - } - - [RelayCommand] - public void ConvertTableToChart() - { - if (TableElement != null && SelectedPage != null) - { - var bindingService = new PdfEditorApp.Core.Data.DataBindingService(); - var chartModel = bindingService.ConvertTableToChart((PdfTableElement)TableElement.ToModel()); - var chartVm = new ChartElementViewModel(); - chartVm.LoadFromModel(chartModel); - - var oldTable = TableElement; - var page = SelectedPage; - int idx = page.Elements.IndexOf(oldTable); - - page.Elements.Remove(oldTable); - page.Elements.Insert(Math.Max(0, idx), chartVm); - page.SelectedElement = chartVm; - - UndoRedo?.RecordAction( - "Convert Table to Chart", - () => - { - int cIdx = page.Elements.IndexOf(chartVm); - if (cIdx >= 0) page.Elements.RemoveAt(cIdx); - page.Elements.Insert(Math.Max(0, idx), oldTable); - page.SelectedElement = oldTable; - }, - () => - { - int tIdx = page.Elements.IndexOf(oldTable); - if (tIdx >= 0) page.Elements.RemoveAt(tIdx); - page.Elements.Insert(Math.Max(0, idx), chartVm); - page.SelectedElement = chartVm; - } - ); - } - } - - [RelayCommand] - public void ConvertChartToTable() - { - if (ChartElement != null && SelectedPage != null) - { - var bindingService = new PdfEditorApp.Core.Data.DataBindingService(); - var tableModel = bindingService.ConvertChartToTable((PdfChartElement)ChartElement.ToModel()); - var tableVm = new TableElementViewModel(); - tableVm.LoadFromModel(tableModel); - - var oldChart = ChartElement; - var page = SelectedPage; - int idx = page.Elements.IndexOf(oldChart); - - page.Elements.Remove(oldChart); - page.Elements.Insert(Math.Max(0, idx), tableVm); - page.SelectedElement = tableVm; - - UndoRedo?.RecordAction( - "Convert Chart to Table", - () => - { - int tIdx = page.Elements.IndexOf(tableVm); - if (tIdx >= 0) page.Elements.RemoveAt(tIdx); - page.Elements.Insert(Math.Max(0, idx), oldChart); - page.SelectedElement = oldChart; - }, - () => - { - int cIdx = page.Elements.IndexOf(oldChart); - if (cIdx >= 0) page.Elements.RemoveAt(cIdx); - page.Elements.Insert(Math.Max(0, idx), tableVm); - page.SelectedElement = tableVm; - } - ); - } - } - - [RelayCommand] - public async Task QuickPasteDataToChart() - { - if (ChartElement != null && DataStudio != null) - { - await DataStudio.PasteFromClipboardAsync(); - DataStudio.OpenForChart(ChartElement, SelectedPage); - DataStudio.ApplyData(); - } - } - - [RelayCommand] - public async Task QuickPasteDataToTable() - { - if (TableElement != null && DataStudio != null) - { - await DataStudio.PasteFromClipboardAsync(); - DataStudio.OpenForTable(TableElement, SelectedPage); - DataStudio.ApplyData(); - } - } - - [RelayCommand] - public void SetBarcodeFormat(string formatStr) - { - if (BarcodeElement != null && BarcodeElement.BarcodeFormat != formatStr) - { - var el = BarcodeElement; - string oldFmt = el.BarcodeFormat; - el.SetFormat(formatStr); - UndoRedo?.RecordAction( - $"Barcode Format: {formatStr}", - () => el.BarcodeFormat = oldFmt, - () => el.BarcodeFormat = formatStr - ); - } - } - - [RelayCommand] - public void ApplyQrPreset(string presetStr) - { - if (QrCodeElement != null) - { - var el = QrCodeElement; - string oldContent = el.Content; - string oldLabel = el.Label; - el.ApplyPresetType(presetStr); - string newContent = el.Content; - string newLabel = el.Label; - - UndoRedo?.RecordAction( - $"QR Preset: {presetStr}", - () => { el.Content = oldContent; el.Label = oldLabel; }, - () => { el.Content = newContent; el.Label = newLabel; } - ); - } - } - - // --- TYPOGRAPHY EXTENSIONS --- - - [RelayCommand] - public void ToggleStrikethrough() - { - if (TextElement != null) - { - if (TryApplyInlineFormatting(InlineFormatType.Strikethrough, "Format Strikethrough Selection")) return; - - var el = TextElement; - bool oldVal = el.IsStrikethrough; - bool newVal = !oldVal; - el.IsStrikethrough = newVal; - UndoRedo?.RecordAction( - newVal ? "Format Strikethrough" : "Remove Strikethrough", - () => el.IsStrikethrough = oldVal, - () => el.IsStrikethrough = newVal - ); - } - } - - [RelayCommand] - public void TransformTextCase(string caseType) - { - if (TextElement != null) - { - var el = TextElement; - string oldText = el.Text; - if (caseType.Equals("Upper", StringComparison.OrdinalIgnoreCase)) el.TransformUppercase(); - else if (caseType.Equals("Lower", StringComparison.OrdinalIgnoreCase)) el.TransformLowercase(); - else if (caseType.Equals("Title", StringComparison.OrdinalIgnoreCase)) el.TransformTitleCase(); - string newText = el.Text; - - UndoRedo?.RecordAction( - $"Case: {caseType}", - () => el.Text = oldText, - () => el.Text = newText - ); - } - } - - [RelayCommand] - public void ToggleBullets() - { - if (TextElement != null) - { - var el = TextElement; - string oldText = el.Text; - el.ToggleBulletList(); - string newText = el.Text; - - UndoRedo?.RecordAction( - "Toggle Bullet List", - () => el.Text = oldText, - () => el.Text = newText - ); - } - } - - [RelayCommand] - public void ToggleNumbering() - { - if (TextElement != null) - { - var el = TextElement; - string oldText = el.Text; - el.ToggleNumberedList(); - string newText = el.Text; - - UndoRedo?.RecordAction( - "Toggle Numbered List", - () => el.Text = oldText, - () => el.Text = newText - ); - } - } - - [RelayCommand] - public void ToggleDoubleUnderline() - { - if (TextElement != null) - { - var el = TextElement; - bool oldVal = el.IsDoubleUnderline; - bool newVal = !oldVal; - el.IsDoubleUnderline = newVal; - UndoRedo?.RecordAction( - newVal ? "Format Double Underline" : "Remove Double Underline", - () => el.IsDoubleUnderline = oldVal, - () => el.IsDoubleUnderline = newVal - ); - } - } - - [RelayCommand] - public void SetVerticalAlignment(string vAlignStr) - { - if (TextElement != null && Enum.TryParse(vAlignStr, true, out var vAlign) && TextElement.VerticalAlignment != vAlign) - { - var el = TextElement; - var oldVal = el.VerticalAlignment; - el.VerticalAlignment = vAlign; - UndoRedo?.RecordAction( - $"Vertical Align {vAlign}", - () => el.VerticalAlignment = oldVal, - () => el.VerticalAlignment = vAlign - ); - } - } - - [RelayCommand] - public void SetTextShapeMode(string modeStr) - { - if (TextElement != null && Enum.TryParse(modeStr, true, out var mode) && TextElement.ShapeMode != mode) - { - var el = TextElement; - var oldVal = el.ShapeMode; - el.ShapeMode = mode; - UndoRedo?.RecordAction( - $"Change Text Shape {mode}", - () => el.ShapeMode = oldVal, - () => el.ShapeMode = mode - ); - } - } - - [RelayCommand] - public void SetCircularPlacement(string placementStr) - { - if (TextElement != null && Enum.TryParse(placementStr, true, out var placement) && TextElement.CircularPlacement != placement) - { - var el = TextElement; - var oldVal = el.CircularPlacement; - el.CircularPlacement = placement; - UndoRedo?.RecordAction( - $"Circular Placement {placement}", - () => el.CircularPlacement = oldVal, - () => el.CircularPlacement = placement - ); - } - } - - [RelayCommand] - public void SetBezierPreset(string presetStr) - { - if (TextElement != null && Enum.TryParse(presetStr, true, out var preset)) - { - var el = TextElement; - var oldPreset = el.BezierPreset; - double p0x = el.BezierP0X, p0y = el.BezierP0Y; - double p1x = el.BezierP1X, p1y = el.BezierP1Y; - double p2x = el.BezierP2X, p2y = el.BezierP2Y; - double p3x = el.BezierP3X, p3y = el.BezierP3Y; - - el.ApplyBezierPreset(preset); - - UndoRedo?.RecordAction( - $"Bézier Curve Preset: {preset}", - () => - { - el.BezierPreset = oldPreset; - el.BezierP0X = p0x; el.BezierP0Y = p0y; - el.BezierP1X = p1x; el.BezierP1Y = p1y; - el.BezierP2X = p2x; el.BezierP2Y = p2y; - el.BezierP3X = p3x; el.BezierP3Y = p3y; - }, - () => el.ApplyBezierPreset(preset) - ); - } - } - - [RelayCommand] - public void ResetBezierControlPoints() - { - if (TextElement != null) - { - var el = TextElement; - el.ApplyBezierPreset(BezierCurvePreset.Wave); - } - } - - [RelayCommand] - public void ApplyTypographyPreset(string presetName) - { - if (TextElement != null) - { - var el = TextElement; - var oldModel = (PdfTextElement)el.ToModel(); - el.ApplyTypographyPreset(presetName); - var newModel = (PdfTextElement)el.ToModel(); - - UndoRedo?.RecordAction( - $"Typography Preset: {presetName}", - () => el.LoadFromModel(oldModel), - () => el.LoadFromModel(newModel) - ); - } - } - - [RelayCommand] - public void ToggleTextStroke() - { - if (TextElement != null) - { - var el = TextElement; - bool oldVal = el.HasStroke; - bool newVal = !oldVal; - el.HasStroke = newVal; - UndoRedo?.RecordAction( - newVal ? "Enable Text Outline" : "Disable Text Outline", - () => el.HasStroke = oldVal, - () => el.HasStroke = newVal - ); - } - } - - [RelayCommand] - public void SetTextStrokeColor(string hex) - { - if (TextElement != null && TextElement.StrokeColorHex != hex) - { - var el = TextElement; - string oldHex = el.StrokeColorHex; - el.StrokeColorHex = hex; - el.HasStroke = true; - UndoRedo?.RecordAction( - "Change Text Stroke Color", - () => el.StrokeColorHex = oldHex, - () => el.StrokeColorHex = hex - ); - } - } - - [RelayCommand] - public void ToggleTextShadow() - { - if (TextElement != null) - { - var el = TextElement; - bool oldVal = el.HasShadow; - bool newVal = !oldVal; - el.HasShadow = newVal; - UndoRedo?.RecordAction( - newVal ? "Enable Text Shadow" : "Disable Text Shadow", - () => el.HasShadow = oldVal, - () => el.HasShadow = newVal - ); - } - } - - [RelayCommand] - public void SetTextShadowColor(string hex) - { - if (TextElement != null && TextElement.ShadowColorHex != hex) - { - var el = TextElement; - string oldHex = el.ShadowColorHex; - el.ShadowColorHex = hex; - el.HasShadow = true; - UndoRedo?.RecordAction( - "Change Text Shadow Color", - () => el.ShadowColorHex = oldHex, - () => el.ShadowColorHex = hex - ); - } - } - - [RelayCommand] - public void ToggleCurveDirection() - { - if (TextElement != null) - { - var el = TextElement; - bool oldVal = el.CurveClockwise; - bool newVal = !oldVal; - el.CurveClockwise = newVal; - UndoRedo?.RecordAction( - newVal ? "Curve Clockwise" : "Curve Counter-Clockwise", - () => el.CurveClockwise = oldVal, - () => el.CurveClockwise = newVal - ); - } - } - - [RelayCommand] - public void ToggleCurveInvert() - { - if (TextElement != null) - { - var el = TextElement; - bool oldVal = el.CurveInvert; - bool newVal = !oldVal; - el.CurveInvert = newVal; - UndoRedo?.RecordAction( - newVal ? "Invert Text Orientation" : "Normal Text Orientation", - () => el.CurveInvert = oldVal, - () => el.CurveInvert = newVal - ); - } - } - - [RelayCommand] - public void AutoFitTextWidth() - { - if (TextElement != null) - { - var el = TextElement; - double oldW = el.Width; - double newW = el.CalculateRequiredWidth(); - if (Math.Abs(newW - oldW) > 0.5) - { - el.Width = newW; - UndoRedo?.RecordAction( - "Auto-Fit Text Width", - () => el.Width = oldW, - () => el.Width = newW - ); - } - } - } - - [RelayCommand] - public void AutoFitTextBoth() - { - if (TextElement != null) - { - var el = TextElement; - double oldW = el.Width, oldH = el.Height; - double newW = el.CalculateRequiredWidth(); - double newH = el.CalculateRequiredHeight(); - el.Width = newW; - el.Height = newH; - UndoRedo?.RecordAction( - "Auto-Fit Text Dimensions", - () => { el.Width = oldW; el.Height = oldH; }, - () => { el.Width = newW; el.Height = newH; } - ); - } - } - - [RelayCommand] - public void IncreaseFontSize() - { - if (TextElement != null) - { - double step = TextElement.FontSize < 24 ? 1.0 : (TextElement.FontSize < 48 ? 2.0 : 4.0); - double newSize = Math.Min(288, Math.Round(TextElement.FontSize + step)); - SelectedFontSize = newSize; - } - } - - [RelayCommand] - public void DecreaseFontSize() - { - if (TextElement != null) - { - double step = TextElement.FontSize <= 24 ? 1.0 : (TextElement.FontSize <= 48 ? 2.0 : 4.0); - double newSize = Math.Max(4, Math.Round(TextElement.FontSize - step)); - SelectedFontSize = newSize; - } - } - - [RelayCommand] - public void StartEditMode() - { - if (SelectedElement != null) - { - SelectedElement.IsInEditMode = true; - } - } - - [RelayCommand] - public void ToggleEditMode() - { - if (SelectedElement != null) - { - SelectedElement.IsInEditMode = !SelectedElement.IsInEditMode; - } - } - - [RelayCommand] - public void FinishEditMode() - { - if (SelectedElement != null) - { - SelectedElement.IsInEditMode = false; - } - } - - [RelayCommand] - public void ResetTextTransforms() - { - if (TextElement != null) - { - var el = TextElement; - double sx = el.ScaleX, sy = el.ScaleY, bShift = el.BaselineShift, cRot = el.CharacterRotation; - bool fx = el.FlipX, fy = el.FlipY; - - el.ResetTransforms(); - UndoRedo?.RecordAction( - "Reset Text Transforms", - () => { el.ScaleX = sx; el.ScaleY = sy; el.FlipX = fx; el.FlipY = fy; el.BaselineShift = bShift; el.CharacterRotation = cRot; }, - () => { el.ResetTransforms(); } - ); - } - } - - [RelayCommand] - public void ToggleTextFlipX() - { - if (TextElement != null) - { - var el = TextElement; - bool oldVal = el.FlipX; - bool newVal = !oldVal; - el.FlipX = newVal; - UndoRedo?.RecordAction( - newVal ? "Flip Text Horizontally" : "Unflip Text Horizontally", - () => el.FlipX = oldVal, - () => el.FlipX = newVal - ); - } - } - - [RelayCommand] - public void ToggleTextFlipY() - { - if (TextElement != null) - { - var el = TextElement; - bool oldVal = el.FlipY; - bool newVal = !oldVal; - el.FlipY = newVal; - UndoRedo?.RecordAction( - newVal ? "Flip Text Vertically" : "Unflip Text Vertically", - () => el.FlipY = oldVal, - () => el.FlipY = newVal - ); - } - } - - [RelayCommand] - public void SetTextBackground(string hex) - { - if (TextElement != null) - { - var el = TextElement; - string oldHex = el.BackgroundColorHex; - el.BackgroundColorHex = hex; - UndoRedo?.RecordAction( - "Text Background Color", - () => el.BackgroundColorHex = oldHex, - () => el.BackgroundColorHex = hex - ); - } - } - - // --- FORM FIELD VALIDATION --- - - [RelayCommand] - public void SetFormFieldValidation(string valTypeStr) - { - if (FormFieldElement != null && Enum.TryParse(valTypeStr, true, out var valType)) - { - var el = FormFieldElement; - var oldType = el.ValidationType; - el.ValidationType = valType; - UndoRedo?.RecordAction( - $"Field Validation: {valType}", - () => el.ValidationType = oldType, - () => el.ValidationType = valType - ); - } - } - - [RelayCommand] - public void SetDividerStyle(string styleStr) - { - if (DividerElement != null && Enum.TryParse(styleStr, true, out var style)) - { - var el = DividerElement; - var oldStyle = el.Style; - el.Style = style; - UndoRedo?.RecordAction($"Divider Style: {style}", () => el.Style = oldStyle, () => el.Style = style); - } - } - - [RelayCommand] - public void SetDividerDashStyle(string dashStr) - { - if (DividerElement != null && Enum.TryParse(dashStr, true, out var dash)) - { - var el = DividerElement; - var oldDash = el.DashStyle; - el.DashStyle = dash; - UndoRedo?.RecordAction($"Divider Pattern: {dash}", () => el.DashStyle = oldDash, () => el.DashStyle = dash); - } - } - - [RelayCommand] - public void SetShapeDashStyle(string dashStr) - { - if (ShapeElement != null && Enum.TryParse(dashStr, true, out var dash)) - { - var el = ShapeElement; - var oldDash = el.DashStyle; - el.DashStyle = dash; - UndoRedo?.RecordAction($"Line Pattern: {dash}", () => el.DashStyle = oldDash, () => el.DashStyle = dash); - } - } - - [RelayCommand] - public void SetShapeStartCap(string capStr) - { - if (ShapeElement != null && Enum.TryParse(capStr, true, out var cap)) - { - var el = ShapeElement; - var oldCap = el.StartCap; - el.StartCap = cap; - UndoRedo?.RecordAction($"Start Cap: {cap}", () => el.StartCap = oldCap, () => el.StartCap = cap); - } - } - - [RelayCommand] - public void SetShapeEndCap(string capStr) - { - if (ShapeElement != null && Enum.TryParse(capStr, true, out var cap)) - { - var el = ShapeElement; - var oldCap = el.EndCap; - el.EndCap = cap; - UndoRedo?.RecordAction($"End Cap: {cap}", () => el.EndCap = oldCap, () => el.EndCap = cap); - } - } - - [RelayCommand] - public void ToggleInkSmoothSpline() - { - if (InkElement != null) - { - var el = InkElement; - bool oldVal = el.IsSmoothSpline; - bool newVal = !oldVal; - el.IsSmoothSpline = newVal; - UndoRedo?.RecordAction(newVal ? "Enable Bézier Ink Smoothing" : "Disable Bézier Ink Smoothing", - () => el.IsSmoothSpline = oldVal, () => el.IsSmoothSpline = newVal); - } - } - - [RelayCommand] - public void SetDividerColor(string hex) - { - if (DividerElement != null) - { - var el = DividerElement; - string oldHex = el.ColorHex; - el.ColorHex = hex; - UndoRedo?.RecordAction("Change Divider Color", () => el.ColorHex = oldHex, () => el.ColorHex = hex); - } - } - - [RelayCommand] - public void SetInkColor(string hex) - { - if (InkElement != null) - { - var el = InkElement; - string oldHex = el.StrokeColorHex; - el.StrokeColorHex = hex; - UndoRedo?.RecordAction("Change Ink Color", () => el.StrokeColorHex = oldHex, () => el.StrokeColorHex = hex); - } - } - - [RelayCommand] - public void SetStickyNoteColor(string hex) - { - if (StickyNoteElement != null) - { - var el = StickyNoteElement; - string oldHex = el.ColorHex; - el.ColorHex = hex; - UndoRedo?.RecordAction("Change Note Color", () => el.ColorHex = oldHex, () => el.ColorHex = hex); - } - } - - [RelayCommand] - public void SetRedactionFillColor(string hex) - { - if (RedactionElement != null) - { - var el = RedactionElement; - string oldHex = el.FillColorHex; - el.FillColorHex = hex; - UndoRedo?.RecordAction("Change Redaction Fill", () => el.FillColorHex = oldHex, () => el.FillColorHex = hex); - } - } - - [RelayCommand] - public void SetImageBorderColor(string hex) - { - if (ImageElement != null) - { - var el = ImageElement; - string oldHex = el.BorderColorHex; - el.BorderColorHex = hex; - UndoRedo?.RecordAction("Change Image Border Color", () => el.BorderColorHex = oldHex, () => el.BorderColorHex = hex); - } - } - - [RelayCommand] - public void SetFormFieldBackgroundColor(string hex) - { - if (FormFieldElement != null) - { - var el = FormFieldElement; - string oldHex = el.BackgroundColorHex; - el.BackgroundColorHex = hex; - UndoRedo?.RecordAction("Change Field Background", () => el.BackgroundColorHex = oldHex, () => el.BackgroundColorHex = hex); - } - } - - [RelayCommand] - public void SetFormFieldBorderColor(string hex) - { - if (FormFieldElement != null) - { - var el = FormFieldElement; - string oldHex = el.BorderColorHex; - el.BorderColorHex = hex; - UndoRedo?.RecordAction("Change Field Border", () => el.BorderColorHex = oldHex, () => el.BorderColorHex = hex); - } - } - - [RelayCommand] - public void SetFormFieldType(string typeStr) - { - if (FormFieldElement != null && Enum.TryParse(typeStr, true, out var type)) - { - var el = FormFieldElement; - var oldType = el.FieldType; - el.FieldType = type; - UndoRedo?.RecordAction($"Change Field to {type}", () => el.FieldType = oldType, () => el.FieldType = type); - } - } - - [RelayCommand] - public void SetShapeLabelColor(string hex) - { - if (ShapeElement != null) - { - var el = ShapeElement; - string? oldHex = el.LabelColorHex; - el.LabelColorHex = hex; - UndoRedo?.RecordAction("Change Shape Label Color", () => el.LabelColorHex = oldHex, () => el.LabelColorHex = hex); - } - } - - [RelayCommand] - public void SetQrCodeDarkColor(string hex) - { - if (QrCodeElement != null) - { - var el = QrCodeElement; - string oldHex = el.DarkColorHex; - el.DarkColorHex = hex; - UndoRedo?.RecordAction("Change QR Color", () => el.DarkColorHex = oldHex, () => el.DarkColorHex = hex); - } - } - - [RelayCommand] - public void SetQrCodeLightColor(string hex) - { - if (QrCodeElement != null) - { - var el = QrCodeElement; - string oldHex = el.LightColorHex; - el.LightColorHex = hex; - UndoRedo?.RecordAction("Change QR Background", () => el.LightColorHex = oldHex, () => el.LightColorHex = hex); - } - } - - [RelayCommand] - public void SetQrCodeEccLevel(string eccStr) - { - if (QrCodeElement != null && Enum.TryParse(eccStr, true, out var ecc)) - { - var el = QrCodeElement; - var oldEcc = el.EccLevel; - el.EccLevel = ecc; - UndoRedo?.RecordAction($"Change QR ECC: {ecc}", () => el.EccLevel = oldEcc, () => el.EccLevel = ecc); - } - } - - [RelayCommand] - public void SetBarcodeColor(string hex) - { - if (BarcodeElement != null) - { - var el = BarcodeElement; - string oldHex = el.BarColorHex; - el.BarColorHex = hex; - UndoRedo?.RecordAction("Change Barcode Color", () => el.BarColorHex = oldHex, () => el.BarColorHex = hex); - } - } - - [RelayCommand] - public void SetFormFieldCalculation(string calcStr) - { - if (FormFieldElement != null && Enum.TryParse(calcStr, true, out var calc)) - { - var el = FormFieldElement; - var oldCalc = el.CalculationFormula; - el.CalculationFormula = calc; - SelectedPage?.RecalculateFormFields(); - UndoRedo?.RecordAction($"Set Formula to {calc}", () => { el.CalculationFormula = oldCalc; SelectedPage?.RecalculateFormFields(); }, () => { el.CalculationFormula = calc; SelectedPage?.RecalculateFormFields(); }); - } - } - - [RelayCommand] - public void SetMeasurementColor(string hex) - { - if (MeasurementElement != null) - { - var el = MeasurementElement; - string oldHex = el.StrokeColorHex; - el.StrokeColorHex = hex; - UndoRedo?.RecordAction("Change Measurement Color", () => el.StrokeColorHex = oldHex, () => el.StrokeColorHex = hex); - } - } - - // --- DOCUMENT & PAGE LEVEL INSPECTOR COMMANDS --- - - [RelayCommand] - public void RotatePageClockwise() - { - if (SelectedPage != null) - { - var page = SelectedPage; - int oldAngle = page.RotationAngle; - int newAngle = (oldAngle + 90) % 360; - page.RotationAngle = newAngle; - UndoRedo?.RecordAction("Rotate Page 90° CW", () => page.RotationAngle = oldAngle, () => page.RotationAngle = newAngle); - } - } - - [RelayCommand] - public void RotatePageCounterClockwise() - { - if (SelectedPage != null) - { - var page = SelectedPage; - int oldAngle = page.RotationAngle; - int newAngle = (oldAngle + 270) % 360; - page.RotationAngle = newAngle; - UndoRedo?.RecordAction("Rotate Page 90° CCW", () => page.RotationAngle = oldAngle, () => page.RotationAngle = newAngle); - } - } - - [RelayCommand] - public void SetPageFormat(string formatStr) - { - if (SelectedPage != null && Enum.TryParse(formatStr, true, out var fmt)) - { - var page = SelectedPage; - var oldFmt = page.Format; - double oldW = page.Width; - double oldH = page.Height; - - (double w, double h) = fmt switch - { - PageFormat.A4 => (800.0, 1131.0), - PageFormat.Letter => (816.0, 1056.0), - PageFormat.Legal => (816.0, 1344.0), - PageFormat.A3 => (1131.0, 1600.0), - PageFormat.A5 => (565.0, 800.0), - PageFormat.Tabloid => (1056.0, 1632.0), - _ => (800.0, 1131.0) - }; - - if (page.Orientation == PageOrientation.Landscape) - { - (w, h) = (h, w); - } - - page.Format = fmt; - page.Width = w; - page.Height = h; - - UndoRedo?.RecordAction( - $"Page Format: {fmt}", - () => { page.Format = oldFmt; page.Width = oldW; page.Height = oldH; }, - () => { page.Format = fmt; page.Width = w; page.Height = h; } - ); - } - } - - [RelayCommand] - public void SetPageOrientation(string orientStr) - { - if (SelectedPage != null && Enum.TryParse(orientStr, true, out var orient)) - { - var page = SelectedPage; - if (page.Orientation == orient) return; - - var oldOrient = page.Orientation; - double oldW = page.Width; - double oldH = page.Height; - - double newW = oldH; - double newH = oldW; - - page.Orientation = orient; - page.Width = newW; - page.Height = newH; - - UndoRedo?.RecordAction( - $"Orientation: {orient}", - () => { page.Orientation = oldOrient; page.Width = oldW; page.Height = oldH; }, - () => { page.Orientation = orient; page.Width = newW; page.Height = newH; } - ); - } - } - - [RelayCommand] - public void SetPageBackgroundColor(string hex) - { - if (SelectedPage != null && SelectedPage.BackgroundColorHex != hex) - { - var page = SelectedPage; - string oldHex = page.BackgroundColorHex; - page.BackgroundColorHex = hex; - UndoRedo?.RecordAction("Change Page Background Color", () => page.BackgroundColorHex = oldHex, () => page.BackgroundColorHex = hex); - } - } - - [RelayCommand] - public void ApplyHeaderFooterPreset(string preset) - { - if (SelectedPage != null) - { - var page = SelectedPage; - page.ShowHeaderFooter = true; - if (preset == "Standard") - { - page.FooterLeft = "CONFIDENTIAL & PROPRIETARY"; - page.FooterRight = $"Page {page.PageNumber}"; - } - else if (preset == "Minimal") - { - page.FooterLeft = ""; - page.FooterRight = $"{page.PageNumber}"; - } - else if (preset == "Corporate") - { - page.HeaderLeft = "CodeFryDev"; - page.HeaderRight = DateTime.Now.ToString("yyyy-MM-dd"); - page.FooterLeft = "INTERNAL USE ONLY"; - page.FooterRight = $"Page {page.PageNumber}"; - } - } - } - - [RelayCommand] - public void SetSvgPreset(string preset) - { - if (SvgElement != null) - { - var el = SvgElement; - string? oldPreset = el.PresetName; - string oldSource = el.SvgSource; - el.ApplyPreset(preset); - UndoRedo?.RecordAction( - $"SVG Preset: {preset}", - () => { el.PresetName = oldPreset; el.SvgSource = oldSource; }, - () => el.ApplyPreset(preset) - ); - } - } - - [RelayCommand] - public void SetSvgTintColor(string hex) - { - if (SvgElement != null) - { - var el = SvgElement; - string? oldTint = el.TintColorHex; - string oldSource = el.SvgSource; - el.TintColorHex = hex; - if (!string.IsNullOrEmpty(el.PresetName)) - { - el.SvgSource = SvgOrnamentLibrary.GetSvg(el.PresetName, hex); - } - UndoRedo?.RecordAction( - "Change SVG Tint Color", - () => { el.TintColorHex = oldTint; el.SvgSource = oldSource; }, - () => { - el.TintColorHex = hex; - if (!string.IsNullOrEmpty(el.PresetName)) - el.SvgSource = SvgOrnamentLibrary.GetSvg(el.PresetName, hex); - } - ); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/MainViewModel.Elements.cs b/src/PdfEditorApp/ViewModels/MainViewModel.Elements.cs deleted file mode 100644 index 869e577..0000000 --- a/src/PdfEditorApp/ViewModels/MainViewModel.Elements.cs +++ /dev/null @@ -1,1891 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.MathEngine; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels; - -public partial class MainViewModel -{ - // --- ELEMENT UNDO/REDO WRAPPER --- - - public void AddElementWithUndo(ElementViewModelBase element, string description = "") - { - if (CurrentPage == null) return; - var page = CurrentPage; - page.AddElement(element); - string desc = string.IsNullOrEmpty(description) ? $"Add {element.DisplayName}" : description; - - UndoRedo.RecordAction( - desc, - () => page.RemoveElement(element), - () => page.AddElement(element) - ); - - ShowToast(desc, "PlusCircleOutline"); - } - - public IReadOnlyList InsertableElements => _elementService.GetInsertableElements(); - - [RelayCommand] - public void InsertCanvasElement(string elementTypeId) - { - if (CurrentPage == null) return; - var descriptor = _elementService.GetDescriptor(elementTypeId); - if (descriptor == null) return; - - var vm = _elementService.CreateViewModel(elementTypeId); - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, descriptor.DefaultWidth, descriptor.DefaultHeight); - vm.X = posX; - vm.Y = posY; - vm.Width = descriptor.DefaultWidth; - vm.Height = descriptor.DefaultHeight; - - AddElementWithUndo(vm, $"Added {descriptor.DisplayName}"); - CurrentPage.SelectedElement = vm; - Inspector.SelectedElement = vm; - } - - // --- ELEMENT CREATION COMMANDS --- - - [RelayCommand] - public void AddTextElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 400, 80); - var textEl = new TextElementViewModel - { - X = posX, - Y = posY, - Width = 400, - Height = 80, - Text = "New editable paragraph. Double-click or use inspector to customize text, fonts, colors, and alignments.", - FontSize = 13, - TextColorHex = "#201F1E" - }; - - AddElementWithUndo(textEl, "Added Text Paragraph"); - } - - [RelayCommand] - public void AddHeadingElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 500, 45); - var headingEl = new TextElementViewModel - { - X = posX, - Y = posY, - Width = 500, - Height = 45, - Text = "Section Heading", - FontSize = 22, - FontFamily = "Georgia", - IsBold = true, - TextColorHex = "#111827" - }; - - AddElementWithUndo(headingEl, "Added Section Heading"); - } - - [RelayCommand] - public void AddShapeElement(string? shapeTypeStr = "Rectangle") - { - if (CurrentPage == null) return; - - var shapeType = ShapeType.Rectangle; - if (!string.IsNullOrEmpty(shapeTypeStr) && Enum.TryParse(shapeTypeStr, true, out var parsed)) - { - shapeType = parsed; - } - - double width = shapeType == ShapeType.Circle ? 120 : 240; - double height = 120; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, width, height); - - var shapeEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = width, - Height = height, - ShapeType = shapeType, - FillColorHex = "#F0F7FD", - StrokeColorHex = "#0F6CBD", - StrokeThickness = 1.5, - CornerRadius = shapeType == ShapeType.Circle ? 60 : (shapeType == ShapeType.RoundedRectangle ? 16 : 6) - }; - - AddElementWithUndo(shapeEl, $"Added Shape ({shapeType})"); - } - - [RelayCommand] - public void AddMedalBadgeElement() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 140, 170); - - var badgeEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 140, - Height = 170, - ShapeType = ShapeType.MedalRibbonBadge, - FillColorHex = "#F59E0B", - StrokeColorHex = "#B45309", - StrokeThickness = 2.5, - SecondaryFillColorHex = "#990000", - ZIndex = 5 - }; - - AddElementWithUndo(badgeEl, "Added Medal Ribbon Badge"); - } - - [RelayCommand] - public void AddLaurelSealElement() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 110, 110); - - var sealEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 110, - Height = 110, - ShapeType = ShapeType.LaurelWreathSeal, - FillColorHex = "#FEF3C7", - StrokeColorHex = "#B45309", - StrokeThickness = 2.0, - Label = "OFFICIAL", - LabelColorHex = "#92400E", - LabelFontSize = 10, - ZIndex = 5 - }; - - AddElementWithUndo(sealEl, "Added Laurel Gold Seal"); - } - - [RelayCommand] - public void AddRibbonBannerElement() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 320, 50); - - var bannerEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 320, - Height = 50, - ShapeType = ShapeType.RibbonBanner, - FillColorHex = "#990000", - StrokeColorHex = "#F59E0B", - StrokeThickness = 1.5, - Label = "AWARD OF EXCELLENCE", - LabelColorHex = "#FFFFFF", - LabelFontSize = 13, - ZIndex = 5 - }; - - AddElementWithUndo(bannerEl, "Added Ribbon Banner"); - } - - [RelayCommand] - public void AddCornerAccentElement(string? corner = "TopLeft") - { - if (CurrentPage == null) return; - - bool isBottomRight = string.Equals(corner, "BottomRight", StringComparison.OrdinalIgnoreCase); - double w = 240; - double h = 240; - double posX = isBottomRight ? Math.Max(0, CurrentPage.Width - w) : 0; - double posY = isBottomRight ? Math.Max(0, CurrentPage.Height - h) : 0; - - var cornerEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = w, - Height = h, - ShapeType = isBottomRight ? ShapeType.CornerPolygonalAccentBottomRight : ShapeType.CornerPolygonalAccentTopLeft, - FillColorHex = "#990000", - StrokeColorHex = "#F59E0B", - StrokeThickness = 2.0, - ZIndex = 1 - }; - - AddElementWithUndo(cornerEl, $"Added Corner Accent ({corner})"); - } - - [RelayCommand] - public void AddSignatureBlock() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 260, 110); - - var scriptEl = new TextElementViewModel - { - X = posX, - Y = posY, - Width = 260, - Height = 45, - Text = "John Smith", - FontSize = 28, - FontFamily = "Great Vibes", - IsItalic = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }; - - var lineEl = new DividerElementViewModel - { - X = posX, - Y = posY + 50, - Width = 260, - Thickness = 1.5, - ColorHex = "#1E293B", - ZIndex = 5 - }; - - var nameEl = new TextElementViewModel - { - X = posX, - Y = posY + 58, - Width = 260, - Height = 24, - Text = "Mr. John Smith", - FontSize = 15, - FontFamily = "Inter", - IsBold = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }; - - var titleEl = new TextElementViewModel - { - X = posX, - Y = posY + 82, - Width = 260, - Height = 22, - Text = "President & Dean", - FontSize = 13, - FontFamily = "Inter", - TextColorHex = "#64748B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }; - - AddElementWithUndo(scriptEl, "Added Signature Block"); - if (CurrentPage != null) - { - CurrentPage.AddElement(lineEl); - CurrentPage.AddElement(nameEl); - CurrentPage.AddElement(titleEl); - } - } - - [RelayCommand] - public void AddDateBlock() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 240, 70); - - var dateEl = new TextElementViewModel - { - X = posX, - Y = posY, - Width = 240, - Height = 28, - Text = DateTime.Now.ToString("MMMM dd, yyyy"), - FontSize = 15, - FontFamily = "Inter", - IsBold = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }; - - var lineEl = new DividerElementViewModel - { - X = posX, - Y = posY + 32, - Width = 240, - Thickness = 1.5, - ColorHex = "#1E293B", - ZIndex = 5 - }; - - var labelEl = new TextElementViewModel - { - X = posX, - Y = posY + 40, - Width = 240, - Height = 24, - Text = "Date of Issuance", - FontSize = 14, - FontFamily = "Inter", - IsBold = true, - TextColorHex = "#1E293B", - Alignment = TextAlignmentMode.Center, - ZIndex = 5 - }; - - AddElementWithUndo(dateEl, "Added Date Block"); - if (CurrentPage != null) - { - CurrentPage.AddElement(lineEl); - CurrentPage.AddElement(labelEl); - } - } - - [RelayCommand] - public async Task AddImageElementAsync() - { - if (CurrentPage == null) return; - - try - { - if (StorageProvider != null) - { - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Insert Image", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("Image Files (*.png, *.jpg, *.jpeg, *.webp)") - { - Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp" } - } - } - }); - - if (files.Count > 0) - { - string filePath = files[0].Path.LocalPath; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 260, 180); - var imgEl = new ImageElementViewModel - { - X = posX, - Y = posY, - Width = 260, - Height = 180, - ImagePath = filePath - }; - - AddElementWithUndo(imgEl, $"Inserted Image: {Path.GetFileName(filePath)}"); - return; - } - } - - // Fallback placeholder image element - var (fallbackX, fallbackY) = _placementService.GetPlacementPosition(CurrentPage, 260, 180); - var fallbackImg = new ImageElementViewModel - { - X = fallbackX, - Y = fallbackY, - Width = 260, - Height = 180, - AltText = "Inserted Graphic" - }; - AddElementWithUndo(fallbackImg, "Inserted Image Placeholder"); - } - catch (Exception ex) - { - ShowToast($"Image error: {ex.Message}", "AlertCircleOutline"); - } - } - - [RelayCommand] - public void AddStampElement(string? stampTypeStr = "Approved") - { - if (CurrentPage == null) return; - - string label = stampTypeStr?.ToUpper() ?? "APPROVED"; - string fillHex = stampTypeStr?.ToLower() switch - { - "approved" => "#DCFCE7", - "confidential" => "#FEE2E2", - "draft" => "#F1F5F9", - "urgent" => "#FEF3C7", - "void" => "#FFEDD5", - _ => "#EFF6FF" - }; - string strokeHex = stampTypeStr?.ToLower() switch - { - "approved" => "#16A34A", - "confidential" => "#DC2626", - "draft" => "#64748B", - "urgent" => "#D97706", - "void" => "#EA580C", - _ => "#0F6CBD" - }; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 180, 60); - var stampEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 180, - Height = 60, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 8, - FillColorHex = fillHex, - StrokeColorHex = strokeHex, - StrokeThickness = 2.0, - Label = label, - LabelColorHex = strokeHex, - LabelFontSize = 18 - }; - - AddElementWithUndo(stampEl, $"Added Stamp ({label})"); - } - - [RelayCommand] - public void AddStickyNoteElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 200, 150); - var noteEl = new StickyNoteElementViewModel - { - X = posX, - Y = posY, - Width = 200, - Height = 150, - Author = "Lead Reviewer", - NoteText = "Please verify financial data and audit metrics prior to final executive sign-off.", - Status = "Pending Review", - ColorHex = "#FEF3C7", - BorderColorHex = "#F59E0B" - }; - - AddElementWithUndo(noteEl, "Added Sticky Note"); - RefreshComments(); - } - - [RelayCommand] - public void AddDividerElement() - { - if (CurrentPage == null) return; - - double width = Math.Max(200, CurrentPage.Width - 80); - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, width, 16); - var divEl = new DividerElementViewModel - { - X = posX, - Y = posY, - Width = width, - Height = 3, - Thickness = 2, - ColorHex = "#0F6CBD" - }; - - AddElementWithUndo(divEl, "Added Divider Line"); - } - - [RelayCommand] - public void AddWaveDividerElement() - { - if (CurrentPage == null) return; - double width = Math.Max(200, CurrentPage.Width - 80); - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, width, 24); - var divEl = new DividerElementViewModel - { - X = posX, - Y = posY, - Width = width, - Height = 24, - Thickness = 2.0, - Style = DividerStyle.Wave, - WaveAmplitude = 8.0, - WaveFrequency = 5.0, - ColorHex = "#2563EB" - }; - AddElementWithUndo(divEl, "Added Wave Divider"); - } - - [RelayCommand] - public void AddFlourishDividerElement() - { - if (CurrentPage == null) return; - double width = Math.Max(200, CurrentPage.Width - 80); - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, width, 24); - var divEl = new DividerElementViewModel - { - X = posX, - Y = posY, - Width = width, - Height = 24, - Thickness = 1.8, - Style = DividerStyle.CalligraphicFlourish, - WaveAmplitude = 6.0, - ColorHex = "#D97706" - }; - AddElementWithUndo(divEl, "Added Calligraphic Flourish Divider"); - } - - [RelayCommand] - public void AddBezierCurveElement() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 220, 90); - var curveEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 220, - Height = 90, - ShapeType = ShapeType.BezierCurve, - FillColorHex = "#00000000", - StrokeColorHex = "#2563EB", - StrokeThickness = 2.5, - StartCap = LineEndCap.None, - EndCap = LineEndCap.None - }; - AddElementWithUndo(curveEl, "Added Bézier Curve Line"); - } - - [RelayCommand] - public void AddCurvedArrowElement() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 220, 90); - var arrowEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 220, - Height = 90, - ShapeType = ShapeType.CurvedArrow, - FillColorHex = "#00000000", - StrokeColorHex = "#7C3AED", - StrokeThickness = 2.5, - EndCap = LineEndCap.Arrow - }; - AddElementWithUndo(arrowEl, "Added Curved Arrow"); - } - - [RelayCommand] - public void AddCurlyBraceElement() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 40, 160); - var braceEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 40, - Height = 160, - ShapeType = ShapeType.CurlyBrace, - FillColorHex = "#00000000", - StrokeColorHex = "#0F6CBD", - StrokeThickness = 2.0 - }; - AddElementWithUndo(braceEl, "Added Calligraphic Curly Brace"); - } - - [RelayCommand] - public void AddTableElement() - { - if (CurrentPage == null) return; - - double width = Math.Max(300, CurrentPage.Width - 80); - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, width, 180); - var tableEl = new TableElementViewModel - { - X = posX, - Y = posY, - Width = width, - Height = 180 - }; - - AddElementWithUndo(tableEl, "Added Table"); - } - - [RelayCommand] - public void AddChartElement(string? chartTypeStr = "BarColumn") - { - if (CurrentPage == null) return; - - var chartType = ChartType.BarColumn; - if (!string.IsNullOrEmpty(chartTypeStr) && Enum.TryParse(chartTypeStr, true, out var parsed)) - { - chartType = parsed; - } - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 400, 220); - var chartEl = new ChartElementViewModel - { - X = posX, - Y = posY, - Width = 400, - Height = 220, - ChartType = chartType, - Title = $"{chartType} Chart Analysis" - }; - - AddElementWithUndo(chartEl, $"Added {chartType} Chart"); - } - - [RelayCommand] - public void OpenDataStudio(string? mode = "NewChart") - { - DataStudio.StorageProvider = StorageProvider; - if (Inspector.ChartElement != null) - { - DataStudio.OpenForChart(Inspector.ChartElement, CurrentPage); - } - else if (Inspector.TableElement != null) - { - DataStudio.OpenForTable(Inspector.TableElement, CurrentPage); - } - else - { - DataStudio.OpenForNew(mode ?? "NewChart", CurrentPage); - } - } - - [RelayCommand] - public void OpenBatchGeneration(string? preset = null) - { - BatchGeneration.StorageProvider = StorageProvider; - var doc = (IsEditorVisible && Pages.Count > 0) ? ToDocumentModel() : _templateService.CreateEmployeePayslipTemplate(); - BatchGeneration.OpenWithDocument(doc); - if (preset == "payslip") - { - BatchGeneration.LoadDefaultSamplePayslipDataset(); - } - } - - [RelayCommand] - public async Task ImportExcelToChartAsync() - { - DataStudio.StorageProvider = StorageProvider; - DataStudio.OpenForNew("NewChart", CurrentPage); - DataStudio.SelectedTabIndex = 0; - await DataStudio.BrowseFileAsync(); - } - - [RelayCommand] - public async Task ImportExcelToTableAsync() - { - DataStudio.StorageProvider = StorageProvider; - DataStudio.OpenForNew("NewTable", CurrentPage); - DataStudio.SelectedTabIndex = 0; - await DataStudio.BrowseFileAsync(); - } - - [RelayCommand] - public void FetchRestApiToChart() - { - DataStudio.StorageProvider = StorageProvider; - DataStudio.OpenForNew("NewChart", CurrentPage); - DataStudio.SelectedTabIndex = 1; - } - - [RelayCommand] - public void FetchRestApiToTable() - { - DataStudio.StorageProvider = StorageProvider; - DataStudio.OpenForNew("NewTable", CurrentPage); - DataStudio.SelectedTabIndex = 1; - } - - [RelayCommand] - public void AddWatermarkElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 400, 100); - var wmEl = new WatermarkElementViewModel - { - X = posX, - Y = posY, - Text = "CONFIDENTIAL", - FontSize = 56, - ColorHex = "#DC2626", - Opacity = 0.15, - Angle = -35 - }; - - AddElementWithUndo(wmEl, "Added Watermark Overlay"); - } - - [RelayCommand] - public void AddFormFieldElement(string? formTypeStr = "Text") - { - if (CurrentPage == null) return; - - var fieldType = FormFieldType.Text; - if (!string.IsNullOrEmpty(formTypeStr) && Enum.TryParse(formTypeStr, true, out var parsed)) - { - fieldType = parsed; - } - - double width = fieldType == FormFieldType.Checkbox ? 180 : (fieldType == FormFieldType.Signature ? 260 : 340); - double height = fieldType == FormFieldType.Signature ? 90 : (fieldType == FormFieldType.MultilineText ? 80 : 42); - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, width, height); - - var formEl = new FormFieldElementViewModel - { - X = posX, - Y = posY, - Width = width, - Height = height, - FieldType = fieldType, - Label = fieldType switch - { - FormFieldType.Text => "Full Legal Name:", - FormFieldType.MultilineText => "Additional Notes / Comments:", - FormFieldType.Checkbox => "I accept the Terms & Conditions", - FormFieldType.Radio => "Select Option:", - FormFieldType.Dropdown => "Country / Jurisdiction:", - FormFieldType.Signature => "Authorized Officer Signature:", - _ => "Field:" - }, - Placeholder = fieldType == FormFieldType.Signature ? "Click to Sign / Verify Identity" : "Enter value..." - }; - - AddElementWithUndo(formEl, $"Added Form Field ({fieldType})"); - } - - [RelayCommand] - public void AddQrCodeElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 130, 150); - var qrEl = new QrCodeElementViewModel - { - X = posX, - Y = posY, - Width = 130, - Height = 150, - Content = "https://github.com/PrashantUnity/PDFCreator", - Label = "SCAN TO VERIFY CREDENTIAL" - }; - - AddElementWithUndo(qrEl, "Added Vector QR Code"); - } - - [RelayCommand] - public void AddBarcodeElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 240, 65); - var barcodeEl = new BarcodeElementViewModel - { - X = posX, - Y = posY, - Width = 240, - Height = 65, - CodeValue = $"DOC-2026-{Random.Shared.Next(100000, 999999)}" - }; - - AddElementWithUndo(barcodeEl, "Added Barcode"); - } - - [RelayCommand] - public void AddRedactionElement(string? exemptionCode = "[REDACTED - (b)(4) PRIVILEGED]") - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 320, 36); - var redEl = new RedactionElementViewModel - { - X = posX, - Y = posY, - Width = 320, - Height = 36, - ExemptionCode = exemptionCode ?? "[REDACTED]" - }; - - AddElementWithUndo(redEl, "Added Redaction Box"); - } - - [RelayCommand] - public void AddInkElement(object? isHighlighterParam = null) - { - if (CurrentPage == null) return; - - bool isHighlighter = isHighlighterParam switch - { - bool b => b, - string s when bool.TryParse(s, out var parsed) => parsed, - _ => false - }; - - double height = isHighlighter ? 24 : 12; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 260, height); - - var inkEl = new InkElementViewModel - { - X = posX, - Y = posY, - Width = 260, - Height = height, - StrokeColorHex = isHighlighter ? "#FEF08A" : "#0F6CBD", - StrokeThickness = isHighlighter ? 14.0 : 3.0, - Opacity = isHighlighter ? 0.45 : 1.0, - IsHighlighter = isHighlighter - }; - - AddElementWithUndo(inkEl, isHighlighter ? "Added Highlighter Stroke" : "Added Freehand Ink Stroke"); - } - - [RelayCommand] - public void AddSvgElement(string? presetName = null) - { - if (CurrentPage == null) return; - - string preset = string.IsNullOrWhiteSpace(presetName) ? "GaneshaCrest" : presetName; - double w = preset switch - { - "MarigoldToran" => 500, - "DottedFloralDivider" => 400, - "MandapArch" => 480, - "PlantainTrees" => 90, - "TraditionalDeepam" => 60, - _ => 150 - }; - double h = preset switch - { - "MarigoldToran" => 75, - "DottedFloralDivider" => 25, - "MandapArch" => 55, - "PlantainTrees" => 180, - "TraditionalDeepam" => 140, - _ => 150 - }; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, w, h); - var svgEl = new SvgElementViewModel - { - X = posX, - Y = posY, - Width = w, - Height = h, - PresetName = preset, - SvgSource = SvgOrnamentLibrary.GetSvg(preset) - }; - - AddElementWithUndo(svgEl, $"Added Vector Ornament ({preset})"); - } - - [RelayCommand] - public void AddOrnamentElement(string ornamentName) - { - AddSvgElement(ornamentName); - } - - // --- MATHEMATICAL EQUATIONS & FORMULA STUDIO --- - - [RelayCommand] - public void AddMathElement(string? presetId = null) - { - if (CurrentPage == null) return; - - var mathEl = new MathElementViewModel(); - - if (!string.IsNullOrEmpty(presetId)) - { - var preset = MathPresetsLibrary.FindById(presetId) ?? MathPresetsLibrary.FindByName(presetId); - if (preset != null) - { - mathEl.Formula = preset.Formula; - mathEl.PresetName = preset.Name; - mathEl.Description = preset.Description; - mathEl.Category = preset.Category; - mathEl.EquationNumber = preset.DefaultEquationNumber; - mathEl.Width = preset.DefaultWidth; - mathEl.Height = preset.DefaultHeight; - } - } - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, mathEl.Width, mathEl.Height); - mathEl.X = posX; - mathEl.Y = posY; - mathEl.RenderSvg(); - - AddElementWithUndo(mathEl, $"Added Equation: {mathEl.DisplayName}"); - } - - [RelayCommand] - public void OpenMathStudio(MathElementViewModel? target = null) - { - EditingMathElement = target; - if (target != null) - { - MathStudioFormula = target.Formula; - MathStudioPresetName = target.PresetName ?? "Custom Formula"; - MathStudioEquationNumber = target.EquationNumber; - MathStudioShowNumber = target.ShowEquationNumber; - MathStudioCategory = target.Category; - } - else - { - MathStudioFormula = @"\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}"; - MathStudioPresetName = "Gaussian Integral"; - MathStudioEquationNumber = "(1)"; - MathStudioShowNumber = false; - MathStudioCategory = MathCategory.Calculus; - } - - UpdateMathStudioPreview(); - IsMathStudioOpen = true; - OpenRegisteredDialog("frypdf.dialog.math"); - } - - [RelayCommand] - public void CloseMathStudio() - { - IsMathStudioOpen = false; - EditingMathElement = null; - CloseDynamicDialog(); - } - - [RelayCommand] - public void ApplyMathStudioEquation() - { - if (EditingMathElement != null) - { - var el = EditingMathElement; - string oldFormula = el.Formula; - string newFormula = MathStudioFormula; - bool oldNum = el.ShowEquationNumber; - bool newNum = MathStudioShowNumber; - string oldNumVal = el.EquationNumber; - string newNumVal = MathStudioEquationNumber; - - el.Formula = newFormula; - el.ShowEquationNumber = newNum; - el.EquationNumber = newNumVal; - el.PresetName = MathStudioPresetName; - el.Category = MathStudioCategory; - el.RenderSvg(); - - UndoRedo.RecordAction( - "Edit Equation", - () => { el.Formula = oldFormula; el.ShowEquationNumber = oldNum; el.EquationNumber = oldNumVal; el.RenderSvg(); }, - () => { el.Formula = newFormula; el.ShowEquationNumber = newNum; el.EquationNumber = newNumVal; el.RenderSvg(); } - ); - - ShowToast("Equation Updated", "Sigma"); - } - else if (CurrentPage != null) - { - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 320, 60); - var newEl = new MathElementViewModel - { - X = posX, - Y = posY, - Width = 320, - Height = 60, - Formula = MathStudioFormula, - PresetName = MathStudioPresetName, - ShowEquationNumber = MathStudioShowNumber, - EquationNumber = MathStudioEquationNumber, - Category = MathStudioCategory - }; - newEl.RenderSvg(); - AddElementWithUndo(newEl, "Inserted Math Equation"); - } - - CloseMathStudio(); - } - - [RelayCommand] - public void InsertMathStudioSymbol(string snippet) - { - if (string.IsNullOrEmpty(snippet)) return; - string resolved = MathPresetsLibrary.ResolveSnippet(snippet); - MathStudioFormula = string.IsNullOrWhiteSpace(MathStudioFormula) ? resolved : $"{MathStudioFormula} {resolved}"; - } - - [RelayCommand] - public void ApplyMathStudioPreset(string presetId) - { - if (string.IsNullOrEmpty(presetId)) return; - var preset = MathPresetsLibrary.FindById(presetId) ?? MathPresetsLibrary.FindByName(presetId); - if (preset != null) - { - MathStudioFormula = preset.Formula; - MathStudioPresetName = preset.Name; - MathStudioEquationNumber = preset.DefaultEquationNumber; - MathStudioCategory = preset.Category; - } - } - - // --- FILL & SIGN / DIGITAL SIGNATURE STUDIO --- - - [RelayCommand] - public void OpenSignatureStudio() - { - IsSignatureStudioOpen = true; - OpenRegisteredDialog("frypdf.dialog.signature"); - } - - [RelayCommand] - public void CloseSignatureStudio() - { - IsSignatureStudioOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void PlaceSignatureFromStudio() - { - if (CurrentPage == null) return; - string name = string.IsNullOrWhiteSpace(SignatureSignerName) ? "Jane Doe" : SignatureSignerName.Trim(); - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 260, 55); - var sigEl = _signatureService.CreateCursiveSignatureElement(name, SelectedSignatureStyle, posX, posY); - var vm = new TextElementViewModel(); - vm.LoadFromModel(sigEl); - - AddElementWithUndo(vm, $"Placed Signature ({name})"); - CloseSignatureStudio(); - } - - [RelayCommand] - public void AddDateStamp() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 180, 35); - var dateEl = _signatureService.CreateDateStampElement(posX, posY); - var vm = new TextElementViewModel(); - vm.LoadFromModel(dateEl); - AddElementWithUndo(vm, "Added Date Stamp"); - } - - [RelayCommand] - public void AddInitialsBadge(string? initials = "JD") - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 55, 55); - var initEl = _signatureService.CreateInitialsElement(initials ?? "JD", posX, posY); - var vm = new ShapeElementViewModel(); - vm.LoadFromModel(initEl); - AddElementWithUndo(vm, $"Added Initials ({initEl.Label})"); - } - - [RelayCommand] - public void AddCheckmarkBadge() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 42, 42); - var badge = _signatureService.CreateMarkupBadge("✓", "#16A34A", posX, posY); - var vm = new ShapeElementViewModel(); - vm.LoadFromModel(badge); - AddElementWithUndo(vm, "Added Checkmark (✓)"); - } - - [RelayCommand] - public void AddCrossBadge() - { - if (CurrentPage == null) return; - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 42, 42); - var badge = _signatureService.CreateMarkupBadge("✕", "#DC2626", posX, posY); - var vm = new ShapeElementViewModel(); - vm.LoadFromModel(badge); - AddElementWithUndo(vm, "Added Cross Mark (✕)"); - } - - // --- WATERMARK MANAGER --- - - [RelayCommand] - public void OpenWatermarkManager() - { - IsWatermarkManagerOpen = true; - OpenRegisteredDialog("frypdf.dialog.watermark"); - } - - [RelayCommand] - public void CloseWatermarkManager() - { - IsWatermarkManagerOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void ApplyWatermarkToAllPages() - { - string text = string.IsNullOrWhiteSpace(WatermarkPresetText) ? "CONFIDENTIAL" : WatermarkPresetText.Trim(); - foreach (var page in Pages) - { - var wm = new WatermarkElementViewModel - { - Text = text, - ColorHex = WatermarkColorHex, - Opacity = WatermarkOpacity, - Angle = WatermarkAngle, - FontSize = 56, - X = Math.Max(0, (page.Width - 400) / 2), - Y = Math.Max(0, (page.Height - 100) / 2) - }; - - // Remove existing watermark if any - var existing = page.Elements.OfType().ToList(); - foreach (var ex in existing) page.RemoveElement(ex); - - page.AddElement(wm); - } - - CloseWatermarkManager(); - ShowToast($"Applied watermark '{text}' to all {Pages.Count} pages", "Watermark"); - } - - [RelayCommand] - public void RemoveAllWatermarks() - { - int count = 0; - foreach (var page in Pages) - { - var existing = page.Elements.OfType().ToList(); - foreach (var ex in existing) - { - page.RemoveElement(ex); - count++; - } - } - CloseWatermarkManager(); - ShowToast($"Removed {count} watermarks from document", "WatermarkOff"); - } - - // --- SEARCH & REDACT (FOIA / PRIVACY) --- - - [RelayCommand] - public void OpenSearchRedactDialog() - { - IsSearchRedactDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.searchredact"); - } - - [RelayCommand] - public void CloseSearchRedactDialog() - { - IsSearchRedactDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void ExecuteSearchAndRedact() - { - if (string.IsNullOrWhiteSpace(SearchRedactQuery) || CurrentPage == null) return; - string query = SearchRedactQuery.Trim(); - string exemption = SelectedExemptionCode ?? "[REDACTED]"; - - int matchesCount = 0; - var textElements = CurrentPage.Elements.OfType().ToList(); - - foreach (var textEl in textElements) - { - if (textEl.Text.Contains(query, StringComparison.OrdinalIgnoreCase)) - { - var redEl = new RedactionElementViewModel - { - X = textEl.X, - Y = textEl.Y, - Width = textEl.Width, - Height = textEl.Height, - ExemptionCode = exemption - }; - CurrentPage.AddElement(redEl); - matchesCount++; - } - } - - CloseSearchRedactDialog(); - ShowToast($"Placed {matchesCount} redaction boxes matching '{query}'", "EyeOffOutline"); - } - - [RelayCommand] - public void BurnInAllRedactions() - { - int burned = 0; - foreach (var page in Pages) - { - var redactions = page.Elements.OfType().ToList(); - foreach (var red in redactions) - { - red.FillColorHex = "#000000"; - red.BorderColorHex = "#000000"; - red.TextColorHex = "#FFFFFF"; - red.ShowOverlayText = true; - burned++; - } - } - ShowToast($"Permanently committed & burned in {burned} redactions", "ShieldCheckOutline"); - } - - // --- CUSTOM STAMP CREATOR --- - - [RelayCommand] - public void OpenCustomStampDialog() - { - IsCustomStampDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.customstamp"); - } - - [RelayCommand] - public void CloseCustomStampDialog() - { - IsCustomStampDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void PlaceCustomStamp() - { - if (CurrentPage == null) return; - string text = string.IsNullOrWhiteSpace(CustomStampText) ? "RECEIVED" : CustomStampText.Trim().ToUpper(); - string color = string.IsNullOrWhiteSpace(CustomStampColorHex) ? "#0F6CBD" : CustomStampColorHex; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 220, 65); - var stampEl = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 220, - Height = 65, - ShapeType = ShapeType.RoundedRectangle, - CornerRadius = 8, - FillColorHex = "#FFFFFF", - StrokeColorHex = color, - StrokeThickness = 2.5, - Label = $"{text}\n{DateTime.Now:yyyy-MM-dd HH:mm}", - LabelColorHex = color, - LabelFontSize = 13 - }; - - AddElementWithUndo(stampEl, $"Placed Custom Stamp ({text})"); - CloseCustomStampDialog(); - } - - // --- PREFLIGHT & HEALTH DIAGNOSTICS --- - - [RelayCommand] - public async Task OpenPreflightDialogAsync() - { - IsPreflightDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.preflight"); - IsAuditRunning = true; - try - { - var docModel = ToDocumentModel(); - ActiveAuditReport = await _auditService.RunAuditAsync(docModel); - } - finally - { - IsAuditRunning = false; - } - } - - [RelayCommand] - public async Task AutoFixPreflightIssuesAsync() - { - var docModel = ToDocumentModel(); - int fixedCount = _auditService.AutoFixAllIssues(docModel); - - if (fixedCount > 0) - { - for (int pIdx = 0; pIdx < docModel.Pages.Count && pIdx < Pages.Count; pIdx++) - { - var pModel = docModel.Pages[pIdx]; - var pVm = Pages[pIdx]; - - for (int eIdx = 0; eIdx < pModel.Elements.Count && eIdx < pVm.Elements.Count; eIdx++) - { - var elModel = pModel.Elements[eIdx]; - var elVm = pVm.Elements[eIdx]; - - if (elModel is PdfTextElement textModel && elVm is TextElementViewModel textVm) - { - textVm.TextColorHex = textModel.TextColorHex; - } - else if (elModel is PdfImageElement imgModel && elVm is ImageElementViewModel imgVm) - { - imgVm.AltText = imgModel.AltText; - } - } - } - - if (!string.IsNullOrWhiteSpace(docModel.Title)) DocumentTitle = docModel.Title; - - IsAuditRunning = true; - try - { - ActiveAuditReport = await _auditService.RunAuditAsync(docModel); - } - finally - { - IsAuditRunning = false; - } - ShowToast($"Auto-remediated {fixedCount} compliance issues", "CheckCircleOutline"); - } - else - { - ShowToast("No auto-fixable issues detected", "InformationOutline"); - } - } - - [RelayCommand] - public void NavigateToPreflightIssue(AuditIssueItem? issue) - { - if (issue == null) return; - - int pageIdx = Math.Clamp(issue.PageIndex - 1, 0, Pages.Count - 1); - if (pageIdx >= 0 && pageIdx < Pages.Count) - { - CurrentPage = Pages[pageIdx]; - - if (!string.IsNullOrWhiteSpace(issue.ElementId)) - { - var el = CurrentPage.Elements.FirstOrDefault(e => e.Id == issue.ElementId); - if (el != null) - { - CurrentPage.SelectElement(el); - Inspector.UpdateSelection(el, CurrentPage); - } - } - - IsPreflightDialogOpen = false; - ShowToast($"Jumped to Page {issue.PageIndex}: {issue.Title}", "Magnify"); - } - } - - [RelayCommand] - public void ClosePreflightDialog() - { - IsPreflightDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public async Task ExportPreflightReportAsync() - { - if (ActiveAuditReport == null) - { - var docModel = ToDocumentModel(); - ActiveAuditReport = await _auditService.RunAuditAsync(docModel); - } - - var sb = new System.Text.StringBuilder(); - sb.AppendLine($"# PDF Preflight Audit Report: {DocumentTitle}"); - sb.AppendLine($"Generated on: {DateTime.Now:yyyy-MM-dd HH:mm:ss} UTC"); - sb.AppendLine($"Health Score: {ActiveAuditReport.HealthScore}/100 (Grade {ActiveAuditReport.Grade})"); - sb.AppendLine($"Total Pages: {ActiveAuditReport.TotalPages} | Total Elements: {ActiveAuditReport.TotalElements}"); - sb.AppendLine($"Word Count: {ActiveAuditReport.TotalWordCount} | Est. Reading Time: {ActiveAuditReport.ReadingTimeDisplay}"); - sb.AppendLine($"Fonts Used ({ActiveAuditReport.UniqueFontsUsed.Count}): {string.Join(", ", ActiveAuditReport.UniqueFontsUsed)}"); - sb.AppendLine(); - sb.AppendLine("## Audit Findings & Checks"); - foreach (var issue in ActiveAuditReport.Issues) - { - sb.AppendLine($"- [{issue.Severity.ToUpper()}] (Page {issue.PageIndex}) {issue.Title}: {issue.Description}"); - } - - string reportText = sb.ToString(); - string reportPath = ""; - - if (StorageProvider != null) - { - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Save Preflight Audit Report", - DefaultExtension = "md", - SuggestedFileName = $"{System.IO.Path.GetFileNameWithoutExtension(DocumentTitle)}_Audit_Report.md" - }); - if (file != null) - { - reportPath = file.Path.LocalPath; - } - else - { - UpdateStatus("Export cancelled."); - return; - } - } - else - { - reportPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"{System.IO.Path.GetFileNameWithoutExtension(DocumentTitle)}_Audit_Report.md"); - } - - await System.IO.File.WriteAllTextAsync(reportPath, reportText); - ShowToast($"Saved audit report to {System.IO.Path.GetFileName(reportPath)}", "FileCheckOutline"); - } - - [RelayCommand] - public async Task ExportCommentsSummaryAsync() - { - var sb = new System.Text.StringBuilder(); - sb.AppendLine($"# Review & Annotations Summary: {DocumentTitle}"); - sb.AppendLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); - sb.AppendLine(); - - RefreshComments(); - if (CommentItems.Count == 0) - { - sb.AppendLine("No comments or sticky notes found in this document."); - } - else - { - foreach (var comment in CommentItems) - { - sb.AppendLine($"### Page {comment.PageIndex} - {comment.Author} ({comment.Status})"); - sb.AppendLine($"> {comment.Text}"); - sb.AppendLine(); - } - } - - string exportPath = ""; - if (StorageProvider != null) - { - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Save Comments Summary", - DefaultExtension = "md", - SuggestedFileName = $"{System.IO.Path.GetFileNameWithoutExtension(DocumentTitle)}_Review_Notes.md" - }); - if (file != null) - { - exportPath = file.Path.LocalPath; - } - else - { - UpdateStatus("Export cancelled."); - return; - } - } - else - { - exportPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"{System.IO.Path.GetFileNameWithoutExtension(DocumentTitle)}_Review_Notes.md"); - } - - await System.IO.File.WriteAllTextAsync(exportPath, sb.ToString()); - ShowToast($"Saved review summary to {System.IO.Path.GetFileName(exportPath)}", "CommentTextMultipleOutline"); - } - - // --- CANVAS GRID & SNAP COMMANDS --- - - [RelayCommand] - public void ToggleGrid() - { - ShowGrid = !ShowGrid; - ShowToast(ShowGrid ? "Canvas Grid Enabled" : "Canvas Grid Disabled", "Grid"); - } - - [RelayCommand] - public void ToggleSnapToGrid() - { - SnapToGrid = !SnapToGrid; - ShowToast(SnapToGrid ? $"Snap to Grid Active ({(int)GridSnapSize}pt)" : "Snap to Grid Disabled", "Magnet"); - } - - [RelayCommand] - public void SetGridSnapSize(string sizeStr) - { - if (Enum.TryParse(sizeStr, true, out var size)) - { - GridSnapSize = size; - ShowToast($"Grid Snap Interval: {(int)size}pt", "GridLarge"); - } - } - - // --- BATES NUMBERING STUDIO COMMANDS --- - - public string BatesSamplePreview => - $"{BatesPrefix}{BatesStartingNumber.ToString().PadLeft(BatesNumberOfDigits, '0')}{BatesSuffix}"; - - [RelayCommand] - public void OpenBatesNumberingDialog() - { - OnPropertyChanged(nameof(BatesSamplePreview)); - IsBatesNumberingDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.bates"); - } - - [RelayCommand] - public void CloseBatesNumberingDialog() - { - IsBatesNumberingDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void SetBatesPosition(BatesPosition pos) - { - BatesPosition = pos; - } - - private CancellationTokenSource? _batesCts; - - [RelayCommand] - public async Task ApplyBatesNumberingAsync() - { - IsApplyingBatesNumbers = true; - BatesProgressPercentage = 0; - _batesCts = new CancellationTokenSource(); - var ct = _batesCts.Token; - - int currentNum = BatesStartingNumber; - int total = Pages.Count; - - try - { - for (int i = 0; i < Pages.Count; i++) - { - ct.ThrowIfCancellationRequested(); - - var page = Pages[i]; - string batesText = $"{BatesPrefix}{currentNum.ToString().PadLeft(BatesNumberOfDigits, '0')}{BatesSuffix}"; - - // Remove any existing Bates on page - var existing = page.Elements.OfType().Where(e => e.Text.StartsWith(BatesPrefix) || e.DisplayName.Contains("Bates")).ToList(); - foreach (var el in existing) page.RemoveElement(el); - - double x = 40; - double y = 40; - double pageWidth = page.Width > 0 ? page.Width : 800; - double pageHeight = page.Height > 0 ? page.Height : 1131; - - switch (BatesPosition) - { - case BatesPosition.TopLeft: - page.HeaderLeft = batesText; - page.ShowHeaderFooter = true; - x = 40; y = 25; break; - case BatesPosition.TopCenter: - page.HeaderCenter = batesText; - page.ShowHeaderFooter = true; - x = (pageWidth - 220) / 2; y = 25; break; - case BatesPosition.TopRight: - page.HeaderRight = batesText; - page.ShowHeaderFooter = true; - x = pageWidth - 240; y = 25; break; - case BatesPosition.BottomLeft: - page.FooterLeft = batesText; - page.ShowHeaderFooter = true; - x = 40; y = pageHeight - 45; break; - case BatesPosition.BottomCenter: - page.FooterCenter = batesText; - page.ShowHeaderFooter = true; - x = (pageWidth - 220) / 2; y = pageHeight - 45; break; - case BatesPosition.BottomRight: - page.FooterRight = batesText; - page.ShowHeaderFooter = true; - x = pageWidth - 240; y = pageHeight - 45; break; - } - - var batesEl = new TextElementViewModel - { - X = x, - Y = y, - Width = 200, - Height = 25, - Text = batesText, - FontSize = BatesFontSize, - FontFamily = "Consolas", - TextColorHex = BatesFontColorHex, - IsBold = true - }; - - page.AddElement(batesEl); - currentNum++; - - BatesProgressPercentage = total > 0 ? (i + 1) * 100.0 / total : 100.0; - if ((i + 1) % 5 == 0) - { - await Task.Yield(); - } - } - - ShowToast($"Applied legal Bates stamp across all {Pages.Count} pages", "Numeric"); - } - catch (OperationCanceledException) - { - ShowToast("Bates numbering cancelled.", "CloseCircleOutline"); - } - finally - { - IsApplyingBatesNumbers = false; - IsBatesNumberingDialogOpen = false; - } - } - - [RelayCommand] - public void CancelBatesNumbering() - { - _batesCts?.Cancel(); - } - - [RelayCommand] - public void RemoveBatesNumbering() - { - IsBatesNumberingDialogOpen = false; - int removed = 0; - foreach (var page in Pages) - { - if (page.HeaderLeft?.StartsWith(BatesPrefix) == true) page.HeaderLeft = ""; - if (page.HeaderCenter?.StartsWith(BatesPrefix) == true) page.HeaderCenter = ""; - if (page.HeaderRight?.StartsWith(BatesPrefix) == true) page.HeaderRight = ""; - if (page.FooterLeft?.StartsWith(BatesPrefix) == true) page.FooterLeft = ""; - if (page.FooterCenter?.StartsWith(BatesPrefix) == true) page.FooterCenter = ""; - if (page.FooterRight?.StartsWith(BatesPrefix) == true) page.FooterRight = ""; - - var existing = page.Elements.OfType().Where(e => e.Text.StartsWith(BatesPrefix) || e.DisplayName.Contains("Bates")).ToList(); - foreach (var el in existing) - { - page.RemoveElement(el); - removed++; - } - } - ShowToast($"Removed {removed} Bates stamps from document", "DeleteOutline"); - } - - // --- DOCUMENT COMPARISON DIFF COMMANDS --- - - [RelayCommand] - public async Task OpenCompareDialogAsync() - { - IsCompareDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.compare"); - IsComparing = true; - try - { - var compareService = new DocumentCompareService(); - var currentDoc = ToDocumentModel(); - var templateDoc = _templateService.CreateAnnualReportTemplate(); // Compare against standard baseline - ActiveComparisonReport = await compareService.CompareDocumentsAsync(templateDoc, currentDoc); - } - finally - { - IsComparing = false; - } - } - - [RelayCommand] - public void CloseCompareDialog() - { - IsCompareDialogOpen = false; - CloseDynamicDialog(); - } - - // --- IN-CANVAS FIND & REPLACE COMMANDS --- - - [RelayCommand] - public void OpenFindReplace() - { - IsFindReplaceOpen = true; - if (!string.IsNullOrWhiteSpace(FindQuery)) - { - FindNext(); - } - } - - [RelayCommand] - public void CloseFindReplace() - { - IsFindReplaceOpen = false; - } - - [RelayCommand] - public void FindNext() - { - if (string.IsNullOrWhiteSpace(FindQuery) || Pages.Count == 0) return; - - var comparison = FindMatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - int totalMatches = 0; - bool found = false; - - foreach (var page in Pages) - { - var textElements = page.Elements.OfType().ToList(); - foreach (var el in textElements) - { - if (el.Text.Contains(FindQuery, comparison)) - { - totalMatches++; - if (!found) - { - SelectPage(page); - page.SelectElement(el); - found = true; - } - } - } - } - - FindMatchesCount = totalMatches; - if (totalMatches > 0) - { - ShowToast($"Found {totalMatches} match(es) for \"{FindQuery}\"", "Magnify"); - } - else - { - ShowToast($"No matches found for \"{FindQuery}\"", "InformationOutline"); - } - } - - [RelayCommand] - public void FindPrevious() - { - FindNext(); - } - - [RelayCommand] - public void ReplaceNext() - { - if (CurrentPage?.SelectedElement is TextElementViewModel txt && !string.IsNullOrWhiteSpace(FindQuery)) - { - var comparison = FindMatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - if (txt.Text.Contains(FindQuery, comparison)) - { - txt.Text = txt.Text.Replace(FindQuery, ReplaceQuery ?? "", comparison); - ShowToast("Replaced occurrence", "FindReplace"); - FindNext(); - } - } - else - { - FindNext(); - } - } - - [RelayCommand] - public void ReplaceAll() - { - if (string.IsNullOrWhiteSpace(FindQuery)) return; - - var comparison = FindMatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - int replacements = 0; - - foreach (var page in Pages) - { - var textElements = page.Elements.OfType().ToList(); - foreach (var el in textElements) - { - if (el.Text.Contains(FindQuery, comparison)) - { - el.Text = el.Text.Replace(FindQuery, ReplaceQuery ?? "", comparison); - replacements++; - } - } - } - - ShowToast($"Replaced {replacements} occurrences with \"{ReplaceQuery}\"", "FindReplace"); - FindMatchesCount = 0; - } - - // --- MEASUREMENT & ACROBAT ANNOTATION COMMANDS --- - - [RelayCommand] - public void AddMeasurementElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 240, 36); - var measureEl = new MeasurementElementViewModel - { - X = posX, - Y = posY, - Width = 240, - Height = 36, - Unit = RulerUnit, - StrokeColorHex = "#DC2626", - StrokeThickness = 1.5, - FontSize = 10 - }; - - AddElementWithUndo(measureEl, "Added Measurement Dimension"); - } - - [RelayCommand] - public void AddCalloutElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 220, 80); - var callout = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 220, - Height = 80, - ShapeType = ShapeType.Callout, - FillColorHex = "#EFF6FF", - StrokeColorHex = "#0F6CBD", - StrokeThickness = 1.5, - Label = "Important: Review Section", - LabelFontSize = 11, - LabelColorHex = "#0F6CBD" - }; - - AddElementWithUndo(callout, "Added Callout Note"); - } - - [RelayCommand] - public void AddRevisionCloudElement() - { - if (CurrentPage == null) return; - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, 260, 140); - var cloud = new ShapeElementViewModel - { - X = posX, - Y = posY, - Width = 260, - Height = 140, - ShapeType = ShapeType.RevisionCloud, - FillColorHex = "#FEF2F2", - StrokeColorHex = "#DC2626", - StrokeThickness = 2.0, - Opacity = 0.85 - }; - - AddElementWithUndo(cloud, "Added Revision Cloud Markup"); - } - - // --- RULERS, PRESENTATION & THEME COMMANDS --- - - [RelayCommand] - public void ToggleRulers() - { - ShowRulers = !ShowRulers; - ShowToast(ShowRulers ? "Canvas Rulers Visible" : "Canvas Rulers Hidden", "Ruler"); - } - - [RelayCommand] - public void SetRulerUnit(string unitStr) - { - if (Enum.TryParse(unitStr, true, out var unit)) - { - RulerUnit = unit; - ShowToast($"Ruler Units: {unit}", "RulerSquare"); - } - } - - [RelayCommand] - public void TogglePresentationMode() - { - IsPresentationMode = !IsPresentationMode; - ShowToast(IsPresentationMode ? "Entered Full-Screen Presentation Mode (Esc to Exit)" : "Exited Presentation Mode", "Presentation"); - } -} diff --git a/src/PdfEditorApp/ViewModels/MainViewModel.Pages.cs b/src/PdfEditorApp/ViewModels/MainViewModel.Pages.cs deleted file mode 100644 index 00d3b5b..0000000 --- a/src/PdfEditorApp/ViewModels/MainViewModel.Pages.cs +++ /dev/null @@ -1,693 +0,0 @@ -using System; -using System.Linq; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels; - -public partial class MainViewModel -{ - // --- PAGE COLLECTION & SELECTION COMMANDS --- - - [RelayCommand] - public void SelectPage(PageViewModel? page) - { - if (page == null) return; - - foreach (var p in Pages) - { - p.IsSelected = (p == page); - } - - CurrentPage = page; - OnPropertyChanged(nameof(CurrentPageNumber)); - OnPropertyChanged(nameof(PageDimensionsDisplay)); - - Inspector.UpdateSelection(page.SelectedElement, page); - UpdateStatus($"Viewing Page {page.PageNumber} of {Pages.Count}"); - } - - [RelayCommand] - public void SelectSidebarTab(SidebarTabKind tab) - { - ActiveDynamicSidebarTabId = null; - ActiveDynamicSidebarView = null; - ActiveSidebarTab = tab; - if (IsLeftSidebarCollapsed) - { - IsLeftSidebarCollapsed = false; - } - if (tab == SidebarTabKind.Outline) RefreshOutline(); - if (tab == SidebarTabKind.Comments) RefreshComments(); - if (tab == SidebarTabKind.AiAssistant) OpenAiAssistant(); - } - - [RelayCommand] - public void SelectDynamicSidebarTab(DynamicSidebarTabViewModel tabVm) - { - if (tabVm == null) return; - var descriptor = tabVm.Descriptor; - ActiveDynamicSidebarTabId = descriptor.Id; - ActiveSidebarTab = (SidebarTabKind)(-1); - foreach (var t in DynamicSidebarTabs) - { - t.IsActive = string.Equals(t.Id, descriptor.Id, StringComparison.OrdinalIgnoreCase); - } - if (IsLeftSidebarCollapsed) - { - IsLeftSidebarCollapsed = false; - } - - try - { - var sp = _pluginHost?.Context ?? (IServiceProvider)this; - if (descriptor.ViewFactory != null) - { - ActiveDynamicSidebarView = descriptor.ViewFactory(sp); - } - else if (descriptor.ViewModelFactory != null) - { - ActiveDynamicSidebarView = descriptor.ViewModelFactory(sp); - } - } - catch (Exception ex) - { - ShowToast($"Failed to open sidebar '{descriptor.Title}': {ex.Message}", ToastNotificationType.Danger, "AlertCircleOutline"); - } - } - - [RelayCommand] - public void AddPage() - { - var newPage = new PageViewModel - { - PageNumber = Pages.Count + 1, - Format = CurrentPage?.Format ?? PageFormat.A4, - Orientation = CurrentPage?.Orientation ?? PageOrientation.Portrait, - Width = CurrentPage?.Width ?? 800, - Height = CurrentPage?.Height ?? 1131, - FooterRight = $"Page {Pages.Count + 1} of {Pages.Count + 1}" - }; - - newPage.SelectionChanged += OnElementSelectionChanged; - Pages.Add(newPage); - SelectPage(newPage); - - int addedIndex = Pages.IndexOf(newPage); - UndoRedo.RecordAction( - $"Add Page {newPage.PageNumber}", - () => - { - if (Pages.Contains(newPage)) - { - Pages.Remove(newPage); - RenumberPages(); - if (Pages.Count > 0) SelectPage(Pages[Math.Max(0, addedIndex - 1)]); - } - }, - () => - { - if (!Pages.Contains(newPage)) - { - Pages.Insert(Math.Min(addedIndex, Pages.Count), newPage); - RenumberPages(); - SelectPage(newPage); - } - } - ); - - ShowToast($"Added Page {newPage.PageNumber}", "FilePlusOutline"); - } - - [RelayCommand] - public void DuplicateCurrentPage() - { - if (CurrentPage == null) return; - var model = CurrentPage.ToModel(); - var clonedPage = new PageViewModel(); - clonedPage.LoadFromModel(model); - clonedPage.PageNumber = Pages.Count + 1; - clonedPage.SelectionChanged += OnElementSelectionChanged; - Pages.Add(clonedPage); - SelectPage(clonedPage); - - int addedIndex = Pages.IndexOf(clonedPage); - UndoRedo.RecordAction( - $"Duplicate Page {CurrentPage.PageNumber}", - () => - { - if (Pages.Contains(clonedPage)) - { - Pages.Remove(clonedPage); - RenumberPages(); - if (Pages.Count > 0) SelectPage(Pages[Math.Max(0, addedIndex - 1)]); - } - }, - () => - { - if (!Pages.Contains(clonedPage)) - { - Pages.Insert(Math.Min(addedIndex, Pages.Count), clonedPage); - RenumberPages(); - SelectPage(clonedPage); - } - } - ); - - ShowToast($"Duplicated Page {CurrentPage.PageNumber}", "FileMultipleOutline"); - } - - [RelayCommand] - public void DeleteCurrentPage() - { - if (Pages.Count <= 1 || CurrentPage == null) - { - ShowToast("Cannot delete the only page in the document", "AlertCircleOutline"); - return; - } - - int index = Pages.IndexOf(CurrentPage); - var removedPage = CurrentPage; - Pages.Remove(removedPage); - RenumberPages(); - - int newIndex = Math.Min(index, Pages.Count - 1); - SelectPage(Pages[newIndex]); - - UndoRedo.RecordAction( - $"Delete Page {removedPage.PageNumber}", - () => - { - Pages.Insert(Math.Min(index, Pages.Count), removedPage); - RenumberPages(); - SelectPage(removedPage); - }, - () => - { - if (Pages.Contains(removedPage)) - { - Pages.Remove(removedPage); - RenumberPages(); - SelectPage(Pages[Math.Min(index, Pages.Count - 1)]); - } - } - ); - - ShowToast($"Deleted Page {index + 1}", "DeleteOutline"); - } - - [RelayCommand] - public void RotateCurrentPage() - { - if (CurrentPage == null) return; - var targetPage = CurrentPage; - int oldAngle = targetPage.RotationAngle; - targetPage.RotateClockwise(); - int newAngle = targetPage.RotationAngle; - - UndoRedo.RecordAction( - $"Rotate Page {targetPage.PageNumber}", - () => targetPage.RotationAngle = oldAngle, - () => targetPage.RotationAngle = newAngle - ); - - ShowToast($"Page rotated to {newAngle}°", "RotateRight"); - } - - [RelayCommand] - public void RotateCurrentPageCounterClockwise() - { - if (CurrentPage == null) return; - var targetPage = CurrentPage; - int oldAngle = targetPage.RotationAngle; - targetPage.RotationAngle = (targetPage.RotationAngle + 270) % 360; - int newAngle = targetPage.RotationAngle; - - UndoRedo.RecordAction( - $"Rotate Page {targetPage.PageNumber}", - () => targetPage.RotationAngle = oldAngle, - () => targetPage.RotationAngle = newAngle - ); - - ShowToast($"Page rotated to {newAngle}°", "RotateLeft"); - } - - [RelayCommand] - public void MovePageUp() - { - if (CurrentPage == null) return; - int idx = Pages.IndexOf(CurrentPage); - if (idx > 0) - { - Pages.Move(idx, idx - 1); - RenumberPages(); - SelectPage(Pages[idx - 1]); - - UndoRedo.RecordAction( - $"Move Page {idx + 1} Up", - () => { Pages.Move(idx - 1, idx); RenumberPages(); SelectPage(Pages[idx]); }, - () => { Pages.Move(idx, idx - 1); RenumberPages(); SelectPage(Pages[idx - 1]); } - ); - - ShowToast($"Moved Page {idx + 1} to position {idx}", "ChevronUp"); - } - } - - [RelayCommand] - public void MovePageDown() - { - if (CurrentPage == null) return; - int idx = Pages.IndexOf(CurrentPage); - if (idx < Pages.Count - 1) - { - Pages.Move(idx, idx + 1); - RenumberPages(); - SelectPage(Pages[idx + 1]); - - UndoRedo.RecordAction( - $"Move Page {idx + 1} Down", - () => { Pages.Move(idx + 1, idx); RenumberPages(); SelectPage(Pages[idx]); }, - () => { Pages.Move(idx, idx + 1); RenumberPages(); SelectPage(Pages[idx + 1]); } - ); - - ShowToast($"Moved Page {idx + 1} to position {idx + 2}", "ChevronDown"); - } - } - - public void ReorderPage(int fromIndex, int toIndex) - { - if (fromIndex < 0 || fromIndex >= Pages.Count || toIndex < 0 || toIndex >= Pages.Count || fromIndex == toIndex) - return; - - Pages.Move(fromIndex, toIndex); - RenumberPages(); - SelectPage(Pages[toIndex]); - - UndoRedo.RecordAction( - $"Reorder Page {fromIndex + 1} to {toIndex + 1}", - () => { Pages.Move(toIndex, fromIndex); RenumberPages(); SelectPage(Pages[fromIndex]); }, - () => { Pages.Move(fromIndex, toIndex); RenumberPages(); SelectPage(Pages[toIndex]); } - ); - - ShowToast($"Reordered Page {fromIndex + 1} to position {toIndex + 1}", "SwapVertical"); - } - - // --- PAGE NAVIGATION SHORTCUT COMMANDS --- - - [RelayCommand] - public void NextPage() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.NextPage(); - return; - } - if (CurrentPage == null || Pages.Count == 0) return; - int idx = Pages.IndexOf(CurrentPage); - if (idx < Pages.Count - 1) - { - SelectPage(Pages[idx + 1]); - } - } - - [RelayCommand] - public void PreviousPage() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.PreviousPage(); - return; - } - if (CurrentPage == null || Pages.Count == 0) return; - int idx = Pages.IndexOf(CurrentPage); - if (idx > 0) - { - SelectPage(Pages[idx - 1]); - } - } - - [RelayCommand] - public void FirstPage() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.FirstPage(); - return; - } - if (Pages.Count > 0) - { - SelectPage(Pages[0]); - } - } - - [RelayCommand] - public void LastPage() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.LastPage(); - return; - } - if (Pages.Count > 0) - { - SelectPage(Pages[^1]); - } - } - - private void RenumberPages() - { - for (int i = 0; i < Pages.Count; i++) - { - Pages[i].PageNumber = i + 1; - } - OnPropertyChanged(nameof(CurrentPageNumber)); - OnPropertyChanged(nameof(TotalPagesCount)); - } - - // --- OUTLINE & COMMENTS STREAM SYNC --- - - public void RefreshOutline() - { - OutlineItems.Clear(); - for (int i = 0; i < Pages.Count; i++) - { - var p = Pages[i]; - var headings = p.Elements.OfType() - .Where(t => t.IsBold || t.FontSize >= 16) - .ToList(); - - if (headings.Count > 0) - { - foreach (var h in headings) - { - OutlineItems.Add(new OutlineItem - { - Title = string.IsNullOrWhiteSpace(h.Text) ? "Section Header" : h.Text.Trim(), - PageIndex = i + 1, - Kind = h.FontSize >= 20 ? "Heading 1" : "Heading 2" - }); - } - } - else - { - OutlineItems.Add(new OutlineItem - { - Title = $"Page {i + 1}", - PageIndex = i + 1, - Kind = "Page" - }); - } - } - } - - public void RefreshComments() - { - CommentItems.Clear(); - for (int i = 0; i < Pages.Count; i++) - { - var p = Pages[i]; - var stickies = p.Elements.OfType().ToList(); - foreach (var note in stickies) - { - CommentItems.Add(new CommentItem - { - Author = note.Author, - Timestamp = "Today, 10:30 AM", - Text = note.NoteText, - Status = note.Status, - PageIndex = i + 1 - }); - } - } - } - - [RelayCommand] - public void JumpToOutlineItem(OutlineItem? item) - { - if (item != null && item.PageIndex >= 1 && item.PageIndex <= Pages.Count) - { - SelectPage(Pages[item.PageIndex - 1]); - } - } - - [RelayCommand] - public void JumpToCommentItem(CommentItem? item) - { - if (item != null && item.PageIndex >= 1 && item.PageIndex <= Pages.Count) - { - SelectPage(Pages[item.PageIndex - 1]); - } - } - - // --- HEADER & FOOTER MANAGER --- - - [RelayCommand] - public void OpenHeaderFooterDialog() - { - if (CurrentPage != null) - { - HeaderLeftText = CurrentPage.HeaderLeft ?? ""; - HeaderRightText = CurrentPage.HeaderRight ?? ""; - FooterLeftText = CurrentPage.FooterLeft ?? "CONFIDENTIAL & PROPRIETARY"; - FooterRightText = CurrentPage.FooterRight ?? "Page {P} of {N}"; - } - IsHeaderFooterDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.headerfooter"); - } - - [RelayCommand] - public void CloseHeaderFooterDialog() - { - IsHeaderFooterDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void ApplyHeaderFooterToAllPages() - { - for (int i = 0; i < Pages.Count; i++) - { - var p = Pages[i]; - p.ShowHeaderFooter = true; - p.HeaderLeft = ExpandMacros(HeaderLeftText, i + 1, Pages.Count); - p.HeaderRight = ExpandMacros(HeaderRightText, i + 1, Pages.Count); - p.FooterLeft = ExpandMacros(FooterLeftText, i + 1, Pages.Count); - p.FooterRight = ExpandMacros(FooterRightText, i + 1, Pages.Count); - } - - CloseHeaderFooterDialog(); - ShowToast($"Updated headers & footers across {Pages.Count} pages", "PageLayoutHeaderFooter"); - } - - private string ExpandMacros(string? template, int pageNum, int totalPages) - { - if (string.IsNullOrEmpty(template)) return ""; - return template - .Replace("{P}", pageNum.ToString()) - .Replace("{N}", totalPages.ToString()) - .Replace("[PageNumber]", pageNum.ToString()) - .Replace("[TotalPages]", totalPages.ToString()) - .Replace("[Title]", DocumentTitle) - .Replace("[Author]", DocumentAuthor) - .Replace("[Date]", DateTime.Now.ToString("yyyy-MM-dd")); - } - - // --- DOCUMENT SECURITY & SANITIZATION --- - - [RelayCommand] - public void OpenSecurityDialog() - { - IsSecurityDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.security"); - } - - [RelayCommand] - public void CloseSecurityDialog() - { - IsSecurityDialogOpen = false; - CloseDynamicDialog(); - OnPropertyChanged(nameof(SecurityStatusDisplay)); - } - - [RelayCommand] - public void SanitizeDocument() - { - DocumentAuthor = "Anonymous"; - DocumentSubject = ""; - - int commentsRemoved = 0; - foreach (var page in Pages) - { - var notes = page.Elements.OfType().ToList(); - foreach (var note in notes) - { - page.RemoveElement(note); - commentsRemoved++; - } - } - - SecuritySettings.ScrubMetadataOnExport = true; - SecuritySettings.RemoveCommentsOnExport = true; - - RefreshComments(); - ShowToast($"Sanitized document (cleared metadata & removed {commentsRemoved} internal review notes)", "ShieldCheck"); - } - - // --- PAGE SETUP & GEOMETRY COMMANDS --- - - [RelayCommand] - public void SetPageFormat(string formatStr) - { - if (CurrentPage == null) return; - if (Enum.TryParse(formatStr, true, out var format)) - { - CurrentPage.Format = format; - double w = format switch - { - PageFormat.A4 => 800, - PageFormat.Letter => 816, - PageFormat.Legal => 816, - PageFormat.Executive => 700, - PageFormat.A3 => 1131, - PageFormat.A5 => 565, - PageFormat.Poster => 2592, - _ => 800 - }; - double h = format switch - { - PageFormat.A4 => 1131, - PageFormat.Letter => 1056, - PageFormat.Legal => 1344, - PageFormat.Executive => 950, - PageFormat.A3 => 1600, - PageFormat.A5 => 800, - PageFormat.Poster => 3456, - _ => 1131 - }; - - if (CurrentPage.Orientation == PageOrientation.Landscape) - { - (w, h) = (h, w); - } - - CurrentPage.Width = w; - CurrentPage.Height = h; - OnPropertyChanged(nameof(PageDimensionsDisplay)); - ShowToast($"Page format changed to {format}", "AspectRatio"); - } - } - - [RelayCommand] - public void SetPageOrientation(string orientationStr) - { - if (CurrentPage == null) return; - if (Enum.TryParse(orientationStr, true, out var orient)) - { - if (CurrentPage.Orientation != orient) - { - CurrentPage.Orientation = orient; - (CurrentPage.Width, CurrentPage.Height) = (CurrentPage.Height, CurrentPage.Width); - OnPropertyChanged(nameof(PageDimensionsDisplay)); - ShowToast($"Orientation changed to {orient}", "PhoneRotateLandscape"); - } - } - } - - // --- ORGANIZE: SPLIT, EXTRACT & BATCH ROTATION COMMANDS --- - - [RelayCommand] - public void OpenSplitExtractDialog() - { - IsSplitExtractDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.splitextract"); - } - - [RelayCommand] - public void CloseSplitExtractDialog() - { - IsSplitExtractDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void SetSplitExtractMode(SplitExtractMode mode) - { - SplitExtractMode = mode; - } - - [RelayCommand] - public void ExecuteSplitExtract() - { - IsSplitExtractDialogOpen = false; - if (Pages.Count == 0) return; - - var currentDoc = ToDocumentModel(); - - if (SplitExtractMode == SplitExtractMode.ExtractSelectedPages) - { - if (CurrentPage != null) - { - int currentIdx = Math.Clamp(Pages.IndexOf(CurrentPage), 0, Pages.Count - 1); - var extracted = _pageOrganizerService.ExtractPages(currentDoc, new[] { currentIdx }); - LoadFromDocumentModel(extracted); - ShowToast($"Extracted Page {currentIdx + 1} to new active project", "CallSplit"); - } - } - else if (SplitExtractMode == SplitExtractMode.SplitEveryNPages) - { - int interval = Math.Max(1, SplitPageInterval); - var parts = _pageOrganizerService.SplitEveryNPages(currentDoc, interval); - if (parts.Count > 0) - { - // Open first split part as active document - LoadFromDocumentModel(parts[0]); - ShowToast($"Document split into {parts.Count} parts (Loaded Part 1 of {parts.Count})", "CallSplit"); - } - } - else if (SplitExtractMode == SplitExtractMode.SplitByPageRanges) - { - var parts = _pageOrganizerService.SplitByRanges(currentDoc, SplitPageRanges); - if (parts.Count > 0) - { - LoadFromDocumentModel(parts[0]); - ShowToast($"Split document into {parts.Count} ranges (Loaded Range 1)", "CallSplit"); - } - else - { - ShowToast("Invalid page ranges specified (e.g. use '1-2, 3-4')", "AlertCircleOutline"); - } - } - } - - [RelayCommand] - public void BatchRotatePages(string target) - { - var targetEnum = target.ToLowerInvariant() switch - { - "all" => PageFilterTarget.All, - "even" => PageFilterTarget.EvenPages, - "odd" => PageFilterTarget.OddPages, - "landscape" => PageFilterTarget.LandscapePages, - "portrait" => PageFilterTarget.PortraitPages, - _ => PageFilterTarget.All - }; - - var doc = ToDocumentModel(); - int rotatedCount = _pageOrganizerService.BatchRotatePages(doc, targetEnum, 90); - if (rotatedCount > 0) - { - for (int i = 0; i < doc.Pages.Count && i < Pages.Count; i++) - { - Pages[i].RotationAngle = doc.Pages[i].RotationAngle; - } - ShowToast($"Rotated {rotatedCount} {target} pages by 90°", "RotateRight"); - } - else - { - ShowToast($"No {target} pages matched for rotation", "InformationOutline"); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/MainViewModel.Palette.cs b/src/PdfEditorApp/ViewModels/MainViewModel.Palette.cs deleted file mode 100644 index cc97deb..0000000 --- a/src/PdfEditorApp/ViewModels/MainViewModel.Palette.cs +++ /dev/null @@ -1,943 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; -using Avalonia.Input.Platform; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Tools.Core; - -namespace PdfEditorApp.ViewModels; - -public partial class MainViewModel -{ - public string AppVersion - { - get - { - try - { - var assembly = Assembly.GetExecutingAssembly(); - var infoVer = assembly.GetCustomAttribute()?.InformationalVersion; - if (!string.IsNullOrWhiteSpace(infoVer)) - { - return infoVer.Split('+')[0]; - } - - var ver = assembly.GetName().Version; - if (ver != null) - { - return $"{ver.Major}.{ver.Minor}.{ver.Build}"; - } - } - catch - { - // fallback - } - - return "1.0.0"; - } - } - - public string AppVersionDisplay => $"v{AppVersion} Open Source"; - - // --- COMMAND PALETTE & SHORTCUTS HELP STATE --- - - [ObservableProperty] - private bool _isCommandPaletteOpen; - - [ObservableProperty] - private string _commandSearchQuery = ""; - - [ObservableProperty] - private int _selectedPaletteIndex; - - [ObservableProperty] - private bool _isShortcutsHelpDialogOpen; - - [ObservableProperty] - private bool _isAboutDialogOpen; - - [ObservableProperty] - private bool _isPluginsDialogOpen; - - [ObservableProperty] - private string _pluginSearchQuery = ""; - - public ObservableCollection FilteredPluginsList { get; } = new(); - private readonly List _allLoadedPlugins = new(); - - partial void OnPluginSearchQueryChanged(string value) - { - FilterLoadedPlugins(value); - } - - private void FilterLoadedPlugins(string query) - { - FilteredPluginsList.Clear(); - var q = query.Trim().ToLowerInvariant(); - foreach (var p in _allLoadedPlugins) - { - if (string.IsNullOrWhiteSpace(q) || - p.Name.ToLowerInvariant().Contains(q) || - p.Id.ToLowerInvariant().Contains(q) || - p.Category.ToLowerInvariant().Contains(q) || - p.Description.ToLowerInvariant().Contains(q)) - { - FilteredPluginsList.Add(p); - } - } - } - - [RelayCommand] - public void OpenPluginsDialog() - { - PopulateLoadedPlugins(); - PluginSearchQuery = ""; - FilterLoadedPlugins(""); - IsPluginsDialogOpen = true; - // Route through plugin dialog registry - OpenRegisteredDialog("frypdf.dialog.plugins"); - } - - [ObservableProperty] - private string _activeProfileName = "desktop"; - - [RelayCommand] - public void SwitchProfile(string profileName) - { - try - { - var host = PluginHost ?? _pluginHost ?? App.Services?.GetService(); - if (host == null) return; - - string profilePath = System.IO.Path.Combine(AppContext.BaseDirectory, "profiles", $"{profileName}.profile.json"); - if (!System.IO.File.Exists(profilePath)) - { - profilePath = $"profiles/{profileName}.profile.json"; - } - - if (System.IO.File.Exists(profilePath)) - { - var profile = PdfEditorApp.Core.Plugins.Profiles.ProfileLoader.LoadFromFile(profilePath); - ActiveProfileName = profile.ProfileName; - - var availableBundles = new PdfEditorApp.Core.Plugins.Profiles.IFryPluginBundle[] - { - new PdfEditorApp.Plugins.Bundles.ToolsOrganizeBundle(), - new PdfEditorApp.Plugins.Bundles.ToolsSecurityBundle(), - new PdfEditorApp.Plugins.Bundles.ToolsConversionBundle(), - new PdfEditorApp.Plugins.Bundles.ToolsIntelligenceBundle(), - new PdfEditorApp.Plugins.Bundles.DataStudioBundle(), - new PdfEditorApp.Plugins.Bundles.CanvasElementsBundle(), - new PdfEditorApp.Plugins.Bundles.DocumentIoBundle(), - new PdfEditorApp.Plugins.Bundles.AiProvidersBundle(), - new PdfEditorApp.Plugins.Bundles.OcrEnginesBundle(), - new PdfEditorApp.Plugins.Bundles.StandardTemplatesBundle(), - new PdfEditorApp.Plugins.Bundles.StatusBarBundle(), - new PdfEditorApp.Plugins.Bundles.InspectorBundle(), - new PdfEditorApp.Plugins.Bundles.CommandPaletteBundle(), - new PdfEditorApp.Plugins.Bundles.WorkspacePagesBundle(), - new PdfEditorApp.Plugins.Bundles.DialogsBundle(), - new PdfEditorApp.Plugins.Bundles.EditorSidebarsBundle() - }; - - PdfEditorApp.Core.Plugins.Profiles.ProfileLoader.ApplyProfile(profile, host, availableBundles); - PopulateLoadedPlugins(); - ShowToast($"Switched to '{profileName}' profile!", ToastNotificationType.Success, "Tune"); - } - } - catch (Exception ex) - { - ShowToast($"Failed to switch profile: {ex.Message}", ToastNotificationType.Danger, "AlertCircleOutline"); - } - } - - [RelayCommand] - public void ClosePluginsDialog() - { - IsPluginsDialogOpen = false; - CloseDynamicDialog(); - } - - public void PopulateLoadedPlugins() - { - _allLoadedPlugins.Clear(); - try - { - var host = PluginHost ?? _pluginHost ?? App.Services?.GetService(); - var toolRegistry = _toolRegistry ?? App.Services?.GetService(); - var toolDefs = toolRegistry?.GetAllTools() ?? Array.Empty(); - var toolMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var t in toolDefs) - { - if (!string.IsNullOrWhiteSpace(t.StringId)) - { - toolMap[t.StringId] = t; - } - } - - if (host != null) - { - var pluginsToShow = host.RegisteredPlugins.Count > 0 ? host.RegisteredPlugins : host.LoadedPlugins; - foreach (var plugin in pluginsToShow) - { - toolMap.TryGetValue(plugin.Id, out var tool); - - string category = tool?.CategoryDisplayName ?? "Plugin Extension"; - string description = tool?.Description ?? $"Plugin component '{plugin.Name}'"; - string iconKind = tool?.IconKind ?? "PuzzleOutline"; - string iconColor = tool?.IconColorHex ?? "#7C3AED"; - - if (tool == null) - { - if (plugin.Id.StartsWith("frypdf.page.", StringComparison.OrdinalIgnoreCase)) - { - category = "Workspace Navigation"; - iconKind = "ViewDashboardOutline"; - iconColor = "#0D9488"; - description = $"Modular workspace view and navigation page '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.overlay", StringComparison.OrdinalIgnoreCase)) - { - category = "Shell Overlays"; - iconKind = "GamepadVariantOutline"; - iconColor = "#10B981"; - description = $"Floating interactive shell overlay '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.dialog", StringComparison.OrdinalIgnoreCase)) - { - category = "Modal Studios & Dialogs"; - iconKind = "WindowMaximize"; - iconColor = "#F97316"; - description = $"Modal studio overlay and interactive dialog '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.sidebar", StringComparison.OrdinalIgnoreCase)) - { - category = "Editor Sidebars"; - iconKind = "DockLeft"; - iconColor = "#84CC16"; - description = $"Document editor sidebar panel '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.element.", StringComparison.OrdinalIgnoreCase)) - { - category = "Canvas Elements"; - iconKind = "ShapeOutline"; - iconColor = "#0284C7"; - description = $"Canvas element provider '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.io.", StringComparison.OrdinalIgnoreCase)) - { - category = "Document I/O"; - iconKind = "SwapHorizontal"; - iconColor = "#10B981"; - description = $"Document format import/export filter '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.ai.", StringComparison.OrdinalIgnoreCase)) - { - category = "AI Providers"; - iconKind = "Brain"; - iconColor = "#8B5CF6"; - description = $"LLM intelligence and analysis provider '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.ocr.", StringComparison.OrdinalIgnoreCase)) - { - category = "OCR Engines"; - iconKind = "TextRecognition"; - iconColor = "#F59E0B"; - description = $"Optical character recognition engine '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.template", StringComparison.OrdinalIgnoreCase)) - { - category = "Document Templates"; - iconKind = "FileDocumentOutline"; - iconColor = "#EC4899"; - description = $"Structured document template pack '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.status", StringComparison.OrdinalIgnoreCase)) - { - category = "Status Bar Widgets"; - iconKind = "DockBottom"; - iconColor = "#06B6D4"; - description = $"Status bar metric and diagnostic widget '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.inspector.", StringComparison.OrdinalIgnoreCase)) - { - category = "Property Inspector"; - iconKind = "CardBulletedOutline"; - iconColor = "#3B82F6"; - description = $"Live element property inspector section '{plugin.Name}'"; - } - else if (plugin.Id.StartsWith("frypdf.command", StringComparison.OrdinalIgnoreCase) || plugin.Id.StartsWith("frypdf.palette", StringComparison.OrdinalIgnoreCase)) - { - category = "Command Palette"; - iconKind = "ConsoleLine"; - iconColor = "#6366F1"; - description = $"Quick command and shortcut provider '{plugin.Name}'"; - } - } - - _allLoadedPlugins.Add(new PluginItemViewModel - { - Id = plugin.Id, - Name = plugin.Name, - Version = plugin.Version.ToString(), - Category = category, - Description = description, - IconKind = iconKind, - IconColorHex = iconColor, - IsActive = host.IsPluginActive(plugin.Id), - IsExternal = plugin.GetType().Assembly != typeof(MainViewModel).Assembly, - SourceAssembly = plugin.GetType().Assembly.GetName().Name ?? "FryPDF", - SettingsSchema = plugin.SettingsSchema - }); - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[Plugins] Failed to populate plugins: {ex.Message}"); - } - } - - [ObservableProperty] - private PluginItemViewModel? _selectedConfiguringPlugin; - - [ObservableProperty] - private bool _isConfiguringPluginOpen; - - public ObservableCollection ActivePluginConfigSettings { get; } = new(); - - [RelayCommand] - public void OpenPluginSettings(PluginItemViewModel plugin) - { - if (plugin?.SettingsSchema == null || plugin.SettingsSchema.Count == 0) return; - SelectedConfiguringPlugin = plugin; - ActivePluginConfigSettings.Clear(); - - var store = App.Services?.GetService(); - - foreach (var (key, def) in plugin.SettingsSchema) - { - var item = new PluginSettingItemViewModel - { - Key = key, - Label = string.IsNullOrWhiteSpace(def.Label) ? key : def.Label, - Description = def.Description, - Type = def.Type?.ToLowerInvariant() ?? "string", - Options = def.Options ?? new() - }; - - if (store != null) - { - if (item.Type == "boolean") - { - bool defBool = def.DefaultValue is bool b ? b : (bool.TryParse(def.DefaultValue?.ToString(), out var parsedB) && parsedB); - item.BoolValue = store.GetSetting(plugin.Id, key, defBool); - } - else if (item.Type == "number") - { - double defNum = def.DefaultValue is double d ? d : (double.TryParse(def.DefaultValue?.ToString(), out var parsedD) ? parsedD : 0); - item.NumberValue = store.GetSetting(plugin.Id, key, defNum); - } - else - { - string defStr = def.DefaultValue?.ToString() ?? ""; - item.StringValue = store.GetSetting(plugin.Id, key, defStr); - } - } - else - { - item.StringValue = def.DefaultValue?.ToString() ?? ""; - if (def.DefaultValue is bool b) item.BoolValue = b; - if (def.DefaultValue is double d) item.NumberValue = d; - } - - ActivePluginConfigSettings.Add(item); - } - - IsConfiguringPluginOpen = true; - } - - [RelayCommand] - public void SavePluginSettings() - { - if (SelectedConfiguringPlugin == null) return; - var store = App.Services?.GetService(); - if (store != null) - { - foreach (var item in ActivePluginConfigSettings) - { - if (item.Type == "boolean") - { - store.SetSetting(SelectedConfiguringPlugin.Id, item.Key, item.BoolValue); - } - else if (item.Type == "number") - { - store.SetSetting(SelectedConfiguringPlugin.Id, item.Key, item.NumberValue); - } - else - { - store.SetSetting(SelectedConfiguringPlugin.Id, item.Key, item.StringValue); - } - } - store.Save(); - ShowToast($"Saved preferences for {SelectedConfiguringPlugin.Name}!", ToastNotificationType.Success, "CheckCircle"); - } - IsConfiguringPluginOpen = false; - } - - [RelayCommand] - public void ClosePluginSettings() - { - IsConfiguringPluginOpen = false; - } - - [RelayCommand] - public async System.Threading.Tasks.Task LoadExternalPluginDialogAsync() - { - if (StorageProvider == null) return; - - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Select Plugin Package (.fryplugin) or Assembly (.dll)", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("FryPDF Plugins (*.fryplugin; *.dll)") - { - Patterns = new[] { "*.fryplugin", "*.dll" } - }, - new FilePickerFileType("FryPDF Plugin Package (*.fryplugin)") - { - Patterns = new[] { "*.fryplugin" } - }, - new FilePickerFileType(".NET Assembly (*.dll)") - { - Patterns = new[] { "*.dll" } - } - } - }); - - if (files.Count > 0) - { - var filePath = files[0].Path.LocalPath; - await InstallAndMountPluginPathAsync(filePath); - } - } - - public async System.Threading.Tasks.Task InstallAndMountPluginPathAsync(string filePath) - { - try - { - var host = App.Services?.GetService(); - if (host == null) return; - - IReadOnlyList plugins; - string displayName; - - if (string.Equals(System.IO.Path.GetExtension(filePath), ".fryplugin", StringComparison.OrdinalIgnoreCase)) - { - var pkgResult = PdfEditorApp.Plugins.Loader.FryPluginPackageLoader.UnpackAndLoad(filePath); - plugins = pkgResult.AssemblyPackage.Plugins; - displayName = pkgResult.Manifest.Name; - } - else - { - var pkg = PdfEditorApp.Plugins.Loader.PluginAssemblyLoader.LoadPluginAssembly(filePath); - plugins = pkg.Plugins; - displayName = System.IO.Path.GetFileName(filePath); - } - - if (plugins.Count == 0) - { - ShowToast("No IFryPlugin implementations found in package.", ToastNotificationType.Warning, "AlertCircleOutline"); - return; - } - - foreach (var p in plugins) - { - host.RegisterPlugin(p); - } - await host.StartAsync(); - - PopulateLoadedPlugins(); - FilterLoadedPlugins(PluginSearchQuery); - ShowToast($"Successfully mounted {plugins.Count} plugin(s) from {displayName}!", ToastNotificationType.Success, "CheckCircle"); - } - catch (Exception ex) - { - ShowToast($"Failed to load plugin: {ex.Message}", ToastNotificationType.Danger, "AlertCircleOutline"); - } - } - - public ObservableCollection FilteredPaletteCommands { get; } = new(); - public List AllPaletteCommands { get; } = new(); - - partial void OnCommandSearchQueryChanged(string value) - { - FilterPaletteCommands(value); - } - - [RelayCommand] - public void OpenCommandPalette() - { - InitCommandPalette(); - CommandSearchQuery = ""; - FilterPaletteCommands(""); - IsCommandPaletteOpen = true; - } - - [RelayCommand] - public void CloseCommandPalette() - { - IsCommandPaletteOpen = false; - CommandSearchQuery = ""; - } - - [RelayCommand] - public void OpenShortcutsHelp() - { - IsShortcutsHelpDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.shortcuts"); - } - - [RelayCommand] - public void CloseShortcutsHelp() - { - IsShortcutsHelpDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void OpenAboutDialog() - { - IsAboutDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.about"); - } - - [RelayCommand] - public void CloseAboutDialog() - { - IsAboutDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void NavigateToLicensing() - { - IsAboutDialogOpen = false; - CloseDynamicDialog(); - IsHomePageVisible = true; - IsEditorVisible = false; - IsPdfViewerVisible = false; - Home.SelectNavSectionCommand.Execute("Licensing"); - } - - [RelayCommand] - public void NavigateToPluginsPage() - { - IsPluginsDialogOpen = false; - CloseDynamicDialog(); - IsHomePageVisible = true; - IsEditorVisible = false; - IsPdfViewerVisible = false; - Home.SelectNavSectionCommand.Execute("Plugins"); - } - - [RelayCommand] - public void NavigateToHelp(string? topicId = null) - { - IsAboutDialogOpen = false; - IsShortcutsHelpDialogOpen = false; - CloseDynamicDialog(); - IsHomePageVisible = true; - IsEditorVisible = false; - IsPdfViewerVisible = false; - Home.OpenHelpGuideCommand.Execute(topicId); - } - - [RelayCommand] - public void NavigateToSettings() - { - IsAboutDialogOpen = false; - IsShortcutsHelpDialogOpen = false; - CloseDynamicDialog(); - IsHomePageVisible = true; - IsEditorVisible = false; - IsPdfViewerVisible = false; - Home.SelectNavSectionCommand.Execute("Settings"); - } - - [RelayCommand] - public async System.Threading.Tasks.Task CopySupportEmail() - { - const string email = "codefrydev@gmail.com"; - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(email); - } - } - catch { } - ShowToast($"Copied support email: {email}", "EmailOutline"); - } - - [RelayCommand] - public async System.Threading.Tasks.Task OpenCompanyWebsite() - { - const string url = "https://codefrydev.in"; - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Launcher != null) - { - await desktop.MainWindow.Launcher.LaunchUriAsync(new Uri(url)); - } - else - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = url, UseShellExecute = true }); - } - } - catch - { - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(url); - } - } - catch { } - ShowToast($"Copied website link: {url}", "Web"); - return; - } - ShowToast("Opening codefrydev.in...", "Web"); - } - - [RelayCommand] - public async System.Threading.Tasks.Task OpenMicrosoftStore() - { - const string storeUrl = "https://apps.microsoft.com/detail/9P5GW2Q81B33"; - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Launcher != null) - { - await desktop.MainWindow.Launcher.LaunchUriAsync(new Uri(storeUrl)); - } - else - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = storeUrl, UseShellExecute = true }); - } - } - catch - { - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(storeUrl); - } - } - catch { } - ShowToast($"Copied Microsoft Store link: {storeUrl}", "Microsoft"); - return; - } - ShowToast("Opening Microsoft Store...", "Microsoft"); - } - - [RelayCommand] - public async System.Threading.Tasks.Task OpenGitHub() - { - const string url = "https://github.com/codefrydev"; - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Launcher != null) - { - await desktop.MainWindow.Launcher.LaunchUriAsync(new Uri(url)); - } - else - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = url, UseShellExecute = true }); - } - } - catch - { - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(url); - } - } - catch { } - ShowToast($"Copied GitHub link: {url}", "Github"); - return; - } - ShowToast("Opening GitHub...", "Github"); - } - - [RelayCommand] - public async System.Threading.Tasks.Task CopyDiagnostics() - { - var diagnostics = $"FryPDF Open-Source Edition v{AppVersion}\n" + - $"Publisher: Code Fry Dev (CN=7E83DE15-E15F-41B6-B068-989D9548D0BF)\n" + - $"Package Family Name: CodeFryDev.FryPDF_ntemjm2faw5zw\n" + - $"Store ID: 9P5GW2Q81B33 (https://apps.microsoft.com/detail/9P5GW2Q81B33)\n" + - $"MSA App ID: 4d091113-f7b6-4421-9318-220eb8b7234e\n" + - $"Website: https://codefrydev.in\n" + - $"GitHub: https://github.com/codefrydev\n" + - $"Support: codefrydev@gmail.com\n" + - $"OS: {System.Runtime.InteropServices.RuntimeInformation.OSDescription} ({System.Runtime.InteropServices.RuntimeInformation.OSArchitecture})\n" + - $"Runtime: .NET {Environment.Version}\n" + - $"Avalonia UI: 12.1.1\n" + - $"Engine: QuestPDF 2026.8.0 + SkiaSharp\n" + - $"Timestamp: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(diagnostics); - } - } - catch { } - ShowToast("System diagnostics copied to clipboard", "InformationOutline"); - } - - public void SelectNextPaletteCommand() - { - if (FilteredPaletteCommands.Count == 0) return; - SelectedPaletteIndex = (SelectedPaletteIndex + 1) % FilteredPaletteCommands.Count; - } - - public void SelectPreviousPaletteCommand() - { - if (FilteredPaletteCommands.Count == 0) return; - SelectedPaletteIndex = (SelectedPaletteIndex - 1 + FilteredPaletteCommands.Count) % FilteredPaletteCommands.Count; - } - - public void ExecuteSelectedPaletteCommand() - { - if (SelectedPaletteIndex >= 0 && SelectedPaletteIndex < FilteredPaletteCommands.Count) - { - var item = FilteredPaletteCommands[SelectedPaletteIndex]; - CloseCommandPalette(); - item.Action?.Invoke(); - } - } - - public void InitCommandPalette() - { - AllPaletteCommands.Clear(); - - // 1. File Operations - AllPaletteCommands.Add(new CommandPaletteItem { Title = "AI Assistant: Generate Canvas Elements", Subtitle = "Prompt local Ollama or cloud AI to generate document elements", Category = "AI Studio", IconKind = "AutoFixHigh", Shortcut = "⌘I", Action = () => OpenAiAssistantCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Open PDF to Read (PDF Reader Mode)", Subtitle = "Acrobat-style distraction-free reading, bookmarks & text search", Category = "File", IconKind = "BookOpenPageVariantOutline", Action = () => OpenPdfReaderCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Save Project", Subtitle = "Save editable FryPDF project archive (.frypdf)", Category = "File", IconKind = "ContentSaveOutline", Shortcut = "⌘S", Action = () => SaveProjectCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Open Project", Subtitle = "Open existing FryPDF project archive (.frypdf)", Category = "File", IconKind = "FolderOpenOutline", Shortcut = "⌘O", Action = () => OpenProjectCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Rename Document...", Subtitle = "Rename the current document file or title", Category = "File", IconKind = "PencilOutline", Shortcut = "F2", Action = () => RenameCurrentDocumentCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Duplicate Document", Subtitle = "Create a copy of current document on disk", Category = "File", IconKind = "ContentCopy", Action = () => DuplicateCurrentDocumentCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Reveal in File Manager / Finder", Subtitle = "Highlight document file in native OS file explorer", Category = "File", IconKind = "FolderOpenOutline", Action = () => RevealCurrentDocumentInFileManagerCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Copy Document Path", Subtitle = "Copy absolute file path to clipboard", Category = "File", IconKind = "ClipboardTextOutline", Action = () => _ = CopyCurrentDocumentPathCommand.ExecuteAsync(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Delete Document File...", Subtitle = "Permanently delete document from disk and recents", Category = "File", IconKind = "TrashCanOutline", Action = () => DeleteCurrentDocumentCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "New Document / Templates", Subtitle = "Browse and create from executive templates", Category = "File", IconKind = "FilePlusOutline", Shortcut = "⌘N", Action = () => OpenNewDocumentDialogCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Export Production PDF", Subtitle = "Compile document to high-resolution vector PDF", Category = "File", IconKind = "FilePdfBox", Shortcut = "⌘E", Action = () => ExportPdfCommand.Execute(null) }); - - // 2. Edit & History - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Undo Action", Subtitle = "Revert last canvas or page operation", Category = "Edit", IconKind = "Undo", Shortcut = "⌘Z", Action = () => UndoCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Redo Action", Subtitle = "Reapply reverted operation", Category = "Edit", IconKind = "Redo", Shortcut = "⌘Y", Action = () => RedoCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Copy Element", Subtitle = "Copy selected element to internal clipboard", Category = "Edit", IconKind = "ContentCopy", Shortcut = "⌘C", Action = () => CopyCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Cut Element", Subtitle = "Cut selected element to internal clipboard", Category = "Edit", IconKind = "ContentCut", Shortcut = "⌘X", Action = () => CutCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Paste Element", Subtitle = "Paste element from clipboard to current page", Category = "Edit", IconKind = "ContentPaste", Shortcut = "⌘V", Action = () => PasteCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Duplicate Element", Subtitle = "Clone selected element with offset", Category = "Edit", IconKind = "ContentDuplicate", Shortcut = "⌘D", Action = () => DuplicateCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Delete Selected Element", Subtitle = "Remove active element from canvas", Category = "Edit", IconKind = "DeleteOutline", Shortcut = "⌫", Action = () => Inspector.DeleteSelectedElementCommand.Execute(null) }); - - // 3. Insert Elements - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Text Paragraph", Subtitle = "Add multi-line editable rich text block", Category = "Insert", IconKind = "FormatColorText", Shortcut = "T", Action = () => AddTextElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Section Heading", Subtitle = "Add bold Georgia 22pt section title", Category = "Insert", IconKind = "FormatHeader1", Shortcut = "H", Action = () => AddHeadingElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Image Graphic", Subtitle = "Import PNG, JPEG, or WebP graphic from disk", Category = "Insert", IconKind = "ImageOutline", Shortcut = "⌘I", Action = () => AddImageElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Divider Line", Subtitle = "Add horizontal section divider line", Category = "Insert", IconKind = "VectorLine", Action = () => AddDividerElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Data Table", Subtitle = "Add customizable multi-column data grid", Category = "Insert", IconKind = "TableLarge", Action = () => AddTableElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Watermark Overlay", Subtitle = "Add confidential watermark stamp", Category = "Insert", IconKind = "Watermark", Action = () => AddWatermarkElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Math Equation", Subtitle = "Add vector LaTeX mathematical equation / formula", Category = "Equations", IconKind = "Sigma", Shortcut = "⌘M", Action = () => AddMathElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Open Math Equation Studio", Subtitle = "Visual formula builder, live preview, and symbol palettes", Category = "Equations", IconKind = "FunctionVariant", Action = () => OpenMathStudioCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Gaussian Integral", Subtitle = "∫ e^(-x²) dx = √π definite integral", Category = "Equations", IconKind = "Sigma", Action = () => AddMathElementCommand.Execute("gaussian_integral") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Quadratic Formula", Subtitle = "x = (-b ± √(b² - 4ac)) / 2a", Category = "Equations", IconKind = "Calculator", Action = () => AddMathElementCommand.Execute("quadratic_formula") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Schrödinger Equation", Subtitle = "iℏ ∂Ψ/∂t = ĤΨ quantum wave equation", Category = "Equations", IconKind = "Atom", Action = () => AddMathElementCommand.Execute("schrodinger_time_dependent") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Black-Scholes PDE", Subtitle = "∂V/∂t + ½σ²S² ∂²V/∂S² + rS ∂V/∂S - rV = 0 derivative pricing", Category = "Equations", IconKind = "ChartLineVariant", Action = () => AddMathElementCommand.Execute("black_scholes_pde") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Normal Distribution PDF", Subtitle = "Gaussian bell-curve probability density function", Category = "Equations", IconKind = "ChartBellCurve", Action = () => AddMathElementCommand.Execute("normal_distribution") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Euler's Identity", Subtitle = "e^(iπ) + 1 = 0 fundamental constants relation", Category = "Equations", IconKind = "Pi", Action = () => AddMathElementCommand.Execute("eulers_identity") }); - - // 4. Shapes & Stamps - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Rectangle Shape", Subtitle = "Add geometric rectangle with fill & stroke", Category = "Shapes", IconKind = "SquareOutline", Shortcut = "R", Action = () => AddShapeElementCommand.Execute("Rectangle") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Circle / Ellipse Shape", Subtitle = "Add circular vector shape", Category = "Shapes", IconKind = "CircleOutline", Action = () => AddShapeElementCommand.Execute("Circle") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert 5-Point Star", Subtitle = "Add decorative badge star", Category = "Shapes", IconKind = "StarOutline", Action = () => AddShapeElementCommand.Execute("Star5") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert 'APPROVED' Stamp", Subtitle = "Green legal certification stamp", Category = "Stamps", IconKind = "CheckCircleOutline", Action = () => AddStampElementCommand.Execute("Approved") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert 'CONFIDENTIAL' Stamp", Subtitle = "Red security classification stamp", Category = "Stamps", IconKind = "ShieldLockOutline", Action = () => AddStampElementCommand.Execute("Confidential") }); - - // 5. Data & Charts - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Bar / Column Chart", Subtitle = "LiveCharts2 vertical bar comparison chart", Category = "Charts", IconKind = "ChartBar", Action = () => AddChartElementCommand.Execute("BarColumn") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Donut Pie Chart", Subtitle = "LiveCharts2 proportional breakdown donut chart", Category = "Charts", IconKind = "ChartPie", Action = () => AddChartElementCommand.Execute("DonutPie") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Smooth Spline Chart", Subtitle = "LiveCharts2 fluid bezier curve chart", Category = "Charts", IconKind = "ChartLineSmooth", Action = () => AddChartElementCommand.Execute("SmoothLine") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Area Gradient Chart", Subtitle = "LiveCharts2 filled area volume chart", Category = "Charts", IconKind = "ChartAreaspline", Action = () => AddChartElementCommand.Execute("Area") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Candlestick Chart", Subtitle = "LiveCharts2 OHLC stock and crypto financial chart", Category = "Charts", IconKind = "ChartCandlestick", Action = () => AddChartElementCommand.Execute("Candlestick") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Radar / Spider Chart", Subtitle = "LiveCharts2 multi-metric evaluation polygon", Category = "Charts", IconKind = "Radar", Action = () => AddChartElementCommand.Execute("Radar") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert KPI Gauge Dial", Subtitle = "LiveCharts2 speedometer metric gauge", Category = "Charts", IconKind = "Gauge", Action = () => AddChartElementCommand.Execute("GaugeProgress") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Waterfall Chart", Subtitle = "LiveCharts2 financial revenue-to-profit variance bridge", Category = "Charts", IconKind = "ChartWaterfall", Action = () => AddChartElementCommand.Execute("Waterfall") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Open Data Studio & Connector", Subtitle = "Ingest data from Excel (.xlsx), CSV, REST API, or Clipboard", Category = "Data", IconKind = "DatabaseArrowDownOutline", Action = () => OpenDataStudioCommand.Execute("NewChart") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Batch Mail Merge & Mass PDF", Subtitle = "Generate 100s of personalized PDFs (payslips, invoices, certificates) in 1-click", Category = "Data", IconKind = "DatabaseArrowDownOutline", Action = () => OpenBatchGenerationCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Mass Payslips Generator", Subtitle = "Generate Employee Monthly Payslips from salary data", Category = "Data", IconKind = "CashMultiple", Action = () => OpenBatchGenerationCommand.Execute("payslip") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Import Excel (.xlsx) to Chart", Subtitle = "Generate LiveCharts2 visualization from Excel workbook", Category = "Data", IconKind = "FileExcelOutline", Action = () => _ = ImportExcelToChartAsync() }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Import Excel (.xlsx) to Table", Subtitle = "Generate formatted Table from Excel workbook", Category = "Data", IconKind = "TableLarge", Action = () => _ = ImportExcelToTableAsync() }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Fetch REST API / JSON to Chart", Subtitle = "Live HTTP JSON endpoint ingestion for charts", Category = "Data", IconKind = "Api", Action = () => FetchRestApiToChartCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Fetch REST API / JSON to Table", Subtitle = "Live HTTP JSON endpoint ingestion for tables", Category = "Data", IconKind = "CodeJson", Action = () => FetchRestApiToTableCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Convert Table to Chart", Subtitle = "Transform selected Table into interactive LiveCharts2 chart", Category = "Data", IconKind = "ChartBoxOutline", Action = () => Inspector.ConvertTableToChartCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Convert Chart to Table", Subtitle = "Transform selected Chart into editable structured Table", Category = "Data", IconKind = "Table", Action = () => Inspector.ConvertChartToTableCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Vector QR Code", Subtitle = "Dynamic URL, Wi-Fi, or vCard QR generator", Category = "Data", IconKind = "Qrcode", Action = () => AddQrCodeElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Barcode", Subtitle = "Code128 / EAN / PDF417 optical barcode", Category = "Data", IconKind = "Barcode", Action = () => AddBarcodeElementCommand.Execute(null) }); - - // 6. Security & Markup - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Sticky Review Note", Subtitle = "Collaborative annotation note", Category = "Markup", IconKind = "NoteTextOutline", Shortcut = "N", Action = () => AddStickyNoteElementCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Highlighter Stroke", Subtitle = "Yellow semi-transparent highlight marker", Category = "Markup", IconKind = "Marker", Shortcut = "H", Action = () => AddInkElementCommand.Execute(true) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Freehand Ink Drawing", Subtitle = "Freehand pen stroke vector element", Category = "Markup", IconKind = "DrawPen", Shortcut = "D", Action = () => AddInkElementCommand.Execute(false) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Insert Redaction Blackout Box", Subtitle = "Permanent FOIA / GDPR privileged blackout", Category = "Security", IconKind = "EyeOffOutline", Action = () => AddRedactionElementCommand.Execute("[REDACTED]") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Search & Redact Pattern", Subtitle = "Auto-redact text occurrences on current page", Category = "Security", IconKind = "DatabaseSearchOutline", Action = () => OpenSearchRedactDialogCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Burn In All Redactions", Subtitle = "Permanently commit solid blackouts to PDF", Category = "Security", IconKind = "ShieldCheckOutline", Action = () => BurnInAllRedactionsCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Document Security & Passwords", Subtitle = "Configure password protection and permissions", Category = "Security", IconKind = "ShieldLockOutline", Action = () => OpenSecurityDialogCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Sanitize Document", Subtitle = "Scrub author metadata and internal review notes", Category = "Security", IconKind = "ShieldCheck", Action = () => SanitizeDocumentCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Apply Bates Numbering", Subtitle = "Sequential legal discovery numbering (CONF-BATES-000001)", Category = "Security", IconKind = "Numeric", Action = () => ApplyBatesNumberingCommand.Execute(null) }); - - // 7. Fill & Sign Studio - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Open Signature Studio", Subtitle = "Draw, type cursive calligraphy, or upload digital signature", Category = "Sign", IconKind = "Draw", Action = () => OpenSignatureStudioCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Stamp Today's Date", Subtitle = "Insert dynamic verified date badge", Category = "Sign", IconKind = "CalendarClockOutline", Action = () => AddDateStampCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Stamp Signer Initials", Subtitle = "Insert circular monogram initial stamp", Category = "Sign", IconKind = "AccountOutline", Action = () => AddInitialsBadgeCommand.Execute("JD") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Stamp Checkmark (✓)", Subtitle = "Insert green verification checkmark", Category = "Sign", IconKind = "CheckBold", Action = () => AddCheckmarkBadgeCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Stamp Cross (✕)", Subtitle = "Insert red rejection cross mark", Category = "Sign", IconKind = "CloseThick", Action = () => AddCrossBadgeCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Custom Stamp Creator", Subtitle = "Create timestamped custom legal certification stamp", Category = "Stamps", IconKind = "Stamp", Action = () => OpenCustomStampDialogCommand.Execute(null) }); - - // 8. Watermarks & Headers/Footers - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Header & Footer Studio", Subtitle = "Configure multi-zone header/footer with dynamic macros", Category = "Organize", IconKind = "PageLayoutHeaderFooter", Action = () => OpenHeaderFooterDialogCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Watermark Manager", Subtitle = "Apply CONFIDENTIAL/DRAFT watermark across all pages", Category = "Organize", IconKind = "Watermark", Action = () => OpenWatermarkManagerCommand.Execute(null) }); - - // 9. Preflight Audit & Health Diagnostics - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Preflight Health Check & Audit", Subtitle = "Inspect PDF compliance, fonts, broken links, accessibility", Category = "Audit", IconKind = "FileCheckOutline", Action = () => OpenPreflightDialogCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Export Comments Summary", Subtitle = "Export all review notes to Markdown document", Category = "Audit", IconKind = "CommentTextMultipleOutline", Action = () => ExportCommentsSummaryCommand.Execute(null) }); - - // 10. Pages & Navigation - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Add Blank Page", Subtitle = "Insert new page at end of document", Category = "Pages", IconKind = "FilePlusOutline", Shortcut = "⌘⇧N", Action = () => AddPageCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Duplicate Current Page", Subtitle = "Clone active page with all elements", Category = "Pages", IconKind = "FileMultipleOutline", Shortcut = "⌘⇧D", Action = () => DuplicateCurrentPageCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Rotate Page 90° Clockwise", Subtitle = "Rotate current page orientation", Category = "Pages", IconKind = "RotateRight", Shortcut = "⌘⇧R", Action = () => RotateCurrentPageCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Delete Current Page", Subtitle = "Remove active page from document", Category = "Pages", IconKind = "DeleteOutline", Shortcut = "⌘⇧⌫", Action = () => DeleteCurrentPageCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Next Page", Subtitle = "Go to next document page", Category = "Navigation", IconKind = "ChevronRight", Shortcut = "PgDn", Action = () => NextPageCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Previous Page", Subtitle = "Go to previous document page", Category = "Navigation", IconKind = "ChevronLeft", Shortcut = "PgUp", Action = () => PreviousPageCommand.Execute(null) }); - - // 11. View, Workspace Panels & Guides - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Toggle Ribbon Toolbar", Subtitle = "Collapse or expand top ribbon tools panel", Category = "View", IconKind = "ViewAgendaOutline", Shortcut = "⌘F1", Action = () => ToggleRibbonCollapseCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Toggle Pages Sidebar", Subtitle = "Collapse or expand left thumbnails & outline sidebar", Category = "View", IconKind = "DockLeft", Shortcut = "⌘B", Action = () => ToggleLeftSidebarCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Toggle Properties Inspector", Subtitle = "Collapse or expand right formatting & properties panel", Category = "View", IconKind = "DockRight", Shortcut = "⌘⇧P", Action = () => ToggleInspectorCollapseCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Expand All Panels", Subtitle = "Restore all ribbon and sidebar panels", Category = "View", IconKind = "ViewQuiltOutline", Action = () => ExpandAllPanelsCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Toggle Canvas Grid", Subtitle = "Show/hide alignment grid dots", Category = "View", IconKind = "Grid", Action = () => ToggleGridCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Toggle Snap to Grid", Subtitle = "Snap elements to precise 20pt intervals", Category = "View", IconKind = "Magnet", Action = () => ToggleSnapToGridCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Zoom In", Subtitle = "Increase canvas scale by 10%", Category = "View", IconKind = "MagnifyPlusOutline", Shortcut = "⌘+", Action = () => ZoomInCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Zoom Out", Subtitle = "Decrease canvas scale by 10%", Category = "View", IconKind = "MagnifyMinusOutline", Shortcut = "⌘-", Action = () => ZoomOutCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Reset Zoom (100%)", Subtitle = "Reset canvas view to 1:1 scale", Category = "View", IconKind = "Magnify", Shortcut = "⌘0", Action = () => ResetZoomCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Fit to Width", Subtitle = "Scale page to fill viewport width", Category = "View", IconKind = "ArrowExpandHorizontal", Shortcut = "⌘1", Action = () => FitToWidthCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Fit to Page", Subtitle = "Scale page to view whole sheet", Category = "View", IconKind = "FitToPageOutline", Shortcut = "⌘9", Action = () => FitToPageCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Toggle Dark / Light Theme", Subtitle = "Switch between Dark and Light studio theme variants", Category = "Theme", IconKind = "ThemeLightDark", Shortcut = "⌘⇧T", Action = () => ToggleThemeCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Switch to Dark Theme", Subtitle = "Activate deep dark studio palette for low-light environments", Category = "Theme", IconKind = "WeatherNight", Action = () => SetDarkThemeCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Switch to Light Theme", Subtitle = "Activate crisp clean light studio palette", Category = "Theme", IconKind = "WeatherSunny", Action = () => SetLightThemeCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Use System Theme", Subtitle = "Follow operating system dark/light theme preference", Category = "Theme", IconKind = "Laptop", Action = () => SetSystemThemeCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Help & User Guides", Subtitle = "Open comprehensive guides for all 32 tools, editor & workflows", Category = "Help", IconKind = "HelpCircleOutline", Action = () => NavigateToHelpCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Help: 32 PDF Tools Guide", Subtitle = "Step-by-step guides, formats & pro tips for every PDF tool", Category = "Help", IconKind = "Tools", Action = () => NavigateToHelpCommand.Execute("tool-merge") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Help: Live Document Editor Guide", Subtitle = "Guide to canvas tools, typography, math LaTeX & vector layers", Category = "Help", IconKind = "Draw", Action = () => NavigateToHelpCommand.Execute("editor-canvas-basics") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Help: Batch PDF Generation Guide", Subtitle = "Guide to CSV/JSON Data Studio and mass PDF generation", Category = "Help", IconKind = "DatabaseArrowRightOutline", Action = () => NavigateToHelpCommand.Execute("automation-data-studio") }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Plugins & Extensions Studio", Subtitle = "Full-screen plugin store, installed extensions, and configuration studio", Category = "Preferences", IconKind = "PuzzleOutline", Action = () => NavigateToPluginsPageCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Plugins & Extensions (Quick Dialog)", Subtitle = "Quick popup to toggle plugins, profiles, and load assemblies", Category = "Help", IconKind = "PuzzleOutline", Action = () => OpenPluginsDialogCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "About FryPDF", Subtitle = "View app version, system info, open-source credits, and support", Category = "Help", IconKind = "InformationOutline", Action = () => OpenAboutDialogCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Keyboard Shortcuts Reference", Subtitle = "Open keyboard cheatsheet dialog", Category = "Help", IconKind = "KeyboardOutline", Shortcut = "F1", Action = () => OpenShortcutsHelpCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Open Source Licenses & Third-Party Tools", Subtitle = "View all 12 libraries, licenses, maintainers & attribution text", Category = "Help", IconKind = "CertificateOutline", Action = () => NavigateToLicensingCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Copy Open Source Attributions", Subtitle = "Copy full open source license notices & copyright text", Category = "Help", IconKind = "ContentCopy", Action = () => { _ = Home.CopyFullAttributionsCommand.ExecuteAsync(null); ShowToast("Copied open source license attributions", "CertificateOutline"); } }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Contact Support (codefrydev@gmail.com)", Subtitle = "Copy developer support email to clipboard", Category = "Help", IconKind = "EmailOutline", Action = () => CopySupportEmailCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Visit CodeFryDev Website", Subtitle = "Open official codefrydev.in developer portal", Category = "Help", IconKind = "Web", Action = () => OpenCompanyWebsiteCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "GitHub Repository", Subtitle = "View open-source repository and star the project", Category = "Help", IconKind = "Github", Action = () => OpenGitHubCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Microsoft Store Page", Subtitle = "View FryPDF on Microsoft Store (9P5GW2Q81B33)", Category = "Help", IconKind = "Microsoft", Action = () => OpenMicrosoftStoreCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Copy System Diagnostics", Subtitle = "Copy OS, framework, store identity, and app version report", Category = "Help", IconKind = "BugOutline", Action = () => CopyDiagnosticsCommand.Execute(null) }); - AllPaletteCommands.Add(new CommandPaletteItem { Title = "Settings & UI Preferences", Subtitle = "Configure notification placement, snackbar style, themes, and workspace defaults", Category = "Preferences", IconKind = "CogOutline", Shortcut = "⌘,", Action = () => NavigateToSettingsCommand.Execute(null) }); - - // 12. Dynamic Plugin Tools - var tools = _toolRegistry?.GetAllTools() ?? (IReadOnlyList)Array.Empty(); - foreach (var tool in tools) - { - var targetTool = tool; - AllPaletteCommands.Add(new CommandPaletteItem - { - Title = $"Tool: {targetTool.Name}", - Subtitle = targetTool.Description, - Category = "PDF Tools", - IconKind = targetTool.IconKind, - Action = () => OpenTool(targetTool.Id) - }); - } - - // 13. Dynamic Plugin Ribbon Actions - var actions = _ribbonRegistry?.GetAllActions() ?? (IReadOnlyList)Array.Empty(); - foreach (var action in actions) - { - var targetAction = action; - AllPaletteCommands.Add(new CommandPaletteItem - { - Title = targetAction.Label, - Subtitle = targetAction.Tooltip ?? $"Contributed by plugin ({targetAction.TabId} tab)", - Category = "Plugins", - IconKind = targetAction.IconKind, - Action = () => ExecuteRibbonAction(targetAction) - }); - } - - // 14. Dynamic Plugin Contributed Commands - var dynamicCommands = _commandPaletteRegistry?.GetAllCommands() ?? (IReadOnlyList)Array.Empty(); - foreach (var cmd in dynamicCommands) - { - var targetCmd = cmd; - AllPaletteCommands.Add(new CommandPaletteItem - { - Title = targetCmd.Title, - Subtitle = targetCmd.Subtitle, - Category = targetCmd.Category, - IconKind = targetCmd.IconKind, - Shortcut = targetCmd.Shortcut ?? "", - Action = () => targetCmd.Action?.Invoke(_pluginHost?.Context ?? (IServiceProvider)this) - }); - } - - FilterPaletteCommands(""); - } - - public void FilterPaletteCommands(string query) - { - FilteredPaletteCommands.Clear(); - var q = query?.Trim() ?? ""; - - var matching = string.IsNullOrEmpty(q) - ? AllPaletteCommands - : AllPaletteCommands.Where(c => - c.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || - c.Subtitle.Contains(q, StringComparison.OrdinalIgnoreCase) || - c.Category.Contains(q, StringComparison.OrdinalIgnoreCase) || - c.Shortcut.Contains(q, StringComparison.OrdinalIgnoreCase)); - - foreach (var cmd in matching) - { - FilteredPaletteCommands.Add(cmd); - } - - SelectedPaletteIndex = FilteredPaletteCommands.Count > 0 ? 0 : -1; - } -} diff --git a/src/PdfEditorApp/ViewModels/MainViewModel.Persistence.cs b/src/PdfEditorApp/ViewModels/MainViewModel.Persistence.cs deleted file mode 100644 index 806bf3a..0000000 --- a/src/PdfEditorApp/ViewModels/MainViewModel.Persistence.cs +++ /dev/null @@ -1,444 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels; - -public partial class MainViewModel -{ - // --- PERSISTENCE & EXPORT STATE --- - - [ObservableProperty] - private bool _isNewDocumentDialogOpen; - - [ObservableProperty] - private bool _isExportSuccessDialogOpen; - - [ObservableProperty] - private string _lastExportedFilePath = ""; - - // --- EXPORT & PERSISTENCE COMMANDS --- - - [RelayCommand] - public async Task ExportPdfAsync() - { - try - { - UpdateStatus("Generating PDF with QuestPDF engine..."); - - string defaultFileName = Path.ChangeExtension(DocumentTitle, ".pdf"); - if (string.IsNullOrEmpty(defaultFileName)) defaultFileName = "Document.pdf"; - - string exportPath = ""; - - if (StorageProvider != null) - { - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Export PDF Document", - DefaultExtension = "pdf", - SuggestedFileName = defaultFileName, - FileTypeChoices = new[] - { - new FilePickerFileType("PDF Documents (*.pdf)") - { - Patterns = new[] { "*.pdf" } - } - } - }); - - if (file != null) - { - exportPath = file.Path.LocalPath; - } - else - { - UpdateStatus("Export cancelled."); - return; - } - } - else - { - exportPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), defaultFileName); - } - - var docModel = ToDocumentModel(); - IsBusy = true; - try - { - var progress = new Progress(p => UpdateStatus($"Generating PDF... {p:F0}%")); - await _exportService.ExportToFileAsync(docModel, exportPath, progress); - } - finally - { - IsBusy = false; - } - - LastExportedFilePath = exportPath; - IsExportSuccessDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.exportsuccess"); - ShowToast($"Exported PDF: {Path.GetFileName(exportPath)}", "ExportVariant"); - } - catch (Exception ex) - { - IsBusy = false; - ShowToast($"Export error: {ex.Message}", "AlertCircleOutline"); - } - } - - [RelayCommand] - public async Task ExportDocumentAsAsync(string exporterId) - { - try - { - var exporterRegistry = _pluginHost?.Context.GetService(); - var exporter = exporterRegistry?.GetExporter(exporterId); - if (exporter == null) - { - await ExportPdfAsync(); - return; - } - - UpdateStatus($"Exporting document using {exporter.DisplayName}..."); - string defaultFileName = Path.ChangeExtension(DocumentTitle, exporter.DefaultExtension); - string exportPath = ""; - - if (StorageProvider != null) - { - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = $"Export {exporter.DisplayName}", - DefaultExtension = exporter.DefaultExtension.TrimStart('.'), - SuggestedFileName = defaultFileName, - FileTypeChoices = new[] - { - new FilePickerFileType(exporter.FileFilterDescription) - { - Patterns = new[] { $"*{exporter.DefaultExtension}" } - } - } - }); - - if (file != null) - { - exportPath = file.Path.LocalPath; - } - else - { - UpdateStatus("Export cancelled."); - return; - } - } - else - { - exportPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), defaultFileName); - } - - var docModel = ToDocumentModel(); - var bytes = await exporter.ExportAsync(docModel, new DocumentExportOptions { OutputPath = exportPath }); - await File.WriteAllBytesAsync(exportPath, bytes); - - LastExportedFilePath = exportPath; - IsExportSuccessDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.exportsuccess"); - UpdateStatus($"Exported to {Path.GetFileName(exportPath)} successfully!"); - ShowToast($"Exported {Path.GetFileName(exportPath)} successfully!", "CheckCircleOutline"); - } - catch (Exception ex) - { - UpdateStatus($"Export failed: {ex.Message}"); - ShowToast($"Export failed: {ex.Message}", ToastNotificationType.Danger, "AlertCircleOutline"); - } - } - - [RelayCommand] - public async Task SaveProjectAsync() - { - try - { - string savePath = ""; - string defaultFileName = Path.ChangeExtension(DocumentTitle, ".frypdf"); - - if (StorageProvider != null) - { - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Save FryPDF Project", - DefaultExtension = "frypdf", - SuggestedFileName = defaultFileName, - FileTypeChoices = new[] - { - new FilePickerFileType("FryPDF Project (*.frypdf)") - { - Patterns = new[] { "*.frypdf", "*.pdfproj", "*.json" } - } - } - }); - - if (file != null) - { - savePath = file.Path.LocalPath; - } - else - { - UpdateStatus("Save cancelled."); - return; - } - } - else - { - savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), defaultFileName); - } - - var docModel = ToDocumentModel(); - await _persistenceService.SaveProjectAsync(docModel, savePath); - CurrentFilePath = savePath; - // Record in recent documents - _recentService.Add(new RecentDocumentItem - { - FilePath = savePath, - Title = DocumentTitle, - LastOpened = DateTime.UtcNow - }); - Home.RefreshRecent(); - ShowToast($"Project saved to {Path.GetFileName(savePath)}", "ContentSaveOutline"); - } - catch (Exception ex) - { - ShowToast($"Save error: {ex.Message}", "AlertCircleOutline"); - } - } - - [RelayCommand] - public async Task OpenProjectAsync() - { - // When in editor, use the file picker directly - await OpenProjectAndEnterEditorAsync(); - } - - // --- TEMPLATE SELECTION & GALLERY DIALOG STATE --- - - [ObservableProperty] - private string _templateSearchQuery = ""; - - [ObservableProperty] - private string _selectedTemplateCategory = "All"; - - [RelayCommand] - public void SetTemplateCategory(string? category) - { - SelectedTemplateCategory = string.IsNullOrWhiteSpace(category) ? "All" : category; - } - - [RelayCommand] - public void ClearTemplateSearch() - { - TemplateSearchQuery = ""; - } - - partial void OnTemplateSearchQueryChanged(string value) - { - NotifyTemplateVisibilities(); - } - - partial void OnSelectedTemplateCategoryChanged(string value) - { - NotifyTemplateVisibilities(); - } - - private void NotifyTemplateVisibilities() - { - OnPropertyChanged(nameof(IsAnnualReportTemplateVisible)); - OnPropertyChanged(nameof(IsInvoiceTemplateVisible)); - OnPropertyChanged(nameof(IsResumeTemplateVisible)); - OnPropertyChanged(nameof(IsAcademicPaperTemplateVisible)); - OnPropertyChanged(nameof(IsCertificateTemplateVisible)); - OnPropertyChanged(nameof(IsBlankTemplateVisible)); - OnPropertyChanged(nameof(HasNoMatchingTemplates)); - } - - private bool CheckTemplateMatch(string templateId, string category, string name, string description) - { - if (SelectedTemplateCategory != "All" && !string.Equals(SelectedTemplateCategory, category, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (string.IsNullOrWhiteSpace(TemplateSearchQuery)) - { - return true; - } - - string q = TemplateSearchQuery.Trim(); - return name.Contains(q, StringComparison.OrdinalIgnoreCase) - || description.Contains(q, StringComparison.OrdinalIgnoreCase) - || category.Contains(q, StringComparison.OrdinalIgnoreCase) - || templateId.Contains(q, StringComparison.OrdinalIgnoreCase); - } - - public bool IsAnnualReportTemplateVisible => CheckTemplateMatch("annualreport", "Corporate", "Annual Corporate Report", "Executive summary, financial metrics, and chart"); - public bool IsInvoiceTemplateVisible => CheckTemplateMatch("invoice", "Finance", "Modern Service Invoice", "Itemized billing table and payment terms"); - public bool IsResumeTemplateVisible => CheckTemplateMatch("resume", "Career", "Executive Resume / CV", "Complete CV with QR code, competencies, and verified metrics"); - public bool IsAcademicPaperTemplateVisible => CheckTemplateMatch("academic", "Academic", "Academic Research Paper", "2-column formatted research paper layout"); - public bool IsCertificateTemplateVisible => CheckTemplateMatch("certificate", "Certificates", "Certificate of Achievement", "Award of excellence and official recognition credential"); - public bool IsBlankTemplateVisible => CheckTemplateMatch("blank", "General", "Blank Canvas", "Start fresh with a clean customizable canvas"); - - public bool HasNoMatchingTemplates => !IsAnnualReportTemplateVisible - && !IsInvoiceTemplateVisible - && !IsResumeTemplateVisible - && !IsAcademicPaperTemplateVisible - && !IsCertificateTemplateVisible - && !IsBlankTemplateVisible; - - // --- TEMPLATE SELECTION DIALOG COMMANDS --- - - [RelayCommand] - public void OpenNewDocumentDialog() - { - TemplateSearchQuery = ""; - SelectedTemplateCategory = "All"; - IsNewDocumentDialogOpen = true; - OpenRegisteredDialog("frypdf.dialog.newdocument"); - } - - [RelayCommand] - public void CloseNewDocumentDialog() - { - IsNewDocumentDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void CloseExportSuccessDialog() - { - IsExportSuccessDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void CloseExportDialog() - { - IsExportSuccessDialogOpen = false; - CloseDynamicDialog(); - } - - [RelayCommand] - public void SelectTemplate(string? templateName) - { - CloseDynamicDialog(); - OpenEditorWithTemplate(templateName); - } - - [RelayCommand] - public void CreateNewFromTemplate(string? templateName) - { - CloseDynamicDialog(); - OpenEditorWithTemplate(templateName); - } - - // --- MODEL CONVERSION & SERIALIZATION --- - - public PdfDocumentModel ToDocumentModel() - { - var doc = new PdfDocumentModel - { - Title = DocumentTitle, - Author = DocumentAuthor, - Subject = DocumentSubject, - Keywords = DocumentKeywords, - Creator = string.IsNullOrWhiteSpace(DocumentCreator) ? "FryPDF" : DocumentCreator, - Producer = string.IsNullOrWhiteSpace(DocumentProducer) ? "codefrydev.in" : DocumentProducer, - SecuritySettings = SecuritySettings.Clone() - }; - - foreach (var pageVm in Pages) - { - doc.Pages.Add(pageVm.ToModel()); - } - - return doc; - } - - public void LoadFromDocumentModel(PdfDocumentModel model) - { - ApplyDocumentMetadata(model); - - // The previous document's undo/redo history is meaningless once its pages are gone — - // undoing after switching documents must never resurrect a replaced document's state. - // This also releases any deleted chart/image bitmaps still pinned by that history. - UndoRedo.Clear(); - - Pages.Clear(); - foreach (var pageModel in model.Pages) - { - var pageVm = new PageViewModel(); - pageVm.LoadFromModel(pageModel); - pageVm.SelectionChanged += OnElementSelectionChanged; - Pages.Add(pageVm); - } - - FinishLoadingDocument(); - } - - /// - /// Same as but yields back to the dispatcher every - /// few pages, so opening a large document doesn't freeze the UI thread for the whole - /// build — used by every user-facing "open a document" path. - /// - public async Task LoadFromDocumentModelAsync(PdfDocumentModel model) - { - ApplyDocumentMetadata(model); - - // See LoadFromDocumentModel — the outgoing document's undo/redo history must not - // survive the switch. - UndoRedo.Clear(); - - Pages.Clear(); - int i = 0; - foreach (var pageModel in model.Pages) - { - var pageVm = new PageViewModel(); - pageVm.LoadFromModel(pageModel); - pageVm.SelectionChanged += OnElementSelectionChanged; - Pages.Add(pageVm); - - if (++i % 8 == 0) - { - await Task.Yield(); - } - } - - FinishLoadingDocument(); - } - - private void ApplyDocumentMetadata(PdfDocumentModel model) - { - DocumentTitle = model.Title; - DocumentAuthor = model.Author; - DocumentSubject = model.Subject; - DocumentKeywords = model.Keywords ?? ""; - DocumentCreator = string.IsNullOrWhiteSpace(model.Creator) ? "FryPDF" : model.Creator; - DocumentProducer = string.IsNullOrWhiteSpace(model.Producer) ? "codefrydev.in" : model.Producer; - SecuritySettings = model.SecuritySettings?.Clone() ?? new PdfSecuritySettings(); - } - - private void FinishLoadingDocument() - { - if (Pages.Count > 0) - { - SelectPage(Pages[0]); - } - - RefreshOutline(); - RefreshComments(); - UpdateStatus($"Loaded document: {DocumentTitle}"); - IsBusy = false; - } -} diff --git a/src/PdfEditorApp/ViewModels/MainViewModel.cs b/src/PdfEditorApp/ViewModels/MainViewModel.cs deleted file mode 100644 index 0abb8d2..0000000 --- a/src/PdfEditorApp/ViewModels/MainViewModel.cs +++ /dev/null @@ -1,2277 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.Core.Data; -using PdfEditorApp.ViewModels.DataStudio; -using PdfEditorApp.ViewModels.BatchGeneration; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Plugins; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Platform.Storage; -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Canvas; -using PdfEditorApp.Services.Ribbon; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels; - -public partial class MainViewModel : ViewModelBase -{ - private readonly IPdfExportService _exportService; - private readonly ITemplateService _templateService; - private readonly IProjectPersistenceService _persistenceService; - private readonly IDocumentAuditService _auditService; - private readonly ISignatureService _signatureService; - private readonly ISmartPlacementService _placementService; - private readonly IRecentDocumentsService _recentService; - private readonly IPageOrganizerService _pageOrganizerService; - private readonly IPdfDocumentOperationsService _pdfOperationsService; - private readonly IPdfToolRegistry _toolRegistry; - private readonly ICanvasElementService _elementService; - private readonly IRibbonRegistry _ribbonRegistry; - private readonly PdfEditorApp.Core.Plugins.Descriptors.ISidebarRegistry? _sidebarRegistry; - private readonly PdfEditorApp.Core.Plugins.Descriptors.IDialogRegistry? _dialogRegistry; - private readonly PdfEditorApp.Core.Plugins.Descriptors.IOverlayRegistry? _overlayRegistry; - - public IRibbonRegistry RibbonRegistry => _ribbonRegistry; - public PdfEditorApp.Core.Plugins.Descriptors.ISidebarRegistry? SidebarRegistry => _sidebarRegistry; - public PdfEditorApp.Core.Plugins.Descriptors.IDialogRegistry? DialogRegistry => _dialogRegistry; - public PdfEditorApp.Core.Plugins.Descriptors.IOverlayRegistry? OverlayRegistry => _overlayRegistry; - public ObservableCollection ActiveOverlays => (_overlayRegistry as PdfEditorApp.Services.Overlays.OverlayRegistry)?.ActiveOverlays ?? _fallbackActiveOverlays; - private readonly ObservableCollection _fallbackActiveOverlays = new(); - - public ObservableCollection PluginRibbonActions { get; } = new(); - public ObservableCollection DynamicRibbonTabs { get; } = new(); - public ObservableCollection DynamicRibbonGroups { get; } = new(); - public ObservableCollection DynamicGroupsForActiveTab { get; } = new(); - public ObservableCollection DynamicSidebarTabs { get; } = new(); - - [ObservableProperty] - private string? _activeDynamicRibbonTabId; - - [ObservableProperty] - private string? _activeDynamicSidebarTabId; - - [ObservableProperty] - private object? _activeDynamicSidebarView; - - [ObservableProperty] - private object? _activeDynamicDialog; - - [ObservableProperty] - private bool _isDynamicDialogOpen; - - public ISmartPlacementService SmartPlacement => _placementService; - public IPageOrganizerService PageOrganizer => _pageOrganizerService; - public IPdfDocumentOperationsService PdfOperations => _pdfOperationsService; - - // PDF Tool Studio Subsystems - public PdfToolRunnerViewModel ToolRunner { get; } - - public WorkflowBuilderViewModel WorkflowBuilder { get; } - public PdfViewerViewModel PdfViewer { get; } - public FryPdfViewer.FryPdfViewerViewModel FryPdfViewer { get; } - public DataStudioViewModel DataStudio { get; } - public BatchGenerationViewModel BatchGeneration { get; } - public AiAssistantViewModel AiAssistant { get; } - - // --- HOME / EDITOR / VIEWER VIEW-SWITCHING --- - - [ObservableProperty] - private bool _isHomePageVisible = true; - - [ObservableProperty] - private bool _isEditorVisible = false; - - [ObservableProperty] - private bool _isPdfViewerVisible = false; - - [ObservableProperty] - private bool _isFryPdfViewerVisible = false; - - [ObservableProperty] - private WindowState _mainWindowState = WindowState.Normal; - - /// - /// Dynamic window title that accurately reflects the active application context: - /// Home dashboard ("FryPDF - Privacy-First PDF Studio"), specific tool ("Merge PDF - FryPDF"), - /// PDF Reader ("Document.pdf - FryPDF"), or Editor Studio ("Document.pdf - FryPDF"). - /// - public string WindowTitle - { - get - { - if (IsHomePageVisible) - { - if (Home != null && Home.IsToolPageActive && Home.ActiveToolCard != null) - { - return $"{Home.ActiveToolCard.Name} - FryPDF"; - } - if (Home != null && Home.IsToolPageActive && Home.ActiveToolViewModel != null) - { - return $"{Home.ActiveToolViewModel.Tool.Name} - FryPDF"; - } - if (Home != null && Home.SelectedNavSection == HomeNavSection.Licensing) - { - return "Licenses & Third-Party Tools - FryPDF"; - } - return "FryPDF - Privacy-First PDF Studio"; - } - else if (IsFryPdfViewerVisible) - { - if (FryPdfViewer != null && !string.IsNullOrWhiteSpace(FryPdfViewer.DocumentTitle)) - { - return $"{FryPdfViewer.DocumentTitle} - Interactive Reader - FryPDF"; - } - return "Interactive Reader - FryPDF"; - } - else if (IsPdfViewerVisible) - { - if (PdfViewer != null && !string.IsNullOrWhiteSpace(PdfViewer.DocumentTitle) && PdfViewer.DocumentTitle != "Document.pdf") - { - return $"{PdfViewer.DocumentTitle} - FryPDF"; - } - return "PDF Reader - FryPDF"; - } - else if (IsEditorVisible) - { - if (!string.IsNullOrWhiteSpace(DocumentTitle)) - { - return $"{DocumentTitle} - FryPDF"; - } - return "FryPDF Editor"; - } - return "FryPDF"; - } - } - - partial void OnIsHomePageVisibleChanged(bool value) => OnPropertyChanged(nameof(WindowTitle)); - partial void OnIsEditorVisibleChanged(bool value) => OnPropertyChanged(nameof(WindowTitle)); - partial void OnIsPdfViewerVisibleChanged(bool value) => OnPropertyChanged(nameof(WindowTitle)); - partial void OnIsFryPdfViewerVisibleChanged(bool value) => OnPropertyChanged(nameof(WindowTitle)); - partial void OnDocumentTitleChanged(string value) => OnPropertyChanged(nameof(WindowTitle)); - - /// ViewModel for the Home / Start Screen. - public HomeViewModel Home { get; } - - private List _clipboardElements = new(); - private CancellationTokenSource? _toastCts; - - // --- CORE OBSERVABLE PROPERTIES --- - - [ObservableProperty] - private string _documentTitle = "Document_2026.pdf"; - - [ObservableProperty] - private string _currentFilePath = ""; - - [ObservableProperty] - private string _documentAuthor = "CodeFryDev"; - - [ObservableProperty] - private string _documentSubject = "PDF Document"; - - [ObservableProperty] - private string _documentKeywords = ""; - - [ObservableProperty] - private string _documentCreator = "FryPDF"; - - [ObservableProperty] - private string _documentProducer = "codefrydev.in"; - - [ObservableProperty] - private RibbonTabKind _activeRibbonTab = RibbonTabKind.Edit; - - [ObservableProperty] - private SidebarTabKind _activeSidebarTab = SidebarTabKind.Thumbnails; - - // Workspace Panel Collapse & Expand States - [ObservableProperty] - private bool _isRibbonCollapsed; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsLeftSidebarResizable))] - private bool _isLeftSidebarCollapsed; - - [ObservableProperty] - private GridLength _leftSidebarGridLength = new(340, GridUnitType.Pixel); - - private double _savedExpandedLeftSidebarWidth = 340; - - public bool IsLeftSidebarResizable => !IsLeftSidebarCollapsed && !IsPresentationMode; - - partial void OnIsLeftSidebarCollapsedChanged(bool value) - { - if (value) - { - if (LeftSidebarGridLength.IsAbsolute && LeftSidebarGridLength.Value > 100) - { - _savedExpandedLeftSidebarWidth = LeftSidebarGridLength.Value; - } - LeftSidebarGridLength = new GridLength(38, GridUnitType.Pixel); - } - else - { - LeftSidebarGridLength = new GridLength(Math.Max(260, _savedExpandedLeftSidebarWidth), GridUnitType.Pixel); - } - OnPropertyChanged(nameof(IsLeftSidebarResizable)); - } - - partial void OnLeftSidebarGridLengthChanged(GridLength value) - { - if (!IsLeftSidebarCollapsed && value.IsAbsolute && value.Value >= 200) - { - _savedExpandedLeftSidebarWidth = value.Value; - } - } - - [ObservableProperty] - private bool _isInspectorCollapsed; - - [ObservableProperty] - private ToolMode _activeToolMode = ToolMode.Select; - - [ObservableProperty] - private double _zoomLevel = 1.0; // 100% - - [ObservableProperty] - private string _statusMessage = "Ready"; - - /// - /// True while a document open/template load/export is in progress. Drives the - /// status-bar busy indicator so the app never looks idle while it's actually working. - /// - [ObservableProperty] - private bool _isBusy; - - [ObservableProperty] - private string _searchQuery = ""; - - [ObservableProperty] - private PageViewModel? _currentPage; - - partial void OnCurrentPageChanged(PageViewModel? oldValue, PageViewModel? newValue) - { - if (oldValue != null) - { - oldValue.SelectionChanged -= OnElementSelectionChanged; - oldValue.MultiSelectionChanged -= OnMultiSelectionChanged; - } - - if (newValue != null) - { - newValue.SelectionChanged += OnElementSelectionChanged; - newValue.MultiSelectionChanged += OnMultiSelectionChanged; - Inspector.UpdateSelection(newValue.SelectedElement, newValue); - } - else - { - Inspector.UpdateSelection(null, null); - } - } - - private void OnMultiSelectionChanged() - { - Inspector.UpdateSelection(CurrentPage?.SelectedElement, CurrentPage); - } - - // Canvas Grid & Snap-to-Grid - [ObservableProperty] - private bool _showGrid; - - [ObservableProperty] - private bool _snapToGrid; - - [ObservableProperty] - private GridSnapSize _gridSnapSize = GridSnapSize.Points20; - - // Toast HUD State - [ObservableProperty] - private string _toastMessage = ""; - - [ObservableProperty] - private string _toastIcon = "CheckCircleOutline"; - - [ObservableProperty] - private bool _isToastVisible; - - [ObservableProperty] - private ToastNotificationType _toastType = ToastNotificationType.Primary; - - [ObservableProperty] - private ToastPosition _toastPosition = ToastPosition.BottomCenter; - - [ObservableProperty] - private ToastStyleVariant _toastStyleVariant = ToastStyleVariant.Solid; - - [ObservableProperty] - private bool _toastShowCloseButton = true; - - partial void OnToastTypeChanged(ToastNotificationType value) => RefreshToastVisuals(); - partial void OnToastPositionChanged(ToastPosition value) => RefreshToastVisuals(); - partial void OnToastStyleVariantChanged(ToastStyleVariant value) => RefreshToastVisuals(); - partial void OnToastShowCloseButtonChanged(bool value) => RefreshToastVisuals(); - - public bool ToastIsSolid - { - get - { - if (ToastStyleVariant == ToastStyleVariant.Auto) - return IsDarkMode; - return ToastStyleVariant == ToastStyleVariant.Solid; - } - } - - public IBrush ToastBackgroundBrush - { - get - { - if (ToastIsSolid) - { - return ToastType switch - { - ToastNotificationType.Primary => Brush("#0F6CBD"), - ToastNotificationType.Success => Brush("#15803D"), - ToastNotificationType.Danger => Brush("#DC2626"), - ToastNotificationType.Warning => Brush("#D97706"), - ToastNotificationType.General => Brush("#1E293B"), - _ => Brush("#1E293B") - }; - } - - if (IsDarkMode) - { - return ToastType switch - { - ToastNotificationType.Primary => Brush("#172554"), - ToastNotificationType.Success => Brush("#052E16"), - ToastNotificationType.Danger => Brush("#450A0A"), - ToastNotificationType.Warning => Brush("#451A03"), - ToastNotificationType.General => Brush("#18181B"), - _ => Brush("#18181B") - }; - } - else - { - return ToastType switch - { - ToastNotificationType.Primary => Brush("#EFF6FF"), - ToastNotificationType.Success => Brush("#F0FDF4"), - ToastNotificationType.Danger => Brush("#FEF2F2"), - ToastNotificationType.Warning => Brush("#FFFBEB"), - ToastNotificationType.General => Brush("#F8FAFC"), - _ => Brush("#F8FAFC") - }; - } - } - } - - public IBrush ToastForegroundBrush - { - get - { - if (ToastIsSolid) return WhiteBrush; - - if (IsDarkMode) - { - return ToastType switch - { - ToastNotificationType.Primary => Brush("#93C5FD"), - ToastNotificationType.Success => Brush("#86EFAC"), - ToastNotificationType.Danger => Brush("#FCA5A5"), - ToastNotificationType.Warning => Brush("#FCD34D"), - ToastNotificationType.General => Brush("#E2E8F0"), - _ => Brush("#E2E8F0") - }; - } - else - { - return ToastType switch - { - ToastNotificationType.Primary => Brush("#1D4ED8"), - ToastNotificationType.Success => Brush("#15803D"), - ToastNotificationType.Danger => Brush("#B91C1C"), - ToastNotificationType.Warning => Brush("#B45309"), - ToastNotificationType.General => Brush("#334155"), - _ => Brush("#334155") - }; - } - } - } - - public IBrush ToastBorderBrush - { - get - { - if (ToastIsSolid) - { - return ToastType switch - { - ToastNotificationType.Primary => Brush("#0D5CA0"), - ToastNotificationType.Success => Brush("#166534"), - ToastNotificationType.Danger => Brush("#B91C1C"), - ToastNotificationType.Warning => Brush("#B45309"), - ToastNotificationType.General => Brush("#0F172A"), - _ => Brush("#0F172A") - }; - } - - if (IsDarkMode) - { - return ToastType switch - { - ToastNotificationType.Primary => Brush("#1E40AF"), - ToastNotificationType.Success => Brush("#166534"), - ToastNotificationType.Danger => Brush("#991B1B"), - ToastNotificationType.Warning => Brush("#92400E"), - ToastNotificationType.General => Brush("#3F3F46"), - _ => Brush("#3F3F46") - }; - } - else - { - return ToastType switch - { - ToastNotificationType.Primary => Brush("#BFDBFE"), - ToastNotificationType.Success => Brush("#BBF7D0"), - ToastNotificationType.Danger => Brush("#FECACA"), - ToastNotificationType.Warning => Brush("#FDE68A"), - ToastNotificationType.General => Brush("#CBD5E1"), - _ => Brush("#CBD5E1") - }; - } - } - } - - public IBrush ToastIconBrush => ToastIsSolid ? WhiteBrush : ToastForegroundBrush; - public IBrush ToastCloseBrush => ToastIsSolid ? WhiteBrush : ToastForegroundBrush; - - public HorizontalAlignment ToastHorizontalAlignment => ToastPosition switch - { - ToastPosition.TopLeft or ToastPosition.BottomLeft => HorizontalAlignment.Left, - ToastPosition.TopRight or ToastPosition.BottomRight => HorizontalAlignment.Right, - _ => HorizontalAlignment.Center - }; - - public VerticalAlignment ToastVerticalAlignment => ToastPosition switch - { - ToastPosition.TopLeft or ToastPosition.TopCenter or ToastPosition.TopRight => VerticalAlignment.Top, - _ => VerticalAlignment.Bottom - }; - - public Thickness ToastMargin => ToastPosition switch - { - ToastPosition.TopLeft => new Thickness(32, 54, 0, 0), - ToastPosition.TopCenter => new Thickness(0, 54, 0, 0), - ToastPosition.TopRight => new Thickness(0, 54, 32, 0), - ToastPosition.BottomLeft => new Thickness(32, 0, 0, 48), - ToastPosition.BottomCenter => new Thickness(0, 0, 0, 48), - ToastPosition.BottomRight => new Thickness(0, 0, 32, 48), - _ => new Thickness(0, 0, 0, 48) - }; - - private static readonly SolidColorBrush WhiteBrush = new(Color.Parse("#FFFFFF")); - private static SolidColorBrush Brush(string hex) => new(Color.Parse(hex)); - - public void RefreshToastVisuals() - { - OnPropertyChanged(nameof(ToastIsSolid)); - OnPropertyChanged(nameof(ToastBackgroundBrush)); - OnPropertyChanged(nameof(ToastForegroundBrush)); - OnPropertyChanged(nameof(ToastBorderBrush)); - OnPropertyChanged(nameof(ToastIconBrush)); - OnPropertyChanged(nameof(ToastCloseBrush)); - OnPropertyChanged(nameof(ToastHorizontalAlignment)); - OnPropertyChanged(nameof(ToastVerticalAlignment)); - OnPropertyChanged(nameof(ToastMargin)); - } - - // Acrobat Suite Dialogs State - [ObservableProperty] - private bool _isSignatureStudioOpen; - - [ObservableProperty] - private string _signatureSignerName = "Jane Doe"; - - [ObservableProperty] - private SignatureStyle _selectedSignatureStyle = SignatureStyle.CursiveElegance; - - [ObservableProperty] - private bool _isSecurityDialogOpen; - - [ObservableProperty] - private PdfSecuritySettings _securitySettings = new(); - - [ObservableProperty] - private bool _isPreflightDialogOpen; - - [ObservableProperty] - private DocumentAuditReport? _activeAuditReport; - - [ObservableProperty] - private bool _isAuditRunning; - - [ObservableProperty] - private bool _isHeaderFooterDialogOpen; - - [ObservableProperty] - private string _headerLeftText = ""; - - [ObservableProperty] - private string _headerCenterText = ""; - - [ObservableProperty] - private string _headerRightText = ""; - - [ObservableProperty] - private string _footerLeftText = "CONFIDENTIAL & PROPRIETARY"; - - [ObservableProperty] - private string _footerCenterText = ""; - - [ObservableProperty] - private string _footerRightText = "Page {P} of {N}"; - - [ObservableProperty] - private bool _isWatermarkManagerOpen; - - [ObservableProperty] - private string _watermarkPresetText = "CONFIDENTIAL"; - - [ObservableProperty] - private string _watermarkColorHex = "#DC2626"; - - [ObservableProperty] - private double _watermarkOpacity = 0.15; - - [ObservableProperty] - private double _watermarkAngle = -35; - - [ObservableProperty] - private bool _isSearchRedactDialogOpen; - - [ObservableProperty] - private string _searchRedactQuery = ""; - - [ObservableProperty] - private string _selectedExemptionCode = "[REDACTED - (b)(4) PRIVILEGED]"; - - [ObservableProperty] - private bool _isCustomStampDialogOpen; - - [ObservableProperty] - private string _customStampText = "CERTIFIED TRUE COPY"; - - [ObservableProperty] - private string _customStampColorHex = "#0F6CBD"; - - // Bates Numbering Studio - [ObservableProperty] - private bool _isBatesNumberingDialogOpen; - - [ObservableProperty] - private string _batesPrefix = "CONF-BATES-"; - - [ObservableProperty] - private string _batesSuffix = ""; - - [ObservableProperty] - private int _batesStartingNumber = 1; - - [ObservableProperty] - private int _batesNumberOfDigits = 6; - - [ObservableProperty] - private BatesPosition _batesPosition = BatesPosition.BottomLeft; - - [ObservableProperty] - private string _batesFontColorHex = "#0F172A"; - - [ObservableProperty] - private double _batesFontSize = 9.0; - - [ObservableProperty] - private bool _isApplyingBatesNumbers; - - [ObservableProperty] - private double _batesProgressPercentage; - - // Organize Pages: Split & Extract Studio - [ObservableProperty] - private bool _isSplitExtractDialogOpen; - - [ObservableProperty] - private SplitExtractMode _splitExtractMode = SplitExtractMode.SplitEveryNPages; - - [ObservableProperty] - private int _splitPageInterval = 1; - - [ObservableProperty] - private string _splitPageRanges = "1-2"; - - // Document Comparison Diff Studio - [ObservableProperty] - private bool _isCompareDialogOpen; - - [ObservableProperty] - private DocumentComparisonReport? _activeComparisonReport; - - [ObservableProperty] - private bool _isComparing; - - // Math & Scientific Equation Studio - [ObservableProperty] - private bool _isMathStudioOpen; - - [ObservableProperty] - private string _mathStudioFormula = @"\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}"; - - [ObservableProperty] - private string _mathStudioPresetName = "Gaussian Integral"; - - [ObservableProperty] - private string _mathStudioEquationNumber = "(1)"; - - [ObservableProperty] - private bool _mathStudioShowNumber = false; - - [ObservableProperty] - private MathCategory _mathStudioCategory = MathCategory.Calculus; - - [ObservableProperty] - private string _mathStudioSvgPreview = ""; - - [ObservableProperty] - private MathElementViewModel? _editingMathElement; - - partial void OnMathStudioFormulaChanged(string value) => UpdateMathStudioPreview(); - partial void OnMathStudioShowNumberChanged(bool value) => UpdateMathStudioPreview(); - partial void OnMathStudioEquationNumberChanged(string value) => UpdateMathStudioPreview(); - - public void UpdateMathStudioPreview() - { - var options = new Services.MathEngine.MathRenderOptions( - FontSize: 18, - TextColorHex: "#0F172A", - ShowEquationNumber: MathStudioShowNumber, - EquationNumber: MathStudioEquationNumber, - TargetWidth: 460, - TargetHeight: 80 - ); - var result = Services.MathEngine.MathLayoutEngine.RenderToSvg(MathStudioFormula, options); - MathStudioSvgPreview = result.SvgXml; - } - - // In-Canvas Interactive Find & Replace - [ObservableProperty] - private bool _isFindReplaceOpen; - - [ObservableProperty] - private string _findQuery = ""; - - [ObservableProperty] - private string _replaceQuery = ""; - - [ObservableProperty] - private bool _findMatchCase; - - [ObservableProperty] - private int _findMatchesCount; - - // Canvas Rulers & Measure Tool - [ObservableProperty] - private bool _showRulers = true; - - [ObservableProperty] - private RulerUnit _rulerUnit = RulerUnit.Points; - - [ObservableProperty] - private double _cursorCanvasX; - - [ObservableProperty] - private double _cursorCanvasY; - - // Presentation / Read Mode & Theme - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsLeftSidebarResizable))] - private bool _isPresentationMode; - - [ObservableProperty] - private bool _isDarkMode; - - // Collections - public ObservableCollection Pages { get; } = new(); - public ObservableCollection OutlineItems { get; } = new(); - public ObservableCollection CommentItems { get; } = new(); - - // Child Subsystems - public InspectorViewModel Inspector { get; } - public UndoRedoService UndoRedo { get; } = new(); - public static IStorageProvider? StorageProvider { get; set; } - - // Calculated Properties - public int CurrentPageNumber => CurrentPage != null ? Pages.IndexOf(CurrentPage) + 1 : 1; - public int TotalPagesCount => Pages.Count; - public string PageDimensionsDisplay => CurrentPage != null ? $"{CurrentPage.Width:F0} × {CurrentPage.Height:F0} pt" : "800 × 1131 pt"; - public string SecurityStatusDisplay => SecuritySettings.IsPasswordProtected ? "Protected (Password Required)" : "Standard (No Security)"; - - // --- CONSTRUCTORS --- - - public MainViewModel() : this( - new PdfExportService(), - new TemplateService(), - new ProjectPersistenceService(), - new DocumentAuditService(), - new SignatureService(), - new SmartPlacementService(), - new RecentDocumentsService(), - new PageOrganizerService(), - null, - null, - new ThemeService(), - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - new UiSettingsService()) - { - } - - private readonly IThemeService? _themeService; - private readonly IUiSettingsService? _uiSettingsService; - private readonly PluginHost? _pluginHost; - private readonly PdfEditorApp.Core.Plugins.Descriptors.ICommandPaletteRegistry? _commandPaletteRegistry; - private readonly PdfEditorApp.Core.Plugins.Descriptors.IStatusBarRegistry? _statusBarRegistry; - - public ObservableCollection LeftStatusBarWidgets { get; } = new(); - public ObservableCollection RightStatusBarWidgets { get; } = new(); - - public PluginHost? PluginHost { get; set; } - - public MainViewModel( - IPdfExportService exportService, - ITemplateService templateService, - IProjectPersistenceService persistenceService, - IDocumentAuditService? auditService = null, - ISignatureService? signatureService = null, - ISmartPlacementService? placementService = null, - IRecentDocumentsService? recentService = null, - IPageOrganizerService? pageOrganizerService = null, - IPdfDocumentOperationsService? pdfOperationsService = null, - IPdfToolRegistry? toolRegistry = null, - IThemeService? themeService = null, - IPdfToolViewModelFactory? toolViewModelFactory = null, - IPdfWorkflowEngine? workflowEngine = null, - IDataSourceService? dataSourceService = null, - IDataBindingService? dataBindingService = null, - IDataMergeEngine? dataMergeEngine = null, - IBatchPdfGenerator? batchPdfGenerator = null, - PdfToolRunnerViewModel? toolRunner = null, - WorkflowBuilderViewModel? workflowBuilder = null, - DataStudioViewModel? dataStudio = null, - BatchGenerationViewModel? batchGeneration = null, - HomeViewModel? homeViewModel = null, - PdfViewerViewModel? pdfViewer = null, - InspectorViewModel? inspector = null, - IUiSettingsService? uiSettingsService = null, - PluginHost? pluginHost = null, - ICanvasElementService? elementService = null, - IRibbonRegistry? ribbonRegistry = null, - PdfEditorApp.Core.Plugins.Descriptors.ICommandPaletteRegistry? commandPaletteRegistry = null, - PdfEditorApp.Core.Plugins.Descriptors.IStatusBarRegistry? statusBarRegistry = null, - PdfEditorApp.Core.Plugins.Descriptors.ISidebarRegistry? sidebarRegistry = null, - PdfEditorApp.Core.Plugins.Descriptors.IDialogRegistry? dialogRegistry = null, - PdfEditorApp.Core.Plugins.Descriptors.INavigationRegistry? navigationRegistry = null, - PdfEditorApp.Core.Plugins.Descriptors.IOverlayRegistry? overlayRegistry = null) - { - _exportService = exportService; - _templateService = templateService; - _persistenceService = persistenceService; - _auditService = auditService ?? new DocumentAuditService(); - _signatureService = signatureService ?? new SignatureService(); - _placementService = placementService ?? new SmartPlacementService(); - _recentService = recentService ?? new RecentDocumentsService(); - _pageOrganizerService = pageOrganizerService ?? new PageOrganizerService(); - _themeService = themeService; - _toolRegistry = toolRegistry ?? new PdfToolRegistry(); - _uiSettingsService = uiSettingsService ?? new UiSettingsService(); - _pluginHost = pluginHost; - PluginHost = pluginHost; - _elementService = elementService ?? PageViewModel.DefaultElementService; - _ribbonRegistry = ribbonRegistry ?? new RibbonRegistry(); - _ribbonRegistry.RegistryChanged += RefreshPluginRibbonActions; - RefreshPluginRibbonActions(); - - _commandPaletteRegistry = commandPaletteRegistry; - _statusBarRegistry = statusBarRegistry; - _sidebarRegistry = sidebarRegistry; - _dialogRegistry = dialogRegistry; - _overlayRegistry = overlayRegistry; - - if (_overlayRegistry != null) - { - _overlayRegistry.ActiveOverlaysChanged += () => OnPropertyChanged(nameof(ActiveOverlays)); - } - - if (_statusBarRegistry != null) - { - _statusBarRegistry.RegistryChanged += RefreshStatusBarWidgets; - RefreshStatusBarWidgets(); - } - - if (_sidebarRegistry != null) - { - _sidebarRegistry.RegistryChanged += RefreshSidebarTabs; - RefreshSidebarTabs(); - } - - - if (_uiSettingsService != null) - { - var s = _uiSettingsService.Settings; - ToastPosition = s.ToastPosition; - ToastStyleVariant = s.ToastStyleVariant; - ToastShowCloseButton = s.ToastShowCloseButton; - _uiSettingsService.SettingsChanged += (newSettings) => - { - ToastPosition = newSettings.ToastPosition; - ToastStyleVariant = newSettings.ToastStyleVariant; - ToastShowCloseButton = newSettings.ToastShowCloseButton; - RefreshToastVisuals(); - }; - } - - if (_themeService != null) - { - IsDarkMode = _themeService.IsDarkMode; - _themeService.ThemeChanged += (mode) => - { - IsDarkMode = _themeService.IsDarkMode; - if (Home != null) Home.IsDarkMode = IsDarkMode; - RefreshToastVisuals(); - }; - } - - var effectiveWorkflowEngine = workflowEngine ?? (pdfOperationsService?.WorkflowEngine) ?? new PdfWorkflowEngine(); - _pdfOperationsService = pdfOperationsService ?? new PdfDocumentOperationsService( - _toolRegistry, - new PdfPageService(), - new PdfOptimizationService(), - new PdfSecurityService(), - new PdfConversionService(), - new PdfOcrService(), - new PdfFormService(), - new AiDocumentService(), - new DocumentTranslationService(), - effectiveWorkflowEngine); - - var effectiveToolFactory = toolViewModelFactory ?? new PdfToolViewModelFactory(_pdfOperationsService, _toolRegistry); - - ToolRunner = toolRunner ?? new PdfToolRunnerViewModel(_pdfOperationsService); - - WorkflowBuilder = workflowBuilder ?? new WorkflowBuilderViewModel(effectiveWorkflowEngine, _toolRegistry); - - var effectiveDataSource = dataSourceService ?? new DataSourceService(); - var effectiveDataBinding = dataBindingService ?? new DataBindingService(); - var effectiveMerge = dataMergeEngine ?? new DataMergeEngine(); - var effectiveBatch = batchPdfGenerator ?? new BatchPdfGeneratorService(effectiveMerge, _exportService); - - DataStudio = dataStudio ?? new DataStudioViewModel(effectiveDataSource, effectiveDataBinding); - DataStudio.UndoRedo = UndoRedo; - DataStudio.OnElementCreated += (el, desc) => AddElementWithUndo(el, desc); - - BatchGeneration = batchGeneration ?? new BatchGenerationViewModel(effectiveDataSource, effectiveMerge, effectiveBatch, _templateService); - DataStudio.OpenBatchMergeRequested += (matrix) => - { - BatchGeneration.StorageProvider = StorageProvider; - var doc = (IsEditorVisible && Pages.Count > 0) ? ToDocumentModel() : _templateService.CreateEmployeePayslipTemplate(); - BatchGeneration.OpenWithDocument(doc, matrix); - }; - - Inspector = inspector ?? new InspectorViewModel(); - Inspector.UndoRedo = UndoRedo; - Inspector.DataStudio = DataStudio; - - PdfViewer = pdfViewer ?? new PdfViewerViewModel(); - FryPdfViewer = new FryPdfViewer.FryPdfViewerViewModel(_persistenceService, _exportService); - if (_themeService != null) - { - PdfViewer.ReadingTheme = _themeService.ReadingTheme; - FryPdfViewer.ReadingTheme = _themeService.ReadingTheme; - _themeService.ReadingThemeChanged += (rt) => - { - if (PdfViewer.ReadingTheme != rt) - { - PdfViewer.ReadingTheme = rt; - } - if (FryPdfViewer.ReadingTheme != rt) - { - FryPdfViewer.ReadingTheme = rt; - } - }; - } - - var aiService = new Services.AI.AiService(); - var studioAgent = new Services.AI.PdfStudioAgentService(aiService, UndoRedo); - AiAssistant = new AiAssistantViewModel(studioAgent, _uiSettingsService ?? new UiSettingsService(), aiService) - { - GetCurrentPage = () => CurrentPage, - GetSelectedElement = () => CurrentPage?.SelectedElement ?? Inspector.SelectedElement, - UndoRedo = UndoRedo, - RequestOpenSettings = () => NavigateToSettingsCommand.Execute(null), - RequestReturnToThumbnails = () => SelectSidebarTab(SidebarTabKind.Thumbnails) - }; - - // Set up Home page - Home = homeViewModel ?? new HomeViewModel(_recentService, _templateService, _persistenceService, _toolRegistry, ToolRunner, effectiveToolFactory, _themeService, _uiSettingsService, navigationRegistry, _pluginHost?.Context); - - UndoRedo.StateChanged += (s, e) => - { - OnPropertyChanged(nameof(CanUndo)); - OnPropertyChanged(nameof(CanRedo)); - UndoCommand.NotifyCanExecuteChanged(); - RedoCommand.NotifyCanExecuteChanged(); - }; - - RegisterMessengerHandlers(); - - RefreshToastVisuals(); - - // Synchronize inspector when selection changes - Inspector.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(Inspector.SelectedElement)) - { - OnPropertyChanged(nameof(CurrentPage)); - } - }; - - // Update window title dynamically when Home state or PDF Viewer title changes - Home.PropertyChanged += (s, e) => - { - if (e.PropertyName is nameof(Home.IsToolPageActive) or nameof(Home.ActiveToolViewModel) or nameof(Home.SelectedNavSection)) - { - OnPropertyChanged(nameof(WindowTitle)); - } - }; - - PdfViewer.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(PdfViewer.DocumentTitle)) - { - OnPropertyChanged(nameof(WindowTitle)); - } - }; - - FryPdfViewer.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(FryPdfViewer.DocumentTitle)) - { - OnPropertyChanged(nameof(WindowTitle)); - } - else if (e.PropertyName == nameof(FryPdfViewer.IsPresentationMode)) - { - MainWindowState = FryPdfViewer.IsPresentationMode ? WindowState.FullScreen : WindowState.Normal; - } - }; - - // Initialize quick command palette indexing - InitCommandPalette(); - - // Initialize default document model - var defaultDoc = _templateService.CreateAnnualReportTemplate(); - LoadFromDocumentModel(defaultDoc); - } - - private void RegisterMessengerHandlers() - { - WeakReferenceMessenger.Default.Register(this, (r, m) => r.ShowToast(m.Message, m.Type, m.ActionLabel)); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.NavigateToHome()); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.OpenEditorWithFile(m.FilePath)); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.OpenInViewer(m.FilePath)); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.OpenInFryPdfViewer(m.FilePath)); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.OpenInteractiveDeckTemplate()); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.OpenToolWithInitialFile(m.ToolId, m.FilePath)); - WeakReferenceMessenger.Default.Register(this, (r, m) => - { - if (r._themeService != null && r._themeService.ReadingTheme != m.Theme) - { - r._themeService.SetReadingTheme(m.Theme); - } - }); - WeakReferenceMessenger.Default.Register(this, (r, m) => - { - if (string.Equals(r.CurrentFilePath, m.OldPath, StringComparison.OrdinalIgnoreCase)) - { - r.CurrentFilePath = m.NewPath; - r.DocumentTitle = Path.GetFileName(m.NewPath); - } - if (string.Equals(r.PdfViewer.CurrentFilePath, m.OldPath, StringComparison.OrdinalIgnoreCase)) - { - r.PdfViewer.CurrentFilePath = m.NewPath; - r.PdfViewer.DocumentTitle = Path.GetFileName(m.NewPath); - } - }); - WeakReferenceMessenger.Default.Register(this, (r, m) => - { - if (string.Equals(r.CurrentFilePath, m.DeletedPath, StringComparison.OrdinalIgnoreCase)) - { - r.CurrentFilePath = ""; - r.DocumentTitle = "Untitled.pdf"; - r.IsEditorVisible = false; - r.IsHomePageVisible = true; - } - if (string.Equals(r.PdfViewer.CurrentFilePath, m.DeletedPath, StringComparison.OrdinalIgnoreCase)) - { - r.PdfViewer.CurrentFilePath = ""; - r.IsPdfViewerVisible = false; - r.IsHomePageVisible = true; - } - }); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.Home.PromptRename(m.FilePath)); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.Home.PromptDelete(m.FilePath)); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.OpenEditorWithTemplate(m.TemplateName)); - WeakReferenceMessenger.Default.Register(this, (r, m) => _ = r.OpenProjectAndEnterEditorAsync()); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.OpenWorkflowStudio()); - WeakReferenceMessenger.Default.Register(this, (r, m) => r.OpenBatchGeneration()); - WeakReferenceMessenger.Default.Register(this, (r, m) => _ = r.OpenPdfToReadWithPickerAsync()); - } - - [RelayCommand] - public void OpenTool(PdfToolId toolId) - { - IsHomePageVisible = true; - IsEditorVisible = false; - IsPdfViewerVisible = false; - Home.OpenToolPage(toolId); - } - - public void OpenToolWithInitialFile(PdfToolId toolId, string initialFilePath) - { - IsHomePageVisible = true; - IsEditorVisible = false; - IsPdfViewerVisible = false; - Home.OpenToolPage(toolId, initialFilePath); - } - - [RelayCommand] - public void OpenWorkflowStudio() - { - WorkflowBuilder.StorageProvider = StorageProvider; - WorkflowBuilder.Open(); - } - - [RelayCommand] - public void OpenAiAssistant() - { - if (IsLeftSidebarCollapsed) - { - IsLeftSidebarCollapsed = false; - } - if (LeftSidebarGridLength.IsAbsolute && LeftSidebarGridLength.Value < 340) - { - LeftSidebarGridLength = new GridLength(340, GridUnitType.Pixel); - } - ActiveDynamicSidebarTabId = null; - ActiveDynamicSidebarView = null; - ActiveSidebarTab = SidebarTabKind.AiAssistant; - - var target = CurrentPage?.SelectedElement ?? Inspector.SelectedElement; - if (target != null) - { - AiAssistant.OpenForElement(target); - } - else - { - AiAssistant.Open(); - } - } - - [RelayCommand] - public void AskAiToModify(ElementViewModelBase? element = null) - { - if (IsLeftSidebarCollapsed) - { - IsLeftSidebarCollapsed = false; - } - if (LeftSidebarGridLength.IsAbsolute && LeftSidebarGridLength.Value < 340) - { - LeftSidebarGridLength = new GridLength(340, GridUnitType.Pixel); - } - ActiveDynamicSidebarTabId = null; - ActiveDynamicSidebarView = null; - ActiveSidebarTab = SidebarTabKind.AiAssistant; - - var target = element ?? CurrentPage?.SelectedElement ?? Inspector.SelectedElement; - if (target != null) - { - AiAssistant.OpenForElement(target); - } - else - { - AiAssistant.Open(); - } - } - - [ObservableProperty] - private (double X, double Y)? _lastCanvasClickPoint; - - [RelayCommand] - public void AskAiAtPoint(object? param = null) - { - if (IsLeftSidebarCollapsed) - { - IsLeftSidebarCollapsed = false; - } - if (LeftSidebarGridLength.IsAbsolute && LeftSidebarGridLength.Value < 340) - { - LeftSidebarGridLength = new GridLength(340, GridUnitType.Pixel); - } - ActiveDynamicSidebarTabId = null; - ActiveDynamicSidebarView = null; - ActiveSidebarTab = SidebarTabKind.AiAssistant; - - double targetX = LastCanvasClickPoint?.X ?? 100; - double targetY = LastCanvasClickPoint?.Y ?? 150; - - if (param is (double px, double py)) - { - targetX = px; - targetY = py; - } - - AiAssistant.TargetPointOnPage(targetX, targetY, CurrentPage); - } - - [RelayCommand] - public void SelectEntirePageForAi() - { - if (CurrentPage != null) - { - CurrentPage.ClearSelection(); - } - if (IsLeftSidebarCollapsed) - { - IsLeftSidebarCollapsed = false; - } - if (LeftSidebarGridLength.IsAbsolute && LeftSidebarGridLength.Value < 340) - { - LeftSidebarGridLength = new GridLength(340, GridUnitType.Pixel); - } - ActiveDynamicSidebarTabId = null; - ActiveDynamicSidebarView = null; - ActiveSidebarTab = SidebarTabKind.AiAssistant; - - AiAssistant.TargetEntirePage(CurrentPage); - } - - [RelayCommand] - public void ToggleTheme() - { - if (_themeService != null) - { - _themeService.ToggleTheme(); - IsDarkMode = _themeService.IsDarkMode; - Home.IsDarkMode = IsDarkMode; - ShowToast(IsDarkMode ? "Switched to Dark Studio Theme" : "Switched to Light Studio Theme", IsDarkMode ? "WeatherNight" : "WeatherSunny"); - } - else - { - IsDarkMode = !IsDarkMode; - Home.IsDarkMode = IsDarkMode; - } - } - - [RelayCommand] - public void SetLightTheme() - { - _themeService?.SetTheme(AppThemeMode.Light); - IsDarkMode = false; - Home.IsDarkMode = false; - ShowToast("Switched to Light Studio Theme", "WeatherSunny"); - } - - [RelayCommand] - public void SetDarkTheme() - { - _themeService?.SetTheme(AppThemeMode.Dark); - IsDarkMode = true; - Home.IsDarkMode = true; - ShowToast("Switched to Dark Studio Theme", "WeatherNight"); - } - - [RelayCommand] - public void SetSystemTheme() - { - _themeService?.SetTheme(AppThemeMode.System); - IsDarkMode = _themeService?.IsDarkMode ?? false; - Home.IsDarkMode = IsDarkMode; - ShowToast("Using System Theme Preference", "Laptop"); - } - - // --- HOME / EDITOR / VIEWER NAVIGATION --- - - /// Switches to the editor and loads the requested template. - public void OpenEditorWithTemplate(string? templateName) - { - IsHomePageVisible = false; - IsPdfViewerVisible = false; - IsEditorVisible = true; - - var model = string.IsNullOrWhiteSpace(templateName) - ? _templateService.CreateBlankDocument() - : _templateService.CreateTemplate(templateName); - - LoadFromDocumentModel(model); - ShowToast($"Created new document from {templateName ?? "Blank"} template", "FilePlusOutline"); - } - - /// Switches to the editor and loads a project from a file path. - public void OpenEditorWithFile(string path) - { - _ = OpenEditorWithFileAsync(path); - } - - /// Asynchronously switches to the editor and loads a project from a file path. - public async Task OpenEditorWithFileAsync(string path) - { - // Navigate to the (still-empty) editor shell immediately so the user sees the app - // respond right away, then populate it — mirrors OpenInViewerAsync's approach. - IsHomePageVisible = false; - IsPdfViewerVisible = false; - IsFryPdfViewerVisible = false; - IsEditorVisible = true; - IsBusy = true; - UpdateStatus($"Opening {Path.GetFileName(path)}..."); - - try - { - var model = await _persistenceService.LoadProjectAsync(path); - if (model != null) - { - await LoadFromDocumentModelAsync(model); - CurrentFilePath = path; - _recentService.Add(new RecentDocumentItem - { - FilePath = path, - Title = model.Title, - LastOpened = DateTime.UtcNow - }); - Home.RefreshRecent(); - ShowToast($"Opened: {Path.GetFileName(path)}", "FolderOpenOutline"); - } - else - { - IsBusy = false; - IsEditorVisible = false; - IsHomePageVisible = true; - ShowToast("Could not open file: unrecognized or corrupted document.", "AlertCircleOutline"); - } - } - catch (Exception ex) - { - IsBusy = false; - IsEditorVisible = false; - IsHomePageVisible = true; - ShowToast($"Could not open file: {ex.Message}", "AlertCircleOutline"); - } - } - - /// Opens any PDF in the dedicated PDF Viewer subsystem. - [RelayCommand] - public void OpenPdfReader() - { - _ = OpenPdfToReadWithPickerAsync(); - } - - /// Opens any PDF in the dedicated PDF Viewer subsystem. - public void OpenInViewer(string path) - { - if (string.IsNullOrEmpty(path)) - { - _ = OpenPdfToReadWithPickerAsync(); - } - else - { - _ = OpenInViewerAsync(path); - } - } - - /// Prompts user to select a PDF file and opens it in PDF Reader mode. - public async Task OpenPdfToReadWithPickerAsync() - { - try - { - if (StorageProvider != null) - { - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Open PDF Document to Read (PDF Reader Mode)", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("PDF Documents (*.pdf)") - { - Patterns = new[] { "*.pdf" } - } - } - }); - - if (files.Count > 0) - { - string chosenPath = files[0].Path.LocalPath; - await OpenInViewerAsync(chosenPath); - } - } - } - catch (Exception ex) - { - ShowToast($"Could not open PDF: {ex.Message}", "AlertCircleOutline"); - } - } - - /// Asynchronously opens a PDF document in the viewer. - public async Task OpenInViewerAsync(string path) - { - try - { - // Immediately transition to PDF Viewer with zero delay - DocumentTitle = Path.GetFileName(path); - IsHomePageVisible = false; - IsEditorVisible = false; - IsFryPdfViewerVisible = false; - IsPdfViewerVisible = true; - ShowToast($"Opening: {Path.GetFileName(path)}", "FilePdfBox"); - - PdfViewer.StorageProvider = StorageProvider; - await PdfViewer.LoadDocumentAsync(path); - - _recentService.Add(new RecentDocumentItem - { - FilePath = path, - Title = Path.GetFileName(path), - LastOpened = DateTime.UtcNow - }); - Home.RefreshRecent(); - } - catch (Exception ex) - { - ShowToast($"Could not open in Viewer: {ex.Message}", "AlertCircleOutline"); - } - } - - /// Asynchronously opens a .frypdf document in the interactive presentation reader. - public async Task OpenInFryPdfViewerAsync(string path) - { - try - { - DocumentTitle = Path.GetFileName(path); - IsHomePageVisible = false; - IsEditorVisible = false; - IsPdfViewerVisible = false; - IsFryPdfViewerVisible = true; - ShowToast($"Reading interactive: {Path.GetFileName(path)}", "BookOpenPageVariantOutline"); - - FryPdfViewer.StorageProvider = StorageProvider; - await FryPdfViewer.LoadDocumentAsync(path); - - _recentService.Add(new RecentDocumentItem - { - FilePath = path, - Title = Path.GetFileName(path), - LastOpened = DateTime.UtcNow - }); - Home.RefreshRecent(); - } - catch (Exception ex) - { - ShowToast($"Could not open in Interactive Viewer: {ex.Message}", "AlertCircleOutline"); - } - } - - /// Opens a .frypdf document in the interactive presentation reader. - public void OpenInFryPdfViewer(string path) - { - if (string.IsNullOrEmpty(path)) - { - _ = OpenFryPdfToReadWithPickerAsync(); - } - else - { - _ = OpenInFryPdfViewerAsync(path); - } - } - - /// Prompts user to select a .frypdf file and opens it in Interactive Reader mode. - public async Task OpenFryPdfToReadWithPickerAsync() - { - try - { - if (StorageProvider != null) - { - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Open FryPDF Project to Read (Interactive Reader Mode)", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("FryPDF Projects (*.frypdf, *.json)") - { - Patterns = new[] { "*.frypdf", "*.json" } - } - } - }); - - if (files.Count > 0) - { - string chosenPath = files[0].Path.LocalPath; - await OpenInFryPdfViewerAsync(chosenPath); - } - } - } - catch (Exception ex) - { - ShowToast($"Could not open project: {ex.Message}", "AlertCircleOutline"); - } - } - - /// Switches the currently active editor document into the Interactive Reader. - [RelayCommand] - public void OpenCurrentInInteractiveReader() - { - var model = ToDocumentModel(); - IsHomePageVisible = false; - IsEditorVisible = false; - IsPdfViewerVisible = false; - IsFryPdfViewerVisible = true; - FryPdfViewer.StorageProvider = StorageProvider; - FryPdfViewer.LoadFromModel(model, CurrentFilePath); - } - - /// Launches the showcase landscape interactive presentation deck directly in the Interactive Reader. - [RelayCommand] - public void OpenInteractiveDeckTemplate() - { - var model = _templateService.CreateInteractiveExecutiveBriefTemplate(); - DocumentTitle = "QuantumScale_Global_Executive_Strategic_Brief_2026.frypdf"; - IsHomePageVisible = false; - IsEditorVisible = false; - IsPdfViewerVisible = false; - IsFryPdfViewerVisible = true; - FryPdfViewer.StorageProvider = StorageProvider; - FryPdfViewer.LoadFromModel(model, "QuantumScale_Global_Executive_Strategic_Brief_2026.frypdf"); - ShowToast("Loaded QuantumScale Interactive Landscape Presentation Deck", "PresentationPlay"); - } - - private async Task OpenProjectAndEnterEditorAsync() - { - try - { - if (StorageProvider != null) - { - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Open PDF Document or FryPDF Project", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("All Supported Documents (*.pdf, *.frypdf, *.pdfproj, *.json)") - { - Patterns = new[] { "*.pdf", "*.frypdf", "*.pdfproj", "*.json" } - }, - new FilePickerFileType("PDF Documents (*.pdf)") - { - Patterns = new[] { "*.pdf" } - }, - new FilePickerFileType("FryPDF Projects (*.frypdf, *.pdfproj, *.json)") - { - Patterns = new[] { "*.frypdf", "*.pdfproj", "*.json" } - } - } - }); - - if (files.Count > 0) - { - string chosenPath = files[0].Path.LocalPath; - OpenEditorWithFile(chosenPath); - } - } - } - catch (Exception ex) - { - ShowToast($"Open error: {ex.Message}", "AlertCircleOutline"); - } - } - - /// Returns to the Home page from the editor or viewer. - [RelayCommand] - public void NavigateToHome() - { - Home.RefreshRecent(); - Home.BackToTools(); - Home.SelectedNavSection = HomeNavSection.Home; - IsEditorVisible = false; - IsPdfViewerVisible = false; - IsFryPdfViewerVisible = false; - IsHomePageVisible = true; - } - - public void OnElementSelectionChanged(ElementViewModelBase? selectedElement) - { - Inspector.UpdateSelection(selectedElement, CurrentPage); - if (ActiveSidebarTab == SidebarTabKind.AiAssistant) - { - AiAssistant.UpdateTargetElement(selectedElement); - } - } - - // --- TOAST HUD NOTIFICATION FEEDBACK --- - - [RelayCommand] - public void DismissToast() - { - _toastCts?.Cancel(); - IsToastVisible = false; - } - - public void ShowToast(string message, string? iconKind = null) - { - var inferred = InferToastType(message, iconKind); - ShowToast(message, inferred, iconKind); - } - - public void ShowToast(string message, ToastNotificationType type, string? iconKind = null, int? customDurationMs = null) - { - _toastCts?.Cancel(); - _toastCts = new CancellationTokenSource(); - var token = _toastCts.Token; - - ToastMessage = message; - ToastType = type; - ToastIcon = !string.IsNullOrWhiteSpace(iconKind) ? iconKind : GetDefaultIconForType(type); - RefreshToastVisuals(); - IsToastVisible = true; - UpdateStatus(message); - - int duration = customDurationMs ?? (_uiSettingsService?.Settings.ToastDurationMs ?? 3500); - if (duration > 0) - { - Task.Delay(duration, token).ContinueWith(t => - { - if (!t.IsCanceled) - { - Dispatcher.UIThread.Post(() => IsToastVisible = false); - } - }, TaskScheduler.Default); - } - } - - public static ToastNotificationType InferToastType(string message, string? iconKind) - { - if (string.IsNullOrWhiteSpace(message)) - return ToastNotificationType.General; - - var lowerMsg = message.ToLowerInvariant(); - var lowerIcon = iconKind?.ToLowerInvariant() ?? ""; - - if (lowerMsg.Contains("error") || lowerMsg.Contains("failed") || lowerMsg.Contains("could not") || - lowerMsg.Contains("corrupt") || lowerMsg.Contains("unrecognized") || lowerIcon.Contains("alertcircle") || lowerIcon.Contains("alertoctagon")) - { - return ToastNotificationType.Danger; - } - - if (lowerMsg.Contains("warning") || lowerMsg.Contains("caution") || lowerMsg.Contains("nothing to")) - { - return ToastNotificationType.Warning; - } - - if (lowerMsg.Contains("saved") || lowerMsg.Contains("success") || lowerMsg.Contains("copied") || - lowerMsg.Contains("pasted") || lowerMsg.Contains("done") || lowerMsg.Contains("applied") || - lowerMsg.Contains("created new document") || lowerIcon.Contains("check")) - { - return ToastNotificationType.Success; - } - - if (lowerMsg.Contains("open") || lowerMsg.Contains("zoom") || lowerMsg.Contains("fit to") || - lowerMsg.Contains("switched to") || lowerMsg.Contains("active tool") || lowerMsg.Contains("ribbon") || - lowerMsg.Contains("sidebar") || lowerMsg.Contains("inspector") || lowerMsg.Contains("undone") || lowerMsg.Contains("redone")) - { - return ToastNotificationType.Primary; - } - - return ToastNotificationType.General; - } - - public static string GetDefaultIconForType(ToastNotificationType type) => type switch - { - ToastNotificationType.Primary => "InformationOutline", - ToastNotificationType.Success => "CheckCircleOutline", - ToastNotificationType.Danger => "AlertOctagonOutline", - ToastNotificationType.Warning => "AlertOutline", - ToastNotificationType.General => "InformationOutline", - _ => "InformationOutline" - }; - - public void UpdateStatus(string message) - { - StatusMessage = message; - } - - // --- TAB & TOOL MODE SELECTION --- - - [RelayCommand] - public void SelectDynamicRibbonTab(string tabId) - { - ActiveDynamicRibbonTabId = tabId; - if (IsRibbonCollapsed) - { - IsRibbonCollapsed = false; - } - RefreshActiveRibbonGroups(); - } - - [RelayCommand] - public void SelectRibbonTab(RibbonTabKind tab) - { - ActiveDynamicRibbonTabId = null; - ActiveRibbonTab = tab; - if (IsRibbonCollapsed) - { - IsRibbonCollapsed = false; - } - RefreshActiveRibbonGroups(); - } - - public void RefreshActiveRibbonGroups() - { - Dispatcher.UIThread.Post(() => - { - DynamicGroupsForActiveTab.Clear(); - - string currentTabId = ActiveDynamicRibbonTabId ?? ActiveRibbonTab.ToString(); - - // 1. Get explicitly registered groups for this tab - var groups = _ribbonRegistry.GetGroupsForTab(currentTabId); - var seenActionIds = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var grp in groups) - { - var groupVm = new DynamicRibbonGroupViewModel - { - Id = grp.Id, - TabId = grp.TabId, - Title = grp.Title - }; - - var actions = _ribbonRegistry.GetActionsForGroup(currentTabId, grp.Id); - foreach (var act in actions) - { - groupVm.Actions.Add(act); - seenActionIds.Add(act.Id); - } - - if (groupVm.Actions.Count > 0) - { - DynamicGroupsForActiveTab.Add(groupVm); - } - } - - // 2. Any orphan actions for this tab that weren't assigned to an explicit group - var tabActions = _ribbonRegistry.GetActionsForTab(currentTabId); - var orphanActions = tabActions.Where(a => !seenActionIds.Contains(a.Id)).ToList(); - if (orphanActions.Count > 0) - { - var defaultGroup = new DynamicRibbonGroupViewModel - { - Id = "default.plugins", - TabId = currentTabId, - Title = "Extensions" - }; - foreach (var act in orphanActions) - { - defaultGroup.Actions.Add(act); - } - DynamicGroupsForActiveTab.Add(defaultGroup); - } - }); - } - - private void RefreshPluginRibbonActions() - { - Dispatcher.UIThread.Post(() => - { - PluginRibbonActions.Clear(); - foreach (var action in _ribbonRegistry.GetAllActions()) - { - PluginRibbonActions.Add(action); - } - - DynamicRibbonTabs.Clear(); - var allTabs = _ribbonRegistry.GetAllTabs(); - foreach (var tab in allTabs) - { - DynamicRibbonTabs.Add(new DynamicRibbonTabViewModel - { - Descriptor = tab, - IsActive = string.Equals(tab.Id, ActiveDynamicRibbonTabId, StringComparison.OrdinalIgnoreCase) - }); - } - - DynamicRibbonGroups.Clear(); - foreach (var group in _ribbonRegistry.GetGroupsForTab(ActiveDynamicRibbonTabId ?? ActiveRibbonTab.ToString())) - { - DynamicRibbonGroups.Add(group); - } - - if (ActiveDynamicRibbonTabId != null && !allTabs.Any(t => string.Equals(t.Id, ActiveDynamicRibbonTabId, StringComparison.OrdinalIgnoreCase))) - { - ActiveDynamicRibbonTabId = null; - ActiveRibbonTab = RibbonTabKind.Home; - } - - RefreshActiveRibbonGroups(); - }); - } - - private void RefreshSidebarTabs() - { - if (_sidebarRegistry == null) return; - Dispatcher.UIThread.Post(() => - { - DynamicSidebarTabs.Clear(); - var allTabs = _sidebarRegistry.GetAllTabs(); - foreach (var tab in allTabs) - { - DynamicSidebarTabs.Add(new DynamicSidebarTabViewModel - { - Descriptor = tab, - IsActive = string.Equals(tab.Id, ActiveDynamicSidebarTabId, StringComparison.OrdinalIgnoreCase) - }); - } - - if (ActiveDynamicSidebarTabId != null && !allTabs.Any(t => string.Equals(t.Id, ActiveDynamicSidebarTabId, StringComparison.OrdinalIgnoreCase))) - { - ActiveDynamicSidebarTabId = null; - ActiveDynamicSidebarView = null; - ActiveSidebarTab = SidebarTabKind.Thumbnails; - } - }); - } - - [RelayCommand] - public void OpenRegisteredDialog(string dialogId) - { - if (_dialogRegistry == null) return; - var desc = _dialogRegistry.GetDialog(dialogId); - if (desc == null) return; - - object? dialogView = null; - if (desc.ViewFactory != null) - { - try - { - dialogView = desc.ViewFactory(_pluginHost?.Context ?? (IServiceProvider)this); - } - catch { } - } - else if (desc.ViewType != null) - { - try - { - dialogView = Activator.CreateInstance(desc.ViewType); - } - catch { } - } - - if (dialogView != null) - { - ActiveDynamicDialog = dialogView; - IsDynamicDialogOpen = true; - } - } - - [RelayCommand] - public void CloseDynamicDialog() - { - IsDynamicDialogOpen = false; - ActiveDynamicDialog = null; - } - - [RelayCommand] - public void ToggleOverlay(string overlayId) - { - _overlayRegistry?.ToggleOverlay(overlayId); - } - - [RelayCommand] - public void ShowOverlay(string overlayId) - { - _overlayRegistry?.ShowOverlay(overlayId); - } - - [RelayCommand] - public void HideOverlay(string overlayId) - { - _overlayRegistry?.HideOverlay(overlayId); - } - - - private void RefreshStatusBarWidgets() - { - if (_statusBarRegistry == null) return; - Dispatcher.UIThread.Post(() => - { - LeftStatusBarWidgets.Clear(); - RightStatusBarWidgets.Clear(); - - var left = _statusBarRegistry.GetWidgets(PdfEditorApp.Core.Plugins.Descriptors.StatusBarAlignment.Left); - foreach (var w in left) - { - var item = w.Factory(_pluginHost?.Context ?? (IServiceProvider)this); - if (item is StatusBarWidgetViewModel vm) - { - LeftStatusBarWidgets.Add(vm); - } - else if (item != null) - { - LeftStatusBarWidgets.Add(new StatusBarWidgetViewModel - { - WidgetId = w.WidgetId, - ToolTip = w.ToolTip, - Content = item - }); - } - } - - var right = _statusBarRegistry.GetWidgets(PdfEditorApp.Core.Plugins.Descriptors.StatusBarAlignment.Right); - foreach (var w in right) - { - var item = w.Factory(_pluginHost?.Context ?? (IServiceProvider)this); - if (item is StatusBarWidgetViewModel vm) - { - RightStatusBarWidgets.Add(vm); - } - else if (item != null) - { - RightStatusBarWidgets.Add(new StatusBarWidgetViewModel - { - WidgetId = w.WidgetId, - ToolTip = w.ToolTip, - Content = item - }); - } - } - }); - } - - [RelayCommand] - public void ExecuteRibbonAction(RibbonActionDescriptor action) - { - if (action?.Action != null) - { - try - { - action.Action(_pluginHost?.Context ?? (IServiceProvider)this); - } - catch (Exception ex) - { - ShowToast($"Plugin action error: {ex.Message}", ToastNotificationType.Danger, "AlertCircleOutline"); - } - } - } - - [RelayCommand] - public void ToggleRibbonCollapse() - { - IsRibbonCollapsed = !IsRibbonCollapsed; - ShowToast(IsRibbonCollapsed ? "Ribbon Minimized (⌘F1 to expand)" : "Ribbon Expanded", IsRibbonCollapsed ? "ChevronDown" : "ChevronUp"); - } - - [RelayCommand] - public void ToggleLeftSidebar() - { - IsLeftSidebarCollapsed = !IsLeftSidebarCollapsed; - ShowToast(IsLeftSidebarCollapsed ? "Pages Sidebar Minimized (⌘B to expand)" : "Pages Sidebar Expanded", IsLeftSidebarCollapsed ? "DockLeft" : "FileDocumentMultipleOutline"); - } - - [RelayCommand] - public void ToggleInspectorCollapse() - { - IsInspectorCollapsed = !IsInspectorCollapsed; - ShowToast(IsInspectorCollapsed ? "Inspector Minimized (⌘⇧P to expand)" : "Properties Inspector Expanded", IsInspectorCollapsed ? "DockRight" : "TuneVariant"); - } - - [RelayCommand] - public void ExpandAllPanels() - { - IsRibbonCollapsed = false; - IsLeftSidebarCollapsed = false; - IsInspectorCollapsed = false; - ShowToast("All Workspace Panels Expanded", "ViewQuiltOutline"); - } - - [RelayCommand] - public void SetToolMode(string modeStr) - { - if (Enum.TryParse(modeStr, true, out var parsed)) - { - ActiveToolMode = parsed; - ShowToast($"Active Tool: {parsed}", "CursorDefaultOutline"); - } - } - - // --- UNDO / REDO & CLIPBOARD COMMANDS --- - - public bool CanUndo => UndoRedo.CanUndo; - public bool CanRedo => UndoRedo.CanRedo; - - [RelayCommand(CanExecute = nameof(CanUndo))] - public void Undo() - { - if (UndoRedo.CanUndo) - { - var desc = UndoRedo.Undo(); - ShowToast($"Undone: {desc ?? "Action"}", "Undo"); - } - else - { - ShowToast("Nothing to undo", "InformationOutline"); - } - } - - [RelayCommand(CanExecute = nameof(CanRedo))] - public void Redo() - { - if (UndoRedo.CanRedo) - { - var desc = UndoRedo.Redo(); - ShowToast($"Redone: {desc ?? "Action"}", "Redo"); - } - else - { - ShowToast("Nothing to redo", "InformationOutline"); - } - } - - [RelayCommand] - public void Copy() - { - if (CurrentPage == null) return; - if (CurrentPage.SelectedElements.Count > 0) - { - _clipboardElements = CurrentPage.SelectedElements.ToList(); - ShowToast(_clipboardElements.Count == 1 ? $"Copied: {_clipboardElements[0].DisplayName}" : $"Copied {_clipboardElements.Count} Elements", "ContentCopy"); - } - } - - [RelayCommand] - public void Cut() - { - if (CurrentPage == null || CurrentPage.SelectedElements.Count == 0) return; - _clipboardElements = CurrentPage.SelectedElements.ToList(); - var page = CurrentPage; - var targets = _clipboardElements.ToList(); - - foreach (var el in targets) page.RemoveElement(el); - Inspector.UpdateSelection(null, page); - - UndoRedo.RecordAction( - targets.Count == 1 ? $"Cut {targets[0].DisplayName}" : $"Cut {targets.Count} Elements", - () => { - foreach (var el in targets) page.AddElement(el); - page.SelectElements(targets); - Inspector.UpdateSelection(page.SelectedElement, page); - }, - () => { - foreach (var el in targets) page.RemoveElement(el); - Inspector.UpdateSelection(null, page); - } - ); - - ShowToast(targets.Count == 1 ? $"Cut: {targets[0].DisplayName}" : $"Cut {targets.Count} Elements", "ContentCut"); - } - - [RelayCommand] - public void Paste() - { - if (_clipboardElements.Count == 0 || CurrentPage == null) return; - var page = CurrentPage; - var newVms = new List(); - - if (_clipboardElements.Count == 1) - { - var el = _clipboardElements[0]; - var model = el.ToModel(); - var clone = model.Clone(); - clone.Id = Guid.NewGuid().ToString("N"); - - var (posX, posY) = _placementService.GetPlacementPosition(CurrentPage, clone.Width, clone.Height); - clone.X = posX; - clone.Y = posY; - - ElementViewModelBase newVm = _elementService.CreateViewModel(clone); - page.AddElement(newVm); - newVms.Add(newVm); - } - else - { - double minX = _clipboardElements.Min(e => e.X); - double minY = _clipboardElements.Min(e => e.Y); - double maxX = _clipboardElements.Max(e => e.X + e.Width); - double maxY = _clipboardElements.Max(e => e.Y + e.Height); - double groupW = maxX - minX; - double groupH = maxY - minY; - - var (targetX, targetY) = _placementService.GetPlacementPosition(CurrentPage, groupW, groupH); - double offsetX = targetX - minX; - double offsetY = targetY - minY; - - foreach (var el in _clipboardElements) - { - var model = el.ToModel(); - var clone = model.Clone(); - clone.Id = Guid.NewGuid().ToString("N"); - clone.X += offsetX; - clone.Y += offsetY; - - ElementViewModelBase newVm = _elementService.CreateViewModel(clone); - page.AddElement(newVm); - newVms.Add(newVm); - } - } - - page.SelectElements(newVms); - Inspector.UpdateSelection(page.SelectedElement, page); - - UndoRedo.RecordAction( - newVms.Count == 1 ? $"Paste {newVms[0].DisplayName}" : $"Paste {newVms.Count} Elements", - () => { - foreach (var el in newVms) page.RemoveElement(el); - Inspector.UpdateSelection(null, page); - }, - () => { - foreach (var el in newVms) page.AddElement(el); - page.SelectElements(newVms); - Inspector.UpdateSelection(page.SelectedElement, page); - } - ); - - ShowToast(newVms.Count == 1 ? $"Pasted: {newVms[0].DisplayName}" : $"Pasted {newVms.Count} Elements", "ContentPaste"); - } - - [RelayCommand] - public void Duplicate() - { - Inspector.DuplicateSelectedElementCommand.Execute(null); - } - - [RelayCommand] - public void SelectAll() - { - CurrentPage?.SelectAll(); - Inspector.UpdateSelection(CurrentPage?.SelectedElement, CurrentPage); - } - - // --- ZOOM COMMANDS --- - - [RelayCommand] - public void ZoomIn() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.ZoomIn(); - return; - } - ZoomLevel = Math.Clamp(Math.Round(ZoomLevel * 1.2, 2), 0.1, 5.0); - ShowToast($"Zoom: {(int)(ZoomLevel * 100)}%", "MagnifyPlusOutline"); - } - - [RelayCommand] - public void ZoomOut() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.ZoomOut(); - return; - } - ZoomLevel = Math.Clamp(Math.Round(ZoomLevel / 1.2, 2), 0.1, 5.0); - ShowToast($"Zoom: {(int)(ZoomLevel * 100)}%", "MagnifyMinusOutline"); - } - - [RelayCommand] - public void ResetZoom() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.ResetZoom(); - return; - } - ZoomLevel = 1.0; - ShowToast("Zoom Reset (100%)", "Magnify"); - } - - [RelayCommand] - public void FitToWidth() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.FitToWidth(); - return; - } - ZoomLevel = 1.15; - ShowToast("Fit to Width", "ArrowExpandHorizontal"); - } - - public void FitToWidthDynamic(double viewportWidth) - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.FitToWidthDynamic(viewportWidth); - return; - } - if (CurrentPage != null && viewportWidth > 100) - { - ZoomLevel = Math.Clamp(Math.Round((viewportWidth - 64.0) / CurrentPage.Width, 2), 0.1, 5.0); - ShowToast($"Fit to Width ({(int)(ZoomLevel * 100)}%)", "ArrowExpandHorizontal"); - } - else - { - FitToWidth(); - } - } - - [RelayCommand] - public void FitToPage() - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.FitToPage(); - return; - } - ZoomLevel = 0.85; - ShowToast("Fit to Page", "FitToPageOutline"); - } - - public void FitToPageDynamic(double viewportWidth, double viewportHeight) - { - if (IsPdfViewerVisible && PdfViewer != null) - { - PdfViewer.FitToPageDynamic(viewportWidth, viewportHeight); - return; - } - if (CurrentPage != null && viewportWidth > 100 && viewportHeight > 100) - { - double scaleX = (viewportWidth - 64.0) / CurrentPage.Width; - double scaleY = (viewportHeight - 64.0) / CurrentPage.Height; - ZoomLevel = Math.Clamp(Math.Round(Math.Min(scaleX, scaleY), 2), 0.1, 5.0); - ShowToast($"Fit to Page ({(int)(ZoomLevel * 100)}%)", "FitToPageOutline"); - } - else - { - FitToPage(); - } - } - - [RelayCommand] - public void RenameCurrentDocument() - { - if (IsPdfViewerVisible && !string.IsNullOrEmpty(PdfViewer.CurrentFilePath)) - { - Home.PromptRename(PdfViewer.CurrentFilePath); - } - else if (IsEditorVisible) - { - if (!string.IsNullOrEmpty(CurrentFilePath) && File.Exists(CurrentFilePath)) - { - Home.PromptRename(CurrentFilePath); - } - else - { - var fallbackPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), DocumentTitle); - Home.PromptRename(fallbackPath); - } - } - } - - [RelayCommand] - public void DuplicateCurrentDocument() - { - var target = IsPdfViewerVisible ? PdfViewer.CurrentFilePath : CurrentFilePath; - if (!string.IsNullOrEmpty(target) && File.Exists(target)) - { - Home.DuplicateDocument(target); - } - else - { - ShowToast("Please save the document before duplicating", "AlertCircleOutline"); - } - } - - [RelayCommand] - public void RevealCurrentDocumentInFileManager() - { - var target = IsPdfViewerVisible ? PdfViewer.CurrentFilePath : CurrentFilePath; - if (!string.IsNullOrEmpty(target) && File.Exists(target)) - { - Home.RevealInFileManager(target); - } - else - { - ShowToast("Document has not been saved to disk yet", "InformationOutline"); - } - } - - [RelayCommand] - public async Task CopyCurrentDocumentPath() - { - var target = IsPdfViewerVisible ? PdfViewer.CurrentFilePath : CurrentFilePath; - if (!string.IsNullOrEmpty(target)) - { - await Home.CopyPath(target); - } - else - { - ShowToast("Document has no saved disk path yet", "InformationOutline"); - } - } - - [RelayCommand] - public void DeleteCurrentDocument() - { - var target = IsPdfViewerVisible ? PdfViewer.CurrentFilePath : CurrentFilePath; - if (!string.IsNullOrEmpty(target) && File.Exists(target)) - { - Home.PromptDelete(target); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/NavigationItemViewModel.cs b/src/PdfEditorApp/ViewModels/NavigationItemViewModel.cs deleted file mode 100644 index 9e89711..0000000 --- a/src/PdfEditorApp/ViewModels/NavigationItemViewModel.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.ViewModels; - -/// -/// ViewModel representing a dynamic workspace navigation section contributed by a plugin. -/// -public partial class NavigationItemViewModel : ObservableObject -{ - [ObservableProperty] - private string _id; - - [ObservableProperty] - private string _title; - - [ObservableProperty] - private string _group; - - [ObservableProperty] - private string _iconKind; - - [ObservableProperty] - private string? _badgeText; - - [ObservableProperty] - private string? _badgeColorHex; - - [ObservableProperty] - private int _order; - - [ObservableProperty] - private bool _isActive; - - public NavigationItemDescriptor Descriptor { get; } - - public NavigationItemViewModel(NavigationItemDescriptor descriptor) - { - Descriptor = descriptor; - _id = descriptor.Id; - _title = descriptor.Title; - _group = descriptor.Group; - _iconKind = descriptor.IconKind; - _badgeText = descriptor.BadgeText; - _badgeColorHex = descriptor.BadgeColorHex; - _order = descriptor.Order; - } -} - -/// -/// Groups a set of navigation items under a single section header for plugin-driven sidebar rendering. -/// -public sealed class NavGroupViewModel -{ - /// Header label displayed above the group (e.g. "OVERVIEW", "CATEGORIES"). - public string Header { get; } - - /// Ordered navigation items belonging to this group. - public IReadOnlyList Items { get; } - - public NavGroupViewModel(string header, IReadOnlyList items) - { - Header = header.ToUpperInvariant(); - Items = items; - } -} diff --git a/src/PdfEditorApp/ViewModels/OverlayInstanceViewModel.cs b/src/PdfEditorApp/ViewModels/OverlayInstanceViewModel.cs deleted file mode 100644 index 02aab29..0000000 --- a/src/PdfEditorApp/ViewModels/OverlayInstanceViewModel.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Plugins.Descriptors; - -namespace PdfEditorApp.ViewModels; - -/// -/// ViewModel representing an active, floating, draggable overlay instance in the 'shell.overlay' slot. -/// Supports 60+ FPS dragging, minimizing to a pill, and clean dismissal. -/// -public partial class OverlayInstanceViewModel : ObservableObject -{ - private readonly Action? _onClose; - - [ObservableProperty] - private string _id = string.Empty; - - [ObservableProperty] - private string _title = string.Empty; - - [ObservableProperty] - private string _iconKind = "WindowRestore"; - - [ObservableProperty] - private double _x; - - [ObservableProperty] - private double _y; - - [ObservableProperty] - private double _width = 340; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(EffectiveHeight))] - private double _height = 420; - - [ObservableProperty] - private int _zIndex = 1; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(CanResize))] - private bool _isResizable = true; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasStandardChrome))] - [NotifyPropertyChangedFor(nameof(HasCustomChrome))] - [NotifyPropertyChangedFor(nameof(HasFloatingPill))] - [NotifyPropertyChangedFor(nameof(CustomChromeContent))] - [NotifyPropertyChangedFor(nameof(StandardChromeContent))] - private OverlayChromeMode _chromeMode = OverlayChromeMode.StandardCard; - - [ObservableProperty] - private bool _isPinned; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(EffectiveHeight))] - [NotifyPropertyChangedFor(nameof(EffectiveMinHeight))] - [NotifyPropertyChangedFor(nameof(CanResize))] - private bool _isMinimized; - - [ObservableProperty] - private bool _isVisible = true; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(CustomChromeContent))] - [NotifyPropertyChangedFor(nameof(StandardChromeContent))] - private object? _content; - - public bool HasStandardChrome => ChromeMode == OverlayChromeMode.StandardCard; - public bool HasCustomChrome => ChromeMode == OverlayChromeMode.CustomChrome; - public bool HasFloatingPill => ChromeMode == OverlayChromeMode.FloatingPill; - - public double EffectiveHeight => IsMinimized ? double.NaN : Height; - public double EffectiveMinHeight => IsMinimized ? 42 : Descriptor.MinHeight; - public bool CanResize => IsResizable && !IsMinimized; - - public object? CustomChromeContent => HasCustomChrome ? Content : null; - public object? StandardChromeContent => HasStandardChrome ? Content : null; - - public OverlayDescriptor Descriptor { get; } - - public OverlayInstanceViewModel(OverlayDescriptor descriptor, Action? onClose = null) - { - Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); - _id = descriptor.Id; - _title = descriptor.Title; - _iconKind = descriptor.IconKind; - _width = descriptor.DefaultWidth; - _height = descriptor.DefaultHeight; - _isResizable = descriptor.IsResizable; - _chromeMode = descriptor.ChromeMode; - _onClose = onClose; - } - - public void Resize(double newWidth, double newHeight, double maxCanvasWidth = double.PositiveInfinity, double maxCanvasHeight = double.PositiveInfinity) - { - var minW = Math.Max(120, Descriptor.MinWidth); - var minH = Math.Max(80, Descriptor.MinHeight); - var maxW = Math.Min(Descriptor.MaxWidth, maxCanvasWidth); - var maxH = Math.Min(Descriptor.MaxHeight, maxCanvasHeight); - - Width = Math.Clamp(newWidth, minW, Math.Max(minW, maxW)); - Height = Math.Clamp(newHeight, minH, Math.Max(minH, maxH)); - } - - [RelayCommand] - public void ToggleMinimize() - { - IsMinimized = !IsMinimized; - } - - [RelayCommand] - public void TogglePin() - { - IsPinned = !IsPinned; - } - - [RelayCommand] - public void Close() - { - IsVisible = false; - _onClose?.Invoke(this); - } - - public void BringToFront(System.Collections.Generic.IEnumerable allOverlays) - { - int max = 0; - foreach (var o in allOverlays) - { - if (o.ZIndex > max) max = o.ZIndex; - } - ZIndex = max + 1; - } -} diff --git a/src/PdfEditorApp/ViewModels/PageViewModel.cs b/src/PdfEditorApp/ViewModels/PageViewModel.cs deleted file mode 100644 index 7868ae3..0000000 --- a/src/PdfEditorApp/ViewModels/PageViewModel.cs +++ /dev/null @@ -1,501 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Avalonia; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Canvas; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.ViewModels; - -public partial class PageViewModel : ViewModelBase -{ - private readonly ICanvasElementService _elementService; - - public PageViewModel(ICanvasElementService? elementService = null) - { - _elementService = elementService ?? DefaultElementService; - } - - private static ICanvasElementService? _defaultElementService; - public static ICanvasElementService DefaultElementService - { - get => _defaultElementService ??= new CanvasElementRegistry(); - set => _defaultElementService = value; - } - - [ObservableProperty] - private string _id = Guid.NewGuid().ToString("N"); - - [ObservableProperty] - private int _pageNumber = 1; - - [ObservableProperty] - private PageFormat _format = PageFormat.A4; - - [ObservableProperty] - private PageOrientation _orientation = PageOrientation.Portrait; - - [ObservableProperty] - private int _rotationAngle = 0; - - [ObservableProperty] - private double _width = 800; - - [ObservableProperty] - private double _height = 1131; - - [ObservableProperty] - private string _backgroundColorHex = "#FFFFFF"; - - [ObservableProperty] - private bool _showHeaderFooter = true; - - [ObservableProperty] - private string? _headerLeft; - - [ObservableProperty] - private string? _headerCenter; - - [ObservableProperty] - private string? _headerRight; - - [ObservableProperty] - private string? _footerLeft = "CONFIDENTIAL & PROPRIETARY"; - - [ObservableProperty] - private string? _footerCenter; - - [ObservableProperty] - private string? _footerRight = "Page 1 of 1"; - - [ObservableProperty] - private bool _isSelected; - - [ObservableProperty] - private ElementViewModelBase? _selectedElement; - - [ObservableProperty] - private bool _hasMultiSelection; - - [ObservableProperty] - private int _selectionCount; - - [ObservableProperty] - private Rect _selectionBoundingBox; - - public ObservableCollection Elements { get; } = new(); - - public ObservableCollection SelectedElements { get; } = new(); - - public event Action? SelectionChanged; - public event Action? MultiSelectionChanged; - - public void SelectElement(ElementViewModelBase? element) - { - SelectedElements.Clear(); - - foreach (var el in Elements) - { - el.IsSelected = (el == element); - if (!el.IsSelected) el.IsInEditMode = false; - } - - if (element != null) - { - SelectedElements.Add(element); - } - - SelectedElement = element; - HasMultiSelection = false; - SelectionCount = SelectedElements.Count; - UpdateSelectionBoundingBox(); - - SelectionChanged?.Invoke(element); - MultiSelectionChanged?.Invoke(); - } - - public void ToggleElementSelection(ElementViewModelBase element) - { - if (SelectedElements.Contains(element)) - { - element.IsSelected = false; - element.IsInEditMode = false; - SelectedElements.Remove(element); - } - else - { - element.IsSelected = true; - SelectedElements.Add(element); - } - - SelectedElement = SelectedElements.LastOrDefault(); - HasMultiSelection = SelectedElements.Count > 1; - SelectionCount = SelectedElements.Count; - UpdateSelectionBoundingBox(); - - SelectionChanged?.Invoke(SelectedElement); - MultiSelectionChanged?.Invoke(); - } - - public void SelectElements(IEnumerable elements) - { - var targetSet = new HashSet(elements); - SelectedElements.Clear(); - - foreach (var el in Elements) - { - bool isSel = targetSet.Contains(el); - el.IsSelected = isSel; - if (!isSel) el.IsInEditMode = false; - if (isSel) SelectedElements.Add(el); - } - - SelectedElement = SelectedElements.LastOrDefault(); - HasMultiSelection = SelectedElements.Count > 1; - SelectionCount = SelectedElements.Count; - UpdateSelectionBoundingBox(); - - SelectionChanged?.Invoke(SelectedElement); - MultiSelectionChanged?.Invoke(); - } - - public void SelectAll() - { - SelectElements(Elements); - } - - public void ClearSelection() - { - SelectElement(null); - } - - public void UpdateSelectionBoundingBox() - { - if (SelectedElements.Count == 0) - { - SelectionBoundingBox = default; - return; - } - - double minX = SelectedElements.Min(e => e.X); - double minY = SelectedElements.Min(e => e.Y); - double maxX = SelectedElements.Max(e => e.X + Math.Max(1, e.Width)); - double maxY = SelectedElements.Max(e => e.Y + Math.Max(1, e.Height)); - - SelectionBoundingBox = new Rect(minX, minY, Math.Max(1, maxX - minX), Math.Max(1, maxY - minY)); - } - - public void AddElement(ElementViewModelBase element) - { - int maxZ = Elements.Count > 0 ? Elements.Max(e => e.ZIndex) + 1 : 1; - element.ZIndex = maxZ; - Elements.Add(element); - SelectElement(element); - } - - public void RemoveElement(ElementViewModelBase element) - { - if (SelectedElements.Contains(element)) - { - SelectedElements.Remove(element); - element.IsSelected = false; - } - - if (SelectedElement == element) - { - SelectedElement = SelectedElements.LastOrDefault(); - } - - Elements.Remove(element); - HasMultiSelection = SelectedElements.Count > 1; - SelectionCount = SelectedElements.Count; - UpdateSelectionBoundingBox(); - SelectionChanged?.Invoke(SelectedElement); - } - - public void BringToFront(ElementViewModelBase element) - { - if (!Elements.Contains(element) || Elements.Count <= 1) return; - int oldIndex = Elements.IndexOf(element); - if (oldIndex != Elements.Count - 1) - { - Elements.Move(oldIndex, Elements.Count - 1); - } - NormalizeZIndices(); - } - - public void SendToBack(ElementViewModelBase element) - { - if (!Elements.Contains(element) || Elements.Count <= 1) return; - int oldIndex = Elements.IndexOf(element); - if (oldIndex != 0) - { - Elements.Move(oldIndex, 0); - } - NormalizeZIndices(); - } - - public void BringForward(ElementViewModelBase element) - { - if (!Elements.Contains(element) || Elements.Count <= 1) return; - int oldIndex = Elements.IndexOf(element); - if (oldIndex < Elements.Count - 1) - { - Elements.Move(oldIndex, oldIndex + 1); - NormalizeZIndices(); - } - } - - public void SendBackward(ElementViewModelBase element) - { - if (!Elements.Contains(element) || Elements.Count <= 1) return; - int oldIndex = Elements.IndexOf(element); - if (oldIndex > 0) - { - Elements.Move(oldIndex, oldIndex - 1); - NormalizeZIndices(); - } - } - - public void NormalizeZIndices() - { - for (int i = 0; i < Elements.Count; i++) - { - Elements[i].ZIndex = i + 1; - } - } - - public void GroupSelected() - { - if (SelectedElements.Count < 2) return; - string newGroupId = Guid.NewGuid().ToString("N"); - foreach (var el in SelectedElements) - { - el.GroupId = newGroupId; - } - } - - public void UngroupSelected() - { - foreach (var el in SelectedElements) - { - el.GroupId = null; - } - } - - public void AlignSelectedLeft() - { - if (SelectedElements.Count < 2) return; - double minX = SelectedElements.Min(e => e.X); - foreach (var el in SelectedElements.Where(e => !e.IsLocked)) - { - el.X = minX; - } - UpdateSelectionBoundingBox(); - } - - public void AlignSelectedRight() - { - if (SelectedElements.Count < 2) return; - double maxRight = SelectedElements.Max(e => e.X + e.Width); - foreach (var el in SelectedElements.Where(e => !e.IsLocked)) - { - el.X = Math.Round(maxRight - el.Width, 1); - } - UpdateSelectionBoundingBox(); - } - - public void AlignSelectedCenter() - { - if (SelectedElements.Count < 2) return; - double minX = SelectedElements.Min(e => e.X); - double maxRight = SelectedElements.Max(e => e.X + e.Width); - double midX = (minX + maxRight) / 2.0; - - foreach (var el in SelectedElements.Where(e => !e.IsLocked)) - { - el.X = Math.Round(midX - (el.Width / 2.0), 1); - } - UpdateSelectionBoundingBox(); - } - - public void AlignSelectedTop() - { - if (SelectedElements.Count < 2) return; - double minY = SelectedElements.Min(e => e.Y); - foreach (var el in SelectedElements.Where(e => !e.IsLocked)) - { - el.Y = minY; - } - UpdateSelectionBoundingBox(); - } - - public void AlignSelectedBottom() - { - if (SelectedElements.Count < 2) return; - double maxBottom = SelectedElements.Max(e => e.Y + e.Height); - foreach (var el in SelectedElements.Where(e => !e.IsLocked)) - { - el.Y = Math.Round(maxBottom - el.Height, 1); - } - UpdateSelectionBoundingBox(); - } - - public void AlignSelectedMiddle() - { - if (SelectedElements.Count < 2) return; - double minY = SelectedElements.Min(e => e.Y); - double maxBottom = SelectedElements.Max(e => e.Y + e.Height); - double midY = (minY + maxBottom) / 2.0; - - foreach (var el in SelectedElements.Where(e => !e.IsLocked)) - { - el.Y = Math.Round(midY - (el.Height / 2.0), 1); - } - UpdateSelectionBoundingBox(); - } - - public void DistributeSelectedHorizontally() - { - if (SelectedElements.Count < 3) return; - var ordered = SelectedElements.Where(e => !e.IsLocked).OrderBy(e => e.X).ToList(); - if (ordered.Count < 3) return; - - double startX = ordered.First().X; - double endX = ordered.Last().X + ordered.Last().Width; - double totalItemW = ordered.Sum(e => e.Width); - double availableGap = endX - startX - totalItemW; - double gapPerItem = Math.Max(0, availableGap / (ordered.Count - 1)); - - double currentX = startX; - for (int i = 0; i < ordered.Count; i++) - { - ordered[i].X = Math.Round(currentX, 1); - currentX += ordered[i].Width + gapPerItem; - } - UpdateSelectionBoundingBox(); - } - - public void DistributeSelectedVertically() - { - if (SelectedElements.Count < 3) return; - var ordered = SelectedElements.Where(e => !e.IsLocked).OrderBy(e => e.Y).ToList(); - if (ordered.Count < 3) return; - - double startY = ordered.First().Y; - double endY = ordered.Last().Y + ordered.Last().Height; - double totalItemH = ordered.Sum(e => e.Height); - double availableGap = endY - startY - totalItemH; - double gapPerItem = Math.Max(0, availableGap / (ordered.Count - 1)); - - double currentY = startY; - for (int i = 0; i < ordered.Count; i++) - { - ordered[i].Y = Math.Round(currentY, 1); - currentY += ordered[i].Height + gapPerItem; - } - UpdateSelectionBoundingBox(); - } - - [RelayCommand] - public void RotateClockwise() - { - RotationAngle = (RotationAngle + 90) % 360; - } - - public PdfPageModel ToModel() - { - var model = new PdfPageModel - { - Id = Id, - PageNumber = PageNumber, - Format = Format, - Orientation = Orientation, - RotationAngle = RotationAngle, - Width = Width, - Height = Height, - BackgroundColorHex = BackgroundColorHex, - ShowHeaderFooter = ShowHeaderFooter, - HeaderLeft = HeaderLeft, - HeaderCenter = HeaderCenter, - HeaderRight = HeaderRight, - FooterLeft = FooterLeft, - FooterCenter = FooterCenter, - FooterRight = FooterRight - }; - - foreach (var el in Elements) - { - model.Elements.Add(el.ToModel()); - } - - return model; - } - - public void LoadFromModel(PdfPageModel model) - { - Id = model.Id; - PageNumber = model.PageNumber; - Format = model.Format; - Orientation = model.Orientation; - RotationAngle = model.RotationAngle; - Width = model.Width; - Height = model.Height; - BackgroundColorHex = model.BackgroundColorHex; - ShowHeaderFooter = model.ShowHeaderFooter; - HeaderLeft = model.HeaderLeft; - HeaderCenter = model.HeaderCenter; - HeaderRight = model.HeaderRight; - FooterLeft = model.FooterLeft; - FooterCenter = model.FooterCenter; - FooterRight = model.FooterRight; - - Elements.Clear(); - SelectedElements.Clear(); - SelectedElement = null; - HasMultiSelection = false; - SelectionCount = 0; - - foreach (var elModel in model.Elements) - { - ElementViewModelBase vm = _elementService.CreateViewModel(elModel); - Elements.Add(vm); - } - } - - public void RecalculateFormFields() - { - var formFields = Elements.OfType().ToList(); - foreach (var field in formFields.Where(f => f.CalculationFormula != CalculationFormula.None)) - { - if (string.IsNullOrWhiteSpace(field.CalculationSourceFields)) continue; - var sourceNames = field.CalculationSourceFields - .Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - var values = formFields - .Where(f => sourceNames.Contains(f.FieldName, StringComparer.OrdinalIgnoreCase)) - .Select(f => double.TryParse(f.Value, out var v) ? v : (double.TryParse(f.DefaultValue, out var dv) ? dv : 0.0)) - .ToList(); - - if (values.Count == 0) continue; - - double result = field.CalculationFormula switch - { - CalculationFormula.Sum => values.Sum(), - CalculationFormula.Average => values.Average(), - CalculationFormula.Product => values.Aggregate(1.0, (acc, x) => acc * x), - CalculationFormula.Min => values.Min(), - CalculationFormula.Max => values.Max(), - _ => 0 - }; - - field.Value = result.ToString("F2"); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/PdfToolCardViewModel.cs b/src/PdfEditorApp/ViewModels/PdfToolCardViewModel.cs deleted file mode 100644 index 28c0978..0000000 --- a/src/PdfEditorApp/ViewModels/PdfToolCardViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels; - -public partial class PdfToolCardViewModel : ViewModelBase -{ - public PdfToolDefinition Definition { get; } - - public PdfToolId Id => Definition.Id; - public string Name => Definition.Name; - public string Description => Definition.Description; - public PdfToolCategory Category => Definition.Category; - public string CategoryDisplayName => Definition.CategoryDisplayName; - public string IconKind => Definition.IconKind; - public string IconColorHex => Definition.IconColorHex; - public string BackgroundAccentHex => Definition.BackgroundAccentHex; - public bool IsNew => Definition.IsNew; - public bool IsWorkflowBanner => Definition.IsWorkflowBanner; - public bool SupportsMultiFile => Definition.SupportsMultiFile; - - [ObservableProperty] - private bool _isStarred; - - public event Action? ToolSelected; - public event Action? StarToggled; - - public PdfToolCardViewModel(PdfToolDefinition definition) - { - Definition = definition; - } - - [RelayCommand] - public void SelectTool() - { - ToolSelected?.Invoke(Id); - } - - [RelayCommand] - public void ToggleStar() - { - IsStarred = !IsStarred; - StarToggled?.Invoke(Id); - } -} diff --git a/src/PdfEditorApp/ViewModels/PdfToolRunnerViewModel.cs b/src/PdfEditorApp/ViewModels/PdfToolRunnerViewModel.cs deleted file mode 100644 index 5949824..0000000 --- a/src/PdfEditorApp/ViewModels/PdfToolRunnerViewModel.cs +++ /dev/null @@ -1,577 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels; - -public partial class PdfToolRunnerViewModel : ViewModelBase -{ - private readonly IPdfDocumentOperationsService _operationsService; - private CancellationTokenSource? _cts; - - public IStorageProvider? StorageProvider { get; set; } - - [ObservableProperty] - private PdfToolDefinition _tool = new(); - - [ObservableProperty] - private bool _isOpen; - - [ObservableProperty] - private bool _isRunning; - - [ObservableProperty] - private bool _isComplete; - - [ObservableProperty] - private bool _hasError; - - [ObservableProperty] - private string _errorMessage = string.Empty; - - [ObservableProperty] - private string _statusMessage = "Ready"; - - [ObservableProperty] - private double _progressPercentage; - - [ObservableProperty] - private string _lastOutputFilePath = string.Empty; - - [ObservableProperty] - private string _resultSummaryMessage = string.Empty; - - public ObservableCollection SelectedFiles { get; } = new(); - - public bool HasSelectedFiles => SelectedFiles.Count > 0; - public string SelectedFilesCountText => SelectedFiles.Count == 1 ? "1 file selected" : $"{SelectedFiles.Count} files selected"; - - // --- TOOL-SPECIFIC OBSERVABLE OPTIONS --- - - // Split - [ObservableProperty] private SplitExtractMode _splitMode = SplitExtractMode.SplitEveryNPages; - [ObservableProperty] private int _splitPagesInterval = 1; - [ObservableProperty] private string _splitRangeExpression = "1-3, 5, 7-10"; - [ObservableProperty] private bool _splitOddEven; - - // Compress - [ObservableProperty] private PdfCompressionLevel _compressionLevel = PdfCompressionLevel.Balanced; - [ObservableProperty] private bool _compressRemoveMetadata; - - // Watermark - [ObservableProperty] private string _watermarkText = "CONFIDENTIAL"; - [ObservableProperty] private double _watermarkOpacity = 0.35; - [ObservableProperty] private double _watermarkRotation = -45; - [ObservableProperty] private WatermarkPosition _watermarkPosition = WatermarkPosition.Center; - [ObservableProperty] private string _watermarkColorHex = "#EF4444"; - [ObservableProperty] private double _watermarkFontSize = 48; - - // Rotate - [ObservableProperty] private int _rotateDegrees = 90; - [ObservableProperty] private PageFilterTarget _rotateFilter = PageFilterTarget.All; - - // Security & Unlock - [ObservableProperty] private string _userPassword = string.Empty; - [ObservableProperty] private string _ownerPassword = string.Empty; - [ObservableProperty] private bool _allowPrinting = true; - [ObservableProperty] private bool _allowCopying = false; - [ObservableProperty] private bool _allowModifying = false; - - // Page Numbers - [ObservableProperty] private PageNumberPosition _pageNumberPosition = PageNumberPosition.BottomCenter; - [ObservableProperty] private string _pageNumberTemplate = "Page {n} of {total}"; - [ObservableProperty] private int _pageNumberStart = 1; - [ObservableProperty] private string _pageNumberColorHex = "#334155"; - - // OCR - [ObservableProperty] private string _ocrLanguage = "eng"; - [ObservableProperty] private bool _ocrSearchablePdf = true; - - // AI Summary - [ObservableProperty] private int _aiMaxBullets = 7; - [ObservableProperty] private bool _aiIncludeExecutive = true; - [ObservableProperty] private bool _aiIncludeActions = true; - - // Translation - [ObservableProperty] private string _targetLanguage = "Spanish"; - - // HTML / Web - [ObservableProperty] private string _htmlInputTextOrUrl = "https://"; - - // Image to PDF / PDF to Image - [ObservableProperty] private string _imageExportFormat = "jpg"; - [ObservableProperty] private int _imageDpi = 300; - [ObservableProperty] private bool _imageFitToPage = true; - - // Cropping - [ObservableProperty] private double _cropMarginTop = 36; - [ObservableProperty] private double _cropMarginBottom = 36; - [ObservableProperty] private double _cropMarginLeft = 36; - [ObservableProperty] private double _cropMarginRight = 36; - - // Scan - [ObservableProperty] private bool _scanAutoDeskew = true; - [ObservableProperty] private bool _scanEnhanceContrast = true; - [ObservableProperty] private bool _scanWhitenBackground = true; - - // Redaction - [ObservableProperty] private string _redactSearchPattern = "CONFIDENTIAL"; - [ObservableProperty] private bool _redactCaseSensitive = false; - [ObservableProperty] private bool _redactPermanentScrub = true; - - // Forms - [ObservableProperty] private bool _formFlattenFields = false; - [ObservableProperty] private bool _formExportJson = false; - - // Markdown - [ObservableProperty] private bool _markdownIncludeTables = true; - [ObservableProperty] private bool _markdownExtractImages = false; - - // Compare - [ObservableProperty] private string _compareDocBPath = string.Empty; - - // Starred state for active tool - [ObservableProperty] private bool _isToolStarred; - - // Navigation events - public event Action? BackRequested; - - public PdfToolRunnerViewModel(IPdfDocumentOperationsService operationsService) - { - _operationsService = operationsService; - } - - [RelayCommand] - public void GoBack() - { - BackRequested?.Invoke(); - } - - [RelayCommand] - public void OpenInEditor() - { - if (!string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath)) - { - WeakReferenceMessenger.Default.Send(new OpenInEditorMessage(LastOutputFilePath)); - } - } - - [RelayCommand] - public void OpenInViewer() - { - string targetPath = !string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath) - ? LastOutputFilePath - : (SelectedFiles.Count > 0 && File.Exists(SelectedFiles[0]) ? SelectedFiles[0] : string.Empty); - - if (!string.IsNullOrEmpty(targetPath)) - { - WeakReferenceMessenger.Default.Send(new OpenInViewerMessage(targetPath)); - } - } - - public void SetupForTool(PdfToolDefinition tool, string? initialFilePath = null) - { - Tool = tool; - SelectedFiles.Clear(); - if (!string.IsNullOrEmpty(initialFilePath) && File.Exists(initialFilePath)) - { - SelectedFiles.Add(initialFilePath); - } - - ResetState(); - IsOpen = true; - } - - public void ResetState() - { - IsRunning = false; - IsComplete = false; - HasError = false; - ErrorMessage = string.Empty; - StatusMessage = "Ready"; - ProgressPercentage = 0; - LastOutputFilePath = string.Empty; - ResultSummaryMessage = string.Empty; - OnPropertyChanged(nameof(HasSelectedFiles)); - OnPropertyChanged(nameof(SelectedFilesCountText)); - } - - [RelayCommand] - public async Task AddFilesAsync() - { - if (StorageProvider == null) return; - - var patterns = Tool.AcceptedFileExtensions.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => e.Trim().StartsWith("*") ? e.Trim() : "*" + e.Trim()) - .ToArray(); - - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = $"Select Files for {Tool.Name}", - AllowMultiple = Tool.SupportsMultiFile, - FileTypeFilter = new[] - { - new FilePickerFileType($"{Tool.Name} Inputs") - { - Patterns = patterns.Length > 0 ? patterns : new[] { "*.pdf" } - } - } - }); - - if (files != null && files.Count > 0) - { - if (!Tool.SupportsMultiFile) SelectedFiles.Clear(); - foreach (var f in files) - { - string path = f.Path.LocalPath; - if (!SelectedFiles.Contains(path)) SelectedFiles.Add(path); - } - ResetState(); - } - } - - [RelayCommand] - public void RemoveFile(string filePath) - { - SelectedFiles.Remove(filePath); - ResetState(); - } - - [RelayCommand] - public void ClearFiles() - { - SelectedFiles.Clear(); - ResetState(); - } - - [RelayCommand] - public void Close() - { - CancelExecution(); - IsOpen = false; - } - - [RelayCommand] - public void CancelExecution() - { - if (_cts != null && !_cts.IsCancellationRequested) - { - _cts.Cancel(); - StatusMessage = "Cancelling..."; - } - } - - [RelayCommand] - public async Task ExecuteToolAsync() - { - if (SelectedFiles.Count == 0 && Tool.Id != PdfToolId.HtmlToPdf) - { - HasError = true; - ErrorMessage = "Please select at least one file to process."; - return; - } - - IsRunning = true; - IsComplete = false; - HasError = false; - ErrorMessage = string.Empty; - ProgressPercentage = 5.0; - StatusMessage = $"Processing {Tool.Name}..."; - - _cts = new CancellationTokenSource(); - var progress = new Progress(p => - { - ProgressPercentage = p; - StatusMessage = $"Processing ({p:F0}%)..."; - }); - - try - { - object options = BuildOptionsObject(); - var result = await _operationsService.ExecuteToolAsync(Tool.Id, options, progress, _cts.Token); - - if (result.Success) - { - IsComplete = true; - LastOutputFilePath = result.OutputFilePath ?? ""; - ResultSummaryMessage = result.Message ?? "Operation completed successfully."; - StatusMessage = "Completed successfully!"; - ProgressPercentage = 100.0; - } - else - { - HasError = true; - ErrorMessage = result.ErrorMessage ?? "Operation failed."; - StatusMessage = "Failed"; - } - } - catch (OperationCanceledException) - { - StatusMessage = "Operation cancelled."; - } - catch (Exception ex) - { - HasError = true; - ErrorMessage = $"Unexpected error: {ex.Message}"; - StatusMessage = "Error"; - } - finally - { - IsRunning = false; - _cts = null; - } - } - - [RelayCommand] - public void OpenOutputFile() - { - if (!string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath)) - { - try - { - Process.Start(new ProcessStartInfo - { - FileName = LastOutputFilePath, - UseShellExecute = true - }); - } - catch { } - } - } - - [RelayCommand] - public void OpenOutputFolder() - { - if (!string.IsNullOrEmpty(LastOutputFilePath)) - { - string? dir = Directory.Exists(LastOutputFilePath) ? LastOutputFilePath : Path.GetDirectoryName(LastOutputFilePath); - if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) - { - try - { - Process.Start(new ProcessStartInfo - { - FileName = dir, - UseShellExecute = true - }); - } - catch { } - } - } - } - - private object BuildOptionsObject() - { - string firstFile = SelectedFiles.FirstOrDefault() ?? ""; - - switch (Tool.Id) - { - case PdfToolId.MergePdf: - return new MergeToolOptions { InputFiles = SelectedFiles.ToList() }; - - case PdfToolId.SplitPdf: - return new SplitToolOptions - { - InputFilePath = firstFile, - Mode = SplitMode, - PagesPerSplit = SplitPagesInterval, - RangeExpression = SplitRangeExpression, - SplitOddEven = SplitOddEven - }; - - case PdfToolId.CompressPdf: - return new CompressToolOptions - { - InputFilePath = firstFile, - Level = CompressionLevel, - RemoveMetadata = CompressRemoveMetadata - }; - - case PdfToolId.Watermark: - return new WatermarkToolOptions - { - InputFilePath = firstFile, - Text = WatermarkText, - Opacity = WatermarkOpacity, - RotationAngle = WatermarkRotation, - Position = WatermarkPosition, - ColorHex = WatermarkColorHex, - FontSize = WatermarkFontSize - }; - - case PdfToolId.RotatePdf: - return new RotateToolOptions - { - InputFilePath = firstFile, - RotationDegrees = RotateDegrees, - TargetFilter = RotateFilter - }; - - case PdfToolId.ProtectPdf: - return new SecurityToolOptions - { - InputFilePath = firstFile, - UserPassword = UserPassword, - OwnerPassword = OwnerPassword, - AllowPrinting = AllowPrinting, - AllowCopying = AllowCopying, - AllowModifying = AllowModifying - }; - - case PdfToolId.UnlockPdf: - return new UnlockToolOptions - { - InputFilePath = firstFile, - Password = UserPassword - }; - - case PdfToolId.PageNumbers: - return new PageNumberToolOptions - { - InputFilePath = firstFile, - Template = PageNumberTemplate, - Position = PageNumberPosition, - StartingNumber = PageNumberStart, - ColorHex = PageNumberColorHex - }; - - case PdfToolId.PdfToWord: - return new WordConversionOptions { InputFilePath = firstFile }; - - case PdfToolId.PdfToExcel: - return new ExcelConversionOptions { InputFilePath = firstFile }; - - case PdfToolId.PdfToPowerPoint: - return new PptxConversionOptions { InputFilePath = firstFile }; - - case PdfToolId.WordToPdf: - case PdfToolId.ExcelToPdf: - case PdfToolId.PowerPointToPdf: - return new OfficeToPdfOptions { InputFilePath = firstFile }; - - case PdfToolId.PdfToJpg: - return new ImageConversionOptions - { - InputFilePath = firstFile, - OutputFormat = ImageExportFormat, - Dpi = ImageDpi - }; - - case PdfToolId.JpgToPdf: - return new ImagesToPdfOptions - { - ImageFiles = SelectedFiles.ToList(), - FitToPage = ImageFitToPage - }; - - case PdfToolId.HtmlToPdf: - return new HtmlToPdfOptions - { - HtmlContentOrUrl = HtmlInputTextOrUrl, - IsUrl = HtmlInputTextOrUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase) - }; - - case PdfToolId.PdfToPdfA: - return new PdfAToolOptions { InputFilePath = firstFile }; - - case PdfToolId.RepairPdf: - return new RepairToolOptions { InputFilePath = firstFile }; - - case PdfToolId.OcrPdf: - return new OcrToolOptions { InputFilePath = firstFile, Language = OcrLanguage }; - - case PdfToolId.ScanToPdf: - return new ScanToolOptions - { - InputImageFiles = SelectedFiles.ToList(), - AutoDeskew = ScanAutoDeskew, - EnhanceContrast = ScanEnhanceContrast, - WhitenBackground = ScanWhitenBackground - }; - - case PdfToolId.AiSummarizer: - return new AiSummaryOptions - { - InputFilePath = firstFile, - MaxBulletPoints = AiMaxBullets, - IncludeExecutiveSummary = AiIncludeExecutive, - IncludeActionItems = AiIncludeActions - }; - - case PdfToolId.TranslatePdf: - return new TranslationOptions - { - InputFilePath = firstFile, - TargetLanguage = TargetLanguage - }; - - case PdfToolId.PdfToMarkdown: - return new MarkdownConversionOptions - { - InputFilePath = firstFile, - IncludeTables = MarkdownIncludeTables, - IncludeImages = MarkdownExtractImages - }; - - case PdfToolId.SignPdf: - return new SignToolOptions - { - InputFilePath = firstFile, - SignerName = "Authorized Signatory", - Style = SignatureStyle.CursiveElegance - }; - - case PdfToolId.RedactPdf: - return new RedactionToolOptions - { - InputFilePath = firstFile, - SearchPatternToRedact = RedactSearchPattern, - CaseSensitive = RedactCaseSensitive, - PermanentScrubText = RedactPermanentScrub - }; - - case PdfToolId.CropPdf: - return new CropToolOptions - { - InputFilePath = firstFile, - CropTopPoints = CropMarginTop, - CropBottomPoints = CropMarginBottom, - CropLeftPoints = CropMarginLeft, - CropRightPoints = CropMarginRight - }; - - case PdfToolId.PdfForms: - return new FormToolOptions - { - InputFilePath = firstFile, - FlattenFields = FormFlattenFields, - ExportFieldValuesJson = FormExportJson - }; - - case PdfToolId.ComparePdf: - return new CompareToolOptions - { - DocumentAPath = firstFile, - DocumentBPath = !string.IsNullOrEmpty(CompareDocBPath) ? CompareDocBPath : (SelectedFiles.Count > 1 ? SelectedFiles[1] : firstFile) - }; - - default: - return new { InputFilePath = firstFile }; - } - } -} diff --git a/src/PdfEditorApp/ViewModels/PdfViewerModels.cs b/src/PdfEditorApp/ViewModels/PdfViewerModels.cs deleted file mode 100644 index 3475c2d..0000000 --- a/src/PdfEditorApp/ViewModels/PdfViewerModels.cs +++ /dev/null @@ -1,398 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using Avalonia; -using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels; - -public enum PdfViewerSidebarTab -{ - Thumbnails, - Bookmarks, - Annotations, - Search, - Info -} - -public enum PdfViewerSelectionMode -{ - Text, - Area -} - -public class PdfViewerGlyphItem -{ - public char Character { get; set; } - public Rect Bounds { get; set; } -} - -public class PdfViewerWordItem -{ - public string Text { get; set; } = string.Empty; - public Rect Bounds { get; set; } - public int LineIndex { get; set; } - public int WordIndex { get; set; } - public List Glyphs { get; } = new(); -} - -public class PdfViewerTextLineItem -{ - public int LineIndex { get; set; } - public Rect Bounds { get; set; } - public string Text { get; set; } = string.Empty; - public List Words { get; } = new(); -} - -public partial class PdfViewerPageItem : ObservableObject, IDisposable -{ - /// - /// Rasterization scale for a page shown at 100% zoom. Matches the ~2x device pixel ratio of - /// a HiDPI display, which is what mainstream PDF viewers target. This was 2.75x, which - /// oversampled by roughly 1.9x in area for no visible benefit — and since render cost scales - /// with pixel count, that directly inflated how long every page took to appear. - /// Every site that renders or records a scale must use this same value: a mismatch makes - /// think the page needs re-rendering and - /// silently doubles the work. - /// - internal const float BasePageRenderScale = 2.0f; - - [ObservableProperty] - private bool _isSelected; - - [ObservableProperty] - private int _rotationAngle; - - [ObservableProperty] - private float _renderedScale = BasePageRenderScale; - - private Bitmap? _thumbnailBitmap; - private Bitmap? _bitmap; - - [ObservableProperty] - private string _selectedText = string.Empty; - - partial void OnSelectedTextChanged(string value) - { - HasSelection = !string.IsNullOrEmpty(value); - } - - [ObservableProperty] - private bool _hasSelection; - - private bool _isDisposed; - - public PdfReaderTheme AppliedReadingTheme { get; set; } = PdfReaderTheme.Default; - - /// Guards against piling up redundant background geometry-extraction tasks - /// while pointer-move events keep firing before the first one completes. - public bool IsGeometryLoading { get; set; } - - /// Guards against queueing duplicate bitmap renders for this page while one is - /// already in flight — scroll events fire far faster than a page can be rasterized. - public bool IsRenderLoading { get; set; } - - public int PageNumber { get; set; } - public double WidthPoints { get; set; } - public double HeightPoints { get; set; } - public string DimensionsText => $"{Math.Round(WidthPoints):F0} × {Math.Round(HeightPoints):F0} pt"; - public string PageLabel => $"Page {PageNumber}"; - public string PageSummary { get; set; } = string.Empty; - public string ExtractedText { get; set; } = string.Empty; - - public List Words { get; set; } = new(); - public List TextLines { get; set; } = new(); - - public List SelectionRects { get; } = new(); - - public event Action? SelectionChanged; - - public void NotifySelectionChanged() - { - OnPropertyChanged(nameof(SelectedText)); - OnPropertyChanged(nameof(HasSelection)); - SelectionChanged?.Invoke(); - } - - public void ClearSelection() - { - if (SelectionRects.Count > 0 || !string.IsNullOrEmpty(SelectedText)) - { - SelectionRects.Clear(); - SelectedText = string.Empty; - NotifySelectionChanged(); - } - } - - public void SelectWord(PdfViewerWordItem word) - { - SelectionRects.Clear(); - SelectionRects.Add(word.Bounds); - SelectedText = word.Text; - NotifySelectionChanged(); - } - - public void SelectLine(PdfViewerTextLineItem line) - { - SelectionRects.Clear(); - SelectionRects.Add(line.Bounds); - SelectedText = line.Text; - NotifySelectionChanged(); - } - - public void SelectAll() - { - SelectionRects.Clear(); - if (Words.Count == 0 && TextLines.Count == 0) - { - SelectedText = string.Empty; - NotifySelectionChanged(); - return; - } - - if (TextLines.Count > 0) - { - foreach (var line in TextLines) - { - SelectionRects.Add(line.Bounds); - } - } - else - { - foreach (var word in Words) - { - SelectionRects.Add(word.Bounds); - } - } - - SelectedText = ExtractedText; - NotifySelectionChanged(); - } - - public void SetSelectionRange(Point start, Point end) - { - SelectionRects.Clear(); - if (Words.Count == 0 || TextLines.Count == 0) - { - SelectedText = string.Empty; - NotifySelectionChanged(); - return; - } - - // Determine natural reading order start & end - bool isStartFirst = (start.Y < end.Y - 4) || (Math.Abs(start.Y - end.Y) <= 4 && start.X <= end.X); - Point firstPoint = isStartFirst ? start : end; - Point secondPoint = isStartFirst ? end : start; - - var startLine = TextLines - .Where(l => firstPoint.Y >= l.Bounds.Top - 4 && firstPoint.Y <= l.Bounds.Bottom + 4) - .OrderBy(l => Math.Max(0, Math.Max(l.Bounds.Left - firstPoint.X, firstPoint.X - l.Bounds.Right))) - .FirstOrDefault() - ?? TextLines.OrderBy(l => Math.Abs(l.Bounds.Center.Y - firstPoint.Y) * 10 + Math.Abs(l.Bounds.Center.X - firstPoint.X)).FirstOrDefault(); - - var endLine = TextLines - .Where(l => secondPoint.Y >= l.Bounds.Top - 4 && secondPoint.Y <= l.Bounds.Bottom + 4) - .OrderBy(l => Math.Max(0, Math.Max(l.Bounds.Left - secondPoint.X, secondPoint.X - l.Bounds.Right))) - .FirstOrDefault() - ?? TextLines.OrderBy(l => Math.Abs(l.Bounds.Center.Y - secondPoint.Y) * 10 + Math.Abs(l.Bounds.Center.X - secondPoint.X)).FirstOrDefault(); - - if (startLine == null || endLine == null) - { - SelectedText = string.Empty; - NotifySelectionChanged(); - return; - } - - int startLineIdx = Math.Min(startLine.LineIndex, endLine.LineIndex); - int endLineIdx = Math.Max(startLine.LineIndex, endLine.LineIndex); - - var sb = new StringBuilder(); - - for (int lIdx = startLineIdx; lIdx <= endLineIdx; lIdx++) - { - var line = TextLines.FirstOrDefault(l => l.LineIndex == lIdx); - if (line == null || line.Words.Count == 0) continue; - - List lineSelectedWords; - - if (startLineIdx == endLineIdx) - { - double minX = Math.Min(start.X, end.X); - double maxX = Math.Max(start.X, end.X); - - lineSelectedWords = line.Words - .Where(w => w.Bounds.Right >= minX && w.Bounds.Left <= maxX) - .ToList(); - - if (lineSelectedWords.Count == 0 && line.Bounds.Left <= maxX && line.Bounds.Right >= minX) - { - lineSelectedWords = line.Words.ToList(); - } - } - else if (lIdx == startLineIdx) - { - double fromX = firstPoint.X; - lineSelectedWords = line.Words - .Where(w => w.Bounds.Right >= fromX) - .ToList(); - - if (lineSelectedWords.Count == 0 && fromX <= line.Bounds.Left) - { - lineSelectedWords = line.Words.ToList(); - } - } - else if (lIdx == endLineIdx) - { - double toX = secondPoint.X; - lineSelectedWords = line.Words - .Where(w => w.Bounds.Left <= toX) - .ToList(); - - if (lineSelectedWords.Count == 0 && toX >= line.Bounds.Right) - { - lineSelectedWords = line.Words.ToList(); - } - } - else - { - lineSelectedWords = line.Words.ToList(); - } - - if (lineSelectedWords.Count > 0) - { - double left = lineSelectedWords.Min(w => w.Bounds.Left); - double right = lineSelectedWords.Max(w => w.Bounds.Right); - double top = lineSelectedWords.Min(w => w.Bounds.Top); - double bottom = lineSelectedWords.Max(w => w.Bounds.Bottom); - SelectionRects.Add(new Rect(left, top, Math.Max(1, right - left), Math.Max(1, bottom - top))); - - string lineTxt = string.Join(" ", lineSelectedWords.Select(w => w.Text)); - if (sb.Length > 0) sb.AppendLine(); - sb.Append(lineTxt); - } - } - - SelectedText = sb.ToString(); - NotifySelectionChanged(); - } - - public Bitmap? ThumbnailBitmap - { - get => _thumbnailBitmap ?? _bitmap; - set - { - var old = _thumbnailBitmap; - if (SetProperty(ref _thumbnailBitmap, value) && old != null && old != _bitmap) - { - old.Dispose(); - } - } - } - - public Bitmap? Bitmap - { - get => _bitmap; - set - { - var old = _bitmap; - if (SetProperty(ref _bitmap, value)) - { - OnPropertyChanged(nameof(ThumbnailBitmap)); - if (old != null && old != _thumbnailBitmap) - { - old.Dispose(); - } - } - } - } - - public ObservableCollection PageAnnotations { get; } = new(); - - /// Releases the rendered bitmaps. Safe to call more than once. - public void Dispose() - { - if (_isDisposed) return; - _isDisposed = true; - - var bmp = _bitmap; - var thumb = _thumbnailBitmap; - _bitmap = null; - _thumbnailBitmap = null; - - bmp?.Dispose(); - if (thumb != null && thumb != bmp) - { - thumb.Dispose(); - } - } -} - -public partial class PdfViewerPageSpreadItem : ObservableObject -{ - [ObservableProperty] - private bool _isSelected; - - public int SpreadIndex { get; set; } - public PdfViewerPageItem? LeftPage { get; set; } - public PdfViewerPageItem? RightPage { get; set; } - public string SpreadLabel { get; set; } = string.Empty; -} - -public class PdfViewerBookmarkItem -{ - public string Title { get; set; } = string.Empty; - public int PageNumber { get; set; } = 1; - public ObservableCollection Children { get; } = new(); - public bool HasChildren => Children.Count > 0; -} - -public partial class PdfViewerAnnotationItem : ObservableObject -{ - [ObservableProperty] - private string _id = Guid.NewGuid().ToString("N"); - - [ObservableProperty] - private string _type = "Highlight"; // Highlight, StickyNote, Stamp, Ink, Signature - - [ObservableProperty] - private int _pageNumber = 1; - - [ObservableProperty] - private string _author = "Reader"; - - [ObservableProperty] - private string _content = string.Empty; - - [ObservableProperty] - private string _colorHex = "#FEF08A"; - - [ObservableProperty] - private string _iconKind = "FormatColorHighlight"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(TimeFormatted))] - private DateTime _createdAt = DateTime.Now; - - public string TimeFormatted => CreatedAt.ToString("HH:mm · MMM d"); - public List HighlightRects { get; set; } = new(); -} - -public class PdfViewerMetadataItem -{ - public string Label { get; set; } = string.Empty; - public string Value { get; set; } = string.Empty; - public string IconKind { get; set; } = "InformationOutline"; -} - -public class PdfViewerSearchMatch -{ - public int PageNumber { get; set; } - public string Snippet { get; set; } = string.Empty; - public int MatchIndex { get; set; } -} - diff --git a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Annotations.cs b/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Annotations.cs deleted file mode 100644 index bc524c9..0000000 --- a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Annotations.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia; -using CommunityToolkit.Mvvm.Input; - -namespace PdfEditorApp.ViewModels; - -public partial class PdfViewerViewModel -{ - // --- Interactive Annotations & Review Markups --- - - [RelayCommand] - public void SetHighlightColor(string? colorHex) - { - if (!string.IsNullOrWhiteSpace(colorHex)) - { - SelectedHighlightColorHex = colorHex; - } - } - - [RelayCommand] - public void AddHighlightAnnotation(string? customColorHex = null) - { - var page = Pages.FirstOrDefault(p => p.PageNumber == ActiveSelectedPageNumber) ?? SelectedPage; - if (page == null) return; - string color = string.IsNullOrWhiteSpace(customColorHex) ? SelectedHighlightColorHex : customColorHex; - string textToHighlight = !string.IsNullOrWhiteSpace(ActiveSelectedText) ? ActiveSelectedText : page.SelectedText; - - var highlightRects = new List(page.SelectionRects); - - var ann = new PdfViewerAnnotationItem - { - Type = "Highlight", - PageNumber = page.PageNumber, - Author = "Reader Reviewer", - Content = !string.IsNullOrWhiteSpace(textToHighlight) ? textToHighlight : $"Highlighted text passage on Page {page.PageNumber}", - ColorHex = color, - IconKind = "FormatColorHighlight", - HighlightRects = highlightRects - }; - Annotations.Add(ann); - page.PageAnnotations.Add(ann); - OnPropertyChanged(nameof(HasAnnotations)); - page.ClearSelection(); - ClearSelection(); - SelectedSidebarTab = PdfViewerSidebarTab.Annotations; - ShowToast($"Added Highlight on Page {page.PageNumber}"); - } - - [RelayCommand] - public void HighlightSelectedText(string? customColorHex = null) - { - AddHighlightAnnotation(customColorHex); - } - - [RelayCommand] - public void OpenAddNoteDialog() - { - NewNoteText = string.Empty; - IsAddNoteOpen = true; - } - - [RelayCommand] - public void AddNoteFromSelection() - { - if (!string.IsNullOrWhiteSpace(ActiveSelectedText)) - { - NewNoteText = $"Re: \"{ActiveSelectedText}\"\n\n"; - } - else - { - NewNoteText = string.Empty; - } - IsAddNoteOpen = true; - } - - [RelayCommand] - public void ConfirmAddNote() - { - if (SelectedPage == null || string.IsNullOrWhiteSpace(NewNoteText)) - { - IsAddNoteOpen = false; - return; - } - - var ann = new PdfViewerAnnotationItem - { - Type = "StickyNote", - PageNumber = SelectedPage.PageNumber, - Author = "Reader Note", - Content = NewNoteText.Trim(), - ColorHex = "#38BDF8", - IconKind = "NoteTextOutline" - }; - Annotations.Add(ann); - SelectedPage.PageAnnotations.Add(ann); - OnPropertyChanged(nameof(HasAnnotations)); - IsAddNoteOpen = false; - SelectedSidebarTab = PdfViewerSidebarTab.Annotations; - ShowToast($"Added Sticky Note on Page {SelectedPage.PageNumber}"); - } - - [RelayCommand] - public void AddStamp(string? stampText) - { - if (SelectedPage == null) return; - string text = string.IsNullOrWhiteSpace(stampText) ? "APPROVED" : stampText; - string color = text switch - { - "REJECTED" => "#EF4444", - "CONFIDENTIAL" => "#DC2626", - "DRAFT" => "#F59E0B", - "FINAL" => "#8B5CF6", - "REVIEWED" => "#3B82F6", - _ => "#10B981" // APPROVED - }; - - var ann = new PdfViewerAnnotationItem - { - Type = "Stamp", - PageNumber = SelectedPage.PageNumber, - Author = "Auditor", - Content = $"Stamp: {text}", - ColorHex = color, - IconKind = "Stamp" - }; - Annotations.Add(ann); - SelectedPage.PageAnnotations.Add(ann); - OnPropertyChanged(nameof(HasAnnotations)); - IsAddStampOpen = false; - SelectedSidebarTab = PdfViewerSidebarTab.Annotations; - ShowToast($"Applied '{text}' stamp on Page {SelectedPage.PageNumber}"); - } - - [RelayCommand] - public void DeleteAnnotation(PdfViewerAnnotationItem? ann) - { - if (ann == null) return; - Annotations.Remove(ann); - foreach (var p in Pages) - { - p.PageAnnotations.Remove(ann); - p.NotifySelectionChanged(); - } - OnPropertyChanged(nameof(HasAnnotations)); - } - -} diff --git a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Document.cs b/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Document.cs deleted file mode 100644 index 83da5fe..0000000 --- a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Document.cs +++ /dev/null @@ -1,723 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Input.Platform; -using Avalonia.Media.Imaging; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Ocr; -using PdfEditorApp.Services.Tools.Core; -using UglyToad.PdfPig; -using UglyToad.PdfPig.Content; -using UglyToad.PdfPig.Outline; -using UglyToad.PdfPig.Rendering.Skia; - -namespace PdfEditorApp.ViewModels; - -public partial class PdfViewerViewModel -{ - // --- Core Document Loading & Text Geometry Extraction --- - - public static (string text, List words, List lines) ExtractPageTextGeometry(Page page) - { - double pageHeight = Math.Max(100, page.Height); - var rawWords = page.GetWords().Where(w => !string.IsNullOrWhiteSpace(w.Text)).ToList(); - if (rawWords.Count == 0) - { - return (page.Text ?? string.Empty, new List(), new List()); - } - - var wordItems = new List(); - int wordIdx = 0; - foreach (var w in rawWords) - { - double x = Math.Max(0, w.BoundingBox.Left); - double y = Math.Max(0, pageHeight - w.BoundingBox.Top); - double width = Math.Max(1, w.BoundingBox.Width); - double height = Math.Max(1, w.BoundingBox.Height); - - var wordItem = new PdfViewerWordItem - { - Text = w.Text, - Bounds = new Rect(x, y, width, height), - WordIndex = wordIdx++ - }; - - if (w.Letters != null && w.Letters.Count > 0) - { - foreach (var letter in w.Letters) - { - if (string.IsNullOrEmpty(letter.Value)) continue; - double lx = Math.Max(0, letter.BoundingBox.Left); - double ly = Math.Max(0, pageHeight - letter.BoundingBox.Top); - double lw = Math.Max(0.5, letter.BoundingBox.Width); - double lh = Math.Max(1, letter.BoundingBox.Height); - - wordItem.Glyphs.Add(new PdfViewerGlyphItem - { - Character = letter.Value[0], - Bounds = new Rect(lx, ly, lw, lh) - }); - } - } - - wordItems.Add(wordItem); - } - - // Group words into lines based on vertical overlap - var sortedWords = wordItems.OrderBy(w => w.Bounds.Top).ThenBy(w => w.Bounds.Left).ToList(); - var lineList = new List(); - - foreach (var word in sortedWords) - { - var line = lineList.FirstOrDefault(l => - { - double overlapTop = Math.Max(l.Bounds.Top, word.Bounds.Top); - double overlapBottom = Math.Min(l.Bounds.Bottom, word.Bounds.Bottom); - double overlap = overlapBottom - overlapTop; - return overlap > Math.Min(l.Bounds.Height, word.Bounds.Height) * 0.45; - }); - - if (line == null) - { - line = new PdfViewerTextLineItem - { - LineIndex = lineList.Count, - Bounds = word.Bounds - }; - line.Words.Add(word); - lineList.Add(line); - } - else - { - line.Words.Add(word); - line.Bounds = line.Bounds.Union(word.Bounds); - } - } - - // Sort words in each line horizontally and build line text - var sb = new StringBuilder(); - int lineIdx = 0; - foreach (var line in lineList.OrderBy(l => l.Bounds.Top)) - { - line.LineIndex = lineIdx++; - var sortedLineWords = line.Words.OrderBy(w => w.Bounds.Left).ToList(); - line.Words.Clear(); - line.Words.AddRange(sortedLineWords); - - foreach (var w in line.Words) - { - w.LineIndex = line.LineIndex; - } - - line.Text = string.Join(" ", line.Words.Select(w => w.Text)); - sb.AppendLine(line.Text); - } - - string fullText = sb.ToString().TrimEnd(); - if (string.IsNullOrWhiteSpace(fullText)) - { - fullText = page.Text ?? string.Empty; - } - - return (fullText, wordItems, lineList.OrderBy(l => l.Bounds.Top).ToList()); - } - - /// - /// Performs OCR on a rendered page image if vector text extraction yields no words (e.g. scanned documents). - /// - public (string text, List words, List lines) ExtractOcrPageTextGeometry(int pageNumber, double width, double height) - { - try - { - var ocrProvider = CompositeOcrProvider.Default; - if (!ocrProvider.IsAvailable) - { - return (string.Empty, new List(), new List()); - } - - byte[]? pagePng = RenderPageBytesAtScale(pageNumber, 1.5f); - if (pagePng == null || pagePng.Length == 0) - { - return (string.Empty, new List(), new List()); - } - - var ocrResult = ocrProvider.RecognizeTextAsync(pagePng).GetAwaiter().GetResult(); - if (!ocrResult.Success || ocrResult.Words.Count == 0) - { - return (string.Empty, new List(), new List()); - } - - var words = new List(); - int wIdx = 0; - foreach (var ow in ocrResult.Words) - { - double wx = Math.Max(0, ow.NormalizedBounds.X * width); - double wy = Math.Max(0, ow.NormalizedBounds.Y * height); - double ww = Math.Max(1, ow.NormalizedBounds.Width * width); - double wh = Math.Max(1, ow.NormalizedBounds.Height * height); - - var wordItem = new PdfViewerWordItem - { - Text = ow.Text, - Bounds = new Rect(wx, wy, ww, wh), - WordIndex = wIdx++ - }; - - double charW = ww / Math.Max(1, ow.Text.Length); - for (int ci = 0; ci < ow.Text.Length; ci++) - { - wordItem.Glyphs.Add(new PdfViewerGlyphItem - { - Character = ow.Text[ci], - Bounds = new Rect(wx + ci * charW, wy, Math.Max(0.5, charW), wh) - }); - } - - words.Add(wordItem); - } - - var lines = new List(); - int lIdx = 0; - foreach (var ol in ocrResult.Lines) - { - double lx = Math.Max(0, ol.NormalizedBounds.X * width); - double ly = Math.Max(0, ol.NormalizedBounds.Y * height); - double lw = Math.Max(1, ol.NormalizedBounds.Width * width); - double lh = Math.Max(1, ol.NormalizedBounds.Height * height); - - var lineItem = new PdfViewerTextLineItem - { - LineIndex = lIdx++, - Bounds = new Rect(lx, ly, lw, lh) - }; - - var lineWords = words - .Where(w => w.Bounds.Top >= ly - 4 && w.Bounds.Bottom <= ly + lh + 4) - .OrderBy(w => w.Bounds.Left) - .ToList(); - - foreach (var lw_item in lineWords) - { - lw_item.LineIndex = lineItem.LineIndex; - } - - lineItem.Words.AddRange(lineWords); - lineItem.Text = string.Join(" ", lineItem.Words.Select(w => w.Text)); - lines.Add(lineItem); - } - - return (ocrResult.FullText, words, lines); - } - catch - { - return (string.Empty, new List(), new List()); - } - } - - public byte[]? CurrentPdfBytes => _currentPdfBytes; - - /// - /// Populates a page's word/line geometry for hit-testing (hover cursor, click-to-select-word). - /// Runs off the UI thread — the first hover/click on a page the background sweep hasn't - /// reached yet used to open and fully parse the PDF synchronously on the dispatcher thread. - /// - public void EnsurePageGeometry(PdfViewerPageItem page) - { - if (page.Words.Count > 0 || page.IsGeometryLoading || _currentPdfBytes == null || _currentPdfBytes.Length == 0) return; - - page.IsGeometryLoading = true; - int pageNumber = page.PageNumber; - - Task.Run(() => - { - string? text = null; - List? words = null; - List? lines = null; - double width = 0, height = 0; - - try - { - lock (_renderLock) - { - var doc = OpenOrReuseDocument(); - if (doc != null && pageNumber >= 1 && pageNumber <= doc.NumberOfPages) - { - var p = doc.GetPage(pageNumber); - (text, words, lines) = ExtractPageTextGeometry(p); - if (p.Width > 0 && p.Height > 0) - { - width = p.Width; - height = p.Height; - } - } - } - } - catch { } - - // Automatic OCR fallback for scanned pages with zero vector text - if ((words == null || words.Count == 0) && width > 0 && height > 0) - { - var (ocrText, ocrWords, ocrLines) = ExtractOcrPageTextGeometry(pageNumber, width, height); - if (ocrWords.Count > 0) - { - text = ocrText; - words = ocrWords; - lines = ocrLines; - } - } - - // Assign geometry data directly so both UI and headless test runners receive words immediately - if (text != null && words != null && lines != null) - { - page.ExtractedText = text; - page.Words = words; - page.TextLines = lines; - if (width > 0 && height > 0) - { - page.WidthPoints = width; - page.HeightPoints = height; - } - } - page.IsGeometryLoading = false; - - try - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - if (SelectedPage != null && SelectedPage.PageNumber == pageNumber) - { - IsCurrentPageScanned = (page.Words.Count == 0 && string.IsNullOrWhiteSpace(page.ExtractedText)); - } - page.NotifySelectionChanged(); - }); - } - catch { } - }); - } - - public async Task LoadDocumentAsync(string filePath, string? password = null) - { - if (!File.Exists(filePath)) - { - StatusMessage = $"File not found: {filePath}"; - return; - } - - try - { - byte[] bytes = await File.ReadAllBytesAsync(filePath); - await LoadDocumentFromBytesAsync(bytes, filePath, password); - } - catch (Exception ex) - { - StatusMessage = $"Error loading document: {ex.Message}"; - } - } - - public Task LoadDocumentBytesAsync(byte[] pdfBytes, string? documentTitle = null, string? password = null) - => LoadDocumentFromBytesAsync(pdfBytes, documentTitle ?? "Document.pdf", password); - - public async Task LoadDocumentFromBytesAsync(byte[] pdfBytes, string sourceFilePath = "", string? password = null) - { - _renderCts?.Cancel(); - _renderCts = new CancellationTokenSource(); - var ct = _renderCts.Token; - - _backgroundRenderCts?.Cancel(); - _backgroundRenderCts = new CancellationTokenSource(); - - IsLoading = true; - IsOpeningDocument = true; - HasDocument = true; - StatusMessage = "Opening PDF document..."; - _currentPdfBytes = pdfBytes; - _currentPassword = password; - CurrentFilePath = sourceFilePath; - DocumentTitle = string.IsNullOrWhiteSpace(sourceFilePath) ? "Document.pdf" : Path.GetFileName(sourceFilePath); - - // Release the previous document's rendered bitmaps before replacing it — this - // ViewModel is a long-lived singleton reused across every document open, so - // nothing else ever frees this memory otherwise. - foreach (var oldPage in Pages) - { - oldPage.Dispose(); - } - _lastVisibleFirstPage = -1; - _lastVisibleLastPage = -1; - - lock (_renderLock) - { - _openDocument?.Dispose(); - _openDocument = null; - } - - Pages.Clear(); - PageSpreads.Clear(); - Bookmarks.Clear(); - Annotations.Clear(); - MetadataItems.Clear(); - SearchResults.Clear(); - - try - { - byte[] sanitizedBytes = PdfFileHelper.SanitizePdfBytes(pdfBytes); - _currentPdfBytes = sanitizedBytes; - - var (metaList, pagesList, total) = await Task.Run(() => - { - var parsingOptions = new ParsingOptions(); - if (!string.IsNullOrEmpty(password)) - { - parsingOptions.Password = password; - } - - PdfDocument? doc = null; - try - { - doc = PdfDocument.Open(sanitizedBytes, parsingOptions); - } - catch - { - try - { - byte[] repaired = PdfFileHelper.SalvageAndRepairPdfBytes(sanitizedBytes); - doc = PdfDocument.Open(repaired, parsingOptions); - } - catch - { - doc = PdfDocument.Open(pdfBytes, parsingOptions); - } - } - - try - { - try - { - PdfPigExtensions.AddSkiaPageFactory(doc); - } - catch { } - - int total = doc.NumberOfPages; - if (total == 0) - { - doc.Dispose(); - return (new List(), new List(), 0); - } - - // 1. Fast Page 1 extraction & immediate render - var firstPage = doc.GetPage(1); - double defaultWidth = Math.Max(100, firstPage.Width); - double defaultHeight = Math.Max(100, firstPage.Height); - int defaultRot = (int)firstPage.Rotation.Value; - var (firstPageText, firstWords, firstLines) = ExtractPageTextGeometry(firstPage); - string firstPageSummary = ""; - if (!string.IsNullOrWhiteSpace(firstPageText)) - { - var firstLine = firstPageText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? ""; - firstPageSummary = firstLine.Length > 50 ? firstLine.Substring(0, 50) + "..." : firstLine; - } - - Bitmap? bmp1 = null; - try - { - using var pngStream = PdfPigExtensions.GetPageAsPng(doc, 1, PdfViewerPageItem.BasePageRenderScale, 100); - if (pngStream != null && pngStream.Length > 0) - { - pngStream.Position = 0; - bmp1 = new Bitmap(pngStream); - } - } - catch { } - - // 2. Instant Skeleton Generation, with REAL per-page dimensions. Reading a - // page's declared size is cheap (the page dictionary's MediaBox) — nothing - // like the cost of full word/text extraction — so it's worth doing for every - // page right now rather than defaulting every page to page 1's size until - // the much slower progressive background sweep happens to reach it. Every - // scroll-position and click-to-navigate calculation sums page heights across - // however many preceding pages there are, so a wrong default anywhere in - // that chain drifts every page after it out of sync until the real value - // loads — for a large document, that's most of the book for a long time. - var pagesList = new List(total); - for (int i = 1; i <= total; i++) - { - double pageWidth = defaultWidth; - double pageHeight = defaultHeight; - int pageRot = (i == 1) ? defaultRot : 0; - - if (i > 1) - { - try - { - var pg = doc.GetPage(i); - if (pg.Width > 0) pageWidth = pg.Width; - if (pg.Height > 0) pageHeight = pg.Height; - pageRot = (int)pg.Rotation.Value; - } - catch { } - } - - pagesList.Add(new PdfViewerPageItem - { - PageNumber = i, - WidthPoints = pageWidth, - HeightPoints = pageHeight, - RotationAngle = pageRot, - ExtractedText = (i == 1) ? firstPageText : "", - Words = (i == 1) ? firstWords : new List(), - TextLines = (i == 1) ? firstLines : new List(), - PageSummary = (i == 1) ? firstPageSummary : "", - Bitmap = (i == 1) ? bmp1 : null, - RenderedScale = PdfViewerPageItem.BasePageRenderScale, - IsSelected = (i == 1) - }); - } - - // 3. Metadata - var info = doc.Information; - string dimsInches = $"{defaultWidth / 72.0:F1}\" × {defaultHeight / 72.0:F1}\""; - string dimsMm = $"{defaultWidth * 25.4 / 72.0:F0} × {defaultHeight * 25.4 / 72.0:F0} mm"; - - var metaList = new List - { - new PdfViewerMetadataItem { Label = "File Name", Value = DocumentTitle, IconKind = "FileDocumentOutline" }, - new PdfViewerMetadataItem { Label = "File Size", Value = PdfFilePreviewItem.FormatBytes(pdfBytes.Length), IconKind = "DatabaseOutline" }, - new PdfViewerMetadataItem { Label = "Total Pages", Value = $"{total} Pages", IconKind = "BookOpenPageVariantOutline" }, - new PdfViewerMetadataItem { Label = "Page Dimensions", Value = $"{dimsInches} ({dimsMm})", IconKind = "AspectRatio" }, - new PdfViewerMetadataItem { Label = "Title", Value = string.IsNullOrWhiteSpace(info.Title) ? "Untitled Document" : info.Title, IconKind = "FormatTitle" }, - new PdfViewerMetadataItem { Label = "Author", Value = string.IsNullOrWhiteSpace(info.Author) ? "Unknown Author" : info.Author, IconKind = "AccountOutline" }, - new PdfViewerMetadataItem { Label = "Subject", Value = string.IsNullOrWhiteSpace(info.Subject) ? "None specified" : info.Subject, IconKind = "Subject" }, - new PdfViewerMetadataItem { Label = "Keywords", Value = string.IsNullOrWhiteSpace(info.Keywords) ? "None" : info.Keywords, IconKind = "TagOutline" }, - new PdfViewerMetadataItem { Label = "Creator Application", Value = string.IsNullOrWhiteSpace(info.Creator) ? "FryPDF" : info.Creator, IconKind = "CogOutline" }, - new PdfViewerMetadataItem { Label = "PDF Producer", Value = string.IsNullOrWhiteSpace(info.Producer) ? "codefrydev.in" : info.Producer, IconKind = "ApplicationOutline" }, - new PdfViewerMetadataItem { Label = "PDF Version", Value = $"PDF {doc.Version}", IconKind = "ShieldCheckOutline" }, - new PdfViewerMetadataItem { Label = "Security Status", Value = doc.IsEncrypted ? "Password Protected (Encrypted)" : "Standard (No Security)", IconKind = doc.IsEncrypted ? "LockOutline" : "LockOpenOutline" } - }; - - // Keep this document open for reuse by on-demand rendering/geometry - // extraction instead of parsing the whole PDF from scratch again on the - // very first page request right after this. - lock (_renderLock) - { - _openDocument?.Dispose(); - _openDocument = doc; - } - - return (metaList, pagesList, total); - } - catch - { - doc.Dispose(); - throw; - } - }, ct); - - foreach (var m in metaList) MetadataItems.Add(m); - foreach (var p in pagesList) Pages.Add(p); - - TotalPagesCount = total; - CurrentPageNumber = 1; - JumpPageText = "1"; - SelectedPage = Pages.FirstOrDefault(); - HasDocument = true; - IsLoading = false; - IsOpeningDocument = false; - StatusMessage = $"Ready • {total} pages"; - - bool page1HasNoText = (SelectedPage != null && SelectedPage.Words.Count == 0 && string.IsNullOrWhiteSpace(SelectedPage.ExtractedText)); - IsScannedDocument = page1HasNoText; - IsCurrentPageScanned = page1HasNoText; - IsScannedBannerDismissed = false; - - RebuildPageSpreads(); - - // Start progressive background worker to render remaining pages, thumbnails, and bookmarks - StartBackgroundWorker(_backgroundRenderCts.Token); - } - catch (OperationCanceledException) - { - StatusMessage = "Loading cancelled."; - IsLoading = false; - IsOpeningDocument = false; - } - catch (Exception ex) - { - StatusMessage = $"Error: {ex.Message}"; - IsLoading = false; - IsOpeningDocument = false; - } - } - - /// Renders a specific page at the specified scale directly from PDF bytes using Skia. - /// Renders a specific page at the specified scale directly to image bytes. - - private static void ExtractBookmarksRecursive(IEnumerable nodes, IList targetList) - { - if (nodes == null) return; - foreach (var node in nodes) - { - if (node is DocumentBookmarkNode docNode) - { - var bm = new PdfViewerBookmarkItem - { - Title = docNode.Title ?? "Section", - PageNumber = Math.Max(1, docNode.PageNumber) - }; - if (docNode.Children != null && docNode.Children.Count > 0) - { - ExtractBookmarksRecursive(docNode.Children, bm.Children); - } - targetList.Add(bm); - } - } - } - - - [RelayCommand] - public void RenameDocument() - { - if (!string.IsNullOrEmpty(CurrentFilePath)) - { - WeakReferenceMessenger.Default.Send(new PromptRenameMessage(CurrentFilePath)); - } - } - - [RelayCommand] - public void DeleteDocument() - { - if (!string.IsNullOrEmpty(CurrentFilePath)) - { - WeakReferenceMessenger.Default.Send(new PromptDeleteMessage(CurrentFilePath)); - } - } - - [RelayCommand] - public void DuplicateDocument() - { - if (!string.IsNullOrEmpty(CurrentFilePath)) - { - if (FileOperationHelper.DuplicateFile(CurrentFilePath, out var newPath, out var error)) - { - ShowToast($"Created duplicate: {Path.GetFileName(newPath!)}"); - } - else - { - ShowToast($"Could not duplicate: {error}"); - } - } - } - - [RelayCommand] - public void RevealInFileManager() - { - if (!string.IsNullOrEmpty(CurrentFilePath)) - { - if (FileOperationHelper.RevealInFileManager(CurrentFilePath, out var error)) - { - ShowToast("Opened containing folder"); - } - else - { - ShowToast($"Could not reveal file: {error}"); - } - } - } - - [RelayCommand] - public async Task CopyPath() - { - if (!string.IsNullOrEmpty(CurrentFilePath)) - { - try - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(CurrentFilePath); - } - } - catch { } - ShowToast("Copied file path to clipboard"); - } - } - - - - [RelayCommand] - public async Task OpenAnotherPdfAsync() - { - if (StorageProvider == null) - { - WeakReferenceMessenger.Default.Send(new OpenPdfPickerMessage()); - return; - } - - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Open PDF Document to Read", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("PDF Documents (*.pdf)") - { - Patterns = new[] { "*.pdf" } - } - } - }); - - if (files.Count > 0) - { - string chosenPath = files[0].Path.LocalPath; - await LoadDocumentAsync(chosenPath); - ShowToast($"Reading: {Path.GetFileName(chosenPath)}"); - } - } - - [RelayCommand] - public async Task SaveAsAsync() - { - if (_currentPdfBytes == null || _currentPdfBytes.Length == 0 || StorageProvider == null) return; - - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Save PDF As", - DefaultExtension = "pdf", - SuggestedFileName = DocumentTitle, - FileTypeChoices = new[] - { - new FilePickerFileType("PDF Documents (*.pdf)") - { - Patterns = new[] { "*.pdf" } - } - } - }); - - if (file != null) - { - try - { - await File.WriteAllBytesAsync(file.Path.LocalPath, _currentPdfBytes); - ShowToast($"Saved copy: {Path.GetFileName(file.Path.LocalPath)}"); - } - catch (IOException) - { - ShowToast($"Cannot save: '{Path.GetFileName(file.Path.LocalPath)}' is open in another program."); - } - catch (Exception ex) - { - ShowToast($"Save failed: {ex.Message}"); - } - } - } - -} diff --git a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Navigation.cs b/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Navigation.cs deleted file mode 100644 index b19d5ab..0000000 --- a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Navigation.cs +++ /dev/null @@ -1,590 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels; - -public partial class PdfViewerViewModel -{ - // --- Navigation & Page Selection --- - - public void RequestScrollToPage(int pageNumber) - { - if (pageNumber >= 1 && pageNumber <= TotalPagesCount) - { - ScrollToPageRequested?.Invoke(pageNumber); - } - } - - - [RelayCommand] - public void SelectPage(PdfViewerPageItem? page) - { - if (page == null) return; - SelectedPage = page; - CurrentPageNumber = page.PageNumber; - RequestScrollToPage(page.PageNumber); - } - - [RelayCommand] - public void SelectSpread(PdfViewerPageSpreadItem? spread) - { - if (spread == null) return; - SelectedSpread = spread; - if (spread.LeftPage != null) - { - CurrentPageNumber = spread.LeftPage.PageNumber; - } - else if (spread.RightPage != null) - { - CurrentPageNumber = spread.RightPage.PageNumber; - } - RequestScrollToPage(CurrentPageNumber); - } - - [RelayCommand] - public void JumpToBookmark(PdfViewerBookmarkItem? bookmark) - { - if (bookmark == null) return; - int pageIndex = bookmark.PageNumber - 1; - if (pageIndex >= 0 && pageIndex < Pages.Count) - { - SelectedPage = Pages[pageIndex]; - CurrentPageNumber = bookmark.PageNumber; - RequestScrollToPage(bookmark.PageNumber); - } - } - - [RelayCommand] - public void JumpToAnnotation(PdfViewerAnnotationItem? ann) - { - if (ann == null) return; - int pageIndex = ann.PageNumber - 1; - if (pageIndex >= 0 && pageIndex < Pages.Count) - { - SelectedPage = Pages[pageIndex]; - CurrentPageNumber = ann.PageNumber; - RequestScrollToPage(ann.PageNumber); - } - } - - [RelayCommand] - public void CommitJumpPage() - { - if (int.TryParse(JumpPageText, out int target) && target >= 1 && target <= TotalPagesCount) - { - CurrentPageNumber = target; - RequestScrollToPage(target); - } - else - { - JumpPageText = CurrentPageNumber.ToString(); - } - } - - [RelayCommand] - public void NextPage() - { - if (IsTwoPageSpreadMode) - { - if (CurrentPageNumber + 2 <= TotalPagesCount) - { - CurrentPageNumber += 2; - } - else if (CurrentPageNumber < TotalPagesCount) - { - CurrentPageNumber = TotalPagesCount; - } - } - else - { - if (CurrentPageNumber < TotalPagesCount) - { - CurrentPageNumber++; - } - } - RequestScrollToPage(CurrentPageNumber); - } - - [RelayCommand] - public void PreviousPage() - { - if (IsTwoPageSpreadMode) - { - if (CurrentPageNumber - 2 >= 1) - { - CurrentPageNumber -= 2; - } - else - { - CurrentPageNumber = 1; - } - } - else - { - if (CurrentPageNumber > 1) - { - CurrentPageNumber--; - } - } - RequestScrollToPage(CurrentPageNumber); - } - - [RelayCommand] - public void FirstPage() - { - if (TotalPagesCount > 0) - { - CurrentPageNumber = 1; - RequestScrollToPage(1); - } - } - - [RelayCommand] - public void LastPage() - { - if (TotalPagesCount > 0) - { - CurrentPageNumber = TotalPagesCount; - RequestScrollToPage(TotalPagesCount); - } - } - - - // --- Layout & View Modes --- - - [RelayCommand] - public void SetContinuousScrollLayout() - { - SetLayoutMode("ContinuousScroll"); - } - - [RelayCommand] - public void SetSinglePageLayout() - { - SetLayoutMode("SinglePage"); - } - - [RelayCommand] - public void SetTwoPageSpreadLayout() - { - SetLayoutMode("TwoPageSpread"); - } - - [RelayCommand] - public void SetLayoutMode(string? modeStr) - { - if (Enum.TryParse(modeStr, true, out var mode)) - { - SelectedLayoutMode = mode; - ShowToast(mode switch - { - PdfViewLayoutMode.ContinuousScroll => "Continuous Scroll Layout", - PdfViewLayoutMode.SinglePage => "Single Page Fit Layout", - PdfViewLayoutMode.TwoPageSpread => "Two-Page Book Spread Layout", - _ => "Layout Changed" - }); - } - } - - // --- Reading Themes --- - - [RelayCommand] - public void SetDefaultReadingTheme() - { - SetReadingTheme("Default"); - } - - [RelayCommand] - public void SetSepiaReadingTheme() - { - SetReadingTheme("Sepia"); - } - - [RelayCommand] - public void SetDarkReadingTheme() - { - SetReadingTheme("Dark"); - } - - [RelayCommand] - public void SetHighContrastReadingTheme() - { - SetReadingTheme("HighContrast"); - } - - [RelayCommand] - public void SetReadingTheme(string? themeStr) - { - if (Enum.TryParse(themeStr, true, out var theme)) - { - ReadingTheme = theme; - ShowToast(theme switch - { - PdfReaderTheme.Sepia => "Warm Sepia Book Reading Mode", - PdfReaderTheme.Dark => "Dark Night Reading Mode", - PdfReaderTheme.HighContrast => "High Contrast Reading Mode", - _ => "Standard Daylight Reading Mode" - }); - } - } - - // --- Zoom Controls --- - - [RelayCommand] - public void ZoomIn() - { - ZoomMode = PdfViewerZoomMode.Custom; - ZoomLevel = Math.Min(5.0, Math.Round(ZoomLevel + 0.25, 2)); - } - - [RelayCommand] - public void ZoomOut() - { - ZoomMode = PdfViewerZoomMode.Custom; - ZoomLevel = Math.Max(0.25, Math.Round(ZoomLevel - 0.25, 2)); - } - - [RelayCommand] - public void ResetZoom() - { - ZoomMode = PdfViewerZoomMode.Custom; - ZoomLevel = 1.0; - } - - [RelayCommand] - public void FitToWidth() - { - if (ViewportSizeProvider != null) - { - var dims = ViewportSizeProvider(); - if (dims.ViewportWidth > 100) - { - FitToWidthDynamic(dims.ViewportWidth, dims.HorizontalPadding); - return; - } - } - - _isApplyingFitZoom = true; - try - { - ZoomMode = PdfViewerZoomMode.FitWidth; - ZoomLevel = 1.35; - } - finally - { - _isApplyingFitZoom = false; - } - } - - public void FitToWidthDynamic(double viewportWidth, double horizontalPadding = 64.0) - { - if (viewportWidth <= 100) return; - - var page = SelectedPage ?? Pages.FirstOrDefault(p => p.PageNumber == CurrentPageNumber) ?? Pages.FirstOrDefault(); - if (page == null) - { - _isApplyingFitZoom = true; - try - { - ZoomMode = PdfViewerZoomMode.FitWidth; - ZoomLevel = 1.35; - } - finally - { - _isApplyingFitZoom = false; - } - return; - } - - double availableWidth = Math.Max(100.0, viewportWidth - horizontalPadding - 8.0); - double targetWidth = (page.RotationAngle % 180 == 0) ? page.WidthPoints : page.HeightPoints; - - if (IsTwoPageSpreadMode && SelectedSpread != null) - { - if (SelectedSpread.LeftPage != null && SelectedSpread.RightPage != null) - { - double leftW = (SelectedSpread.LeftPage.RotationAngle % 180 == 0) ? SelectedSpread.LeftPage.WidthPoints : SelectedSpread.LeftPage.HeightPoints; - double rightW = (SelectedSpread.RightPage.RotationAngle % 180 == 0) ? SelectedSpread.RightPage.WidthPoints : SelectedSpread.RightPage.HeightPoints; - targetWidth = leftW + rightW; - availableWidth = Math.Max(100.0, availableWidth - 16.0); // 16px gap between pages - } - else - { - var single = SelectedSpread.LeftPage ?? SelectedSpread.RightPage ?? page; - targetWidth = (single.RotationAngle % 180 == 0) ? single.WidthPoints : single.HeightPoints; - } - } - - if (targetWidth > 10.0) - { - double calculatedZoom = Math.Clamp(Math.Round(availableWidth / targetWidth, 2), 0.25, 5.0); - if (calculatedZoom * targetWidth > availableWidth && calculatedZoom > 0.25) - { - calculatedZoom = Math.Max(0.25, Math.Round(calculatedZoom - 0.01, 2)); - } - - _isApplyingFitZoom = true; - try - { - ZoomMode = PdfViewerZoomMode.FitWidth; - ZoomLevel = calculatedZoom; - } - finally - { - _isApplyingFitZoom = false; - } - - StatusMessage = $"Fit to Width ({(int)Math.Round(ZoomLevel * 100)}%)"; - ShowToast($"Fit to Width ({(int)Math.Round(ZoomLevel * 100)}%)"); - } - } - - [RelayCommand] - public void FitToPage() - { - if (ViewportSizeProvider != null) - { - var dims = ViewportSizeProvider(); - if (dims.ViewportWidth > 100 && dims.ViewportHeight > 100) - { - FitToPageDynamic(dims.ViewportWidth, dims.ViewportHeight, dims.HorizontalPadding, dims.VerticalPadding); - return; - } - } - - _isApplyingFitZoom = true; - try - { - ZoomMode = PdfViewerZoomMode.FitPage; - ZoomLevel = 0.95; - } - finally - { - _isApplyingFitZoom = false; - } - } - - public void FitToPageDynamic(double viewportWidth, double viewportHeight, double horizontalPadding = 64.0, double verticalPadding = 64.0) - { - if (viewportWidth <= 100 || viewportHeight <= 100) return; - - var page = SelectedPage ?? Pages.FirstOrDefault(p => p.PageNumber == CurrentPageNumber) ?? Pages.FirstOrDefault(); - if (page == null) - { - _isApplyingFitZoom = true; - try - { - ZoomMode = PdfViewerZoomMode.FitPage; - ZoomLevel = 0.95; - } - finally - { - _isApplyingFitZoom = false; - } - return; - } - - double availableWidth = Math.Max(100.0, viewportWidth - horizontalPadding - 8.0); - // Reserve 36px for page footnote indicator and card vertical spacing - double availableHeight = Math.Max(100.0, viewportHeight - verticalPadding - 36.0); - - double targetWidth = (page.RotationAngle % 180 == 0) ? page.WidthPoints : page.HeightPoints; - double targetHeight = (page.RotationAngle % 180 == 0) ? page.HeightPoints : page.WidthPoints; - - if (IsTwoPageSpreadMode && SelectedSpread != null) - { - if (SelectedSpread.LeftPage != null && SelectedSpread.RightPage != null) - { - double leftW = (SelectedSpread.LeftPage.RotationAngle % 180 == 0) ? SelectedSpread.LeftPage.WidthPoints : SelectedSpread.LeftPage.HeightPoints; - double leftH = (SelectedSpread.LeftPage.RotationAngle % 180 == 0) ? SelectedSpread.LeftPage.HeightPoints : SelectedSpread.LeftPage.WidthPoints; - double rightW = (SelectedSpread.RightPage.RotationAngle % 180 == 0) ? SelectedSpread.RightPage.WidthPoints : SelectedSpread.RightPage.HeightPoints; - double rightH = (SelectedSpread.RightPage.RotationAngle % 180 == 0) ? SelectedSpread.RightPage.HeightPoints : SelectedSpread.RightPage.WidthPoints; - targetWidth = leftW + rightW; - targetHeight = Math.Max(leftH, rightH); - availableWidth = Math.Max(100.0, availableWidth - 16.0); - } - else - { - var single = SelectedSpread.LeftPage ?? SelectedSpread.RightPage ?? page; - targetWidth = (single.RotationAngle % 180 == 0) ? single.WidthPoints : single.HeightPoints; - targetHeight = (single.RotationAngle % 180 == 0) ? single.HeightPoints : single.WidthPoints; - } - } - - if (targetWidth > 10.0 && targetHeight > 10.0) - { - double scaleX = availableWidth / targetWidth; - double scaleY = availableHeight / targetHeight; - double calculatedZoom = Math.Clamp(Math.Round(Math.Min(scaleX, scaleY), 2), 0.25, 5.0); - if ((calculatedZoom * targetWidth > availableWidth || calculatedZoom * targetHeight > availableHeight) && calculatedZoom > 0.25) - { - calculatedZoom = Math.Max(0.25, Math.Round(calculatedZoom - 0.01, 2)); - } - - _isApplyingFitZoom = true; - try - { - ZoomMode = PdfViewerZoomMode.FitPage; - ZoomLevel = calculatedZoom; - } - finally - { - _isApplyingFitZoom = false; - } - - StatusMessage = $"Fit to Page ({(int)Math.Round(ZoomLevel * 100)}%)"; - ShowToast($"Fit to Page ({(int)Math.Round(ZoomLevel * 100)}%)"); - } - } - - [RelayCommand] - public void SetZoomPreset(string? preset) - { - if (string.IsNullOrWhiteSpace(preset)) return; - string clean = preset.Replace("%", "").Trim(); - if (double.TryParse(clean, out double val)) - { - ZoomMode = PdfViewerZoomMode.Custom; - ZoomLevel = Math.Clamp(val / 100.0, 0.25, 5.0); - } - } - - // --- Page Operations & Visual Rotation --- - - [RelayCommand] - public void RotateClockwise() - { - var page = SelectedPage ?? Pages.FirstOrDefault(p => p.PageNumber == CurrentPageNumber) ?? Pages.FirstOrDefault(); - if (page != null) - { - SelectedPage = page; - page.RotationAngle = (page.RotationAngle + 90) % 360; - StatusMessage = $"Page {page.PageNumber} rotated 90° CW ({page.RotationAngle}°)."; - ShowToast($"Page {page.PageNumber} rotated 90° CW ({page.RotationAngle}°)"); - - if (IsFitToPageActive) - { - FitToPage(); - } - else if (IsFitToWidthActive) - { - FitToWidth(); - } - } - } - - [RelayCommand] - public void RotateCounterClockwise() - { - var page = SelectedPage ?? Pages.FirstOrDefault(p => p.PageNumber == CurrentPageNumber) ?? Pages.FirstOrDefault(); - if (page != null) - { - SelectedPage = page; - page.RotationAngle = (page.RotationAngle + 270) % 360; - StatusMessage = $"Page {page.PageNumber} rotated 90° CCW ({page.RotationAngle}°)."; - ShowToast($"Page {page.PageNumber} rotated 90° CCW ({page.RotationAngle}°)"); - - if (IsFitToPageActive) - { - FitToPage(); - } - else if (IsFitToWidthActive) - { - FitToWidth(); - } - } - } - - [RelayCommand] - public void RotateAllPagesClockwise() - { - foreach (var p in Pages) - { - p.RotationAngle = (p.RotationAngle + 90) % 360; - } - StatusMessage = "Rotated all pages 90° clockwise."; - ShowToast("Rotated all pages 90° CW"); - - if (IsFitToPageActive) - { - FitToPage(); - } - else if (IsFitToWidthActive) - { - FitToWidth(); - } - } - - - // --- Sidebar & Layout Commands --- - - [RelayCommand] - public void SelectSidebarTab(string tabName) - { - if (Enum.TryParse(tabName, true, out var tab)) - { - SelectedSidebarTab = tab; - IsSidebarOpen = true; - } - } - - [RelayCommand] - public void ToggleSidebar() - { - IsSidebarOpen = !IsSidebarOpen; - } - - [RelayCommand] - public void ToggleFullscreen() - { - IsFullscreen = !IsFullscreen; - string msg = IsFullscreen ? "Entered Presentation Reading Mode (Esc to exit)" : "Exited Fullscreen Mode"; - ShowToast(msg); - } - - // --- Bridge to Studio & Tools --- - - [RelayCommand] - public void EditInStudio() - { - string? targetPath = null; - if (!string.IsNullOrEmpty(CurrentFilePath) && File.Exists(CurrentFilePath)) - { - targetPath = CurrentFilePath; - } - else if (_currentPdfBytes != null) - { - string tempPath = Path.Combine(Path.GetTempPath(), DocumentTitle); - File.WriteAllBytes(tempPath, _currentPdfBytes); - targetPath = tempPath; - } - - if (targetPath != null) - { - WeakReferenceMessenger.Default.Send(new OpenInEditorMessage(targetPath)); - } - } - - [RelayCommand] - public void RunToolOnDocument(string? toolIdStr) - { - if (!string.IsNullOrEmpty(CurrentFilePath) && File.Exists(CurrentFilePath) && Enum.TryParse(toolIdStr, true, out var toolId)) - { - WeakReferenceMessenger.Default.Send(new RunToolMessage(toolId, CurrentFilePath)); - } - } - - [RelayCommand] - public void BackToHome() - { - WeakReferenceMessenger.Default.Send(new NavigateToHomeMessage()); - } -} - diff --git a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Rendering.cs b/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Rendering.cs deleted file mode 100644 index 5fc6bb0..0000000 --- a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Rendering.cs +++ /dev/null @@ -1,468 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Media.Imaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Services; -using SkiaSharp; -using UglyToad.PdfPig.Rendering.Skia; - -namespace PdfEditorApp.ViewModels; - -public partial class PdfViewerViewModel -{ - /// Renders a specific page at the specified scale directly from PDF bytes using Skia. - /// Renders a specific page at the specified scale directly to image bytes. - public byte[]? RenderPageBytesAtScale(int pageNumber, float scale) - { - if (_currentPdfBytes == null) return null; - lock (_renderLock) - { - try - { - var doc = OpenOrReuseDocument(); - if (doc == null) return null; - - using var stream = PdfPigExtensions.GetPageAsPng(doc, pageNumber, scale, 100); - if (stream != null && stream.Length > 0) - { - return stream.ToArray(); - } - } - catch { } - } - return null; - } - - /// Renders a specific page at the specified scale directly from PDF bytes using Skia. - public Bitmap? RenderPageAtScale(int pageNumber, float scale, PdfReaderTheme? theme = null) - { - var bytes = RenderPageBytesAtScale(pageNumber, scale); - if (bytes != null && bytes.Length > 0) - { - try - { - var activeTheme = theme ?? ReadingTheme; - if (activeTheme == PdfReaderTheme.Default) - { - using var ms = new MemoryStream(bytes); - return new Bitmap(ms); - } - - // Apply theme color filter using SkiaSharp - using var stream = new MemoryStream(bytes); - using var skBitmap = SKBitmap.Decode(stream); - if (skBitmap != null) - { - using var themed = ApplyThemeToSkBitmap(skBitmap, activeTheme); - using var img = SKImage.FromBitmap(themed); - using var data = img.Encode(SKEncodedImageFormat.Png, 95); - using var outStream = data.AsStream(); - return new Bitmap(outStream); - } - - using var fallbackMs = new MemoryStream(bytes); - return new Bitmap(fallbackMs); - } - catch { } - } - return null; - } - - public static SKBitmap ApplyThemeToSkBitmap(SKBitmap source, PdfReaderTheme theme) - { - var dest = new SKBitmap(source.Width, source.Height, source.ColorType, source.AlphaType); - using var canvas = new SKCanvas(dest); - using var paint = new SKPaint(); - - if (theme == PdfReaderTheme.Sepia) - { - // Warm eye-care sepia matrix - float[] sepiaMatrix = new float[] - { - 0.393f, 0.769f, 0.189f, 0, 0, - 0.349f, 0.686f, 0.168f, 0, 0, - 0.272f, 0.534f, 0.131f, 0, 0, - 0, 0, 0, 1, 0 - }; - paint.ColorFilter = SKColorFilter.CreateColorMatrix(sepiaMatrix); - } - else if (theme == PdfReaderTheme.Dark) - { - // Comfortable dark mode: converts pure white (1.0) to dark slate (30,41,59) - // and pure black (0.0) to soft off-white (241,245,249) - float scale = -0.827f; - float[] darkMatrix = new float[] - { - scale, 0, 0, 0, 241f / 255f, - 0, scale, 0, 0, 245f / 255f, - 0, 0, scale, 0, 249f / 255f, - 0, 0, 0, 1, 0 - }; - paint.ColorFilter = SKColorFilter.CreateColorMatrix(darkMatrix); - } - else if (theme == PdfReaderTheme.HighContrast) - { - // High contrast accessibility: inverted black background with sharp yellow text - float[] hcMatrix = new float[] - { - -1.0f, 0, 0, 0, 1.0f, - -0.1f, -0.9f, 0, 0, 1.0f, - 0, 0, -1.0f, 0, 40f / 255f, - 0, 0, 0, 1, 0 - }; - paint.ColorFilter = SKColorFilter.CreateColorMatrix(hcMatrix); - } - - canvas.DrawBitmap(source, 0, 0, paint); - return dest; - } - - /// Ensures a page is rendered at the appropriate scale with top priority. - public void EnsurePageRendered(int pageNumber, float? scale = null) - { - var page = Pages.FirstOrDefault(p => p.PageNumber == pageNumber); - if (page == null) return; - - float targetScale = scale ?? Math.Clamp((float)(ZoomLevel * 2.25f), PdfViewerPageItem.BasePageRenderScale, 5.0f); - if (page.Bitmap == null || Math.Abs(page.RenderedScale - targetScale) > 0.5f || page.AppliedReadingTheme != ReadingTheme) - { - // Scrolling fires this repeatedly for the same pages, and a page's Bitmap stays - // null until its render finishes — so without this guard, scrolling back and forth - // queues up redundant renders of pages already being rendered. Since every render - // serializes behind the single render lock, that backlog delays the pages the user - // is actually looking at and makes scrolling feel unresponsive. - if (page.IsRenderLoading) return; - page.IsRenderLoading = true; - Interlocked.Increment(ref _pendingForegroundRenders); - - var theme = ReadingTheme; - Task.Run(() => - { - Bitmap? bmp = null; - try - { - bmp = RenderPageAtScale(pageNumber, targetScale, theme); - } - finally - { - // Released on the worker thread, not inside the dispatcher callback, so the - // background sweep can resume as soon as the render itself is done rather - // than waiting on UI-thread scheduling. - Interlocked.Decrement(ref _pendingForegroundRenders); - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - if (bmp != null) - { - page.Bitmap = bmp; - page.RenderedScale = targetScale; - page.AppliedReadingTheme = theme; - } - page.IsRenderLoading = false; - }); - } - }); - } - } - - private void ReRenderActivePagesForTheme() - { - if (Pages == null || Pages.Count == 0) return; - - foreach (var page in Pages) - { - if (page.Bitmap != null) - { - EnsurePageRendered(page.PageNumber, page.RenderedScale); - } - } - - if (IsSinglePageMode && SelectedPage != null) - { - EnsurePageRendered(SelectedPage.PageNumber); - } - else if (IsTwoPageSpreadMode && SelectedSpread != null) - { - if (SelectedSpread.LeftPage != null) EnsurePageRendered(SelectedSpread.LeftPage.PageNumber); - if (SelectedSpread.RightPage != null) EnsurePageRendered(SelectedSpread.RightPage.PageNumber); - } - } - - /// - /// Tells the viewer which pages are actually on screen right now (called from the view's - /// scroll handler). Renders full-resolution bitmaps for the visible range plus a small - /// lookahead, and releases them for pages that have scrolled well out of view — so memory - /// stays bounded by what's near the viewport instead of growing with document length. - /// - public void RequestPagesVisible(int firstPageNumber, int lastPageNumber) - { - if (Pages.Count == 0) return; - - // Small lookahead by design. Every render serializes behind a single lock, so a wide - // window just builds a queue that delays the pages actually on screen. - const int renderLookahead = 2; - const int keepAliveLookahead = 40; - - int renderFirst = Math.Max(1, firstPageNumber - renderLookahead); - int renderLast = Math.Min(Pages.Count, lastPageNumber + renderLookahead); - - if (renderFirst == _lastVisibleFirstPage && renderLast == _lastVisibleLastPage) return; - _lastVisibleFirstPage = renderFirst; - _lastVisibleLastPage = renderLast; - - // 1. On-screen pages first. Order matters: renders are serialized, so anything queued - // ahead of the visible pages directly delays what the user is waiting to see. This - // used to iterate from (firstVisible - lookahead) upward, which meant the pages just - // ABOVE the viewport were always rasterized before the page being looked at. - for (int p = firstPageNumber; p <= lastPageNumber; p++) - { - EnsurePageRendered(p); - } - - // 2. Then the lookahead, nearest-first outward. - for (int d = 1; d <= renderLookahead; d++) - { - int before = firstPageNumber - d; - if (before >= 1) EnsurePageRendered(before); - - int after = lastPageNumber + d; - if (after <= Pages.Count) EnsurePageRendered(after); - } - - // Note: text geometry is deliberately NOT warmed here. It's as expensive as a render - // and shares the same lock, so warming it for every page in the window starved the - // visible pages' renders. It stays on-demand (first pointer interaction with a page). - - // 3. Release bitmaps for pages far outside the viewport. - int keepFirst = Math.Max(1, firstPageNumber - keepAliveLookahead); - int keepLast = Math.Min(Pages.Count, lastPageNumber + keepAliveLookahead); - - for (int i = 0; i < Pages.Count; i++) - { - int pageNum = i + 1; - if (pageNum < keepFirst || pageNum > keepLast) - { - // The "Fallback when loading" placeholder shows again if the user scrolls back; - // ThumbnailBitmap is left alone since the thumbnail rail may show a wider - // range than the main viewport and thumbnails are cheap to keep resident. - Pages[i].Bitmap = null; - } - } - } - - private void StartBackgroundWorker(CancellationToken ct) - { - Task.Run(async () => - { - if (_currentPdfBytes == null) return; - - // 1. Asynchronously extract Bookmarks/Outlines without blocking initial page display - try - { - List? bookmarksList = null; - lock (_renderLock) - { - var doc = OpenOrReuseDocument(); - if (doc != null && doc.TryGetBookmarks(out var bookmarks) && bookmarks != null && bookmarks.Roots != null) - { - bookmarksList = new List(); - ExtractBookmarksRecursive(bookmarks.Roots, bookmarksList); - } - } - - if (bookmarksList != null && bookmarksList.Count > 0 && !ct.IsCancellationRequested) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - Bookmarks.Clear(); - foreach (var b in bookmarksList) Bookmarks.Add(b); - OnPropertyChanged(nameof(HasBookmarks)); - }); - } - } - catch { } - - // 2. Progressively build thumbnails and extract accurate text / geometry for every - // page — full-resolution page bitmaps are NOT rendered here (beyond the first - // screenful below); those are rendered on demand for the pages actually scrolled - // into view (see RequestPagesVisible), otherwise a large document would render - // every page's full-res bitmap up front and hold them all in memory regardless of - // whether they're ever looked at. - const int eagerFirstScreenfulPages = 8; - for (int i = 1; i <= Pages.Count; i++) - { - if (ct.IsCancellationRequested) return; - - int pageNum = i; - var page = Pages.FirstOrDefault(p => p.PageNumber == pageNum); - if (page != null) - { - // Stand aside whenever the user is waiting on a visible page. Everything in - // this sweep competes for the same render lock, and over a long document - // it's minutes of work — without yielding, a scrolled-to page's render sits - // behind hundreds of thumbnail renders and text extractions and takes so - // long to arrive that scrolling looks like it does nothing at all. - if (pageNum > eagerFirstScreenfulPages) - { - await WaitForForegroundIdleAsync(ct); - if (ct.IsCancellationRequested) return; - } - - // Render a bounded first screenful eagerly and unconditionally — a safety - // net independent of the view's own viewport/scroll wiring, so the pages a - // user sees immediately after opening a document are never left waiting on - // that wiring alone. - if (pageNum <= eagerFirstScreenfulPages && page.Bitmap == null) - { - var eagerBmp = RenderPageAtScale(pageNum, PdfViewerPageItem.BasePageRenderScale); - if (eagerBmp != null && !ct.IsCancellationRequested) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - page.Bitmap = eagerBmp; - page.RenderedScale = PdfViewerPageItem.BasePageRenderScale; - }); - } - } - - // Render lightweight thumbnail (0.4f scale) if needed - if (page.ThumbnailBitmap == null) - { - var thumbBmp = RenderPageAtScale(pageNum, 0.4f); - if (thumbBmp != null && !ct.IsCancellationRequested) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - page.ThumbnailBitmap = thumbBmp; - }); - } - } - - // Extract accurate dimensions and text if page > 1 - if (pageNum > 1 && (page.Words.Count == 0 || string.IsNullOrEmpty(page.ExtractedText))) - { - try - { - string? txt = null; - List? words = null; - List? lines = null; - double w = 0, h = 0; - int rot = 0; - - lock (_renderLock) - { - var doc = OpenOrReuseDocument(); - if (doc != null && pageNum <= doc.NumberOfPages) - { - var p = doc.GetPage(pageNum); - (txt, words, lines) = ExtractPageTextGeometry(p); - w = Math.Max(100, p.Width); - h = Math.Max(100, p.Height); - rot = (int)p.Rotation.Value; - } - } - - if ((words == null || words.Count == 0) && w > 0 && h > 0) - { - var (ocrTxt, ocrWords, ocrLines) = ExtractOcrPageTextGeometry(pageNum, w, h); - if (ocrWords.Count > 0) - { - txt = ocrTxt; - words = ocrWords; - lines = ocrLines; - } - } - - string summary = ""; - if (!string.IsNullOrWhiteSpace(txt)) - { - var firstLine = txt.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? ""; - summary = firstLine.Length > 50 ? firstLine.Substring(0, 50) + "..." : firstLine; - } - - if (txt != null && !ct.IsCancellationRequested) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - page.WidthPoints = w; - page.HeightPoints = h; - page.RotationAngle = rot; - page.ExtractedText = txt; - page.Words = words!; - page.TextLines = lines!; - page.PageSummary = summary; - }); - } - } - catch { } - } - } - - await Task.Yield(); - } - }, ct); - } - - private void RebuildPageSpreads() - { - PageSpreads.Clear(); - if (Pages.Count == 0) return; - - // Page 1 is Cover (Alone on right or single spread) - int spreadIdx = 1; - var coverSpread = new PdfViewerPageSpreadItem - { - SpreadIndex = spreadIdx++, - LeftPage = null, - RightPage = Pages[0], - SpreadLabel = "Page 1 (Cover)", - IsSelected = true - }; - PageSpreads.Add(coverSpread); - - // Subsequent pages paired (2-3, 4-5, etc.) - for (int i = 1; i < Pages.Count; i += 2) - { - var left = Pages[i]; - var right = (i + 1 < Pages.Count) ? Pages[i + 1] : null; - string lbl = right != null ? $"Pages {left.PageNumber} - {right.PageNumber}" : $"Page {left.PageNumber}"; - - PageSpreads.Add(new PdfViewerPageSpreadItem - { - SpreadIndex = spreadIdx++, - LeftPage = left, - RightPage = right, - SpreadLabel = lbl, - IsSelected = false - }); - } - - SelectedSpread = PageSpreads.FirstOrDefault(); - } - - private void UpdateSelectedSpreadForPage(int pageNum) - { - if (PageSpreads.Count == 0) return; - - foreach (var s in PageSpreads) - { - bool match = (s.LeftPage?.PageNumber == pageNum || s.RightPage?.PageNumber == pageNum); - s.IsSelected = match; - if (match) - { - SelectedSpread = s; - } - } - - if (IsTwoPageSpreadMode && SelectedSpread != null) - { - if (SelectedSpread.LeftPage != null) EnsurePageRendered(SelectedSpread.LeftPage.PageNumber); - if (SelectedSpread.RightPage != null) EnsurePageRendered(SelectedSpread.RightPage.PageNumber); - } - } - -} diff --git a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Search.cs b/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Search.cs deleted file mode 100644 index df31437..0000000 --- a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Search.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; -using CommunityToolkit.Mvvm.Input; - -namespace PdfEditorApp.ViewModels; - -public partial class PdfViewerViewModel -{ - // --- Text Search & Find in Document --- - - [RelayCommand] - public void ToggleSearchBar() - { - IsSearchBarVisible = !IsSearchBarVisible; - if (IsSearchBarVisible) - { - SelectedSidebarTab = PdfViewerSidebarTab.Search; - IsSidebarOpen = true; - } - } - - [RelayCommand] - public void PerformSearch() - { - SearchResults.Clear(); - if (string.IsNullOrWhiteSpace(SearchQuery)) - { - TotalMatchesCount = 0; - CurrentMatchIndex = 0; - SearchStatusText = string.Empty; - OnPropertyChanged(nameof(HasSearchResults)); - return; - } - - string q = SearchQuery.Trim(); - int matchIdx = 1; - - if (SearchWholeWord) - { - var regexOptions = SearchMatchCase ? RegexOptions.None : RegexOptions.IgnoreCase; - string pattern = $@"\b{Regex.Escape(q)}\b"; - - foreach (var page in Pages) - { - if (string.IsNullOrEmpty(page.ExtractedText)) continue; - - var matches = Regex.Matches(page.ExtractedText, pattern, regexOptions); - foreach (Match m in matches) - { - int snippetStart = Math.Max(0, m.Index - 25); - int snippetLen = Math.Min(page.ExtractedText.Length - snippetStart, m.Length + 50); - string snippet = "..." + page.ExtractedText.Substring(snippetStart, snippetLen).Replace('\r', ' ').Replace('\n', ' ') + "..."; - - SearchResults.Add(new PdfViewerSearchMatch - { - PageNumber = page.PageNumber, - Snippet = snippet, - MatchIndex = matchIdx++ - }); - } - } - } - else - { - var comp = SearchMatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - foreach (var page in Pages) - { - if (string.IsNullOrEmpty(page.ExtractedText)) continue; - - int startIndex = 0; - while ((startIndex = page.ExtractedText.IndexOf(q, startIndex, comp)) != -1) - { - int snippetStart = Math.Max(0, startIndex - 25); - int snippetLen = Math.Min(page.ExtractedText.Length - snippetStart, q.Length + 50); - string snippet = "..." + page.ExtractedText.Substring(snippetStart, snippetLen).Replace('\r', ' ').Replace('\n', ' ') + "..."; - - SearchResults.Add(new PdfViewerSearchMatch - { - PageNumber = page.PageNumber, - Snippet = snippet, - MatchIndex = matchIdx++ - }); - - startIndex += q.Length; - } - } - } - - TotalMatchesCount = SearchResults.Count; - CurrentMatchIndex = TotalMatchesCount > 0 ? 1 : 0; - SearchStatusText = TotalMatchesCount > 0 ? $"{CurrentMatchIndex} of {TotalMatchesCount} matches" : "No matches found"; - OnPropertyChanged(nameof(HasSearchResults)); - - if (TotalMatchesCount > 0) - { - JumpToMatch(SearchResults[0]); - } - } - - [RelayCommand] - public void NextMatch() - { - if (TotalMatchesCount == 0) return; - CurrentMatchIndex = (CurrentMatchIndex % TotalMatchesCount) + 1; - SearchStatusText = $"{CurrentMatchIndex} of {TotalMatchesCount} matches"; - JumpToMatch(SearchResults[CurrentMatchIndex - 1]); - } - - [RelayCommand] - public void PreviousMatch() - { - if (TotalMatchesCount == 0) return; - CurrentMatchIndex = (CurrentMatchIndex - 2 + TotalMatchesCount) % TotalMatchesCount + 1; - SearchStatusText = $"{CurrentMatchIndex} of {TotalMatchesCount} matches"; - JumpToMatch(SearchResults[CurrentMatchIndex - 1]); - } - - [RelayCommand] - public void JumpToMatch(PdfViewerSearchMatch? match) - { - if (match == null) return; - int pageIdx = match.PageNumber - 1; - if (pageIdx >= 0 && pageIdx < Pages.Count) - { - SelectedPage = Pages[pageIdx]; - CurrentPageNumber = match.PageNumber; - RequestScrollToPage(match.PageNumber); - } - } - - [RelayCommand] - public void ClearSearch() - { - SearchQuery = string.Empty; - SearchResults.Clear(); - TotalMatchesCount = 0; - CurrentMatchIndex = 0; - SearchStatusText = string.Empty; - OnPropertyChanged(nameof(HasSearchResults)); - } - -} diff --git a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Selection.cs b/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Selection.cs deleted file mode 100644 index 2805aea..0000000 --- a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.Selection.cs +++ /dev/null @@ -1,516 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Input.Platform; -using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Ocr; -using SkiaSharp; - -namespace PdfEditorApp.ViewModels; - -public partial class PdfViewerViewModel -{ - // --- Interactive Selection & Clipboard Operations --- - - [RelayCommand] - public async Task CopySelectedTextAsync() - { - if (string.IsNullOrWhiteSpace(ActiveSelectedText)) - { - if (SelectedPage != null && !string.IsNullOrWhiteSpace(SelectedPage.ExtractedText)) - { - await CopyPageTextAsync(); - } - return; - } - - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) - { - var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(desktop.MainWindow); - if (topLevel?.Clipboard != null) - { - await topLevel.Clipboard.SetTextAsync(ActiveSelectedText); - string snippet = ActiveSelectedText.Length > 40 ? ActiveSelectedText.Substring(0, 40) + "..." : ActiveSelectedText; - ShowToast($"Copied: \"{snippet}\""); - } - } - } - - [RelayCommand] - public async Task CopySelectedCitationAsync() - { - if (string.IsNullOrWhiteSpace(ActiveSelectedText)) return; - - string citation = $"\"{ActiveSelectedText}\"\n— Page {ActiveSelectedPageNumber}, {DocumentTitle}"; - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) - { - var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(desktop.MainWindow); - if (topLevel?.Clipboard != null) - { - await topLevel.Clipboard.SetTextAsync(citation); - ShowToast("Copied citation with page reference"); - } - } - } - - [RelayCommand] - public void SearchSelectedText() - { - if (string.IsNullOrWhiteSpace(ActiveSelectedText)) return; - SearchQuery = ActiveSelectedText.Trim(); - IsSearchBarVisible = true; - SelectedSidebarTab = PdfViewerSidebarTab.Search; - IsSidebarOpen = true; - PerformSearch(); - } - - [RelayCommand] - public void SearchWebSelectedText() - { - if (string.IsNullOrWhiteSpace(ActiveSelectedText)) return; - try - { - string query = Uri.EscapeDataString(ActiveSelectedText.Trim()); - string url = $"https://www.google.com/search?q={query}"; - var psi = new System.Diagnostics.ProcessStartInfo - { - FileName = url, - UseShellExecute = true - }; - System.Diagnostics.Process.Start(psi); - } - catch { } - } - - [RelayCommand] - public void SelectAllPageText() - { - var page = SelectedPage ?? Pages.FirstOrDefault(p => p.PageNumber == CurrentPageNumber); - if (page == null) return; - page.SelectAll(); - ActiveSelectedText = page.SelectedText; - ActiveSelectedPageNumber = page.PageNumber; - HasTextSelection = !string.IsNullOrEmpty(ActiveSelectedText); - ShowToast($"Selected all text on Page {page.PageNumber}"); - } - - [RelayCommand] - public void ClearSelection() - { - ActiveSelectedText = string.Empty; - HasTextSelection = false; - LastSelectedAreaRect = null; - foreach (var p in Pages) - { - p.ClearSelection(); - } - } - - [RelayCommand] - public void SetTextSelectionMode() - { - SelectionMode = PdfViewerSelectionMode.Text; - ShowToast("Text Selection Mode (I-Beam)"); - } - - [RelayCommand] - public void SetAreaSelectionMode() - { - SelectionMode = PdfViewerSelectionMode.Area; - ShowToast("Area / Marquee Selection Mode (Box / Snapshot)"); - } - - [RelayCommand] - public void ToggleSelectionMode() - { - if (SelectionMode == PdfViewerSelectionMode.Text) - SetAreaSelectionMode(); - else - SetTextSelectionMode(); - } - - [RelayCommand] - public void DismissScannedBanner() - { - IsScannedBannerDismissed = true; - } - - [RelayCommand] - public void CancelOcr() - { - _ocrCts?.Cancel(); - IsOcrRunning = false; - OcrStatusText = "Cancelled"; - ShowToast("OCR cancelled."); - } - - [RelayCommand] - public async Task RecognizeActivePageOcrAsync() - { - var page = SelectedPage ?? Pages.FirstOrDefault(p => p.PageNumber == CurrentPageNumber) ?? Pages.FirstOrDefault(); - if (page == null || IsOcrRunning) return; - - _ocrCts?.Cancel(); - _ocrCts = new CancellationTokenSource(); - var ct = _ocrCts.Token; - - IsOcrRunning = true; - OcrProgress = 15; - OcrStatusText = $"Recognizing text on Page {page.PageNumber}..."; - - try - { - await Task.Run(() => - { - double width = page.WidthPoints > 0 ? page.WidthPoints : 612; - double height = page.HeightPoints > 0 ? page.HeightPoints : 792; - var (ocrText, ocrWords, ocrLines) = ExtractOcrPageTextGeometry(page.PageNumber, width, height); - - if (ocrWords.Count > 0) - { - page.Words = ocrWords; - page.TextLines = ocrLines; - page.ExtractedText = ocrText; - } - }, ct); - - OcrProgress = 100; - IsCurrentPageScanned = page.Words.Count == 0; - page.NotifySelectionChanged(); - ShowToast($"OCR Complete: {page.Words.Count} words recognized on Page {page.PageNumber}."); - } - catch (OperationCanceledException) - { - ShowToast("OCR cancelled."); - } - catch (Exception ex) - { - ShowToast($"OCR failed: {ex.Message}"); - } - finally - { - IsOcrRunning = false; - OcrStatusText = string.Empty; - } - } - - [RelayCommand] - public async Task RecognizeDocumentOcrAsync() - { - if (Pages.Count == 0 || IsOcrRunning) return; - - _ocrCts?.Cancel(); - _ocrCts = new CancellationTokenSource(); - var ct = _ocrCts.Token; - - IsOcrRunning = true; - int total = Pages.Count; - int recognizedPages = 0; - int totalWords = 0; - - try - { - for (int i = 0; i < total; i++) - { - ct.ThrowIfCancellationRequested(); - var page = Pages[i]; - int pageNum = page.PageNumber; - OcrProgress = (double)(i + 1) / total * 100.0; - OcrStatusText = $"Recognizing text... Page {pageNum} of {total} ({(int)OcrProgress}%)"; - - if (page.Words.Count == 0) - { - await Task.Run(() => - { - double width = page.WidthPoints > 0 ? page.WidthPoints : 612; - double height = page.HeightPoints > 0 ? page.HeightPoints : 792; - var (ocrText, ocrWords, ocrLines) = ExtractOcrPageTextGeometry(pageNum, width, height); - if (ocrWords.Count > 0) - { - page.Words = ocrWords; - page.TextLines = ocrLines; - page.ExtractedText = ocrText; - Interlocked.Add(ref totalWords, ocrWords.Count); - Interlocked.Increment(ref recognizedPages); - } - }, ct); - - page.NotifySelectionChanged(); - } - } - - IsScannedDocument = false; - if (SelectedPage != null) - { - IsCurrentPageScanned = SelectedPage.Words.Count == 0; - } - ShowToast($"OCR Complete: {totalWords} words recognized across {recognizedPages} scanned page(s)."); - } - catch (OperationCanceledException) - { - ShowToast("OCR processing cancelled."); - } - catch (Exception ex) - { - ShowToast($"OCR error: {ex.Message}"); - } - finally - { - IsOcrRunning = false; - OcrStatusText = string.Empty; - } - } - - [RelayCommand] - public async Task RecognizeSelectedTextOcrAsync() - { - var page = SelectedPage ?? Pages.FirstOrDefault(p => p.PageNumber == CurrentPageNumber); - if (page == null || IsOcrRunning) return; - - // Determine bounding box of selection - Rect targetRect = default; - if (LastSelectedAreaRect.HasValue && LastSelectedAreaRect.Value.Width > 5 && LastSelectedAreaRect.Value.Height > 5) - { - targetRect = LastSelectedAreaRect.Value; - } - else if (page.SelectionRects.Count > 0) - { - double minX = page.SelectionRects.Min(r => r.Left); - double minY = page.SelectionRects.Min(r => r.Top); - double maxX = page.SelectionRects.Max(r => r.Right); - double maxY = page.SelectionRects.Max(r => r.Bottom); - targetRect = new Rect(minX, minY, Math.Max(5, maxX - minX), Math.Max(5, maxY - minY)); - } - - if (targetRect.Width <= 2 || targetRect.Height <= 2) - { - await RecognizeActivePageOcrAsync(); - return; - } - - IsOcrRunning = true; - OcrStatusText = "Recognizing selection with OCR..."; - OcrProgress = 30; - - try - { - string ocrText = await ExtractOcrTextFromRegionAsync(page, targetRect, forceOcr: true); - if (!string.IsNullOrWhiteSpace(ocrText)) - { - page.SelectedText = ocrText; - ActiveSelectedText = ocrText; - HasTextSelection = true; - ShowToast("Text recognized with OCR."); - } - else - { - ShowToast("OCR did not detect text in the selected area."); - } - } - catch (Exception ex) - { - ShowToast($"OCR failed: {ex.Message}"); - } - finally - { - IsOcrRunning = false; - OcrStatusText = string.Empty; - } - } - - public async Task ExtractOcrTextFromRegionAsync(PdfViewerPageItem page, Rect region, bool forceOcr = false) - { - if (page == null || region.Width <= 2 || region.Height <= 2) return string.Empty; - - // If words already exist and not forced, check if they are valid readable text - if (!forceOcr && page.Words.Count > 0) - { - var intersectingWords = page.Words - .Where(w => w.Bounds.Intersects(region)) - .OrderBy(w => w.Bounds.Top) - .ThenBy(w => w.Bounds.Left) - .ToList(); - - if (intersectingWords.Count > 0) - { - string combined = string.Join(" ", intersectingWords.Select(w => w.Text)); - if (!IsGarbledText(combined)) - { - return combined; - } - } - } - - // Targeted OCR on cropped rectangle - try - { - var ocrProvider = CompositeOcrProvider.Default; - if (!ocrProvider.IsAvailable) return string.Empty; - - const float ocrScale = 2.0f; - byte[]? pagePng = RenderPageBytesAtScale(page.PageNumber, ocrScale); - if (pagePng == null || pagePng.Length == 0) return string.Empty; - - byte[]? croppedBytes = await Task.Run(() => - { - using var ms = new MemoryStream(pagePng); - using var codec = SKCodec.Create(ms); - if (codec == null) return null; - using var origBitmap = SKBitmap.Decode(codec); - if (origBitmap == null) return null; - - int cropX = Math.Clamp((int)(region.X * ocrScale), 0, Math.Max(0, origBitmap.Width - 1)); - int cropY = Math.Clamp((int)(region.Y * ocrScale), 0, Math.Max(0, origBitmap.Height - 1)); - int cropW = Math.Clamp((int)(region.Width * ocrScale), 1, origBitmap.Width - cropX); - int cropH = Math.Clamp((int)(region.Height * ocrScale), 1, origBitmap.Height - cropY); - - using var cropped = new SKBitmap(); - origBitmap.ExtractSubset(cropped, new SKRectI(cropX, cropY, cropX + cropW, cropY + cropH)); - using var image = SKImage.FromBitmap(cropped); - using var data = image.Encode(SKEncodedImageFormat.Png, 100); - return data.ToArray(); - }); - - if (croppedBytes == null || croppedBytes.Length == 0) return string.Empty; - - var ocrResult = await ocrProvider.RecognizeTextAsync(croppedBytes); - if (ocrResult.Success && !string.IsNullOrWhiteSpace(ocrResult.FullText)) - { - // Register newly extracted words into page geometry so they become permanently selectable - int wIdx = page.Words.Count; - foreach (var ow in ocrResult.Words) - { - double wx = region.X + (ow.NormalizedBounds.X * region.Width); - double wy = region.Y + (ow.NormalizedBounds.Y * region.Height); - double ww = Math.Max(1, ow.NormalizedBounds.Width * region.Width); - double wh = Math.Max(1, ow.NormalizedBounds.Height * region.Height); - - page.Words.Add(new PdfViewerWordItem - { - Text = ow.Text, - Bounds = new Rect(wx, wy, ww, wh), - WordIndex = wIdx++ - }); - } - - return ocrResult.FullText.Trim(); - } - } - catch { } - - return string.Empty; - } - - [RelayCommand] - public async Task CopySelectedAreaImageAsync() - { - var page = SelectedPage ?? Pages.FirstOrDefault(p => p.PageNumber == ActiveSelectedPageNumber); - if (page == null || page.SelectionRects.Count == 0) return; - - var region = page.SelectionRects[0]; - if (region.Width <= 2 || region.Height <= 2) return; - - const float snapshotScale = 2.0f; - byte[]? pagePng = RenderPageBytesAtScale(page.PageNumber, snapshotScale); - if (pagePng == null || pagePng.Length == 0) return; - - byte[]? croppedBytes = await Task.Run(() => - { - try - { - using var ms = new MemoryStream(pagePng); - using var codec = SKCodec.Create(ms); - if (codec == null) return null; - using var origBitmap = SKBitmap.Decode(codec); - if (origBitmap == null) return null; - - int cropX = Math.Clamp((int)(region.X * snapshotScale), 0, Math.Max(0, origBitmap.Width - 1)); - int cropY = Math.Clamp((int)(region.Y * snapshotScale), 0, Math.Max(0, origBitmap.Height - 1)); - int cropW = Math.Clamp((int)(region.Width * snapshotScale), 1, origBitmap.Width - cropX); - int cropH = Math.Clamp((int)(region.Height * snapshotScale), 1, origBitmap.Height - cropY); - - using var cropped = new SKBitmap(); - origBitmap.ExtractSubset(cropped, new SKRectI(cropX, cropY, cropX + cropW, cropY + cropH)); - using var image = SKImage.FromBitmap(cropped); - using var data = image.Encode(SKEncodedImageFormat.Png, 100); - return data.ToArray(); - } - catch { return null; } - }); - - if (croppedBytes != null && croppedBytes.Length > 0) - { - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) - { - var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(desktop.MainWindow); - if (topLevel?.Clipboard != null) - { - string tempPng = Path.Combine(Path.GetTempPath(), $"PdfSnapshot_{Guid.NewGuid():N}.png"); - await File.WriteAllBytesAsync(tempPng, croppedBytes); - await topLevel.Clipboard.SetTextAsync(tempPng); - ShowToast($"Snapshot saved to {Path.GetFileName(tempPng)}"); - } - } - } - } - - [RelayCommand] - public async Task CopyPageTextAsync() - { - if (SelectedPage == null || string.IsNullOrWhiteSpace(SelectedPage.ExtractedText)) - { - ShowToast("No text on current page to copy."); - return; - } - - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) - { - var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(desktop.MainWindow); - if (topLevel?.Clipboard != null) - { - await topLevel.Clipboard.SetTextAsync(SelectedPage.ExtractedText); - ShowToast($"Copied all text from Page {SelectedPage.PageNumber} to Clipboard."); - } - } - } - - [RelayCommand] - public async Task CopyAllDocumentTextAsync() - { - var sb = new StringBuilder(); - foreach (var page in Pages) - { - if (!string.IsNullOrWhiteSpace(page.ExtractedText)) - { - sb.AppendLine($"--- Page {page.PageNumber} ---"); - sb.AppendLine(page.ExtractedText); - sb.AppendLine(); - } - } - - if (sb.Length == 0) - { - ShowToast("No text found in document."); - return; - } - - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) - { - var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(desktop.MainWindow); - if (topLevel?.Clipboard != null) - { - await topLevel.Clipboard.SetTextAsync(sb.ToString()); - ShowToast($"Copied text from all {Pages.Count} pages to Clipboard."); - } - } - } - -} diff --git a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.cs b/src/PdfEditorApp/ViewModels/PdfViewerViewModel.cs deleted file mode 100644 index 5cd23dc..0000000 --- a/src/PdfEditorApp/ViewModels/PdfViewerViewModel.cs +++ /dev/null @@ -1,600 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; -using UglyToad.PdfPig; -using UglyToad.PdfPig.Rendering.Skia; - -namespace PdfEditorApp.ViewModels; - -/// -/// Professional, Adobe Acrobat-style standalone PDF Reader & Viewer ViewModel. -/// Features continuous scroll, single-page, and two-page spread viewing, sharp Skia rendering, -/// eye-comfort reading themes (Sepia, Night/Dark, High Contrast), real page thumbnails, -/// document outline / bookmarks, in-document search, annotations, and seamless bridge to FryPDF Editor. -/// -public partial class PdfViewerViewModel : ViewModelBase -{ - private CancellationTokenSource? _renderCts; - private CancellationTokenSource? _zoomDebounceCts; - private CancellationTokenSource? _backgroundRenderCts; - private readonly object _renderLock = new(); - private byte[]? _currentPdfBytes; - private string? _currentPassword; - private int _lastVisibleFirstPage = -1; - private int _lastVisibleLastPage = -1; - - /// - /// Count of in-flight renders for pages the user is actually looking at. Every rasterization - /// and text extraction serializes behind (the shared PdfPig document - /// is not thread-safe), so the whole-document background sweep must stand aside while this is - /// non-zero — otherwise a visible page's render queues behind hundreds of background jobs and - /// effectively never arrives. - /// - private int _pendingForegroundRenders; - - /// Parks background sweep work while user-visible renders are outstanding. - private async Task WaitForForegroundIdleAsync(CancellationToken ct) - { - while (!ct.IsCancellationRequested && Volatile.Read(ref _pendingForegroundRenders) > 0) - { - try - { - await Task.Delay(40, ct); - } - catch (TaskCanceledException) - { - return; - } - } - } - - /// - /// The currently-open document, kept alive and reused for every on-demand page render and - /// geometry extraction — opening/parsing a large PDF from bytes is expensive, and doing it - /// fresh on every single page request (as this used to) meant every page you touched paid - /// a whole-document re-parse instead of just the cost of that one page. Always access under - /// ; PdfPig documents aren't safe for concurrent multi-thread use. - /// - private PdfDocument? _openDocument; - - /// Returns the shared open document for , opening it - /// (with the same repair fallback used elsewhere) if this is the first access since the - /// document loaded. Caller must hold . - private PdfDocument? OpenOrReuseDocument() - { - if (_openDocument != null) return _openDocument; - if (_currentPdfBytes == null) return null; - - var parsingOptions = new ParsingOptions(); - if (!string.IsNullOrEmpty(_currentPassword)) - { - parsingOptions.Password = _currentPassword; - } - - PdfDocument? doc = null; - try - { - doc = PdfDocument.Open(_currentPdfBytes, parsingOptions); - } - catch - { - try - { - byte[] repaired = PdfFileHelper.SalvageAndRepairPdfBytes(_currentPdfBytes); - doc = PdfDocument.Open(repaired, parsingOptions); - } - catch - { - return null; - } - } - - try { PdfPigExtensions.AddSkiaPageFactory(doc); } catch { } - _openDocument = doc; - return doc; - } - - public IStorageProvider? StorageProvider { get; set; } - - // --- Events --- - public event Action? ScrollToPageRequested; - - /// - /// Broadcasts a user-facing toast notification via WeakReferenceMessenger. - /// - public void ShowToast(string message, ToastNotificationType type = ToastNotificationType.Primary, string? iconKind = null) - { - WeakReferenceMessenger.Default.Send(new ShowToastMessage(message, type, iconKind)); - } - - // --- Observable Properties --- - - [ObservableProperty] - private string _currentFilePath = string.Empty; - - [ObservableProperty] - private string _documentTitle = "Document.pdf"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PageNavigationDisplay))] - private int _currentPageNumber = 1; - - [ObservableProperty] - private string _jumpPageText = "1"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(PageNavigationDisplay))] - private int _totalPagesCount = 0; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ZoomPercentageText))] - private double _zoomLevel = 1.0; // 100% - - [ObservableProperty] - private PdfViewerZoomMode _zoomMode = PdfViewerZoomMode.Custom; - - [ObservableProperty] - private bool _isFitToWidthActive = false; - - [ObservableProperty] - private bool _isFitToPageActive = false; - - private bool _isApplyingFitZoom = false; - - /// - /// Optional provider supplied by the view (e.g. PdfViewerView) to return the current - /// viewport dimensions (ViewportWidth, ViewportHeight, HorizontalPadding, VerticalPadding). - /// - public Func<(double ViewportWidth, double ViewportHeight, double HorizontalPadding, double VerticalPadding)>? ViewportSizeProvider { get; set; } - - // Layout Modes: Continuous Scroll, Single Page, Two-Page Spread - [ObservableProperty] - private PdfViewLayoutMode _selectedLayoutMode = PdfViewLayoutMode.ContinuousScroll; - - [ObservableProperty] - private bool _isContinuousScroll = true; - - [ObservableProperty] - private bool _isSinglePageMode = false; - - [ObservableProperty] - private bool _isTwoPageSpreadMode = false; - - // Reading Themes: Default (White), Sepia (Book comfort), Dark (Night mode), High Contrast - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsThemeDefault))] - [NotifyPropertyChangedFor(nameof(IsThemeSepia))] - [NotifyPropertyChangedFor(nameof(IsThemeDark))] - [NotifyPropertyChangedFor(nameof(IsThemeHighContrast))] - private PdfReaderTheme _readingTheme = PdfReaderTheme.Default; - - [ObservableProperty] - private string _themeBackgroundHex = "#F1F5F9"; - - [ObservableProperty] - private string _themePaperBackgroundHex = "#FFFFFF"; - - [ObservableProperty] - private string _themeTextColorHex = "#0F172A"; - - [ObservableProperty] - private string _themeBorderColorHex = "#E2E8F0"; - - [ObservableProperty] - private PdfViewerSidebarTab _selectedSidebarTab = PdfViewerSidebarTab.Thumbnails; - - [ObservableProperty] - private bool _isSidebarOpen = true; - - [ObservableProperty] - private bool _isLoading = false; - - [ObservableProperty] - private bool _isOpeningDocument = false; - - [ObservableProperty] - private string _statusMessage = "Ready"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ShowScannedDocumentBanner))] - private bool _hasDocument = false; - - [ObservableProperty] - private PdfViewerPageItem? _selectedPage; - - [ObservableProperty] - private PdfViewerPageSpreadItem? _selectedSpread; - - // In-Document Search - [ObservableProperty] - private bool _isSearchBarVisible = false; - - [ObservableProperty] - private string _searchQuery = string.Empty; - - [ObservableProperty] - private bool _searchMatchCase = false; - - [ObservableProperty] - private bool _searchWholeWord = false; - - [ObservableProperty] - private int _currentMatchIndex = 0; - - [ObservableProperty] - private int _totalMatchesCount = 0; - - [ObservableProperty] - private string _searchStatusText = string.Empty; - - // Sticky Note quick add popup - [ObservableProperty] - private bool _isAddNoteOpen = false; - - [ObservableProperty] - private string _newNoteText = string.Empty; - - // Stamp quick add popup - [ObservableProperty] - private bool _isAddStampOpen = false; - - // Highlight color selection - [ObservableProperty] - private string _selectedHighlightColorHex = "#FEF08A"; // Yellow default - - // Presentation / Fullscreen Mode - [ObservableProperty] - private bool _isFullscreen = false; - - // Interactive Text Selection State - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(SelectedTextSnippet))] - [NotifyPropertyChangedFor(nameof(IsSelectionTextGarbled))] - private string _activeSelectedText = string.Empty; - - [ObservableProperty] - private bool _hasTextSelection = false; - - [ObservableProperty] - private int _activeSelectedPageNumber = 1; - - public string SelectedTextSnippet - { - get - { - if (string.IsNullOrWhiteSpace(ActiveSelectedText)) return string.Empty; - string clean = ActiveSelectedText.Replace('\r', ' ').Replace('\n', ' ').Trim(); - return clean.Length > 35 ? clean.Substring(0, 35) + "…" : clean; - } - } - - partial void OnActiveSelectedTextChanged(string value) - { - HasTextSelection = !string.IsNullOrWhiteSpace(value); - } - - public bool IsSelectionTextGarbled => IsGarbledText(ActiveSelectedText); - - public static bool IsGarbledText(string? text) - { - if (string.IsNullOrWhiteSpace(text)) return false; - string t = text.Trim(); - if (t.Length < 2) return false; - - string[] words = t.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (words.Length == 0) return false; - - int strangeWords = 0; - foreach (var w in words) - { - // Standalone or embedded unmapped/corrupted symbols - if (w.Any(c => c == '?' || c == '*' || c == '^' || c == '~' || c == '\uFFFD' || c == '|' || c == '`' || c == '§')) - { - strangeWords++; - } - else if (w.Length >= 4) - { - int upperTransitions = 0; - for (int i = 1; i < w.Length; i++) - { - if (char.IsLower(w[i - 1]) && char.IsUpper(w[i])) upperTransitions++; - } - if (upperTransitions >= 2) strangeWords++; - } - else if (w.Length == 1 && !char.IsLetterOrDigit(w[0])) - { - strangeWords++; - } - } - - if (strangeWords >= 2 || (double)strangeWords / words.Length >= 0.25) - { - return true; - } - - int unmappedChars = t.Count(c => c == '?' || c == '*' || c == '^' || c == '~' || c == '\uFFFD' || c == '|'); - return unmappedChars > 0 && (double)unmappedChars / t.Length >= 0.10; - } - - // Selection Mode (Text vs Area/Marquee) - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsTextSelectionMode))] - [NotifyPropertyChangedFor(nameof(IsAreaSelectionMode))] - private PdfViewerSelectionMode _selectionMode = PdfViewerSelectionMode.Text; - - public bool IsTextSelectionMode => SelectionMode == PdfViewerSelectionMode.Text; - public bool IsAreaSelectionMode => SelectionMode == PdfViewerSelectionMode.Area; - - [ObservableProperty] - private Rect? _lastSelectedAreaRect; - - // Scanned Document Detection & OCR Banner State - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ShowScannedDocumentBanner))] - private bool _isScannedDocument = false; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ShowScannedDocumentBanner))] - private bool _isCurrentPageScanned = false; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ShowScannedDocumentBanner))] - private bool _isScannedBannerDismissed = false; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ShowScannedDocumentBanner))] - private bool _isOcrRunning = false; - - private CancellationTokenSource? _ocrCts; - - [ObservableProperty] - private double _ocrProgress = 0.0; - - [ObservableProperty] - private string _ocrStatusText = string.Empty; - - public bool ShowScannedDocumentBanner => - (IsScannedDocument || IsCurrentPageScanned) && - !IsScannedBannerDismissed && - !IsOcrRunning && - HasDocument; - - // Collections - public ObservableCollection Pages { get; } = new(); - public ObservableCollection PageSpreads { get; } = new(); - public ObservableCollection Bookmarks { get; } = new(); - public ObservableCollection Annotations { get; } = new(); - public ObservableCollection MetadataItems { get; } = new(); - public ObservableCollection SearchResults { get; } = new(); - - public static readonly string[] AvailableZoomPresets = new[] - { - "50%", "75%", "100%", "125%", "150%", "200%", "300%", "400%" - }; - - public bool HasBookmarks => Bookmarks.Count > 0; - public bool HasAnnotations => Annotations.Count > 0; - public bool HasSearchResults => SearchResults.Count > 0; - public int MatchCount => SearchResults.Count; - - public string ZoomPercentageText => $"{(int)Math.Round(ZoomLevel * 100)}%"; - public string PageNavigationDisplay => TotalPagesCount > 0 ? $"{CurrentPageNumber} / {TotalPagesCount}" : "0 / 0"; - - public bool IsThemeDefault => ReadingTheme == PdfReaderTheme.Default; - public bool IsThemeSepia => ReadingTheme == PdfReaderTheme.Sepia; - public bool IsThemeDark => ReadingTheme == PdfReaderTheme.Dark; - public bool IsThemeHighContrast => ReadingTheme == PdfReaderTheme.HighContrast; - - partial void OnZoomModeChanged(PdfViewerZoomMode value) - { - IsFitToWidthActive = (value == PdfViewerZoomMode.FitWidth); - IsFitToPageActive = (value == PdfViewerZoomMode.FitPage); - } - - partial void OnZoomLevelChanged(double value) - { - if (!_isApplyingFitZoom) - { - ZoomMode = PdfViewerZoomMode.Custom; - } - - InvalidateVisiblePageCache(); - - // Dynamic High-DPI Vector Re-render on Zoom Change (Debounced) - _zoomDebounceCts?.Cancel(); - _zoomDebounceCts = new CancellationTokenSource(); - var token = _zoomDebounceCts.Token; - - Task.Run(async () => - { - try - { - await Task.Delay(140, token); - if (token.IsCancellationRequested) return; - - float dynamicScale = Math.Clamp((float)(value * 2.25f), PdfViewerPageItem.BasePageRenderScale, 5.0f); - if (SelectedPage != null && Math.Abs(SelectedPage.RenderedScale - dynamicScale) > 0.4f) - { - var highResBmp = RenderPageAtScale(SelectedPage.PageNumber, dynamicScale); - if (highResBmp != null && !token.IsCancellationRequested) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - SelectedPage.Bitmap = highResBmp; - SelectedPage.RenderedScale = dynamicScale; - }); - } - } - } - catch (OperationCanceledException) { } - }, token); - } - - public void InvalidateVisiblePageCache() - { - _lastVisibleFirstPage = -1; - _lastVisibleLastPage = -1; - } - - partial void OnCurrentPageNumberChanged(int value) - { - JumpPageText = value.ToString(); - if (value >= 1 && value <= Pages.Count) - { - var targetPage = Pages[value - 1]; - if (SelectedPage != targetPage) - { - SelectedPage = targetPage; - } - foreach (var p in Pages) - { - p.IsSelected = (p.PageNumber == value); - } - EnsurePageRendered(value); - if (value > 1) EnsurePageRendered(value - 1); - if (value < Pages.Count) EnsurePageRendered(value + 1); - } - UpdateSelectedSpreadForPage(value); - } - - partial void OnSelectedPageChanged(PdfViewerPageItem? value) - { - if (value != null) - { - foreach (var p in Pages) - { - p.IsSelected = (p.PageNumber == value.PageNumber); - } - if (CurrentPageNumber != value.PageNumber) - { - CurrentPageNumber = value.PageNumber; - } - if (value.Words.Count == 0 && !value.IsGeometryLoading) - { - EnsurePageGeometry(value); - } - IsCurrentPageScanned = (value.Words.Count == 0 && string.IsNullOrWhiteSpace(value.ExtractedText)); - } - } - - partial void OnSelectedLayoutModeChanged(PdfViewLayoutMode value) - { - IsContinuousScroll = (value == PdfViewLayoutMode.ContinuousScroll); - IsSinglePageMode = (value == PdfViewLayoutMode.SinglePage); - IsTwoPageSpreadMode = (value == PdfViewLayoutMode.TwoPageSpread); - - if (IsTwoPageSpreadMode) - { - if (PageSpreads.Count == 0 && Pages.Count > 0) - { - RebuildPageSpreads(); - } - else - { - UpdateSelectedSpreadForPage(CurrentPageNumber); - } - - if (SelectedSpread?.LeftPage != null) EnsurePageRendered(SelectedSpread.LeftPage.PageNumber); - if (SelectedSpread?.RightPage != null) EnsurePageRendered(SelectedSpread.RightPage.PageNumber); - - FitToPage(); - } - else if (IsSinglePageMode) - { - if (CurrentPageNumber >= 1 && CurrentPageNumber <= Pages.Count) - { - SelectedPage = Pages[CurrentPageNumber - 1]; - } - EnsurePageRendered(CurrentPageNumber); - - FitToPage(); - } - else if (IsContinuousScroll) - { - if (IsFitToWidthActive) - { - FitToWidth(); - } - else if (IsFitToPageActive) - { - FitToPage(); - } - } - } - - partial void OnReadingThemeChanged(PdfReaderTheme value) - { - switch (value) - { - case PdfReaderTheme.Sepia: - ThemeBackgroundHex = "#EDE3C9"; - ThemePaperBackgroundHex = "#FBF0D9"; - ThemeTextColorHex = "#433422"; - ThemeBorderColorHex = "#E6D5B8"; - break; - case PdfReaderTheme.Dark: - ThemeBackgroundHex = "#0F172A"; - ThemePaperBackgroundHex = "#1E293B"; - ThemeTextColorHex = "#F1F5F9"; - ThemeBorderColorHex = "#334155"; - break; - case PdfReaderTheme.HighContrast: - ThemeBackgroundHex = "#000000"; - ThemePaperBackgroundHex = "#000000"; - ThemeTextColorHex = "#FFFF00"; - ThemeBorderColorHex = "#FFFF00"; - break; - case PdfReaderTheme.Default: - default: - ThemeBackgroundHex = "#F1F5F9"; - ThemePaperBackgroundHex = "#FFFFFF"; - ThemeTextColorHex = "#0F172A"; - ThemeBorderColorHex = "#E2E8F0"; - break; - } - - ReRenderActivePagesForTheme(); - WeakReferenceMessenger.Default.Send(new ReadingThemeChangedMessage(value)); - } - - partial void OnSearchQueryChanged(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - ClearSearch(); - } - else - { - PerformSearch(); - } - } - - partial void OnSearchMatchCaseChanged(bool value) - { - if (!string.IsNullOrWhiteSpace(SearchQuery)) - { - PerformSearch(); - } - } - - partial void OnSearchWholeWordChanged(bool value) - { - if (!string.IsNullOrWhiteSpace(SearchQuery)) - { - PerformSearch(); - } - } - -} diff --git a/src/PdfEditorApp/ViewModels/PluginItemViewModel.cs b/src/PdfEditorApp/ViewModels/PluginItemViewModel.cs deleted file mode 100644 index 421fa09..0000000 --- a/src/PdfEditorApp/ViewModels/PluginItemViewModel.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; - -namespace PdfEditorApp.ViewModels; - -/// -/// Presentation model for an active or loaded plugin in the FryPDF Plugin & Extension Studio. -/// -public partial class PluginItemViewModel : ViewModelBase -{ - private bool _isToggling; - - [ObservableProperty] - private string _id = string.Empty; - - [ObservableProperty] - private string _name = string.Empty; - - [ObservableProperty] - private string _version = "1.0.0"; - - [ObservableProperty] - private string _category = "General"; - - [ObservableProperty] - private string _description = string.Empty; - - [ObservableProperty] - private string _iconKind = "PuzzleOutline"; - - [ObservableProperty] - private string _iconColorHex = "#7C3AED"; - - [ObservableProperty] - private bool _isActive = true; - - [ObservableProperty] - private bool _isExternal; - - [ObservableProperty] - private string _sourceAssembly = "Built-in"; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(HasSettings))] - private System.Collections.Generic.IReadOnlyDictionary? _settingsSchema; - - public bool HasSettings => SettingsSchema != null && SettingsSchema.Count > 0; - - /// - /// Optional custom handler for toggling active status. - /// - public Func? ToggleHandler { get; set; } - - partial void OnIsActiveChanged(bool value) - { - if (_isToggling) return; - _ = HandleToggleAsync(value); - } - - private async Task HandleToggleAsync(bool active) - { - if (string.IsNullOrWhiteSpace(Id)) return; - - _isToggling = true; - try - { - if (ToggleHandler != null) - { - await ToggleHandler(Id, active); - } - else - { - var host = App.Services?.GetService(); - if (host != null) - { - if (active) - { - await host.EnablePluginAsync(Id); - } - else - { - await host.DisablePluginAsync(Id); - } - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[PluginItemViewModel] Toggle failed for '{Id}': {ex.Message}"); - _isToggling = true; - IsActive = !active; - _isToggling = false; - } - finally - { - _isToggling = false; - } - } -} diff --git a/src/PdfEditorApp/ViewModels/PluginSettingItemViewModel.cs b/src/PdfEditorApp/ViewModels/PluginSettingItemViewModel.cs deleted file mode 100644 index e229b8f..0000000 --- a/src/PdfEditorApp/ViewModels/PluginSettingItemViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace PdfEditorApp.ViewModels; - -/// -/// Presentation model for an individual configurable setting inside a plugin's declarative schema. -/// -public sealed partial class PluginSettingItemViewModel : ViewModelBase -{ - public required string Key { get; init; } - public required string Label { get; init; } - public string Description { get; init; } = ""; - public string Type { get; init; } = "string"; // string, boolean, number, select, secret - public List Options { get; init; } = new(); - - [ObservableProperty] - private string _stringValue = string.Empty; - - [ObservableProperty] - private bool _boolValue; - - [ObservableProperty] - private double _numberValue; -} diff --git a/src/PdfEditorApp/ViewModels/PluginsManagerDetailViewModel.cs b/src/PdfEditorApp/ViewModels/PluginsManagerDetailViewModel.cs deleted file mode 100644 index a8e412c..0000000 --- a/src/PdfEditorApp/ViewModels/PluginsManagerDetailViewModel.cs +++ /dev/null @@ -1,328 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Plugins.Marketplace; - -namespace PdfEditorApp.ViewModels; - -public enum ExtensionDetailTab -{ - Overview, - Contributions, - Settings, - Runtime -} - -/// -/// Detail presentation model for the selected plugin in the Plugins & Extensions Studio. -/// Displays comprehensive metadata, stats, action controls, and 4 dedicated tabs: -/// Overview, Feature Contributions, Declarative Settings, and Runtime/Dependencies. -/// -public partial class PluginsManagerDetailViewModel : ViewModelBase -{ - [ObservableProperty] - private string _id = string.Empty; - - [ObservableProperty] - private string _name = string.Empty; - - [ObservableProperty] - private string _version = "1.0.0"; - - [ObservableProperty] - private string _publisher = "FryPDF Official"; - - [ObservableProperty] - private string _category = "General"; - - [ObservableProperty] - private string _description = string.Empty; - - [ObservableProperty] - private string _longDescription = string.Empty; - - [ObservableProperty] - private string _iconKind = "PuzzleOutline"; - - [ObservableProperty] - private string _iconColorHex = "#7C3AED"; - - [ObservableProperty] - private double _rating = 4.8; - - [ObservableProperty] - private int _ratingCount = 120; - - [ObservableProperty] - private int _installCount = 1500; - - [ObservableProperty] - private string _formattedSize = "1.2 MB"; - - [ObservableProperty] - private string _license = "MIT"; - - [ObservableProperty] - private bool _isOfficial = true; - - [ObservableProperty] - private bool _isVerified = true; - - [ObservableProperty] - private bool _isInstalled = true; - - [ObservableProperty] - private bool _isActive = true; - - [ObservableProperty] - private bool _isExternal; - - [ObservableProperty] - private string _sourceAssembly = "Built-in"; - - [ObservableProperty] - private ExtensionDetailTab _selectedTab = ExtensionDetailTab.Overview; - - [ObservableProperty] - private string _runtimeStatus = "Active (Mounted in Kernel)"; - - [ObservableProperty] - private string _assemblyPath = "In-memory AppContext"; - - [ObservableProperty] - private string _assemblyLoadContextName = "Default AssemblyLoadContext"; - - public ObservableCollection ContributedFeatures { get; } = new(); - public ObservableCollection Highlights { get; } = new(); - public ObservableCollection Dependencies { get; } = new(); - public ObservableCollection SettingsList { get; } = new(); - - public bool HasSettings => SettingsList.Count > 0; - public bool HasContributions => ContributedFeatures.Count > 0; - - public Func? ToggleActiveCallback { get; set; } - public Func? InstallCallback { get; set; } - public Func? UninstallCallback { get; set; } - public Action? CopyToClipboardCallback { get; set; } - public Action? ShowToastCallback { get; set; } - - [RelayCommand] - public void SelectTab(string tabName) - { - if (Enum.TryParse(tabName, true, out var tab)) - { - SelectedTab = tab; - } - } - - [RelayCommand] - public async Task ToggleActiveAsync() - { - if (!IsInstalled || ToggleActiveCallback == null) return; - var next = !IsActive; - IsActive = next; - RuntimeStatus = next ? "Active (Mounted in Kernel)" : "Disabled (Unloaded)"; - await ToggleActiveCallback(Id, next); - } - - [RelayCommand] - public async Task InstallAsync() - { - if (InstallCallback == null) return; - await InstallCallback(Id); - } - - [RelayCommand] - public async Task UninstallAsync() - { - if (UninstallCallback == null) return; - await UninstallCallback(Id); - } - - [RelayCommand] - public void CopyId() - { - if (string.IsNullOrWhiteSpace(Id)) return; - CopyToClipboardCallback?.Invoke(Id); - ShowToastCallback?.Invoke($"Copied extension ID '{Id}' to clipboard"); - } - - [RelayCommand] - public void SaveSettings() - { - ShowToastCallback?.Invoke($"Saved preferences for '{Name}'"); - } - - /// - /// Populates detail from an installed plugin item. - /// - public static PluginsManagerDetailViewModel FromInstalledPlugin( - PluginItemViewModel plugin, - IReadOnlyList? contributions = null) - { - var vm = new PluginsManagerDetailViewModel - { - Id = plugin.Id, - Name = plugin.Name, - Version = plugin.Version, - Publisher = plugin.IsExternal ? "3rd-Party Developer" : "FryPDF Core Team", - Category = plugin.Category, - Description = plugin.Description, - LongDescription = string.IsNullOrWhiteSpace(plugin.Description) - ? $"Official module '{plugin.Name}' providing high-performance capabilities in {plugin.Category}." - : $"{plugin.Description}\n\nThis extension is deeply integrated into FryPDF's modular microkernel architecture. It registers tools, services, and visual controls dynamically with isolated execution and zero memory leak guarantees.", - IconKind = plugin.IconKind, - IconColorHex = plugin.IconColorHex, - IsInstalled = true, - IsActive = plugin.IsActive, - IsExternal = plugin.IsExternal, - IsOfficial = !plugin.IsExternal, - IsVerified = true, - License = "MIT", - FormattedSize = plugin.IsExternal ? "2.1 MB" : "Built-in (.NET 10)", - SourceAssembly = plugin.SourceAssembly, - AssemblyPath = plugin.IsExternal ? plugin.SourceAssembly : "src/PdfEditorApp/bin/Debug/net10.0/PdfEditorApp.dll", - AssemblyLoadContextName = plugin.IsExternal ? "IsolatedAssemblyLoadContext (ALC)" : "System.Runtime.Loader.Default", - RuntimeStatus = plugin.IsActive ? "Active (Mounted in Kernel)" : "Disabled (Suspended)" - }; - - if (contributions != null && contributions.Count > 0) - { - foreach (var c in contributions) - { - vm.ContributedFeatures.Add(c); - } - } - else - { - // Default contributions inferred from ID and category - vm.ContributedFeatures.Add($"Module Component: {plugin.Name}"); - vm.ContributedFeatures.Add($"Category Registration: {plugin.Category}"); - if (plugin.Id.Contains(".tool.")) - { - vm.ContributedFeatures.Add("Interactive Tool: Studio Canvas & Quick Action Ribbon"); - } - if (plugin.Id.Contains(".page.")) - { - vm.ContributedFeatures.Add("Workspace Navigation: First-class studio page"); - } - if (plugin.Id.Contains(".dialog.")) - { - vm.ContributedFeatures.Add("Modal Studio: Material Design 3 Dialog"); - } - if (plugin.Id.Contains(".sidebar.")) - { - vm.ContributedFeatures.Add("Dockable Sidebar: Inspector / Thumbnails / Tool Palette"); - } - } - - if (plugin.Category.Contains("AI", StringComparison.OrdinalIgnoreCase)) - { - vm.Highlights.Add("Multimodal document understanding & streaming intelligence"); - vm.Highlights.Add("Zero data retention with local or privacy-first inference"); - vm.Highlights.Add("Interactive prompts and contextual summaries"); - } - else if (plugin.Category.Contains("Conversion", StringComparison.OrdinalIgnoreCase)) - { - vm.Highlights.Add("High-fidelity document structure and layout reconstruction"); - vm.Highlights.Add("Preserves embedded vector shapes, images, and fonts"); - vm.Highlights.Add("Hardware-accelerated Skia rendering and export"); - } - else if (plugin.Category.Contains("Security", StringComparison.OrdinalIgnoreCase)) - { - vm.Highlights.Add("Enterprise-grade cryptographic security and validation"); - vm.Highlights.Add("Permanent, irreversible sanitization and redaction"); - vm.Highlights.Add("FIPS-compliant AES-256 document protection"); - } - else if (plugin.Category.Contains("Canvas", StringComparison.OrdinalIgnoreCase)) - { - vm.Highlights.Add("Full-fidelity visual canvas editing with drag handles"); - vm.Highlights.Add("Real-time property inspector two-way data binding"); - vm.Highlights.Add("Script-aware layout engine with Unicode font fallback"); - } - else - { - vm.Highlights.Add("High-performance .NET 10 modular execution"); - vm.Highlights.Add("Isolated microkernel lifecycle management"); - vm.Highlights.Add("Integrated undo/redo and atomic command history"); - } - - vm.Dependencies.Add("PdfEditorApp.Core (v1.0.0+)"); - vm.Dependencies.Add("Avalonia UI (v12.x+)"); - - // Load settings schema if available - if (plugin.SettingsSchema != null) - { - foreach (var kvp in plugin.SettingsSchema) - { - var def = kvp.Value; - vm.SettingsList.Add(new PluginSettingItemViewModel - { - Key = kvp.Key, - Label = def.Label ?? kvp.Key, - Description = def.Description ?? "", - Type = def.Type ?? "string", - Options = def.Options != null ? new List(def.Options) : new List(), - StringValue = def.DefaultValue?.ToString() ?? "", - BoolValue = bool.TryParse(def.DefaultValue?.ToString(), out var b) && b - }); - } - } - - return vm; - } - - /// - /// Populates detail from a marketplace extension item. - /// - public static PluginsManagerDetailViewModel FromMarketplaceItem(MarketplacePluginItem item) - { - var isInstalled = item.Status == MarketplacePluginStatus.Installed; - var vm = new PluginsManagerDetailViewModel - { - Id = item.Id, - Name = item.Name, - Version = item.Version, - Publisher = item.Publisher, - Category = item.Category, - Description = item.Description, - LongDescription = !string.IsNullOrWhiteSpace(item.LongDescription) ? item.LongDescription : item.Description, - IconKind = item.IconKind, - IconColorHex = item.IconColorHex, - Rating = item.Rating, - RatingCount = item.RatingCount, - InstallCount = item.InstallCount, - FormattedSize = item.FormattedSize, - License = item.License, - IsOfficial = item.IsOfficial, - IsVerified = item.IsVerified, - IsInstalled = isInstalled, - IsActive = isInstalled, - IsExternal = true, - SourceAssembly = isInstalled ? $"plugins/{item.Id}/{item.Id}.dll" : "FryPDF Marketplace Remote Registry", - AssemblyPath = isInstalled ? $"plugins/{item.Id}/{item.Id}.dll" : "Remote Package Archive (.fryplugin)", - AssemblyLoadContextName = "PluginAssemblyLoadContext (Isolated)", - RuntimeStatus = isInstalled ? "Active (Mounted in Kernel)" : "Available in Marketplace" - }; - - foreach (var c in item.ContributedFeatures) - { - vm.ContributedFeatures.Add(c); - } - - foreach (var h in item.Highlights) - { - vm.Highlights.Add(h); - } - - foreach (var d in item.Dependencies) - { - vm.Dependencies.Add(d); - } - - return vm; - } -} diff --git a/src/PdfEditorApp/ViewModels/PluginsManagerViewModel.cs b/src/PdfEditorApp/ViewModels/PluginsManagerViewModel.cs deleted file mode 100644 index abd51a8..0000000 --- a/src/PdfEditorApp/ViewModels/PluginsManagerViewModel.cs +++ /dev/null @@ -1,802 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia.Input.Platform; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Marketplace; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Plugins.Loader; -using PdfEditorApp.Services.Plugins; -using PdfEditorApp.Services.Tools.Core; - -namespace PdfEditorApp.ViewModels; - -public enum PluginsManagerTab -{ - Installed, - Marketplace, - Updates -} - -/// -/// Master ViewModel for the VS Code-inspired Plugins & Extensions Manager Studio Page. -/// Coordinates search, category filtering, installed modules list, marketplace catalog, -/// local package installation (.fryplugin/.dll), profile switching, and detail inspection. -/// -public partial class PluginsManagerViewModel : ViewModelBase -{ - private readonly PluginHost? _pluginHost; - private readonly IPluginMarketplaceService _marketplaceService; - private readonly IPdfToolRegistry? _toolRegistry; - private readonly List _allInstalled = new(); - private readonly List _allMarketplace = new(); - - public IStorageProvider? StorageProvider { get; set; } - public IClipboard? Clipboard { get; set; } - public Action? ShowToastCallback { get; set; } - - [ObservableProperty] - private PluginsManagerTab _selectedTab = PluginsManagerTab.Installed; - - [ObservableProperty] - private string _searchQuery = string.Empty; - - [ObservableProperty] - private string _selectedCategory = "All"; - - [ObservableProperty] - private string _activeProfileName = "desktop"; - - [ObservableProperty] - private bool _isBusy; - - [ObservableProperty] - private string _statusMessage = string.Empty; - - [ObservableProperty] - private PluginItemViewModel? _selectedInstalledPlugin; - - [ObservableProperty] - private MarketplacePluginItem? _selectedMarketplacePlugin; - - [ObservableProperty] - private PluginsManagerDetailViewModel? _selectedDetail; - - public ObservableCollection FilteredInstalledPlugins { get; } = new(); - public ObservableCollection FilteredMarketplacePlugins { get; } = new(); - public ObservableCollection AvailableCategories { get; } = new(); - - public int InstalledCount => _allInstalled.Count; - public int ActiveInstalledCount => _allInstalled.Count(p => p.IsActive); - public int MarketplaceCount => _allMarketplace.Count; - - public bool HasSelectedDetail => SelectedDetail != null; - - public PluginsManagerViewModel( - PluginHost? pluginHost = null, - IPluginMarketplaceService? marketplaceService = null, - IPdfToolRegistry? toolRegistry = null) - { - _pluginHost = pluginHost; - _marketplaceService = marketplaceService ?? new PluginMarketplaceService(pluginHost); - _toolRegistry = toolRegistry; - - InitializeCategories(); - _ = LoadAllDataAsync(); - } - - private void InitializeCategories() - { - AvailableCategories.Clear(); - var cats = new[] - { - "All", - "Tools & Productivity", - "Conversion & Office", - "AI & Intelligence", - "Security & Signatures", - "Workspace Pages", - "Canvas Elements", - "Document I/O", - "UI & Extensions" - }; - foreach (var c in cats) AvailableCategories.Add(c); - } - - partial void OnSearchQueryChanged(string value) - { - ApplyFilters(); - } - - partial void OnSelectedCategoryChanged(string value) - { - ApplyFilters(); - } - - partial void OnSelectedTabChanged(PluginsManagerTab value) - { - ApplyFilters(); - // Update detail selection based on the new tab - if (value == PluginsManagerTab.Installed && SelectedInstalledPlugin != null) - { - UpdateDetailFromInstalled(SelectedInstalledPlugin); - } - else if (value == PluginsManagerTab.Marketplace) - { - if (SelectedMarketplacePlugin != null) - { - UpdateDetailFromMarketplace(SelectedMarketplacePlugin); - } - - // Auto-sync with remote GitHub CDN registry in background - _ = SyncRemoteRegistrySilentlyAsync(); - } - } - - private async Task SyncRemoteRegistrySilentlyAsync() - { - try - { - await _marketplaceService.FetchRemoteCatalogAsync(); - var catalog = await _marketplaceService.GetCatalogAsync(); - void Update() - { - _allMarketplace.Clear(); - _allMarketplace.AddRange(catalog); - ApplyFilters(); - OnPropertyChanged(nameof(MarketplaceCount)); - } - - if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess()) - { - Update(); - } - else - { - await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(Update); - } - } - catch - { - // Silent fallback to curated offline items - } - } - - partial void OnSelectedInstalledPluginChanged(PluginItemViewModel? value) - { - if (value != null && SelectedTab == PluginsManagerTab.Installed) - { - UpdateDetailFromInstalled(value); - } - } - - partial void OnSelectedMarketplacePluginChanged(MarketplacePluginItem? value) - { - if (value != null && SelectedTab == PluginsManagerTab.Marketplace) - { - UpdateDetailFromMarketplace(value); - } - } - - [RelayCommand] - public void SelectTab(string tabName) - { - if (Enum.TryParse(tabName, true, out var tab)) - { - SelectedTab = tab; - } - } - - [RelayCommand] - public void SelectCategory(string category) - { - SelectedCategory = category; - } - - [RelayCommand] - public async Task RefreshAllAsync() - { - await LoadAllDataAsync(); - } - - public async Task LoadAllDataAsync() - { - IsBusy = true; - StatusMessage = "Refreshing loaded plugins and marketplace catalog..."; - - try - { - PopulateInstalledPlugins(); - await _marketplaceService.FetchRemoteCatalogAsync(); - var catalog = await _marketplaceService.GetCatalogAsync(); - _allMarketplace.Clear(); - _allMarketplace.AddRange(catalog); - - ApplyFilters(); - OnPropertyChanged(nameof(MarketplaceCount)); - - if (SelectedDetail == null && FilteredInstalledPlugins.Count > 0) - { - SelectedInstalledPlugin = FilteredInstalledPlugins[0]; - } - } - catch (Exception ex) - { - StatusMessage = $"Error loading plugins: {ex.Message}"; - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - public async Task SyncRemoteRegistryAsync() - { - try - { - StatusMessage = "Syncing with remote FryPDF registry..."; - await _marketplaceService.FetchRemoteCatalogAsync(forceRefresh: true); - var catalog = await _marketplaceService.GetCatalogAsync(); - _allMarketplace.Clear(); - _allMarketplace.AddRange(catalog); - ApplyFilters(); - OnPropertyChanged(nameof(MarketplaceCount)); - ShowToastCallback?.Invoke($"Registry synced: {catalog.Count} extensions available."); - } - catch (Exception ex) - { - StatusMessage = $"Sync error: {ex.Message}"; - } - } - - [RelayCommand] - public async Task CheckForUpdatesAsync() - { - IsBusy = true; - StatusMessage = "Checking remote registry for plugin updates..."; - - try - { - var updates = await _marketplaceService.CheckForUpdatesAsync(); - if (updates.Count > 0) - { - ShowToastCallback?.Invoke($"Found {updates.Count} plugin update(s) available!"); - } - else - { - ShowToastCallback?.Invoke("All installed plugins are up to date."); - } - await LoadAllDataAsync(); - } - catch (Exception ex) - { - ShowToastCallback?.Invoke($"Failed to check updates: {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - public void PopulateInstalledPlugins() - { - _allInstalled.Clear(); - - if (_pluginHost != null) - { - foreach (var plugin in _pluginHost.LoadedPlugins) - { - var vm = new PluginItemViewModel - { - Id = plugin.Id, - Name = plugin.Name, - Version = plugin.Version?.ToString() ?? "1.0.0", - Category = InferCategory(plugin), - Description = InferDescription(plugin), - IconKind = InferIcon(plugin), - IconColorHex = InferColor(plugin), - IsActive = true, - IsExternal = plugin.GetType().Assembly != typeof(App).Assembly && - plugin.GetType().Assembly != typeof(PluginHost).Assembly, - SourceAssembly = plugin.GetType().Assembly.GetName().Name ?? "Core", - ToggleHandler = async (id, active) => - { - if (active) - { - await _pluginHost.EnablePluginAsync(id); - } - else - { - await _pluginHost.DisablePluginAsync(id); - } - OnPropertyChanged(nameof(ActiveInstalledCount)); - } - }; - _allInstalled.Add(vm); - } - } - - OnPropertyChanged(nameof(InstalledCount)); - OnPropertyChanged(nameof(ActiveInstalledCount)); - } - - private void ApplyFilters() - { - var q = SearchQuery.Trim().ToLowerInvariant(); - var cat = SelectedCategory; - - // Filter Installed - FilteredInstalledPlugins.Clear(); - foreach (var p in _allInstalled) - { - if (cat != "All" && !string.Equals(p.Category, cat, StringComparison.OrdinalIgnoreCase)) - continue; - - if (string.IsNullOrWhiteSpace(q) || - p.Name.ToLowerInvariant().Contains(q) || - p.Id.ToLowerInvariant().Contains(q) || - p.Category.ToLowerInvariant().Contains(q) || - p.Description.ToLowerInvariant().Contains(q)) - { - FilteredInstalledPlugins.Add(p); - } - } - - // Filter Marketplace - var matchedMarketplace = new List(); - foreach (var m in _allMarketplace) - { - if (cat != "All" && !string.Equals(m.Category, cat, StringComparison.OrdinalIgnoreCase)) - continue; - - if (string.IsNullOrWhiteSpace(q) || - m.Name.ToLowerInvariant().Contains(q) || - m.Id.ToLowerInvariant().Contains(q) || - m.Publisher.ToLowerInvariant().Contains(q) || - m.Description.ToLowerInvariant().Contains(q) || - m.Tags.Any(t => t.ToLowerInvariant().Contains(q))) - { - matchedMarketplace.Add(m); - } - } - - if (!string.IsNullOrWhiteSpace(q)) - { - matchedMarketplace = matchedMarketplace - .OrderByDescending(m => - { - var name = m.Name.ToLowerInvariant(); - var id = m.Id.ToLowerInvariant(); - if (name == q || id == q) return 100; - if (name.StartsWith(q)) return 80; - if (name.Contains(q)) return 60; - if (id.Contains(q)) return 40; - return 10; - }) - .ToList(); - } - - FilteredMarketplacePlugins.Clear(); - foreach (var item in matchedMarketplace) - { - FilteredMarketplacePlugins.Add(item); - } - - // Maintain selection stability or select the first item so the detail pane is never left blank unexpectedly - if (SelectedTab == PluginsManagerTab.Installed) - { - if (SelectedInstalledPlugin == null || !FilteredInstalledPlugins.Contains(SelectedInstalledPlugin)) - { - SelectedInstalledPlugin = FilteredInstalledPlugins.Count > 0 ? FilteredInstalledPlugins[0] : null; - } - else - { - UpdateDetailFromInstalled(SelectedInstalledPlugin); - } - } - else if (SelectedTab == PluginsManagerTab.Marketplace) - { - if (SelectedMarketplacePlugin == null || !FilteredMarketplacePlugins.Contains(SelectedMarketplacePlugin)) - { - SelectedMarketplacePlugin = FilteredMarketplacePlugins.Count > 0 ? FilteredMarketplacePlugins[0] : null; - } - else - { - UpdateDetailFromMarketplace(SelectedMarketplacePlugin); - } - } - } - - private void UpdateDetailFromInstalled(PluginItemViewModel plugin) - { - var detail = PluginsManagerDetailViewModel.FromInstalledPlugin(plugin); - WireDetailCallbacks(detail); - SelectedDetail = detail; - OnPropertyChanged(nameof(HasSelectedDetail)); - } - - private void UpdateDetailFromMarketplace(MarketplacePluginItem item) - { - var detail = PluginsManagerDetailViewModel.FromMarketplaceItem(item); - WireDetailCallbacks(detail); - SelectedDetail = detail; - OnPropertyChanged(nameof(HasSelectedDetail)); - } - - private void WireDetailCallbacks(PluginsManagerDetailViewModel detail) - { - detail.ToggleActiveCallback = async (id, active) => - { - if (_pluginHost != null) - { - if (active) await _pluginHost.EnablePluginAsync(id); - else await _pluginHost.DisablePluginAsync(id); - } - OnPropertyChanged(nameof(ActiveInstalledCount)); - }; - - detail.InstallCallback = async (id) => - { - await InstallMarketplacePluginAsync(id); - }; - - detail.UninstallCallback = async (id) => - { - await UninstallMarketplacePluginAsync(id); - }; - - detail.CopyToClipboardCallback = async (text) => - { - if (Clipboard != null) - { - await Clipboard.SetTextAsync(text); - } - }; - - detail.ShowToastCallback = (msg) => - { - ShowToastCallback?.Invoke(msg); - }; - } - - [RelayCommand] - public async Task InstallMarketplacePluginAsync(string pluginId) - { - IsBusy = true; - StatusMessage = $"Installing extension '{pluginId}' from store..."; - - try - { - var progress = new Progress(p => - { - StatusMessage = $"Installing {pluginId} ({(int)(p * 100)}%)..."; - }); - - bool success = await _marketplaceService.InstallPluginAsync(pluginId, progress, s => StatusMessage = s); - if (success) - { - ShowToastCallback?.Invoke($"Installed extension '{pluginId}' successfully!"); - await LoadAllDataAsync(); - SelectedTab = PluginsManagerTab.Installed; - } - else - { - ShowToastCallback?.Invoke($"Failed to install extension '{pluginId}'"); - } - } - catch (Exception ex) - { - ShowToastCallback?.Invoke($"Install error: {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - public async Task UninstallMarketplacePluginAsync(string pluginId) - { - IsBusy = true; - try - { - await _marketplaceService.UninstallPluginAsync(pluginId); - ShowToastCallback?.Invoke($"Uninstalled extension '{pluginId}'"); - await LoadAllDataAsync(); - } - catch (Exception ex) - { - ShowToastCallback?.Invoke($"Uninstall error: {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - public async Task InstallFromFileAsync() - { - if (StorageProvider == null) return; - - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Select FryPDF Plugin Package (.fryplugin) or Assembly (.dll)", - AllowMultiple = false, - FileTypeFilter = new[] - { - new FilePickerFileType("FryPDF Plugins (*.fryplugin; *.dll)") - { - Patterns = new[] { "*.fryplugin", "*.dll" } - }, - new FilePickerFileType("FryPDF Plugin Package (*.fryplugin)") - { - Patterns = new[] { "*.fryplugin" } - }, - new FilePickerFileType(".NET Assembly (*.dll)") - { - Patterns = new[] { "*.dll" } - } - } - }); - - if (files.Count > 0) - { - var path = files[0].Path.LocalPath; - await InstallLocalPackageAsync(path); - } - } - - public async Task InstallLocalPackageAsync(string filePath) - { - if (!File.Exists(filePath)) return; - - IsBusy = true; - StatusMessage = $"Mounting '{Path.GetFileName(filePath)}'..."; - - try - { - if (_pluginHost == null) - { - ShowToastCallback?.Invoke("PluginHost is not initialized."); - return; - } - - IReadOnlyList plugins; - string displayName; - - if (string.Equals(Path.GetExtension(filePath), ".fryplugin", StringComparison.OrdinalIgnoreCase)) - { - var pkgResult = FryPluginPackageLoader.UnpackAndLoad(filePath); - plugins = pkgResult.AssemblyPackage.Plugins; - displayName = pkgResult.Manifest.Name; - } - else - { - var pkg = PluginAssemblyLoader.LoadPluginAssembly(filePath); - plugins = pkg.Plugins; - displayName = Path.GetFileName(filePath); - } - - _pluginHost.RegisterPlugins(plugins); - foreach (var p in plugins) - { - await _pluginHost.EnablePluginAsync(p.Id); - } - - PopulateInstalledPlugins(); - ApplyFilters(); - - ShowToastCallback?.Invoke($"Successfully installed and mounted '{displayName}' ({plugins.Count} plugins)!"); - SelectedTab = PluginsManagerTab.Installed; - } - catch (Exception ex) - { - ShowToastCallback?.Invoke($"Failed to install plugin: {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - public void SwitchProfile(string profileName) - { - try - { - if (_pluginHost == null) return; - - string profilePath = Path.Combine(AppContext.BaseDirectory, "profiles", $"{profileName}.profile.json"); - if (!File.Exists(profilePath)) - { - profilePath = $"profiles/{profileName}.profile.json"; - } - - if (File.Exists(profilePath)) - { - var profile = ProfileLoader.LoadFromFile(profilePath); - ActiveProfileName = profile.ProfileName; - - var availableBundles = new IFryPluginBundle[] - { - new ToolsOrganizeBundle(), - new ToolsSecurityBundle(), - new ToolsConversionBundle(), - new ToolsIntelligenceBundle(), - new DataStudioBundle(), - new CanvasElementsBundle(), - new DocumentIoBundle(), - new AiProvidersBundle(), - new OcrEnginesBundle(), - new StandardTemplatesBundle(), - new StatusBarBundle(), - new InspectorBundle(), - new CommandPaletteBundle(), - new WorkspacePagesBundle(), - new DialogsBundle(), - new EditorSidebarsBundle() - }; - - ProfileLoader.ApplyProfile(profile, _pluginHost, availableBundles); - PopulateInstalledPlugins(); - ApplyFilters(); - ShowToastCallback?.Invoke($"Switched to profile '{profileName}'"); - } - } - catch (Exception ex) - { - ShowToastCallback?.Invoke($"Failed to switch profile: {ex.Message}"); - } - } - - private record struct PluginMeta(string Category, string Description, string Icon, string Color); - - private static readonly Dictionary KnownPluginMetas = new(StringComparer.OrdinalIgnoreCase) - { - // Organize & Page Tools - ["frypdf.tool.merge"] = new("Tools & Productivity", "Combine multiple PDF documents or pages into a single organized file.", "CallMerge", "#3B82F6"), - ["frypdf.tool.split"] = new("Tools & Productivity", "Extract individual pages or split documents by bookmarks and ranges.", "CallSplit", "#6366F1"), - ["frypdf.tool.rotate"] = new("Tools & Productivity", "Rotate portrait/landscape pages 90°, 180°, or 270° with live preview.", "RotateRight", "#8B5CF6"), - ["frypdf.tool.organize"] = new("Tools & Productivity", "Reorder, duplicate, delete, and sort pages visually in grid mode.", "ViewGridOutline", "#EC4899"), - ["frypdf.tool.crop"] = new("Tools & Productivity", "Trim margins and adjust page bounding boxes interactively.", "Crop", "#F43F5E"), - ["frypdf.tool.pagenumbers"] = new("Tools & Productivity", "Add customizable headers, footers, bates stamps, and pagination.", "Numeric1BoxOutline", "#10B981"), - - // Optimize & Security Tools - ["frypdf.tool.compress"] = new("Tools & Productivity", "Optimize raster images and remove redundant streams to reduce file size.", "ZipBoxOutline", "#06B6D4"), - ["frypdf.tool.repair"] = new("Tools & Productivity", "Recover damaged PDF xref tables and repair broken font dictionaries.", "AutoFix", "#F59E0B"), - ["frypdf.tool.protect"] = new("Security & Signatures", "Encrypt documents with AES-256 and restrict printing or copying.", "LockOutline", "#DC2626"), - ["frypdf.tool.unlock"] = new("Security & Signatures", "Remove owner permissions and passwords from authorized files.", "LockOpenVariantOutline", "#EA580C"), - ["frypdf.tool.sign"] = new("Security & Signatures", "Place cryptographic digital signatures, stamps, and handwritten initials.", "Draw", "#7C3AED"), - ["frypdf.tool.watermark"] = new("Tools & Productivity", "Apply transparent diagonal text, company logos, and draft stamps.", "Watermark", "#0284C7"), - ["frypdf.tool.redact"] = new("Security & Signatures", "Permanently sanitize sensitive text, PII, and rectangular regions.", "EyeOffOutline", "#334155"), - ["frypdf.tool.flatten"] = new("Tools & Productivity", "Burn annotations, form fields, and layers into page background raster.", "LayersTripleOutline", "#64748B"), - ["frypdf.tool.metadata"] = new("Tools & Productivity", "Inspect and edit XMP metadata: title, author, subject, keywords.", "CardTextOutline", "#6366F1"), - - // Convert To/From PDF - ["frypdf.tool.excel2pdf"] = new("Conversion & Office", "Convert spreadsheet sheets, tables, and workbooks to PDF.", "FileExcelOutline", "#107C41"), - ["frypdf.tool.pdf2excel"] = new("Conversion & Office", "Extract structured tabular data from PDF into editable XLSX.", "FileExcel", "#107C41"), - ["frypdf.tool.word2pdf"] = new("Conversion & Office", "Convert DOCX/DOC documents preserving typography and formatting.", "FileWordOutline", "#2B579A"), - ["frypdf.tool.pdf2word"] = new("Conversion & Office", "Reconstruct editable Word documents from fixed-layout PDFs.", "FileWord", "#2B579A"), - ["frypdf.tool.ppt2pdf"] = new("Conversion & Office", "Export presentation slides to high-resolution vector PDF.", "FilePowerpointOutline", "#D24726"), - ["frypdf.tool.pdf2ppt"] = new("Conversion & Office", "Convert presentation slides into editable PPTX decks.", "FilePowerpoint", "#D24726"), - ["frypdf.tool.pdf2jpg"] = new("Conversion & Office", "Rasterize PDF pages into high-resolution JPEG images.", "FileImageOutline", "#0284C7"), - ["frypdf.tool.jpg2pdf"] = new("Conversion & Office", "Assemble photo galleries and image scans into a PDF document.", "ImageMultipleOutline", "#0284C7"), - ["frypdf.tool.html2pdf"] = new("Conversion & Office", "Render responsive web pages and HTML reports with CSS styling.", "LanguageHtml5", "#E44D26"), - ["frypdf.tool.pdf2markdown"] = new("Conversion & Office", "Deconstruct PDF text into clean CommonMark/GitHub Markdown.", "LanguageMarkdown", "#0EA5E9"), - ["frypdf.tool.pdf2pdfa"] = new("Conversion & Office", "Convert documents to ISO 19005 archiving standards (PDF/A).", "ShieldCheckOutline", "#059669"), - ["frypdf.tool.scan2pdf"] = new("Tools & Productivity", "Acquire documents from hardware scanners and apply OCR cleanup.", "Scanner", "#64748B"), - - // Intelligence & AI Tools - ["frypdf.tool.aisummarizer"] = new("AI & Intelligence", "Generate executive summaries and key takeaway bullet points using LLMs.", "Creation", "#8B5CF6"), - ["frypdf.tool.translate"] = new("AI & Intelligence", "Translate document text into 50+ languages while preserving layout.", "Translate", "#3B82F6"), - ["frypdf.tool.ocr"] = new("Tools & Productivity", "Recognize scanned text using Tesseract 5 with Indic/CJK script support.", "TextRecognition", "#059669"), - ["frypdf.tool.compare"] = new("Tools & Productivity", "Visual side-by-side diff highlighting text and graphic alterations.", "CompareHorizontal", "#F59E0B"), - ["frypdf.tool.edit"] = new("Tools & Productivity", "Full-fidelity canvas editing: modify text, vectors, shapes, and images.", "FileDocumentEditOutline", "#4F46E5"), - - // AI Inference Providers - ["frypdf.ai.groq"] = new("AI & Intelligence", "Ultra-fast cloud inference provider powered by Groq LPUs.", "LightningBolt", "#F97316"), - ["frypdf.ai.ollama"] = new("AI & Intelligence", "100% private, local on-device LLM inference via Ollama.", "Laptop", "#10B981"), - ["frypdf.ai.openai"] = new("AI & Intelligence", "Cloud multimodal AI intelligence provider via OpenAI.", "Brain", "#06B6D4"), - - // Workspace Navigation Pages - ["frypdf.page.home"] = new("Workspace Pages", "Main dashboard landing page with quick launch and recent files.", "HomeOutline", "#3B82F6"), - ["frypdf.page.reader"] = new("Workspace Pages", "Dedicated full-screen PDF reader with continuous smooth scrolling.", "BookOpenPageVariantOutline", "#0284C7"), - ["frypdf.page.new"] = new("Workspace Pages", "Template gallery for creating professional documents from scratch.", "FileDocumentPlusOutline", "#10B981"), - ["frypdf.page.tools"] = new("Workspace Pages", "Centralized studio catalog featuring all 32 PDF tools.", "ViewGridOutline", "#8B5CF6"), - ["frypdf.page.starred"] = new("Workspace Pages", "Quick-access shelf for your favorite and pinned tools.", "StarOutline", "#F59E0B"), - ["frypdf.page.trash"] = new("Workspace Pages", "Cache management and disk storage cleaner for temp files.", "TrashCanOutline", "#64748B"), - ["frypdf.page.licensing"] = new("Workspace Pages", "Open-source attribution, third-party libraries, and license info.", "License", "#64748B"), - ["frypdf.page.fonts"] = new("Workspace Pages", "On-demand typography downloader and multi-script font packages.", "FormatFont", "#EC4899"), - ["frypdf.page.tesseract"] = new("Workspace Pages", "OCR language pack installer for offline text recognition.", "Translate", "#059669"), - ["frypdf.page.help"] = new("Workspace Pages", "Interactive tutorials, user manuals, and keyboard shortcut guides.", "HelpCircleOutline", "#0284C7"), - ["frypdf.page.settings"] = new("Workspace Pages", "Application preferences, theme switching, and hardware acceleration.", "CogOutline", "#64748B"), - ["frypdf.page.plugins"] = new("Workspace Pages", "Manage installed extensions, install packages, and browse the store.", "PuzzleOutline", "#7C3AED"), - - // Canvas Elements - ["frypdf.element.text"] = new("Canvas Elements", "Interactive typography block with Indic/CJK script-aware rendering.", "FormatText", "#3B82F6"), - ["frypdf.element.image"] = new("Canvas Elements", "Bitmap image container supporting PNG, JPEG, WebP, and SVG vectors.", "ImageOutline", "#10B981"), - ["frypdf.element.shape"] = new("Canvas Elements", "Bézier vector graphics, rectangles, ellipses, and arrow callouts.", "ShapeOutline", "#F59E0B"), - ["frypdf.element.table"] = new("Canvas Elements", "Data grid element with auto-pagination and cell styling.", "TableLarge", "#6366F1"), - ["frypdf.element.chart"] = new("Canvas Elements", "Vector charts: bar, line, pie, donut, and scatter plots.", "ChartBar", "#EC4899"), - ["frypdf.element.math"] = new("Canvas Elements", "LaTeX mathematical formula typesetting rendered via SkiaSharp.", "Sigma", "#8B5CF6"), - ["frypdf.element.form"] = new("Canvas Elements", "Fillable PDF form controls: textboxes, checkboxes, and signatures.", "FormSelect", "#06B6D4"), - - // Document I/O Engines - ["frypdf.io.questpdf"] = new("Document I/O", "High-performance vector PDF rendering and generation engine.", "FilePdfBox", "#DC2626"), - ["frypdf.io.pdfpig"] = new("Document I/O", "Deep structural PDF deconstruction, text clustering, and font extraction.", "BookSearchOutline", "#0284C7"), - ["frypdf.io.skia"] = new("Document I/O", "Cross-platform hardware-accelerated 2D raster and graphics pipeline.", "Draw", "#10B981"), - - // UI & Extensions - ["frypdf.sidebar.thumbnails"] = new("UI & Extensions", "Interactive visual page thumbnail sidebar with drag reordering.", "ViewListOutline", "#64748B"), - ["frypdf.sidebar.inspector"] = new("UI & Extensions", "Context-sensitive property inspector for selected canvas elements.", "TuneVertical", "#4F46E5"), - ["frypdf.dialog.plugins"] = new("UI & Extensions", "Fast-access modal dialog for quickly viewing installed extensions.", "WindowMaximize", "#7C3AED"), - ["frypdf.statusbar.core"] = new("UI & Extensions", "Status bar telemetry showing page count, zoom level, and ALC memory.", "DockBottom", "#64748B"), - ["frypdf.overlay.snake"] = new("UI & Extensions", "Playable, draggable retro-arcade Snake game floating over the application canvas in the 'shell.overlay' slot.", "GamepadVariantOutline", "#10B981") - }; - - private static string InferCategory(IFryPlugin plugin) - { - if (KnownPluginMetas.TryGetValue(plugin.Id, out var meta)) - return meta.Category; - - var id = plugin.Id.ToLowerInvariant(); - if (id.Contains("page")) return "Workspace Pages"; - if (id.Contains("tool")) return "Tools & Productivity"; - if (id.Contains("element")) return "Canvas Elements"; - if (id.Contains("io") || id.Contains("documentio")) return "Document I/O"; - if (id.Contains("ai")) return "AI & Intelligence"; - if (id.Contains("security") || id.Contains("sign") || id.Contains("protect")) return "Security & Signatures"; - if (id.Contains("convert") || id.Contains("excel") || id.Contains("word") || id.Contains("ppt")) return "Conversion & Office"; - if (id.Contains("sidebar") || id.Contains("dialog") || id.Contains("status")) return "UI & Extensions"; - return "General"; - } - - private static string InferDescription(IFryPlugin plugin) - { - if (KnownPluginMetas.TryGetValue(plugin.Id, out var meta)) - return meta.Description; - - return $"Modular studio component '{plugin.Name}' dynamically mounted into FryPDF microkernel."; - } - - private static string InferIcon(IFryPlugin plugin) - { - if (KnownPluginMetas.TryGetValue(plugin.Id, out var meta)) - return meta.Icon; - - var id = plugin.Id.ToLowerInvariant(); - if (id.Contains("ai")) return "Creation"; - if (id.Contains("ocr")) return "TextRecognition"; - if (id.Contains("tool")) return "WrenchOutline"; - if (id.Contains("element")) return "ShapeOutline"; - if (id.Contains("io")) return "FileDocumentOutline"; - if (id.Contains("page")) return "BookOpenPageVariantOutline"; - if (id.Contains("dialog")) return "WindowMaximize"; - if (id.Contains("sidebar")) return "DockRight"; - return "PuzzleOutline"; - } - - private static string InferColor(IFryPlugin plugin) - { - if (KnownPluginMetas.TryGetValue(plugin.Id, out var meta)) - return meta.Color; - - var id = plugin.Id.ToLowerInvariant(); - if (id.Contains("ai")) return "#8B5CF6"; - if (id.Contains("ocr")) return "#10B981"; - if (id.Contains("tool")) return "#3B82F6"; - if (id.Contains("element")) return "#EC4899"; - if (id.Contains("io")) return "#F59E0B"; - if (id.Contains("page")) return "#0284C7"; - return "#7C3AED"; - } -} diff --git a/src/PdfEditorApp/ViewModels/SettingsViewModel.cs b/src/PdfEditorApp/ViewModels/SettingsViewModel.cs deleted file mode 100644 index 6793dd5..0000000 --- a/src/PdfEditorApp/ViewModels/SettingsViewModel.cs +++ /dev/null @@ -1,459 +0,0 @@ -using System; -using Avalonia.Media; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels; - -/// -/// ViewModel managing user UI customization preferences, notification placements, and workspace behaviors. -/// -public partial class SettingsViewModel : ViewModelBase -{ - private readonly IUiSettingsService _uiSettingsService; - private readonly IThemeService? _themeService; - - public void TriggerToast(string message, ToastNotificationType type = ToastNotificationType.Primary, string? icon = null) - { - WeakReferenceMessenger.Default.Send(new ShowToastMessage(message, type, icon)); - } - - [ObservableProperty] - private ToastPosition _toastPosition; - - [ObservableProperty] - private ToastStyleVariant _toastStyleVariant; - - [ObservableProperty] - private int _toastDurationMs; - - [ObservableProperty] - private bool _toastShowCloseButton; - - [ObservableProperty] - private bool _toastSoundEnabled; - - [ObservableProperty] - private AppThemeMode _themeMode; - - [ObservableProperty] - private PdfReaderTheme _readingTheme; - - [ObservableProperty] - private bool _showGridByDefault; - - [ObservableProperty] - private bool _snapToGridByDefault; - - [ObservableProperty] - private GridSnapSize _gridSnapSize; - - [ObservableProperty] - private PdfViewerZoomMode _defaultZoomMode; - - [ObservableProperty] - private bool _compactRibbonByDefault; - - [ObservableProperty] - private bool _autoExpandInspectorOnSelect; - - [ObservableProperty] - private bool _showShortcutHints; - - // --- Interactive Live Preview State --- - - [ObservableProperty] - private ToastNotificationType _previewToastType = ToastNotificationType.Primary; - - [ObservableProperty] - private string _previewToastMessage = "Primary message comes here"; - - [ObservableProperty] - private string _previewToastIcon = "InformationOutline"; - - public bool PreviewIsSolid => ToastStyleVariant switch - { - ToastStyleVariant.Subtle => false, - ToastStyleVariant.Solid => true, - ToastStyleVariant.Auto => _themeService?.IsDarkMode ?? false, - _ => true - }; - - public IBrush PreviewBackgroundBrush - { - get - { - if (PreviewIsSolid) - { - return PreviewToastType switch - { - ToastNotificationType.Primary => Brush("#0F6CBD"), - ToastNotificationType.Success => Brush("#15803D"), - ToastNotificationType.Danger => Brush("#DC2626"), - ToastNotificationType.Warning => Brush("#D97706"), - ToastNotificationType.General => Brush("#1E293B"), - _ => Brush("#1E293B") - }; - } - - return (_themeService?.IsDarkMode == true) ? PreviewToastType switch - { - ToastNotificationType.Primary => Brush("#172554"), - ToastNotificationType.Success => Brush("#052E16"), - ToastNotificationType.Danger => Brush("#450A0A"), - ToastNotificationType.Warning => Brush("#451A03"), - ToastNotificationType.General => Brush("#18181B"), - _ => Brush("#18181B") - } : PreviewToastType switch - { - ToastNotificationType.Primary => Brush("#EFF6FF"), - ToastNotificationType.Success => Brush("#F0FDF4"), - ToastNotificationType.Danger => Brush("#FEF2F2"), - ToastNotificationType.Warning => Brush("#FFFBEB"), - ToastNotificationType.General => Brush("#F8FAFC"), - _ => Brush("#F8FAFC") - }; - } - } - - public IBrush PreviewForegroundBrush - { - get - { - if (PreviewIsSolid) return WhiteBrush; - - return (_themeService?.IsDarkMode == true) ? PreviewToastType switch - { - ToastNotificationType.Primary => Brush("#93C5FD"), - ToastNotificationType.Success => Brush("#86EFAC"), - ToastNotificationType.Danger => Brush("#FCA5A5"), - ToastNotificationType.Warning => Brush("#FCD34D"), - ToastNotificationType.General => Brush("#E2E8F0"), - _ => Brush("#E2E8F0") - } : PreviewToastType switch - { - ToastNotificationType.Primary => Brush("#1D4ED8"), - ToastNotificationType.Success => Brush("#15803D"), - ToastNotificationType.Danger => Brush("#B91C1C"), - ToastNotificationType.Warning => Brush("#B45309"), - ToastNotificationType.General => Brush("#334155"), - _ => Brush("#334155") - }; - } - } - - public IBrush PreviewBorderBrush - { - get - { - if (PreviewIsSolid) - { - return PreviewToastType switch - { - ToastNotificationType.Primary => Brush("#0D5CA0"), - ToastNotificationType.Success => Brush("#166534"), - ToastNotificationType.Danger => Brush("#B91C1C"), - ToastNotificationType.Warning => Brush("#B45309"), - ToastNotificationType.General => Brush("#0F172A"), - _ => Brush("#0F172A") - }; - } - - return (_themeService?.IsDarkMode == true) ? PreviewToastType switch - { - ToastNotificationType.Primary => Brush("#1E40AF"), - ToastNotificationType.Success => Brush("#166534"), - ToastNotificationType.Danger => Brush("#991B1B"), - ToastNotificationType.Warning => Brush("#92400E"), - ToastNotificationType.General => Brush("#3F3F46"), - _ => Brush("#3F3F46") - } : PreviewToastType switch - { - ToastNotificationType.Primary => Brush("#BFDBFE"), - ToastNotificationType.Success => Brush("#BBF7D0"), - ToastNotificationType.Danger => Brush("#FECACA"), - ToastNotificationType.Warning => Brush("#FDE68A"), - ToastNotificationType.General => Brush("#CBD5E1"), - _ => Brush("#CBD5E1") - }; - } - } - - public IBrush PreviewIconBrush => PreviewForegroundBrush; - public IBrush PreviewCloseBrush => PreviewIsSolid ? Brush("#CCFFFFFF") : PreviewForegroundBrush; - - private static readonly SolidColorBrush WhiteBrush = new(Color.Parse("#FFFFFF")); - private static SolidColorBrush Brush(string hex) => new(Color.Parse(hex)); - - public void RefreshPreview() - { - OnPropertyChanged(nameof(PreviewIsSolid)); - OnPropertyChanged(nameof(PreviewBackgroundBrush)); - OnPropertyChanged(nameof(PreviewForegroundBrush)); - OnPropertyChanged(nameof(PreviewBorderBrush)); - OnPropertyChanged(nameof(PreviewIconBrush)); - OnPropertyChanged(nameof(PreviewCloseBrush)); - } - - public AiSettingsViewModel AiSettings { get; } - - public SettingsViewModel() : this(new UiSettingsService(), new ThemeService()) - { - } - - public SettingsViewModel(IUiSettingsService uiSettingsService, IThemeService? themeService = null) - { - _uiSettingsService = uiSettingsService; - _themeService = themeService; - AiSettings = new AiSettingsViewModel(_uiSettingsService, new Services.AI.AiService()); - - LoadFromSettings(_uiSettingsService.Settings); - _uiSettingsService.SettingsChanged += LoadFromSettings; - - if (_themeService != null) - { - _themeService.ThemeChanged += (_) => RefreshPreview(); - } - } - - private void LoadFromSettings(UiSettingsModel s) - { - ToastPosition = s.ToastPosition; - ToastStyleVariant = s.ToastStyleVariant; - ToastDurationMs = s.ToastDurationMs; - ToastShowCloseButton = s.ToastShowCloseButton; - ToastSoundEnabled = s.ToastSoundEnabled; - ThemeMode = s.ThemeMode; - ReadingTheme = s.ReadingTheme; - ShowGridByDefault = s.ShowGridByDefault; - SnapToGridByDefault = s.SnapToGridByDefault; - GridSnapSize = s.GridSnapSize; - DefaultZoomMode = s.DefaultZoomMode; - CompactRibbonByDefault = s.CompactRibbonByDefault; - AutoExpandInspectorOnSelect = s.AutoExpandInspectorOnSelect; - ShowShortcutHints = s.ShowShortcutHints; - RefreshPreview(); - } - - // --- Position Selection Commands --- - - [RelayCommand] - public void SetToastPosition(object? param) - { - ToastPosition pos; - if (param is ToastPosition p) pos = p; - else if (param is string s && Enum.TryParse(s, true, out var parsed)) pos = parsed; - else return; - - ToastPosition = pos; - _uiSettingsService.UpdateSettings(s => s.ToastPosition = pos); - TriggerToast($"Notification placement set to {GetPositionName(pos)}", ToastNotificationType.Primary, "DockBottom"); - RefreshPreview(); - } - - [RelayCommand] - public void SetToastStyleVariant(object? param) - { - ToastStyleVariant variant; - if (param is ToastStyleVariant v) variant = v; - else if (param is string s && Enum.TryParse(s, true, out var parsed)) variant = parsed; - else return; - - ToastStyleVariant = variant; - _uiSettingsService.UpdateSettings(s => s.ToastStyleVariant = variant); - TriggerToast($"Snackbar visual style changed to {variant}", ToastNotificationType.Primary, "PaletteOutline"); - RefreshPreview(); - } - - [RelayCommand] - public void SetToastDuration(object? param) - { - int durationMs; - if (param is int d) durationMs = d; - else if (param is string s && int.TryParse(s, out var parsed)) durationMs = parsed; - else return; - - ToastDurationMs = durationMs; - _uiSettingsService.UpdateSettings(s => s.ToastDurationMs = durationMs); - string durLabel = durationMs > 0 ? $"{durationMs / 1000.0:0.#} seconds" : "Manual Close Only"; - TriggerToast($"Notification timeout set to {durLabel}", ToastNotificationType.General, "ClockOutline"); - } - - [RelayCommand] - public void ToggleToastCloseButton() - { - ToastShowCloseButton = !ToastShowCloseButton; - _uiSettingsService.UpdateSettings(s => s.ToastShowCloseButton = ToastShowCloseButton); - RefreshPreview(); - } - - [RelayCommand] - public void ToggleToastSound() - { - ToastSoundEnabled = !ToastSoundEnabled; - _uiSettingsService.UpdateSettings(s => s.ToastSoundEnabled = ToastSoundEnabled); - } - - [RelayCommand] - public void SetThemeMode(object? param) - { - AppThemeMode mode; - if (param is AppThemeMode m) mode = m; - else if (param is string s && Enum.TryParse(s, true, out var parsed)) mode = parsed; - else return; - - ThemeMode = mode; - _uiSettingsService.UpdateSettings(s => s.ThemeMode = mode); - _themeService?.SetTheme(mode); - RefreshPreview(); - } - - [RelayCommand] - public void SetReadingTheme(object? param) - { - PdfReaderTheme theme; - if (param is PdfReaderTheme t) theme = t; - else if (param is string s && Enum.TryParse(s, true, out var parsed)) theme = parsed; - else return; - - ReadingTheme = theme; - _uiSettingsService.UpdateSettings(s => s.ReadingTheme = theme); - _themeService?.SetReadingTheme(theme); - } - - [RelayCommand] - public void ToggleGridByDefault() - { - ShowGridByDefault = !ShowGridByDefault; - _uiSettingsService.UpdateSettings(s => s.ShowGridByDefault = ShowGridByDefault); - } - - [RelayCommand] - public void ToggleSnapByDefault() - { - SnapToGridByDefault = !SnapToGridByDefault; - _uiSettingsService.UpdateSettings(s => s.SnapToGridByDefault = SnapToGridByDefault); - } - - [RelayCommand] - public void SetGridSnapSize(object? param) - { - GridSnapSize size; - if (param is GridSnapSize g) size = g; - else if (param is string s && Enum.TryParse(s, true, out var parsed)) size = parsed; - else return; - - GridSnapSize = size; - _uiSettingsService.UpdateSettings(s => s.GridSnapSize = size); - } - - [RelayCommand] - public void SetDefaultZoomMode(object? param) - { - PdfViewerZoomMode mode; - if (param is PdfViewerZoomMode m) mode = m; - else if (param is string s && Enum.TryParse(s, true, out var parsed)) mode = parsed; - else return; - - DefaultZoomMode = mode; - _uiSettingsService.UpdateSettings(s => s.DefaultZoomMode = mode); - } - - [RelayCommand] - public void ToggleCompactRibbon() - { - CompactRibbonByDefault = !CompactRibbonByDefault; - _uiSettingsService.UpdateSettings(s => s.CompactRibbonByDefault = CompactRibbonByDefault); - } - - [RelayCommand] - public void ToggleAutoExpandInspector() - { - AutoExpandInspectorOnSelect = !AutoExpandInspectorOnSelect; - _uiSettingsService.UpdateSettings(s => s.AutoExpandInspectorOnSelect = AutoExpandInspectorOnSelect); - } - - [RelayCommand] - public void ToggleShortcutHints() - { - ShowShortcutHints = !ShowShortcutHints; - _uiSettingsService.UpdateSettings(s => s.ShowShortcutHints = ShowShortcutHints); - } - - [RelayCommand] - public void ResetAllDefaults() - { - _uiSettingsService.ResetToDefaults(); - LoadFromSettings(_uiSettingsService.Settings); - RefreshPreview(); - TriggerToast("All UI preferences reset to factory defaults", ToastNotificationType.Success, "Restore"); - } - - // --- Interactive Live Test Playground Commands --- - - [RelayCommand] - public void TestPrimaryNotification() - { - PreviewToastType = ToastNotificationType.Primary; - PreviewToastMessage = "Primary message comes here"; - PreviewToastIcon = "InformationOutline"; - RefreshPreview(); - TriggerToast("Primary message comes here", ToastNotificationType.Primary, "InformationOutline"); - } - - [RelayCommand] - public void TestSuccessNotification() - { - PreviewToastType = ToastNotificationType.Success; - PreviewToastMessage = "Success message comes here"; - PreviewToastIcon = "CheckCircleOutline"; - RefreshPreview(); - TriggerToast("Success message comes here", ToastNotificationType.Success, "CheckCircleOutline"); - } - - [RelayCommand] - public void TestDangerNotification() - { - PreviewToastType = ToastNotificationType.Danger; - PreviewToastMessage = "Danger message comes here"; - PreviewToastIcon = "AlertOctagonOutline"; - RefreshPreview(); - TriggerToast("Danger message comes here", ToastNotificationType.Danger, "AlertOctagonOutline"); - } - - [RelayCommand] - public void TestWarningNotification() - { - PreviewToastType = ToastNotificationType.Warning; - PreviewToastMessage = "Warning message comes here"; - PreviewToastIcon = "AlertOutline"; - RefreshPreview(); - TriggerToast("Warning message comes here", ToastNotificationType.Warning, "AlertOutline"); - } - - [RelayCommand] - public void TestGeneralNotification() - { - PreviewToastType = ToastNotificationType.General; - PreviewToastMessage = "General message comes here"; - PreviewToastIcon = "InformationOutline"; - RefreshPreview(); - TriggerToast("General message comes here", ToastNotificationType.General, "InformationOutline"); - } - - private static string GetPositionName(ToastPosition pos) => pos switch - { - ToastPosition.TopLeft => "Top-Left", - ToastPosition.TopCenter => "Top-Center", - ToastPosition.TopRight => "Top-Right", - ToastPosition.BottomLeft => "Bottom-Left", - ToastPosition.BottomCenter => "Bottom-Center", - ToastPosition.BottomRight => "Bottom-Right", - _ => "Bottom-Center" - }; -} diff --git a/src/PdfEditorApp/ViewModels/Shell/PdfLivePreviewViewModel.cs b/src/PdfEditorApp/ViewModels/Shell/PdfLivePreviewViewModel.cs deleted file mode 100644 index 8399cf5..0000000 --- a/src/PdfEditorApp/ViewModels/Shell/PdfLivePreviewViewModel.cs +++ /dev/null @@ -1,313 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Tools.Core; - -namespace PdfEditorApp.ViewModels.Shell; - -/// -/// Shared live-preview state (page navigation, zoom, rendered pages) for tool screens, -/// built on . One instance is owned by -/// so every tool gets -/// the same reader-style preview instead of reimplementing rendering/zoom/page-nav -/// per tool (as RedactPdfToolViewModel previously did independently). -/// -public partial class PdfLivePreviewViewModel : ObservableObject -{ - private const float ThumbnailScale = 0.35f; - private const double MinZoom = 0.5; - private const double MaxZoom = 3.0; - private const double ZoomStep = 0.25; - - private string? _loadedFilePath; - private CancellationTokenSource? _loadCts; - private CancellationTokenSource? _zoomRenderCts; - - public ObservableCollection Pages { get; } = new(); - - [ObservableProperty] - private int _currentPageNumber = 1; - - [ObservableProperty] - private int _totalPages; - - [ObservableProperty] - private double _zoomLevel = 1.0; - - [ObservableProperty] - private bool _isLoading; - - [ObservableProperty] - private PdfToolPreviewPage? _selectedPage; - - [ObservableProperty] - private bool _isTextDocument; - - [ObservableProperty] - private string _textDocumentContent = string.Empty; - - [ObservableProperty] - private int _textDocumentLinesCount; - - [ObservableProperty] - private int _textDocumentWordsCount; - - [ObservableProperty] - private string _textDocumentFileName = string.Empty; - - public bool HasDocument => TotalPages > 0 || IsTextDocument; - public string ZoomPercentText => $"{ZoomLevel * 100:F0}%"; - public string PageIndicatorText => IsTextDocument - ? (TextDocumentLinesCount > 0 ? $"{TextDocumentLinesCount} lines" : "Text Document") - : (TotalPages > 0 ? $"Page {CurrentPageNumber} of {TotalPages}" : "No document"); - public bool CanGoToPreviousPage => CurrentPageNumber > 1; - public bool CanGoToNextPage => CurrentPageNumber < TotalPages; - public bool CanZoomIn => ZoomLevel < MaxZoom - 0.001; - public bool CanZoomOut => ZoomLevel > MinZoom + 0.001; - - partial void OnCurrentPageNumberChanged(int value) - { - OnPropertyChanged(nameof(PageIndicatorText)); - OnPropertyChanged(nameof(CanGoToPreviousPage)); - OnPropertyChanged(nameof(CanGoToNextPage)); - - foreach (var page in Pages) - { - page.IsSelected = page.PageNumber == value; - } - SelectedPage = Pages.FirstOrDefault(p => p.PageNumber == value); - - EnsurePageRendered(value); - } - - partial void OnTotalPagesChanged(int value) - { - OnPropertyChanged(nameof(HasDocument)); - OnPropertyChanged(nameof(PageIndicatorText)); - OnPropertyChanged(nameof(CanGoToNextPage)); - } - - partial void OnZoomLevelChanged(double value) - { - OnPropertyChanged(nameof(ZoomPercentText)); - OnPropertyChanged(nameof(CanZoomIn)); - OnPropertyChanged(nameof(CanZoomOut)); - - // Debounced so rapid zooming doesn't trigger a render per tick (matches the - // PDF Reader's own zoom-debounce behavior). - _zoomRenderCts?.Cancel(); - var cts = new CancellationTokenSource(); - _zoomRenderCts = cts; - _ = RerenderCurrentPageAfterZoomDebounceAsync(cts.Token); - } - - private async Task RerenderCurrentPageAfterZoomDebounceAsync(CancellationToken ct) - { - try { await Task.Delay(150, ct); } - catch (OperationCanceledException) { return; } - if (ct.IsCancellationRequested) return; - EnsurePageRendered(CurrentPageNumber, forceRerender: true); - } - - [RelayCommand] - private void ZoomIn() => ZoomLevel = Math.Min(MaxZoom, Math.Round(ZoomLevel + ZoomStep, 2)); - - [RelayCommand] - private void ZoomOut() => ZoomLevel = Math.Max(MinZoom, Math.Round(ZoomLevel - ZoomStep, 2)); - - [RelayCommand] - private void ResetZoom() => ZoomLevel = 1.0; - - [RelayCommand] - private void NextPage() - { - if (CanGoToNextPage) CurrentPageNumber++; - } - - [RelayCommand] - private void PreviousPage() - { - if (CanGoToPreviousPage) CurrentPageNumber--; - } - - [RelayCommand] - private void SelectPage(PdfToolPreviewPage? page) - { - if (page != null) CurrentPageNumber = page.PageNumber; - } - - /// - /// Loads (or clears, if is null/empty) the document the - /// preview shows. Safe to call repeatedly as the tool's selected file changes. - /// Supports both PDF documents and plain text / markdown files (.txt, .md, .csv, .log). - /// - public async Task LoadDocumentAsync(string? filePath) - { - _loadCts?.Cancel(); - var cts = new CancellationTokenSource(); - _loadCts = cts; - - Pages.Clear(); - CurrentPageNumber = 1; - TotalPages = 0; - ZoomLevel = 1.0; - SelectedPage = null; - _loadedFilePath = filePath; - IsTextDocument = false; - TextDocumentContent = string.Empty; - TextDocumentFileName = string.Empty; - TextDocumentLinesCount = 0; - TextDocumentWordsCount = 0; - - if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) - { - return; - } - - string ext = Path.GetExtension(filePath).ToLowerInvariant(); - if (ext is ".txt" or ".text" or ".md" or ".json" or ".csv" or ".tsv" or ".log") - { - IsLoading = true; - try - { - string text = await File.ReadAllTextAsync(filePath, cts.Token); - if (cts.Token.IsCancellationRequested || filePath != _loadedFilePath) return; - - TextDocumentContent = text; - TextDocumentFileName = Path.GetFileName(filePath); - var lines = text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); - TextDocumentLinesCount = lines.Length; - TextDocumentWordsCount = text.Split(new[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries).Length; - TotalPages = 1; - CurrentPageNumber = 1; - IsTextDocument = true; - } - catch (Exception ex) - { - TextDocumentContent = $"[Error loading text file: {ex.Message}]"; - IsTextDocument = true; - } - finally - { - IsLoading = false; - } - return; - } - - IsLoading = true; - try - { - int total = await Task.Run(() => PdfFileHelper.InspectPageCountSafely(filePath), cts.Token); - if (cts.Token.IsCancellationRequested || filePath != _loadedFilePath) return; - - TotalPages = total; - for (int i = 1; i <= total; i++) - { - Pages.Add(new PdfToolPreviewPage { PageNumber = i, IsSelected = i == 1 }); - } - SelectedPage = Pages.FirstOrDefault(); - - if (total > 0) - { - await RenderPageCoreAsync(1, BaseScaleForZoom(ZoomLevel), cts.Token); - _ = LoadThumbnailsInBackgroundAsync(filePath, cts.Token); - } - } - finally - { - IsLoading = false; - } - } - - /// - /// Directly loads text content into the live preview canvas without requiring a file on disk. - /// - public void LoadTextContent(string text, string title = "Extracted_Text.txt") - { - _loadCts?.Cancel(); - Pages.Clear(); - SelectedPage = null; - TotalPages = 1; - CurrentPageNumber = 1; - ZoomLevel = 1.0; - _loadedFilePath = null; - - TextDocumentContent = text; - TextDocumentFileName = title; - var lines = text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); - TextDocumentLinesCount = lines.Length; - TextDocumentWordsCount = text.Split(new[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries).Length; - IsTextDocument = true; - } - - private async Task LoadThumbnailsInBackgroundAsync(string filePath, CancellationToken ct) - { - foreach (var page in Pages.ToList()) - { - if (ct.IsCancellationRequested || filePath != _loadedFilePath) return; - - var (bitmap, widthPoints, heightPoints) = await Task.Run( - () => PdfPageRenderer.RenderPageAtScale(filePath, page.PageNumber, ThumbnailScale), ct); - - if (ct.IsCancellationRequested || filePath != _loadedFilePath) return; - - if (widthPoints > 0) page.WidthPoints = widthPoints; - if (heightPoints > 0) page.HeightPoints = heightPoints; - if (bitmap != null) page.ThumbnailBitmap = bitmap; - } - } - - private float BaseScaleForZoom(double zoomLevel) => (float)Math.Clamp(1.75 * zoomLevel, 1.25, 4.0); - - /// Re-renders a page at a zoom-appropriate resolution if it hasn't been rendered yet, or the zoom level has moved enough to need a sharper bitmap. - private void EnsurePageRendered(int pageNumber, bool forceRerender = false) - { - var page = Pages.FirstOrDefault(p => p.PageNumber == pageNumber); - if (page == null || string.IsNullOrEmpty(_loadedFilePath)) return; - - float targetScale = BaseScaleForZoom(ZoomLevel); - if (!forceRerender && page.Bitmap != null && Math.Abs(page.RenderedScale - targetScale) <= 0.4f) return; - - string filePath = _loadedFilePath; - Task.Run(() => - { - var (bitmap, widthPoints, heightPoints) = PdfPageRenderer.RenderPageAtScale(filePath, pageNumber, targetScale); - if (bitmap == null || filePath != _loadedFilePath) return; - - Dispatcher.UIThread.Post(() => - { - if (filePath != _loadedFilePath) return; - page.Bitmap = bitmap; - page.RenderedScale = targetScale; - if (widthPoints > 0) page.WidthPoints = widthPoints; - if (heightPoints > 0) page.HeightPoints = heightPoints; - }); - }); - } - - private async Task RenderPageCoreAsync(int pageNumber, float scale, CancellationToken ct) - { - var page = Pages.FirstOrDefault(p => p.PageNumber == pageNumber); - if (page == null) return; - - string? filePath = _loadedFilePath; - if (string.IsNullOrEmpty(filePath)) return; - - var (bitmap, widthPoints, heightPoints) = await Task.Run( - () => PdfPageRenderer.RenderPageAtScale(filePath, pageNumber, scale), ct); - - if (ct.IsCancellationRequested || filePath != _loadedFilePath) return; - - page.Bitmap = bitmap; - page.RenderedScale = scale; - if (widthPoints > 0) page.WidthPoints = widthPoints; - if (heightPoints > 0) page.HeightPoints = heightPoints; - } -} diff --git a/src/PdfEditorApp/ViewModels/StatusBarWidgetViewModel.cs b/src/PdfEditorApp/ViewModels/StatusBarWidgetViewModel.cs deleted file mode 100644 index 9158cdb..0000000 --- a/src/PdfEditorApp/ViewModels/StatusBarWidgetViewModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Windows.Input; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace PdfEditorApp.ViewModels; - -/// -/// Presentation model for a status bar widget contributed dynamically by a plugin. -/// -public partial class StatusBarWidgetViewModel : ViewModelBase -{ - [ObservableProperty] - private string _widgetId = ""; - - [ObservableProperty] - private string? _label; - - [ObservableProperty] - private string? _toolTip; - - [ObservableProperty] - private string? _iconKind; - - [ObservableProperty] - private bool _isActive = true; - - [ObservableProperty] - private object? _content; - - [ObservableProperty] - private ICommand? _command; - - [ObservableProperty] - private object? _commandParameter; -} diff --git a/src/PdfEditorApp/ViewModels/TemplateCardViewModel.cs b/src/PdfEditorApp/ViewModels/TemplateCardViewModel.cs deleted file mode 100644 index 055a1d4..0000000 --- a/src/PdfEditorApp/ViewModels/TemplateCardViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; - -namespace PdfEditorApp.ViewModels; - -/// -/// Represents a template card in the Home View and Template Gallery, -/// holding its metadata and high-fidelity live preview. -/// -public partial class TemplateCardViewModel : ViewModelBase -{ - [ObservableProperty] - private string _id = ""; - - [ObservableProperty] - private string _name = ""; - - [ObservableProperty] - private string _category = "General"; - - [ObservableProperty] - private string _subtitle = ""; - - [ObservableProperty] - private string _description = ""; - - [ObservableProperty] - private string _badge = ""; - - [ObservableProperty] - private string _accentColorHex = "#0F6CBD"; - - [ObservableProperty] - private string _iconKind = "FileDocumentOutline"; - - [ObservableProperty] - private PageViewModel _pagePreview = new(); - - [ObservableProperty] - private bool _isBlank; - - [ObservableProperty] - private bool _isLandscape; - - [ObservableProperty] - private bool _isFeatured; - - public double AspectRatio => PagePreview.Height > 0 ? PagePreview.Width / PagePreview.Height : 0.707; -} diff --git a/src/PdfEditorApp/ViewModels/TesseractManagerViewModel.cs b/src/PdfEditorApp/ViewModels/TesseractManagerViewModel.cs deleted file mode 100644 index 9684d30..0000000 --- a/src/PdfEditorApp/ViewModels/TesseractManagerViewModel.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Services.Ocr; - -namespace PdfEditorApp.ViewModels; - -/// -/// ViewModel representing an individual downloadable Tesseract OCR language model (.traineddata). -/// -public partial class TesseractLanguageItemViewModel : ObservableObject -{ - private readonly ITesseractModelService _modelService; - - public TesseractLanguagePackageInfo Model { get; } - - public string Code => Model.Code; - public string DisplayName => Model.DisplayName; - public string NativeName => Model.NativeName; - public string FlagEmoji => Model.FlagEmoji; - public string Category => Model.Category; - public string Description => Model.Description; - public string SampleText => Model.SampleText; - public string FormattedSize => Model.FormattedSize; - public string FileName => Model.FileName; - - [ObservableProperty] - private bool _isInstalled; - - [ObservableProperty] - private bool _isDownloading; - - [ObservableProperty] - private double _downloadProgress; - - [ObservableProperty] - private string _statusText = "Available"; - - [ObservableProperty] - private string _progressText = ""; - - public TesseractLanguageItemViewModel(TesseractLanguagePackageInfo model, ITesseractModelService modelService) - { - Model = model; - _modelService = modelService; - RefreshStatus(); - } - - public void RefreshStatus() - { - IsInstalled = _modelService.IsLanguageInstalled(Model.Code); - Model.IsInstalled = IsInstalled; - if (IsInstalled) - { - StatusText = "Installed & Active"; - DownloadProgress = 1.0; - } - else - { - StatusText = $"Available ({FormattedSize})"; - DownloadProgress = 0.0; - } - } - - [RelayCommand] - public async Task DownloadAsync() - { - if (IsDownloading || IsInstalled) return; - - IsDownloading = true; - StatusText = "Connecting..."; - ProgressText = "0%"; - DownloadProgress = 0.0; - - var progress = new Progress(p => - { - DownloadProgress = p; - ProgressText = $"{p * 100:F0}%"; - StatusText = $"Downloading {ProgressText} ({FormattedSize})"; - }); - - bool success = await _modelService.DownloadLanguageAsync(Model.Code, progress, status => - { - StatusText = status; - }); - - IsDownloading = false; - IsInstalled = success; - Model.IsInstalled = success; - - if (success) - { - StatusText = "Installed & Ready"; - DownloadProgress = 1.0; - } - else - { - StatusText = "Download Failed (Retry)"; - } - } - - [RelayCommand] - public async Task DeleteAsync() - { - if (IsDownloading) return; - - bool deleted = await _modelService.DeleteLanguageAsync(Model.Code); - if (deleted) - { - IsInstalled = false; - Model.IsInstalled = false; - StatusText = $"Available ({FormattedSize})"; - DownloadProgress = 0.0; - } - } -} - -/// -/// Main ViewModel for the On-Demand Tesseract OCR Models & Language Studio. -/// Allows users to browse, search, download, and delete trained neural models on demand. -/// -public partial class TesseractManagerViewModel : ViewModelBase -{ - private readonly ITesseractModelService _modelService; - - [ObservableProperty] - private string _searchQuery = ""; - - [ObservableProperty] - private string _selectedCategory = "All"; - - [ObservableProperty] - private string _totalCacheSizeFormatted = "0 MB"; - - [ObservableProperty] - private int _installedCount; - - [ObservableProperty] - private int _totalLanguagesCount; - - [ObservableProperty] - private bool _isDownloadingAll; - - [ObservableProperty] - private string _globalStatusMessage = ""; - - [ObservableProperty] - private bool _isTesseractCliInstalled; - - [ObservableProperty] - private string _tesseractCliStatusText = "Checking runtime..."; - - public string TessDataDirectory => _modelService.TessDataDirectory; - - public ObservableCollection AllLanguages { get; } = new(); - public ObservableCollection FilteredLanguages { get; } = new(); - - public TesseractManagerViewModel() : this(new TesseractModelService()) { } - - public TesseractManagerViewModel(ITesseractModelService modelService) - { - _modelService = modelService; - _modelService.LanguageLibraryChanged += OnLanguageLibraryChanged; - - CheckCliRuntime(); - InitializeLanguages(); - _ = RefreshStatsAsync(); - } - - private void CheckCliRuntime() - { - string? path = _modelService.GetTesseractCliPath(); - IsTesseractCliInstalled = !string.IsNullOrEmpty(path); - TesseractCliStatusText = IsTesseractCliInstalled - ? $"Tesseract Runtime Active ({Path.GetFileName(path)})" - : "No CLI on PATH (OS Native OCR or .traineddata will be used)"; - } - - private void InitializeLanguages() - { - AllLanguages.Clear(); - foreach (var lang in _modelService.AvailableLanguages) - { - AllLanguages.Add(new TesseractLanguageItemViewModel(lang, _modelService)); - } - - TotalLanguagesCount = AllLanguages.Count; - ApplyFilter(); - } - - private void OnLanguageLibraryChanged() - { - foreach (var item in AllLanguages) - { - item.RefreshStatus(); - } - _ = RefreshStatsAsync(); - ApplyFilter(); - } - - public async Task RefreshStatsAsync() - { - long bytes = await _modelService.GetTotalCacheSizeBytesAsync(); - TotalCacheSizeFormatted = TesseractLanguagePackageInfo.FormatBytes(bytes); - InstalledCount = AllLanguages.Count(l => l.IsInstalled); - } - - partial void OnSearchQueryChanged(string value) => ApplyFilter(); - partial void OnSelectedCategoryChanged(string value) => ApplyFilter(); - - [RelayCommand] - public void SelectCategory(string category) - { - SelectedCategory = category; - } - - private void ApplyFilter() - { - FilteredLanguages.Clear(); - string query = SearchQuery.Trim().ToLowerInvariant(); - - foreach (var item in AllLanguages) - { - // Category filter - bool matchesCategory = SelectedCategory switch - { - "All" => true, - "Installed" => item.IsInstalled, - "European" => item.Category.Contains("Latin", StringComparison.OrdinalIgnoreCase) || item.Category.Contains("European", StringComparison.OrdinalIgnoreCase), - "EastAsia" => item.Category.Contains("East Asia", StringComparison.OrdinalIgnoreCase) && !item.Category.Contains("Southeast", StringComparison.OrdinalIgnoreCase), - "SouthAsia" => item.Category.Contains("South Asia", StringComparison.OrdinalIgnoreCase) || item.Category.Contains("Indic", StringComparison.OrdinalIgnoreCase), - "MiddleEast" => item.Category.Contains("Middle East", StringComparison.OrdinalIgnoreCase), - "SoutheastAsia" => item.Category.Contains("Southeast", StringComparison.OrdinalIgnoreCase), - "Specialized" => item.Category.Contains("Specialized", StringComparison.OrdinalIgnoreCase) || item.Category.Contains("Math", StringComparison.OrdinalIgnoreCase), - _ => true - }; - - if (!matchesCategory) continue; - - // Search query filter - if (!string.IsNullOrEmpty(query)) - { - bool matchesSearch = - item.Code.ToLowerInvariant().Contains(query) || - item.DisplayName.ToLowerInvariant().Contains(query) || - item.NativeName.ToLowerInvariant().Contains(query) || - item.Description.ToLowerInvariant().Contains(query) || - item.Category.ToLowerInvariant().Contains(query) || - item.FileName.ToLowerInvariant().Contains(query); - - if (!matchesSearch) continue; - } - - FilteredLanguages.Add(item); - } - } - - [RelayCommand] - public async Task DownloadAllAsync() - { - if (IsDownloadingAll) return; - - IsDownloadingAll = true; - GlobalStatusMessage = "Downloading all OCR language models..."; - - var uninstalled = AllLanguages.Where(l => !l.IsInstalled).ToList(); - int completed = 0; - - foreach (var lang in uninstalled) - { - GlobalStatusMessage = $"Downloading {lang.DisplayName} ({completed + 1}/{uninstalled.Count})..."; - await lang.DownloadAsync(); - completed++; - } - - IsDownloadingAll = false; - GlobalStatusMessage = "All OCR models downloaded successfully!"; - await RefreshStatsAsync(); - } - - [RelayCommand] - public async Task ClearAllCacheAsync() - { - await _modelService.ClearAllCacheAsync(); - foreach (var lang in AllLanguages) - { - lang.RefreshStatus(); - } - await RefreshStatsAsync(); - ApplyFilter(); - GlobalStatusMessage = "OCR models cache cleared. Storage reclaimed."; - } - - [RelayCommand] - public void OpenTessDataFolder() - { - try - { - string dir = TessDataDirectory; - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - if (OperatingSystem.IsMacOS()) - { - Process.Start("open", dir); - } - else if (OperatingSystem.IsWindows()) - { - Process.Start("explorer.exe", dir); - } - else - { - Process.Start("xdg-open", dir); - } - } - catch { } - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/ExcelToPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/ExcelToPdfToolViewModel.cs deleted file mode 100644 index d690d67..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/ExcelToPdfToolViewModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class ExcelToPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private PageOrientation _orientation = PageOrientation.Landscape; - - public override bool UsesWorkspaceShell => true; - - public ExcelToPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new OfficeToPdfOptions - { - InputFilePath = file, - Orientation = Orientation - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/HtmlToPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/HtmlToPdfToolViewModel.cs deleted file mode 100644 index b5be15f..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/HtmlToPdfToolViewModel.cs +++ /dev/null @@ -1,64 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class HtmlToPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private string _htmlContentOrUrl = "https://"; - - [ObservableProperty] - private PageFormat _format = PageFormat.A4; - - [ObservableProperty] - private PageOrientation _orientation = PageOrientation.Portrait; - - [ObservableProperty] - private double _marginPoints = 36; - - [ObservableProperty] - private bool _includePageNumbers = true; - - public override bool UsesWorkspaceShell => true; - - public HtmlToPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override bool ValidateInputs(out string errorMessage) - { - if (string.IsNullOrWhiteSpace(HtmlContentOrUrl)) - { - errorMessage = "Please enter a valid URL or HTML content."; - return false; - } - errorMessage = string.Empty; - return true; - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - bool isUrl = HtmlContentOrUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || - HtmlContentOrUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase); - - var options = new HtmlToPdfOptions - { - HtmlContentOrUrl = HtmlContentOrUrl, - IsUrl = isUrl, - Format = Format, - Orientation = Orientation, - MarginPoints = MarginPoints, - IncludePageNumbers = IncludePageNumbers - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.HtmlToPdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/JpgToPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/JpgToPdfToolViewModel.cs deleted file mode 100644 index 0e912fd..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/JpgToPdfToolViewModel.cs +++ /dev/null @@ -1,55 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class JpgToPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private PageFormat _pageFormat = PageFormat.A4; - - [ObservableProperty] - private PageOrientation _orientation = PageOrientation.Portrait; - - [ObservableProperty] - private bool _autoOrientation = true; - - [ObservableProperty] - private bool _fitToPage = true; - - [ObservableProperty] - private double _marginPoints = 20; - - [ObservableProperty] - private int _imagesPerPage = 1; - - public override bool UsesWorkspaceShell => true; - - public JpgToPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - var options = new ImagesToPdfOptions - { - ImageFiles = SelectedFiles.ToList(), - PageFormat = PageFormat, - Orientation = Orientation, - AutoOrientation = AutoOrientation, - FitToPage = FitToPage, - MarginPoints = MarginPoints, - ImagesPerPage = ImagesPerPage - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.JpgToPdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToExcelToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToExcelToolViewModel.cs deleted file mode 100644 index 0ab21f7..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToExcelToolViewModel.cs +++ /dev/null @@ -1,35 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class PdfToExcelToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private bool _detectAllTables = true; - - [ObservableProperty] - private bool _separateSheetsPerPage; - - public override bool UsesWorkspaceShell => true; - - public PdfToExcelToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new ExcelConversionOptions - { - InputFilePath = file, - DetectAllTables = DetectAllTables, - SeparateSheetsPerPage = SeparateSheetsPerPage - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToJpgToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToJpgToolViewModel.cs deleted file mode 100644 index e98ea91..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToJpgToolViewModel.cs +++ /dev/null @@ -1,51 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class PdfToJpgToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private string _outputFormat = "jpg"; - - [ObservableProperty] - private int _dpi = 300; - - [ObservableProperty] - private int _jpgQuality = 90; - - [ObservableProperty] - private bool _grayscale; - - [ObservableProperty] - private PageTargetSelection _targetPages = PageTargetSelection.AllPages; - - [ObservableProperty] - private string _pageRange = "1"; - - public override bool UsesWorkspaceShell => true; - - public PdfToJpgToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new ImageConversionOptions - { - InputFilePath = file, - OutputFormat = OutputFormat, - Dpi = Dpi, - JpgQuality = JpgQuality, - Grayscale = Grayscale, - TargetPages = TargetPages, - PageRange = PageRange - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToMarkdownToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToMarkdownToolViewModel.cs deleted file mode 100644 index 47465be..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToMarkdownToolViewModel.cs +++ /dev/null @@ -1,39 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class PdfToMarkdownToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private bool _includeTables = true; - - [ObservableProperty] - private bool _includeImages; - - [ObservableProperty] - private bool _includeMetadataHeader = true; - - public override bool UsesWorkspaceShell => true; - - public PdfToMarkdownToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new MarkdownConversionOptions - { - InputFilePath = file, - IncludeTables = IncludeTables, - IncludeImages = IncludeImages, - IncludeMetadataHeader = IncludeMetadataHeader - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToPdfAToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToPdfAToolViewModel.cs deleted file mode 100644 index eda726e..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToPdfAToolViewModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class PdfToPdfAToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private PdfAStandard _standard = PdfAStandard.PdfA2b; - - public override bool UsesWorkspaceShell => true; - - public PdfToPdfAToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new PdfAToolOptions - { - InputFilePath = file, - Standard = Standard - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToPowerPointToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToPowerPointToolViewModel.cs deleted file mode 100644 index 20ceb39..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToPowerPointToolViewModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class PdfToPowerPointToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private bool _editableText = true; - - public override bool UsesWorkspaceShell => true; - - public PdfToPowerPointToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new PptxConversionOptions - { - InputFilePath = file, - EditableText = EditableText - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToWordToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToWordToolViewModel.cs deleted file mode 100644 index eeb1c99..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/PdfToWordToolViewModel.cs +++ /dev/null @@ -1,39 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class PdfToWordToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private bool _extractTables = true; - - [ObservableProperty] - private bool _extractImages = true; - - [ObservableProperty] - private bool _ocrFallback; - - public override bool UsesWorkspaceShell => true; - - public PdfToWordToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new WordConversionOptions - { - InputFilePath = file, - ExtractTables = ExtractTables, - ExtractImages = ExtractImages, - OcrFallback = OcrFallback - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/PowerPointToPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/PowerPointToPdfToolViewModel.cs deleted file mode 100644 index ed59fa8..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/PowerPointToPdfToolViewModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class PowerPointToPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private PageOrientation _orientation = PageOrientation.Landscape; - - public override bool UsesWorkspaceShell => true; - - public PowerPointToPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new OfficeToPdfOptions - { - InputFilePath = file, - Orientation = Orientation - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/ScanToPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/ScanToPdfToolViewModel.cs deleted file mode 100644 index 3815f25..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/ScanToPdfToolViewModel.cs +++ /dev/null @@ -1,51 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class ScanToPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private bool _autoDeskew = true; - - [ObservableProperty] - private bool _enhanceContrast = true; - - [ObservableProperty] - private bool _whitenBackground = true; - - [ObservableProperty] - private bool _convertToGrayscale; - - [ObservableProperty] - private PageFormat _format = PageFormat.A4; - - public override bool UsesWorkspaceShell => true; - - public ScanToPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - var options = new ScanToolOptions - { - InputImageFiles = SelectedFiles.ToList(), - AutoDeskew = AutoDeskew, - EnhanceContrast = EnhanceContrast, - WhitenBackground = WhitenBackground, - ConvertToGrayscale = ConvertToGrayscale, - Format = Format - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.ScanToPdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Conversion/WordToPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Conversion/WordToPdfToolViewModel.cs deleted file mode 100644 index 3b16b7c..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Conversion/WordToPdfToolViewModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Conversion; - -public partial class WordToPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private PageOrientation _orientation = PageOrientation.Portrait; - - public override bool UsesWorkspaceShell => true; - - public WordToPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new OfficeToPdfOptions - { - InputFilePath = file, - Orientation = Orientation - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Core/PdfToolViewModelBase.cs b/src/PdfEditorApp/ViewModels/Tools/Core/PdfToolViewModelBase.cs deleted file mode 100644 index 304b218..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Core/PdfToolViewModelBase.cs +++ /dev/null @@ -1,788 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Shell; -using UglyToad.PdfPig; - -namespace PdfEditorApp.ViewModels.Tools.Core; - -/// -/// Abstract base class for all individual PDF tool ViewModels. -/// Provides unified lifecycle, file management, visual previews, asynchronous execution, cancellation, and navigation. -/// -public abstract partial class PdfToolViewModelBase : ViewModelBase -{ - protected readonly IPdfDocumentOperationsService OperationsService; - private CancellationTokenSource? _cts; - - /// - /// Remembers the user's last chosen save/export directory across tool executions. - /// - public static string? RememberedSaveDirectory { get; set; } - - public IStorageProvider? StorageProvider { get; set; } - - [ObservableProperty] - private PdfToolDefinition _tool = new(); - - [ObservableProperty] - private bool _isOpen; - - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(CanExecuteTool))] - [NotifyCanExecuteChangedFor(nameof(ExecuteToolCommand))] - private bool _isRunning; - - [ObservableProperty] - private bool _isComplete; - - [ObservableProperty] - private bool _hasError; - - [ObservableProperty] - private string _errorMessage = string.Empty; - - [ObservableProperty] - private string _statusMessage = "Ready"; - - [ObservableProperty] - private double _progressPercentage; - - [ObservableProperty] - private string _lastOutputFilePath = string.Empty; - - [ObservableProperty] - private string _resultSummaryMessage = string.Empty; - - [ObservableProperty] - private bool _isToolStarred; - - [ObservableProperty] - private PdfFilePreviewItem? _outputFilePreview; - - [ObservableProperty] - private PdfPagePreviewThumbnail? _selectedOutputPage; - - [ObservableProperty] - private string _savedNotificationMessage = string.Empty; - - [ObservableProperty] - private bool _hasSavedNotification; - - [ObservableProperty] - private long _originalSizeBytes; - - [ObservableProperty] - private long _outputSizeBytes; - - [ObservableProperty] - private double _sizeReductionPercentage; - - public bool HasSizeReduction => OriginalSizeBytes > 0 && OutputSizeBytes > 0 && OutputSizeBytes < OriginalSizeBytes; - public string SizeComparisonText => OriginalSizeBytes > 0 && OutputSizeBytes > 0 - ? $"{PdfFilePreviewItem.FormatBytes(OriginalSizeBytes)} → {PdfFilePreviewItem.FormatBytes(OutputSizeBytes)}" - : ""; - public string SizeReductionBadgeText => $"{SizeReductionPercentage:F0}% SAVED"; - - public ObservableCollection SelectedFiles { get; } = new(); - - public ObservableCollection SelectedFilePreviewItems { get; } = new(); - - /// - /// Reader-style live preview (page render, zoom, page nav) of the currently - /// selected input file, shared by the reader-matching tool shell - /// (). Reloads - /// automatically whenever changes. - /// - public PdfLivePreviewViewModel Preview { get; } = new(); - - /// - /// True for tools migrated to the shared reader-style workspace shell - /// (), which has its own - /// back button, tool identity, and toolbar. - /// hides its separate header banner for these so there's exactly one toolbar row, - /// matching the PDF Reader, instead of two stacked bars. - /// - public virtual bool UsesWorkspaceShell => false; - - public ObservableCollection OutputPageThumbnails { get; } = new(); - - public bool HasSelectedFiles => SelectedFiles.Count > 0; - public virtual bool CanExecuteTool => !IsRunning; - public string SelectedFilesCountText => SelectedFiles.Count == 1 ? "1 file selected" : $"{SelectedFiles.Count} files selected"; - public string PrimaryInputFile => SelectedFiles.FirstOrDefault() ?? string.Empty; - - public int TotalSelectedPages => SelectedFilePreviewItems.Sum(p => p.PageCount); - - public string TotalSummaryText - { - get - { - if (SelectedFilePreviewItems.Count == 0) return "No files selected"; - long totalBytes = SelectedFilePreviewItems.Sum(p => p.FileSizeBytes); - int pages = TotalSelectedPages; - if (SelectedFilePreviewItems.Count == 1) - return $"1 Document · {pages} Page{(pages != 1 ? "s" : "")} · {PdfFilePreviewItem.FormatBytes(totalBytes)}"; - return $"{SelectedFilePreviewItems.Count} Documents · {pages} Total Pages · {PdfFilePreviewItem.FormatBytes(totalBytes)}"; - } - } - - // Events - public event Action? BackRequested; - public event Action? NavigateToToolRequested; - public event Action? HelpGuideRequested; - - [RelayCommand] - public void OpenToolHelpGuide() - { - HelpGuideRequested?.Invoke(Tool.Id); - } - - [RelayCommand] - public void NavigateToTool(string toolName) - { - if (Enum.TryParse(toolName, true, out var toolId)) - { - string targetFile = !string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath) - ? LastOutputFilePath - : PrimaryInputFile; - NavigateToToolRequested?.Invoke(toolId, targetFile); - } - } - - protected PdfToolViewModelBase(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - { - OperationsService = operationsService; - Tool = tool; - SelectedFiles.CollectionChanged += (_, _) => { _ = Preview.LoadDocumentAsync(PrimaryInputFile); }; - } - - [RelayCommand] - public void GoBack() - { - BackRequested?.Invoke(); - } - - [RelayCommand] - public void OpenInEditor() - { - string? targetPath = (!string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath)) - ? LastOutputFilePath - : ((HasSelectedFiles && File.Exists(PrimaryInputFile)) ? PrimaryInputFile : null); - - if (!string.IsNullOrEmpty(targetPath)) - { - WeakReferenceMessenger.Default.Send(new OpenInEditorMessage(targetPath)); - } - } - - [RelayCommand] - public void OpenInViewer() - { - string targetPath = !string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath) - ? LastOutputFilePath - : (HasSelectedFiles && File.Exists(PrimaryInputFile) ? PrimaryInputFile : string.Empty); - - if (!string.IsNullOrEmpty(targetPath)) - { - WeakReferenceMessenger.Default.Send(new OpenInViewerMessage(targetPath)); - } - } - - public virtual void SetupInitialFiles(IEnumerable? filePaths) - { - SelectedFiles.Clear(); - if (filePaths != null) - { - foreach (var file in filePaths) - { - if (!string.IsNullOrEmpty(file) && File.Exists(file)) - { - SelectedFiles.Add(file); - } - } - } - SyncPreviewItems(); - ResetState(); - IsOpen = true; - } - - public virtual void ResetState() - { - IsRunning = false; - IsComplete = false; - HasError = false; - ErrorMessage = string.Empty; - StatusMessage = "Ready"; - ProgressPercentage = 0; - LastOutputFilePath = string.Empty; - ResultSummaryMessage = string.Empty; - OutputFilePreview = null; - OutputPageThumbnails.Clear(); - SelectedOutputPage = null; - SavedNotificationMessage = string.Empty; - HasSavedNotification = false; - OnPropertyChanged(nameof(HasSelectedFiles)); - OnPropertyChanged(nameof(SelectedFilesCountText)); - OnPropertyChanged(nameof(PrimaryInputFile)); - OnPropertyChanged(nameof(TotalSelectedPages)); - OnPropertyChanged(nameof(TotalSummaryText)); - } - - public void SyncPreviewItems() - { - SelectedFilePreviewItems.Clear(); - for (int i = 0; i < SelectedFiles.Count; i++) - { - string path = SelectedFiles[i]; - int pageCount = 1; - if (File.Exists(path)) - { - pageCount = PdfFileHelper.InspectPageCountSafely(path); - } - var item = PdfFilePreviewItem.CreateFromFile(path, i + 1, pageCount); - SelectedFilePreviewItems.Add(item); - } - - OnPropertyChanged(nameof(HasSelectedFiles)); - OnPropertyChanged(nameof(SelectedFilesCountText)); - OnPropertyChanged(nameof(PrimaryInputFile)); - OnPropertyChanged(nameof(TotalSelectedPages)); - OnPropertyChanged(nameof(TotalSummaryText)); - } - - [RelayCommand] - public async Task AddFilesAsync() - { - if (StorageProvider == null) return; - - var patterns = Tool.AcceptedFileExtensions - .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => e.Trim().StartsWith("*") ? e.Trim() : "*" + e.Trim()) - .ToArray(); - - IStorageFolder? startFolder = null; - if (!string.IsNullOrEmpty(RememberedSaveDirectory) && Directory.Exists(RememberedSaveDirectory)) - { - try - { - startFolder = await StorageProvider.TryGetFolderFromPathAsync(RememberedSaveDirectory); - } - catch { } - } - - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = $"Select Files for {Tool.Name}", - AllowMultiple = Tool.SupportsMultiFile, - SuggestedStartLocation = startFolder, - FileTypeFilter = new[] - { - new FilePickerFileType($"{Tool.Name} Inputs") - { - Patterns = patterns.Length > 0 ? patterns : new[] { "*.pdf" } - } - } - }); - - if (files != null && files.Count > 0) - { - if (!Tool.SupportsMultiFile) SelectedFiles.Clear(); - foreach (var f in files) - { - string path = f.Path.LocalPath; - if (!SelectedFiles.Contains(path)) - { - SelectedFiles.Add(path); - } - string? dir = Path.GetDirectoryName(path); - if (!string.IsNullOrEmpty(dir)) RememberedSaveDirectory = dir; - } - SyncPreviewItems(); - ResetState(); - } - } - - [RelayCommand] - public void MoveFileUp(object? parameter) - { - string? filePath = parameter switch - { - string s => s, - PdfFilePreviewItem item => item.FilePath, - _ => null - }; - - if (string.IsNullOrEmpty(filePath)) return; - - int index = SelectedFiles.IndexOf(filePath); - if (index > 0) - { - SelectedFiles.Move(index, index - 1); - SyncPreviewItems(); - ResetState(); - } - } - - [RelayCommand] - public void MoveFileDown(object? parameter) - { - string? filePath = parameter switch - { - string s => s, - PdfFilePreviewItem item => item.FilePath, - _ => null - }; - - if (string.IsNullOrEmpty(filePath)) return; - - int index = SelectedFiles.IndexOf(filePath); - if (index >= 0 && index < SelectedFiles.Count - 1) - { - SelectedFiles.Move(index, index + 1); - SyncPreviewItems(); - ResetState(); - } - } - - [RelayCommand] - public void RemoveFile(object? parameter) - { - string? filePath = parameter switch - { - string s => s, - PdfFilePreviewItem item => item.FilePath, - _ => null - }; - - if (string.IsNullOrEmpty(filePath)) return; - - SelectedFiles.Remove(filePath); - SyncPreviewItems(); - ResetState(); - } - - [RelayCommand] - public void ClearFiles() - { - SelectedFiles.Clear(); - SelectedFilePreviewItems.Clear(); - ResetState(); - } - - [RelayCommand] - public void Close() - { - CancelExecution(); - IsOpen = false; - } - - [RelayCommand] - public void CancelExecution() - { - if (_cts != null && !_cts.IsCancellationRequested) - { - _cts.Cancel(); - StatusMessage = "Cancelling..."; - } - } - - [RelayCommand(CanExecute = nameof(CanExecuteTool))] - public async Task ExecuteToolAsync() - { - if (!ValidateInputs(out var validationError)) - { - HasError = true; - ErrorMessage = validationError; - return; - } - - IsRunning = true; - IsComplete = false; - HasError = false; - ErrorMessage = string.Empty; - ProgressPercentage = 5.0; - StatusMessage = $"Processing {Tool.Name}..."; - - _cts = new CancellationTokenSource(); - var progress = new Progress(p => - { - ProgressPercentage = p; - StatusMessage = $"Processing ({p:F0}%)..."; - }); - - try - { - var result = await ExecuteCoreAsync(progress, _cts.Token); - - if (result.Success) - { - IsComplete = true; - LastOutputFilePath = result.OutputFilePath ?? ""; - ResultSummaryMessage = result.Message ?? "Operation completed successfully."; - StatusMessage = "Completed successfully!"; - ProgressPercentage = 100.0; - - OriginalSizeBytes = result.OriginalSizeBytes; - OutputSizeBytes = result.OutputSizeBytes; - if (result.OriginalSizeBytes > 0 && result.OutputSizeBytes > 0 && result.OutputSizeBytes < result.OriginalSizeBytes) - { - SizeReductionPercentage = ((result.OriginalSizeBytes - result.OutputSizeBytes) / (double)result.OriginalSizeBytes) * 100.0; - } - else - { - SizeReductionPercentage = 0.0; - } - OnPropertyChanged(nameof(HasSizeReduction)); - OnPropertyChanged(nameof(SizeComparisonText)); - OnPropertyChanged(nameof(SizeReductionBadgeText)); - - // Load rich in-app preview for the output document - if (!string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath)) - { - int pages = PdfFileHelper.InspectPageCountSafely(LastOutputFilePath); - OutputFilePreview = PdfFilePreviewItem.CreateFromFile(LastOutputFilePath, 1, pages); - LoadOutputPreviewThumbnails(LastOutputFilePath); - _ = Preview.LoadDocumentAsync(LastOutputFilePath); - } - } - else - { - HasError = true; - ErrorMessage = result.ErrorMessage ?? "Operation failed."; - StatusMessage = "Failed"; - } - } - catch (OperationCanceledException) - { - StatusMessage = "Operation cancelled."; - } - catch (Exception ex) - { - HasError = true; - ErrorMessage = $"Unexpected error: {ex.Message}"; - StatusMessage = "Error"; - } - finally - { - IsRunning = false; - } - } - - private void LoadOutputPreviewThumbnails(string filePath) - { - OutputPageThumbnails.Clear(); - if (!File.Exists(filePath)) return; - - try - { - using var pig = UglyToad.PdfPig.PdfDocument.Open(filePath); - int total = pig.NumberOfPages; - for (int i = 1; i <= total; i++) - { - var page = pig.GetPage(i); - bool isLandscape = page.Width > page.Height; - string summary = string.Empty; - if (!string.IsNullOrWhiteSpace(page.Text)) - { - string firstLine = page.Text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? ""; - if (firstLine.Length > 40) firstLine = firstLine.Substring(0, 40) + "..."; - summary = firstLine; - } - - var thumb = new PdfPagePreviewThumbnail - { - PageNumber = i, - PageLabel = $"Page {i} of {total}", - WidthPoints = Math.Round(page.Width, 1), - HeightPoints = Math.Round(page.Height, 1), - IsLandscape = isLandscape, - DimensionsText = $"{Math.Round(page.Width):F0} × {Math.Round(page.Height):F0} pt", - PageSummary = summary, - IsSelected = (i == 1) - }; - OutputPageThumbnails.Add(thumb); - } - SelectedOutputPage = OutputPageThumbnails.FirstOrDefault(); - } - catch - { - int pageCount = PdfFileHelper.InspectPageCountSafely(filePath); - for (int i = 1; i <= Math.Max(1, pageCount); i++) - { - OutputPageThumbnails.Add(new PdfPagePreviewThumbnail - { - PageNumber = i, - PageLabel = $"Page {i} of {pageCount}", - WidthPoints = 595, - HeightPoints = 842, - IsLandscape = false, - DimensionsText = "595 × 842 pt", - IsSelected = (i == 1) - }); - } - SelectedOutputPage = OutputPageThumbnails.FirstOrDefault(); - } - } - - [RelayCommand] - public void SelectPageThumbnail(PdfPagePreviewThumbnail? page) - { - if (page == null) return; - foreach (var p in OutputPageThumbnails) - { - p.IsSelected = (p.PageNumber == page.PageNumber); - } - SelectedOutputPage = page; - } - - [RelayCommand] - public async Task SaveOutputFileAsAsync() - { - if (StorageProvider == null || string.IsNullOrWhiteSpace(LastOutputFilePath) || !File.Exists(LastOutputFilePath)) return; - - IStorageFolder? startFolder = null; - if (!string.IsNullOrEmpty(RememberedSaveDirectory) && Directory.Exists(RememberedSaveDirectory)) - { - try - { - startFolder = await StorageProvider.TryGetFolderFromPathAsync(RememberedSaveDirectory); - } - catch { } - } - - string suggestedName = Path.GetFileName(LastOutputFilePath); - string ext = Path.GetExtension(LastOutputFilePath).TrimStart('.'); - if (string.IsNullOrEmpty(ext)) ext = "pdf"; - - var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = $"Save {Tool.Name} Output File", - SuggestedFileName = suggestedName, - DefaultExtension = ext, - SuggestedStartLocation = startFolder, - FileTypeChoices = new[] - { - new FilePickerFileType($"{ext.ToUpperInvariant()} File") - { - Patterns = new[] { $"*.{ext}" } - } - } - }); - - if (file != null) - { - SaveOutputFileToPath(file.Path.LocalPath); - } - } - - /// - /// Saves the current output file to . - /// Gracefully prevents self-copy errors and handles locked file IO exceptions. - /// - public void SaveOutputFileToPath(string targetPath) - { - if (string.IsNullOrWhiteSpace(targetPath) || string.IsNullOrWhiteSpace(LastOutputFilePath) || !File.Exists(LastOutputFilePath)) return; - - string fullSource = Path.GetFullPath(LastOutputFilePath); - string fullTarget = Path.GetFullPath(targetPath); - - // If the user picked the exact same path where the output is already stored, - // copying the file onto itself will throw an IOException ("used by another process"). - // Since it's already there, simply update notification and return successfully. - if (string.Equals(fullSource, fullTarget, StringComparison.OrdinalIgnoreCase)) - { - RememberedSaveDirectory = Path.GetDirectoryName(targetPath); - SavedNotificationMessage = $"Saved successfully to: {targetPath}"; - HasSavedNotification = true; - HasError = false; - return; - } - - try - { - File.Copy(LastOutputFilePath, targetPath, overwrite: true); - RememberedSaveDirectory = Path.GetDirectoryName(targetPath); - SavedNotificationMessage = $"Saved successfully to: {targetPath}"; - HasSavedNotification = true; - HasError = false; - } - catch (IOException) - { - HasSavedNotification = false; - HasError = true; - ErrorMessage = $"Cannot save to '{Path.GetFileName(targetPath)}': The file is currently open or in use by another application. Please close it and try again."; - } - catch (UnauthorizedAccessException) - { - HasSavedNotification = false; - HasError = true; - ErrorMessage = $"Cannot save to '{Path.GetFileName(targetPath)}': Access denied. Please choose a different folder."; - } - catch (Exception ex) - { - HasSavedNotification = false; - HasError = true; - ErrorMessage = $"Failed to save file: {ex.Message}"; - } - } - - [RelayCommand] - public void OpenOutputFile() - { - if (string.IsNullOrWhiteSpace(LastOutputFilePath) || !File.Exists(LastOutputFilePath)) return; - - try - { - var psi = new ProcessStartInfo - { - FileName = LastOutputFilePath, - UseShellExecute = true - }; - Process.Start(psi); - } - catch { } - } - - [RelayCommand] - public void OpenOutputFolder() - { - if (string.IsNullOrWhiteSpace(LastOutputFilePath)) return; - - try - { - string? dir = File.Exists(LastOutputFilePath) ? Path.GetDirectoryName(LastOutputFilePath) : LastOutputFilePath; - if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir)) - { - var psi = new ProcessStartInfo - { - FileName = dir, - UseShellExecute = true - }; - Process.Start(psi); - } - } - catch { } - } - - [RelayCommand] - public void StartOver() - { - ClearFiles(); - ResetState(); - } - - [RelayCommand] - public void ToggleStar() - { - IsToolStarred = !IsToolStarred; - } - - protected virtual bool ValidateInputs(out string errorMessage) - { - if (!HasSelectedFiles) - { - errorMessage = "Please select at least one document to proceed."; - return false; - } - errorMessage = string.Empty; - return true; - } - - /// - /// Runs the tool once per file in via the standard - /// PdfToolId dispatch, instead of silently processing only . - /// One failing file does not abort the rest; the aggregated result reports exactly - /// how many files succeeded/failed instead of a blanket success. - /// - protected async Task ExecuteBatchAsync( - Func buildOptions, - IProgress progress, - CancellationToken ct) - { - var files = SelectedFiles.ToList(); - var results = new List<(string File, ToolExecutionResult Result)>(); - - for (int i = 0; i < files.Count; i++) - { - ct.ThrowIfCancellationRequested(); - string file = files[i]; - int index = i; - int total = files.Count; - var fileProgress = new Progress(p => - progress.Report((index + Math.Clamp(p, 0, 100) / 100.0) / total * 100.0)); - - ToolExecutionResult result; - try - { - result = await OperationsService.ExecuteToolAsync(Tool.Id, buildOptions(file), fileProgress, ct); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - result = new ToolExecutionResult { Success = false, ErrorMessage = ex.Message }; - } - - results.Add((file, result)); - } - - progress.Report(100); - - // Single file: return the underlying result untouched so existing single-file - // behavior (messages, ExtraData, etc.) is unaffected. - if (results.Count == 1) - { - return results[0].Result; - } - - var succeeded = results.Where(r => r.Result.Success).ToList(); - var failed = results.Where(r => !r.Result.Success).ToList(); - string DescribeFailure((string File, ToolExecutionResult Result) f) => - $"{Path.GetFileName(f.File)} ({f.Result.ErrorMessage ?? "failed"})"; - - if (succeeded.Count == 0) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = $"All {results.Count} files failed: {string.Join("; ", failed.Select(DescribeFailure))}" - }; - } - - string message = failed.Count == 0 - ? $"Successfully processed {succeeded.Count} of {results.Count} files." - : $"Processed {succeeded.Count} of {results.Count} files. Failed: {string.Join("; ", failed.Select(DescribeFailure))}"; - - return new ToolExecutionResult - { - Success = true, - OutputFilePath = succeeded[^1].Result.OutputFilePath, - OutputFiles = succeeded - .Select(s => s.Result.OutputFilePath) - .Where(p => !string.IsNullOrEmpty(p)) - .Select(p => p!) - .ToList(), - Message = message, - OriginalSizeBytes = succeeded.Sum(s => s.Result.OriginalSizeBytes), - OutputSizeBytes = succeeded.Sum(s => s.Result.OutputSizeBytes) - }; - } - - protected abstract Task ExecuteCoreAsync(IProgress progress, CancellationToken ct); -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Intelligence/AiSummarizerToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Intelligence/AiSummarizerToolViewModel.cs deleted file mode 100644 index 1121daf..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Intelligence/AiSummarizerToolViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Intelligence; - -public partial class AiSummarizerToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private int _maxBulletPoints = 7; - - [ObservableProperty] - private bool _includeExecutiveSummary = true; - - [ObservableProperty] - private bool _includeActionItems = true; - - [ObservableProperty] - private string _targetLanguage = "English"; - - [ObservableProperty] - private string _customPrompt = string.Empty; - - public override bool UsesWorkspaceShell => true; - - public AiSummarizerToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - var options = new AiSummaryOptions - { - InputFilePath = PrimaryInputFile, - MaxBulletPoints = MaxBulletPoints, - IncludeExecutiveSummary = IncludeExecutiveSummary, - IncludeActionItems = IncludeActionItems, - TargetLanguage = TargetLanguage, - CustomPrompt = string.IsNullOrWhiteSpace(CustomPrompt) ? null : CustomPrompt - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.AiSummarizer, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Intelligence/ComparePdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Intelligence/ComparePdfToolViewModel.cs deleted file mode 100644 index 195bfbe..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Intelligence/ComparePdfToolViewModel.cs +++ /dev/null @@ -1,119 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Intelligence; - -public partial class ComparePdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private string _documentAPath = string.Empty; - - [ObservableProperty] - private string _documentBPath = string.Empty; - - [ObservableProperty] - private bool _detectTextDiff = true; - - [ObservableProperty] - private bool _detectVisualDiff = true; - - public override bool UsesWorkspaceShell => true; - - public ComparePdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - [RelayCommand] - public async Task PickDocumentAAsync() - { - if (StorageProvider == null) return; - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Select First PDF (Original Document A)", - AllowMultiple = false, - FileTypeFilter = new[] { new FilePickerFileType("PDF Document") { Patterns = new[] { "*.pdf" } } } - }); - - if (files != null && files.Count > 0) - { - DocumentAPath = files[0].Path.LocalPath; - if (!SelectedFiles.Contains(DocumentAPath)) - { - if (SelectedFiles.Count == 0) SelectedFiles.Add(DocumentAPath); - else SelectedFiles[0] = DocumentAPath; - } - ResetState(); - } - } - - [RelayCommand] - public async Task PickDocumentBAsync() - { - if (StorageProvider == null) return; - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Select Second PDF (Modified Document B)", - AllowMultiple = false, - FileTypeFilter = new[] { new FilePickerFileType("PDF Document") { Patterns = new[] { "*.pdf" } } } - }); - - if (files != null && files.Count > 0) - { - DocumentBPath = files[0].Path.LocalPath; - if (SelectedFiles.Count > 1) - { - SelectedFiles[1] = DocumentBPath; - } - else - { - SelectedFiles.Add(DocumentBPath); - } - ResetState(); - } - } - - public override void SetupInitialFiles(System.Collections.Generic.IEnumerable? filePaths) - { - base.SetupInitialFiles(filePaths); - if (SelectedFiles.Count > 0) DocumentAPath = SelectedFiles[0]; - if (SelectedFiles.Count > 1) DocumentBPath = SelectedFiles[1]; - } - - protected override bool ValidateInputs(out string errorMessage) - { - string docA = !string.IsNullOrEmpty(DocumentAPath) ? DocumentAPath : PrimaryInputFile; - string docB = !string.IsNullOrEmpty(DocumentBPath) ? DocumentBPath : (SelectedFiles.Count > 1 ? SelectedFiles[1] : ""); - - if (string.IsNullOrEmpty(docA) || string.IsNullOrEmpty(docB)) - { - errorMessage = "Please select both Document A and Document B to compare."; - return false; - } - errorMessage = string.Empty; - return true; - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - string docA = !string.IsNullOrEmpty(DocumentAPath) ? DocumentAPath : PrimaryInputFile; - string docB = !string.IsNullOrEmpty(DocumentBPath) ? DocumentBPath : (SelectedFiles.Count > 1 ? SelectedFiles[1] : ""); - - var options = new CompareToolOptions - { - DocumentAPath = docA, - DocumentBPath = docB, - DetectTextDiff = DetectTextDiff, - DetectVisualDiff = DetectVisualDiff - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.ComparePdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Intelligence/EditPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Intelligence/EditPdfToolViewModel.cs deleted file mode 100644 index ad10dde..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Intelligence/EditPdfToolViewModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Intelligence; - -public partial class EditPdfToolViewModel : PdfToolViewModelBase -{ - public EditPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - [RelayCommand] - public void LaunchEditor() - { - OpenInEditor(); - } - - protected override Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - progress.Report(50); - OpenInEditor(); - progress.Report(100); - - return Task.FromResult(new ToolExecutionResult - { - Success = true, - OutputFilePath = PrimaryInputFile, - Message = "Document opened in FryPDF Editor." - }); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Intelligence/OcrPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Intelligence/OcrPdfToolViewModel.cs deleted file mode 100644 index 42771ce..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Intelligence/OcrPdfToolViewModel.cs +++ /dev/null @@ -1,434 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Input.Platform; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Ocr; - -namespace PdfEditorApp.ViewModels.Tools.Intelligence; - -public partial class OcrPdfToolViewModel : PdfToolViewModelBase -{ - private readonly ICompositeOcrProvider _ocrProvider; - private readonly Dictionary _pageTextCache = new(); - private CancellationTokenSource? _pageExtractCts; - - [ObservableProperty] - private string _language = "eng"; - - [ObservableProperty] - private bool _generateSearchablePdf = true; - - [ObservableProperty] - private bool _generateTextFile = true; - - [ObservableProperty] - private bool _extractTextOnly; - - [ObservableProperty] - private OcrEngineType _selectedEngine = OcrEngineType.Auto; - - [ObservableProperty] - private string _activeEngineDescription = string.Empty; - - [ObservableProperty] - private bool _isDownloadingModel; - - [ObservableProperty] - private double _downloadProgress; - - [ObservableProperty] - private string _downloadStatusText = string.Empty; - - [ObservableProperty] - private bool _isSideBySideVisible = true; - - [ObservableProperty] - private string _currentPageExtractedText = string.Empty; - - [ObservableProperty] - private string _fullDocumentExtractedText = string.Empty; - - [ObservableProperty] - private bool _showFullDocumentText; - - [ObservableProperty] - private bool _isExtractingPageText; - - [ObservableProperty] - private string _wordCountText = "0 words"; - - [ObservableProperty] - private string _charCountText = "0 chars"; - - [ObservableProperty] - private string _copyStatusMessage = string.Empty; - - [ObservableProperty] - private string? _lastGeneratedTextFilePath; - - public bool HasGeneratedTextFile => !string.IsNullOrEmpty(LastGeneratedTextFilePath) && File.Exists(LastGeneratedTextFilePath); - - public string ViewModeButtonText => ShowFullDocumentText ? "Doc View" : "Page View"; - - public string ActiveDisplayedText - { - get => ShowFullDocumentText ? FullDocumentExtractedText : CurrentPageExtractedText; - set - { - if (ShowFullDocumentText) - { - FullDocumentExtractedText = value; - } - else - { - CurrentPageExtractedText = value; - if (Preview.SelectedPage != null) - { - _pageTextCache[Preview.SelectedPage.PageNumber] = value; - } - } - UpdateStats(); - } - } - - public ObservableCollection Languages { get; } = new(); - - public override bool UsesWorkspaceShell => true; - - public OcrPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool, ICompositeOcrProvider? ocrProvider = null) - : base(operationsService, tool) - { - _ocrProvider = ocrProvider ?? CompositeOcrProvider.Default; - UpdateActiveEngineDescription(); - LoadLanguages(); - - Preview.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(Preview.SelectedPage) && Preview.SelectedPage != null) - { - _ = ExtractTextForPageAsync(Preview.SelectedPage.PageNumber); - } - }; - } - - partial void OnSelectedEngineChanged(OcrEngineType value) - { - _ocrProvider.PreferredEngine = value; - UpdateActiveEngineDescription(); - } - - partial void OnCurrentPageExtractedTextChanged(string value) - { - OnPropertyChanged(nameof(ActiveDisplayedText)); - UpdateStats(); - } - - partial void OnFullDocumentExtractedTextChanged(string value) - { - OnPropertyChanged(nameof(ActiveDisplayedText)); - UpdateStats(); - } - - partial void OnShowFullDocumentTextChanged(bool value) - { - OnPropertyChanged(nameof(ActiveDisplayedText)); - OnPropertyChanged(nameof(ViewModeButtonText)); - UpdateStats(); - } - - partial void OnLastGeneratedTextFilePathChanged(string? value) - { - OnPropertyChanged(nameof(HasGeneratedTextFile)); - } - - private void UpdateActiveEngineDescription() - { - ActiveEngineDescription = _ocrProvider.ActiveEngine.EngineName; - } - - private void LoadLanguages() - { - Languages.Clear(); - foreach (var lang in _ocrProvider.ModelService.AvailableLanguages) - { - Languages.Add(lang); - } - } - - [RelayCommand] - public void ToggleViewMode() - { - ShowFullDocumentText = !ShowFullDocumentText; - } - - [RelayCommand] - public void ToggleSideBySide() - { - IsSideBySideVisible = !IsSideBySideVisible; - } - - [RelayCommand] - public async Task ExtractCurrentPageTextAsync() - { - if (Preview.SelectedPage != null) - { - _pageTextCache.Remove(Preview.SelectedPage.PageNumber); - await ExtractTextForPageAsync(Preview.SelectedPage.PageNumber); - } - } - - private int _currentlyExtractingPage = -1; - - public async Task ExtractTextForPageAsync(int pageNumber) - { - if (SelectedFiles.Count == 0 || !File.Exists(SelectedFiles[0])) return; - - if (_pageTextCache.TryGetValue(pageNumber, out var cachedText)) - { - CurrentPageExtractedText = cachedText; - return; - } - - if (_currentlyExtractingPage == pageNumber && IsExtractingPageText) - { - return; - } - - _pageExtractCts?.Cancel(); - _pageExtractCts = new CancellationTokenSource(); - var ct = _pageExtractCts.Token; - _currentlyExtractingPage = pageNumber; - - IsExtractingPageText = true; - try - { - string filePath = SelectedFiles[0]; - string recognizedText = await Task.Run(() => - { - using var pdfPigDoc = UglyToad.PdfPig.PdfDocument.Open(filePath); - if (pageNumber < 1 || pageNumber > pdfPigDoc.NumberOfPages) return string.Empty; - - var page = pdfPigDoc.GetPage(pageNumber); - var vectorWords = page.GetWords().ToList(); - if (vectorWords.Count > 0) - { - return page.Text; - } - - // Scanned page: render image and run OCR - try { UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.AddSkiaPageFactory(pdfPigDoc); } catch { } - using var pngStream = UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.GetPageAsPng(pdfPigDoc, pageNumber, 1.5f, 100); - if (pngStream == null || pngStream.Length == 0) return string.Empty; - - var ocrRes = _ocrProvider.RecognizeTextAsync(pngStream.ToArray(), Language, ct).GetAwaiter().GetResult(); - return ocrRes.Success ? ocrRes.FullText : string.Empty; - }, ct); - - if (!ct.IsCancellationRequested) - { - _pageTextCache[pageNumber] = recognizedText; - CurrentPageExtractedText = recognizedText; - } - } - catch (OperationCanceledException) { } - catch (Exception ex) - { - CurrentPageExtractedText = $"[Error extracting page {pageNumber}: {ex.Message}]"; - } - finally - { - if (_currentlyExtractingPage == pageNumber) - { - _currentlyExtractingPage = -1; - IsExtractingPageText = false; - } - } - } - - private void UpdateStats() - { - string text = ActiveDisplayedText; - if (string.IsNullOrWhiteSpace(text)) - { - WordCountText = "0 words"; - CharCountText = "0 chars"; - return; - } - - int words = text.Split(new[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries).Length; - WordCountText = $"{words:N0} words"; - CharCountText = $"{text.Length:N0} chars"; - } - - [RelayCommand] - public async Task CopyCurrentTextAsync() - { - string text = ActiveDisplayedText; - if (string.IsNullOrEmpty(text)) return; - - if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard != null) - { - await desktop.MainWindow.Clipboard.SetTextAsync(text); - } - - CopyStatusMessage = "Copied to clipboard!"; - await Task.Delay(2000); - CopyStatusMessage = string.Empty; - } - - [RelayCommand] - public async Task ExportTextFileAsync() - { - string text = ActiveDisplayedText; - if (string.IsNullOrWhiteSpace(text) && Preview.SelectedPage != null) - { - await ExtractTextForPageAsync(Preview.SelectedPage.PageNumber); - text = ActiveDisplayedText; - } - - if (string.IsNullOrWhiteSpace(text)) return; - - if (StorageProvider != null) - { - string defaultName = SelectedFiles.Count > 0 - ? $"{Path.GetFileNameWithoutExtension(SelectedFiles[0])}_extracted.txt" - : "Extracted_Text.txt"; - - var file = await StorageProvider.SaveFilePickerAsync(new Avalonia.Platform.Storage.FilePickerSaveOptions - { - Title = "Save Extracted Text File", - SuggestedFileName = defaultName, - DefaultExtension = "txt", - FileTypeChoices = new[] - { - new Avalonia.Platform.Storage.FilePickerFileType("Text Document (*.txt)") - { - Patterns = new[] { "*.txt" } - } - } - }); - - if (file != null) - { - await File.WriteAllTextAsync(file.Path.LocalPath, text, Encoding.UTF8); - LastGeneratedTextFilePath = file.Path.LocalPath; - CopyStatusMessage = $"Saved to {Path.GetFileName(file.Path.LocalPath)}!"; - await Task.Delay(2500); - CopyStatusMessage = string.Empty; - } - } - } - - [RelayCommand] - public void OpenGeneratedTextFile() - { - if (!string.IsNullOrEmpty(LastGeneratedTextFilePath) && File.Exists(LastGeneratedTextFilePath)) - { - try - { - Process.Start(new ProcessStartInfo - { - FileName = LastGeneratedTextFilePath, - UseShellExecute = true - }); - } - catch { } - } - } - - [RelayCommand] - public async Task DownloadSelectedLanguageAsync() - { - if (IsDownloadingModel) return; - - IsDownloadingModel = true; - DownloadProgress = 0.0; - DownloadStatusText = $"Starting download for {Language}..."; - - var progress = new Progress(p => DownloadProgress = p * 100); - - try - { - bool ok = await _ocrProvider.ModelService.DownloadLanguageAsync( - Language, - progress, - status => DownloadStatusText = status, - CancellationToken.None); - - LoadLanguages(); - DownloadStatusText = ok ? "Language model ready!" : "Failed to download model."; - } - catch (Exception ex) - { - DownloadStatusText = $"Download error: {ex.Message}"; - } - finally - { - IsDownloadingModel = false; - } - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - _ocrProvider.PreferredEngine = SelectedEngine; - var result = await ExecuteBatchAsync(file => new OcrToolOptions - { - InputFilePath = file, - Language = Language, - GenerateSearchablePdf = GenerateSearchablePdf, - GenerateTextFile = GenerateTextFile, - ExtractTextOnly = ExtractTextOnly - }, progress, ct); - - // Find the generated text file in result.OutputFiles if any - if (result.OutputFiles != null) - { - var txt = result.OutputFiles.FirstOrDefault(f => f.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)); - if (txt != null && File.Exists(txt)) - { - LastGeneratedTextFilePath = txt; - FullDocumentExtractedText = await File.ReadAllTextAsync(txt, ct); - ShowFullDocumentText = true; - } - } - - OnPropertyChanged(nameof(HasSearchablePdfOutput)); - return result; - } - - public bool HasSearchablePdfOutput => !string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath) && LastOutputFilePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase); - - [RelayCommand] - public async Task PreviewTextFileAsync() - { - if (!string.IsNullOrEmpty(LastGeneratedTextFilePath) && File.Exists(LastGeneratedTextFilePath)) - { - await Preview.LoadDocumentAsync(LastGeneratedTextFilePath); - } - else if (!string.IsNullOrEmpty(FullDocumentExtractedText)) - { - Preview.LoadTextContent(FullDocumentExtractedText, "Extracted_Text.txt"); - } - } - - [RelayCommand] - public async Task PreviewSearchablePdfAsync() - { - if (!string.IsNullOrEmpty(LastOutputFilePath) && File.Exists(LastOutputFilePath) && LastOutputFilePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) - { - await Preview.LoadDocumentAsync(LastOutputFilePath); - } - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Intelligence/PdfFormsToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Intelligence/PdfFormsToolViewModel.cs deleted file mode 100644 index 87c8c1d..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Intelligence/PdfFormsToolViewModel.cs +++ /dev/null @@ -1,37 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Intelligence; - -public partial class PdfFormsToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private bool _flattenFields; - - [ObservableProperty] - private bool _exportFieldValuesJson; - - public override bool UsesWorkspaceShell => true; - - public PdfFormsToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - var options = new FormToolOptions - { - InputFilePath = PrimaryInputFile, - FlattenFields = FlattenFields, - ExportFieldValuesJson = ExportFieldValuesJson - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.PdfForms, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Intelligence/TranslatePdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Intelligence/TranslatePdfToolViewModel.cs deleted file mode 100644 index 00e4f80..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Intelligence/TranslatePdfToolViewModel.cs +++ /dev/null @@ -1,41 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Intelligence; - -public partial class TranslatePdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private string _sourceLanguage = "Auto"; - - [ObservableProperty] - private string _targetLanguage = "Spanish"; - - [ObservableProperty] - private bool _preserveLayout = true; - - public override bool UsesWorkspaceShell => true; - - public TranslatePdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - var options = new TranslationOptions - { - InputFilePath = PrimaryInputFile, - SourceLanguage = SourceLanguage, - TargetLanguage = TargetLanguage, - PreserveLayout = PreserveLayout - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.TranslatePdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Organize/CropPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Organize/CropPdfToolViewModel.cs deleted file mode 100644 index 28524f8..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Organize/CropPdfToolViewModel.cs +++ /dev/null @@ -1,51 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Organize; - -public partial class CropPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private double _cropMarginTop = 36; - - [ObservableProperty] - private double _cropMarginBottom = 36; - - [ObservableProperty] - private double _cropMarginLeft = 36; - - [ObservableProperty] - private double _cropMarginRight = 36; - - [ObservableProperty] - private PageTargetSelection _targetPages = PageTargetSelection.AllPages; - - [ObservableProperty] - private string _customRange = string.Empty; - - public override bool UsesWorkspaceShell => true; - - public CropPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new CropToolOptions - { - InputFilePath = file, - CropTopPoints = CropMarginTop, - CropBottomPoints = CropMarginBottom, - CropLeftPoints = CropMarginLeft, - CropRightPoints = CropMarginRight, - TargetPages = TargetPages, - CustomRange = CustomRange - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Organize/MergePdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Organize/MergePdfToolViewModel.cs deleted file mode 100644 index 1aea237..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Organize/MergePdfToolViewModel.cs +++ /dev/null @@ -1,75 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Organize; - -public partial class MergePdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private bool _normalizePageSizes; - - [ObservableProperty] - private bool _preserveBookmarks = true; - - [ObservableProperty] - private PdfProcessingEngine _processingEngine = PdfProcessingEngine.StandardPdfSharp; - - public bool IsQuestPdfEngine - { - get => ProcessingEngine == PdfProcessingEngine.QuestPdfNative; - set - { - if (value) ProcessingEngine = PdfProcessingEngine.QuestPdfNative; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsStandardEngine)); - } - } - - public bool IsStandardEngine - { - get => ProcessingEngine == PdfProcessingEngine.StandardPdfSharp; - set - { - if (value) ProcessingEngine = PdfProcessingEngine.StandardPdfSharp; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsQuestPdfEngine)); - } - } - - public override bool UsesWorkspaceShell => true; - - public MergePdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override bool ValidateInputs(out string errorMessage) - { - if (SelectedFiles.Count < 2) - { - errorMessage = "Please select at least 2 PDF documents to merge."; - return false; - } - errorMessage = string.Empty; - return true; - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - var options = new MergeToolOptions - { - InputFiles = SelectedFiles.ToList(), - NormalizePageSizes = NormalizePageSizes, - PreserveBookmarks = PreserveBookmarks, - Engine = ProcessingEngine - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.MergePdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Organize/OrganizePdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Organize/OrganizePdfToolViewModel.cs deleted file mode 100644 index 7f88f29..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Organize/OrganizePdfToolViewModel.cs +++ /dev/null @@ -1,50 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Organize; - -public partial class OrganizePdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private List _pageOrder = new(); - - [ObservableProperty] - private List _pagesToDelete = new(); - - public override bool UsesWorkspaceShell => true; - - public OrganizePdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - // This standalone view has no reorder/delete/rotate controls yet, so PageOrder and - // PagesToDelete can never be populated here — calling through would silently save an - // unchanged copy while claiming success. Say so honestly instead of faking a result. - if (PageOrder.Count == 0 && PagesToDelete.Count == 0) - { - return new ToolExecutionResult - { - Success = false, - ErrorMessage = "This standalone tool doesn't have page reorder/delete/rotate controls yet, so there's nothing to apply. Use the FryPDF Pages Sidebar in the Editor for visual drag-and-drop reordering, rotation, and deletion." - }; - } - - var options = new OrganizeToolOptions - { - InputFilePath = PrimaryInputFile, - PageOrder = PageOrder, - PagesToDelete = PagesToDelete - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.OrganizePdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Organize/PageNumbersToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Organize/PageNumbersToolViewModel.cs deleted file mode 100644 index 6794579..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Organize/PageNumbersToolViewModel.cs +++ /dev/null @@ -1,63 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Organize; - -public partial class PageNumbersToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private PageNumberPosition _position = PageNumberPosition.BottomCenter; - - [ObservableProperty] - private string _template = "Page {n} of {total}"; - - [ObservableProperty] - private int _startingNumber = 1; - - [ObservableProperty] - private string _fontFamily = "Helvetica"; - - [ObservableProperty] - private double _fontSize = 10; - - [ObservableProperty] - private string _colorHex = "#334155"; - - [ObservableProperty] - private PageTargetSelection _targetPages = PageTargetSelection.AllPages; - - [ObservableProperty] - private string _customRange = string.Empty; - - [ObservableProperty] - private double _marginPoints = 28; - - public override bool UsesWorkspaceShell => true; - - public PageNumbersToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new PageNumberToolOptions - { - InputFilePath = file, - Position = Position, - Template = Template, - StartingNumber = StartingNumber, - FontFamily = FontFamily, - FontSize = FontSize, - ColorHex = ColorHex, - TargetPages = TargetPages, - CustomRange = CustomRange, - MarginPoints = MarginPoints - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Organize/RotatePdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Organize/RotatePdfToolViewModel.cs deleted file mode 100644 index 8b3bbf4..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Organize/RotatePdfToolViewModel.cs +++ /dev/null @@ -1,39 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Organize; - -public partial class RotatePdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private int _rotationDegrees = 90; - - [ObservableProperty] - private PageFilterTarget _targetFilter = PageFilterTarget.All; - - [ObservableProperty] - private string _customRange = string.Empty; - - public override bool UsesWorkspaceShell => true; - - public RotatePdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new RotateToolOptions - { - InputFilePath = file, - RotationDegrees = RotationDegrees, - TargetFilter = TargetFilter, - CustomRange = CustomRange - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Organize/SplitPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Organize/SplitPdfToolViewModel.cs deleted file mode 100644 index b9281e7..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Organize/SplitPdfToolViewModel.cs +++ /dev/null @@ -1,50 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Organize; - -public partial class SplitPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private SplitExtractMode _splitMode = SplitExtractMode.SplitEveryNPages; - - [ObservableProperty] - private int _splitPagesInterval = 1; - - [ObservableProperty] - private string _splitRangeExpression = "1-3, 5, 7-10"; - - [ObservableProperty] - private bool _splitOddEven; - - [ObservableProperty] - private bool _extractOddPages; - - public override bool UsesWorkspaceShell => true; - - public SplitPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - var options = new SplitToolOptions - { - InputFilePath = PrimaryInputFile, - Mode = SplitMode, - PagesPerSplit = SplitPagesInterval, - RangeExpression = SplitRangeExpression, - SplitOddEven = SplitOddEven, - ExtractOddPages = ExtractOddPages - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.SplitPdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Security/CompressPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Security/CompressPdfToolViewModel.cs deleted file mode 100644 index 8755f73..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Security/CompressPdfToolViewModel.cs +++ /dev/null @@ -1,205 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Security; - -public partial class CompressPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private PdfCompressionLevel _compressionLevel = PdfCompressionLevel.Balanced; - - [ObservableProperty] - private int _imageQualityDpi = 150; - - [ObservableProperty] - private int _jpegQuality = 66; - - [ObservableProperty] - private bool _convertToGrayscale; - - [ObservableProperty] - private bool _isCustomMode; - - [ObservableProperty] - private bool _removeMetadata; - - [ObservableProperty] - private bool _removeDuplicateObjects = true; - - [ObservableProperty] - private bool _compressStreams = true; - - [ObservableProperty] - private bool _linearizeForWeb; - - [ObservableProperty] - private string _qualityFeedbackMessage = "✅ Balanced clarity & high compression (~70% reduction). Recommended for general use."; - - [ObservableProperty] - private string _qualityFeedbackSeverity = "Success"; // "Success", "Warning", "Info" - - public override bool UsesWorkspaceShell => true; - - public bool IsExtremeCompression => !IsCustomMode && CompressionLevel == PdfCompressionLevel.MaximumCompression; - public bool IsRecommendedCompression => !IsCustomMode && (CompressionLevel == PdfCompressionLevel.Balanced || CompressionLevel == PdfCompressionLevel.SmallSize); - public bool IsLessCompression => !IsCustomMode && (CompressionLevel == PdfCompressionLevel.HighQuality || CompressionLevel == PdfCompressionLevel.MaximumQuality); - - public CompressPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - UpdateQualityFeedback(); - } - - [RelayCommand] - public void SelectPreset(string preset) - { - IsCustomMode = false; - switch (preset.ToLowerInvariant()) - { - case "extreme": - case "maximum": - CompressionLevel = PdfCompressionLevel.MaximumCompression; - ImageQualityDpi = 96; - JpegQuality = 52; - break; - - case "recommended": - case "balanced": - CompressionLevel = PdfCompressionLevel.Balanced; - ImageQualityDpi = 125; - JpegQuality = 66; - break; - - case "less": - case "high": - CompressionLevel = PdfCompressionLevel.HighQuality; - ImageQualityDpi = 180; - JpegQuality = 82; - break; - - case "custom": - IsCustomMode = true; - break; - } - - RefreshPresetStates(); - UpdateQualityFeedback(); - } - - [RelayCommand] - public void ToggleCustomMode() - { - IsCustomMode = !IsCustomMode; - RefreshPresetStates(); - UpdateQualityFeedback(); - } - - partial void OnImageQualityDpiChanged(int value) - { - // Safe input clamp - int clamped = Math.Clamp(value, 50, 600); - if (clamped != value) - { - ImageQualityDpi = clamped; - return; - } - UpdateQualityFeedback(); - } - - partial void OnJpegQualityChanged(int value) - { - // Safe input clamp - int clamped = Math.Clamp(value, 20, 100); - if (clamped != value) - { - JpegQuality = clamped; - return; - } - UpdateQualityFeedback(); - } - - partial void OnConvertToGrayscaleChanged(bool value) - { - UpdateQualityFeedback(); - } - - partial void OnCompressionLevelChanged(PdfCompressionLevel value) - { - RefreshPresetStates(); - UpdateQualityFeedback(); - } - - partial void OnIsCustomModeChanged(bool value) - { - RefreshPresetStates(); - UpdateQualityFeedback(); - } - - private void RefreshPresetStates() - { - OnPropertyChanged(nameof(IsExtremeCompression)); - OnPropertyChanged(nameof(IsRecommendedCompression)); - OnPropertyChanged(nameof(IsLessCompression)); - } - - private void UpdateQualityFeedback() - { - if (ImageQualityDpi < 72) - { - QualityFeedbackSeverity = "Warning"; - QualityFeedbackMessage = "⚠️ Low DPI (<72 DPI): Scanned text or fine lines may become blurry and difficult to read."; - } - else if (JpegQuality < 45) - { - QualityFeedbackSeverity = "Warning"; - QualityFeedbackMessage = "⚠️ Low JPEG Quality (<45%): Visible compression artifacts, grain, and blockiness will appear around text."; - } - else if (ImageQualityDpi >= 300 && JpegQuality >= 90) - { - QualityFeedbackSeverity = "Info"; - QualityFeedbackMessage = "ℹ️ Ultra-high print resolution selected: Original fidelity is pristine, but file size reduction will be minimal (<25%)."; - } - else if (ConvertToGrayscale) - { - QualityFeedbackSeverity = "Success"; - QualityFeedbackMessage = "✨ Grayscale Mode: Color channels removed. Saves an extra ~20-30% on scanned forms, receipts, and invoices."; - } - else if (IsExtremeCompression || (ImageQualityDpi <= 100 && JpegQuality <= 55)) - { - QualityFeedbackSeverity = "Success"; - QualityFeedbackMessage = "⚡ Maximum Compression (~75–85% reduction): Highly compact output, ideal for email and strict upload limits."; - } - else if (IsLessCompression || (ImageQualityDpi >= 180 && JpegQuality >= 80)) - { - QualityFeedbackSeverity = "Info"; - QualityFeedbackMessage = "💎 High Fidelity Compression (~35–50% reduction): High clarity with moderate file size savings."; - } - else - { - QualityFeedbackSeverity = "Success"; - QualityFeedbackMessage = "✅ Balanced Compression (~65–75% reduction): Great visual quality and substantial file size savings."; - } - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new CompressToolOptions - { - InputFilePath = file, - Level = CompressionLevel, - ImageQualityDpi = Math.Clamp(ImageQualityDpi, 50, 600), - JpegQuality = Math.Clamp(JpegQuality, 20, 100), - ConvertToGrayscale = ConvertToGrayscale, - RemoveMetadata = RemoveMetadata, - RemoveDuplicateObjects = RemoveDuplicateObjects, - CompressStreams = CompressStreams, - LinearizeForWeb = LinearizeForWeb - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Security/ProtectPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Security/ProtectPdfToolViewModel.cs deleted file mode 100644 index f459f73..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Security/ProtectPdfToolViewModel.cs +++ /dev/null @@ -1,127 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Security; - -public partial class ProtectPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private string _userPassword = string.Empty; - - [ObservableProperty] - private string _ownerPassword = string.Empty; - - [ObservableProperty] - private bool _allowPrinting = true; - - [ObservableProperty] - private bool _allowModifying; - - [ObservableProperty] - private bool _allowCopying; - - [ObservableProperty] - private bool _allowAnnotating = true; - - [ObservableProperty] - private bool _allowFormFilling = true; - - [ObservableProperty] - private bool _allowAssembly = true; - - [ObservableProperty] - private bool _encryptMetadata = true; - - [ObservableProperty] - private PdfProcessingEngine _processingEngine = PdfProcessingEngine.StandardPdfSharp; - - [ObservableProperty] - private PdfEncryptionLevel _encryptionLevel = PdfEncryptionLevel.Aes256Bit; - - public bool IsQuestPdfEngine - { - get => ProcessingEngine == PdfProcessingEngine.QuestPdfNative; - set - { - if (value) ProcessingEngine = PdfProcessingEngine.QuestPdfNative; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsStandardEngine)); - } - } - - public bool IsStandardEngine - { - get => ProcessingEngine == PdfProcessingEngine.StandardPdfSharp; - set - { - if (value) ProcessingEngine = PdfProcessingEngine.StandardPdfSharp; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsQuestPdfEngine)); - } - } - - public bool IsAes256 - { - get => EncryptionLevel == PdfEncryptionLevel.Aes256Bit; - set - { - if (value) EncryptionLevel = PdfEncryptionLevel.Aes256Bit; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsAes128)); - } - } - - public bool IsAes128 - { - get => EncryptionLevel == PdfEncryptionLevel.Aes128Bit; - set - { - if (value) EncryptionLevel = PdfEncryptionLevel.Aes128Bit; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsAes256)); - } - } - - public override bool UsesWorkspaceShell => true; - - public ProtectPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override bool ValidateInputs(out string errorMessage) - { - if (!base.ValidateInputs(out errorMessage)) return false; - - if (string.IsNullOrEmpty(UserPassword) && string.IsNullOrEmpty(OwnerPassword)) - { - errorMessage = "Please enter a user or owner password to encrypt the document."; - return false; - } - return true; - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new SecurityToolOptions - { - InputFilePath = file, - UserPassword = UserPassword, - OwnerPassword = OwnerPassword, - AllowPrinting = AllowPrinting, - AllowModifying = AllowModifying, - AllowCopying = AllowCopying, - AllowAnnotating = AllowAnnotating, - AllowFormFilling = AllowFormFilling, - AllowAssembly = AllowAssembly, - EncryptMetadata = EncryptMetadata, - Engine = ProcessingEngine, - EncryptionLevel = EncryptionLevel - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Security/RedactPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Security/RedactPdfToolViewModel.cs deleted file mode 100644 index c1465cf..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Security/RedactPdfToolViewModel.cs +++ /dev/null @@ -1,344 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.ViewModels.Tools.Security; - -public partial class RedactPdfToolViewModel : PdfToolViewModelBase -{ - private List _currentPageWords = new(); - - [ObservableProperty] - private string _searchPattern = "CONFIDENTIAL"; - - [ObservableProperty] - private bool _caseSensitive; - - [ObservableProperty] - private bool _permanentScrubText = true; - - [ObservableProperty] - private bool _isSearching; - - [ObservableProperty] - private string _searchStatusMessage = string.Empty; - - public ObservableCollection Marks { get; } = new(); - - public ObservableCollection CurrentPageMarks { get; } = new(); - - public override bool UsesWorkspaceShell => true; - - public bool HasMarks => Marks.Count > 0; - public bool HasSearchStatusMessage => !string.IsNullOrEmpty(SearchStatusMessage); - - /// - /// Increments each time word geometry finishes refreshing for the current page. - /// Word extraction runs independently of Preview's own page rendering (both react - /// to the same file/page-change signals but complete on their own schedule), so - /// this is the only reliable way to know marking is ready to snap to text — there's - /// no user-visible "loading words" state to show instead. - /// - public int WordsRefreshedCount { get; private set; } - - partial void OnSearchStatusMessageChanged(string value) => OnPropertyChanged(nameof(HasSearchStatusMessage)); - - public RedactPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - Marks.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasMarks)); - - // Rendering/zoom/page-nav now lives entirely in the shared Preview (from - // PdfToolViewModelBase), which reloads itself on SelectedFiles changes. This - // handler only clears the redaction-specific state that Preview doesn't know - // about. - SelectedFiles.CollectionChanged += (_, _) => - { - Marks.Clear(); - CurrentPageMarks.Clear(); - SearchStatusMessage = string.Empty; - }; - - // Marks are positioned/scaled relative to the current page and zoom, and - // word-snap marking needs the current page's word geometry — both need to be - // recomputed whenever Preview's document, page, or zoom changes. - Preview.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(Preview.IsLoading) && !Preview.IsLoading && Preview.HasDocument) - { - _ = RefreshCurrentPageWordsAsync(); - } - else if (e.PropertyName == nameof(Preview.CurrentPageNumber)) - { - _ = RefreshCurrentPageWordsAsync(); - } - else if (e.PropertyName == nameof(Preview.ZoomLevel)) - { - RecomputeCurrentPageMarks(); - } - }; - } - - /// - /// Re-extracts word geometry for whatever page Preview is currently showing, reusing - /// the PDF Reader's own extraction () - /// rather than duplicating it. Independent of Preview's own bitmap rendering — this - /// only needs the page's text layer, not a rasterized image. - /// - private async Task RefreshCurrentPageWordsAsync() - { - string path = PrimaryInputFile; - int pageNumber = Preview.CurrentPageNumber; - - if (string.IsNullOrEmpty(path) || !File.Exists(path) || pageNumber < 1) - { - _currentPageWords = new List(); - RecomputeCurrentPageMarks(); - return; - } - - var words = await Task.Run(() => - { - try - { - byte[] bytes = File.ReadAllBytes(path); - UglyToad.PdfPig.PdfDocument? doc = null; - try - { - doc = UglyToad.PdfPig.PdfDocument.Open(bytes); - } - catch - { - try - { - byte[] repaired = PdfFileHelper.SalvageAndRepairPdfBytes(bytes); - doc = UglyToad.PdfPig.PdfDocument.Open(repaired); - } - catch - { - return new List(); - } - } - - using (doc) - { - if (doc == null || pageNumber > doc.NumberOfPages) return new List(); - - var page = doc.GetPage(pageNumber); - var (_, extractedWords, _) = PdfViewerViewModel.ExtractPageTextGeometry(page); - return extractedWords; - } - } - catch - { - return new List(); - } - }); - - if (path != PrimaryInputFile || pageNumber != Preview.CurrentPageNumber) return; - - _currentPageWords = words; - WordsRefreshedCount++; - RecomputeCurrentPageMarks(); - } - - private void RecomputeCurrentPageMarks() - { - CurrentPageMarks.Clear(); - - double pageWidthPoints = Preview.SelectedPage?.WidthPoints ?? 0; - if (pageWidthPoints <= 0) return; - - // Preview renders each page at WidthPoints * ZoomLevel pixels (same convention as - // the PDF Reader), so ZoomLevel alone is the points-to-pixels scale factor. - double scale = Preview.ZoomLevel; - foreach (var mark in Marks.Where(m => m.Region.PageIndex == Preview.CurrentPageNumber - 1)) - { - mark.DisplayX = mark.Region.X * scale; - mark.DisplayY = mark.Region.Y * scale; - mark.DisplayWidth = Math.Max(2, mark.Region.Width * scale); - mark.DisplayHeight = Math.Max(2, mark.Region.Height * scale); - CurrentPageMarks.Add(mark); - } - } - - [RelayCommand] - private async Task FindAndMarkAsync() - { - if (string.IsNullOrWhiteSpace(SearchPattern) || string.IsNullOrEmpty(PrimaryInputFile)) return; - - IsSearching = true; - try - { - var matches = await OperationsService.SecurityService.FindRedactionMatchesAsync(PrimaryInputFile, SearchPattern, CaseSensitive); - - if (matches.Count == 0) - { - SearchStatusMessage = $"No matches found for \"{SearchPattern}\"."; - return; - } - - int added = 0; - foreach (var region in matches) - { - bool alreadyMarked = Marks.Any(m => - m.Region.PageIndex == region.PageIndex && - Math.Abs(m.Region.X - region.X) < 0.5 && - Math.Abs(m.Region.Y - region.Y) < 0.5); - if (alreadyMarked) continue; - - Marks.Add(new RedactionMarkItem { Region = region, Label = SearchPattern }); - added++; - } - - SearchStatusMessage = added > 0 - ? $"Marked {added} new match(es) for \"{SearchPattern}\" ({matches.Count} found total)." - : $"All {matches.Count} match(es) for \"{SearchPattern}\" were already marked."; - - RecomputeCurrentPageMarks(); - } - finally - { - IsSearching = false; - } - } - - [RelayCommand] - private void RemoveMark(RedactionMarkItem? item) - { - if (item == null) return; - Marks.Remove(item); - CurrentPageMarks.Remove(item); - } - - [RelayCommand] - private void ClearMarks() - { - Marks.Clear(); - CurrentPageMarks.Clear(); - SearchStatusMessage = string.Empty; - } - - /// - /// Adds a manual mark for the given on-screen rectangle. Snaps to touched text by default - /// (matching the PDF Reader's selection UX); holding Alt/Option bypasses text snap and - /// draws a raw box for non-text areas. If no text words are touched, it gracefully falls - /// back to a raw box so the user's selection is never dropped. - /// - public void AddManualMark(Rect displayRect, bool forceDrawBox = false) - { - double pageWidthPoints = Preview.SelectedPage?.WidthPoints ?? 0; - if (pageWidthPoints <= 0 || displayRect.Width < 2 || displayRect.Height < 2) return; - - double scale = Preview.ZoomLevel > 0 ? Preview.ZoomLevel : 1.0; - double pdfX = displayRect.X / scale; - double pdfY = displayRect.Y / scale; - double pdfWidth = Math.Max(1, displayRect.Width / scale); - double pdfHeight = Math.Max(1, displayRect.Height / scale); - int pageIndex = Preview.CurrentPageNumber - 1; - - RedactionRegion region; - string label; - - if (!forceDrawBox) - { - // Use generous vertical tolerance (±8pt) so horizontal text drags comfortably catch words - var searchRect = new Rect(pdfX, Math.Max(0, pdfY - 8), pdfWidth, pdfHeight + 16); - var touched = _currentPageWords.Where(w => w.Bounds.Intersects(searchRect)).ToList(); - - if (touched.Count > 0) - { - double left = touched.Min(w => w.Bounds.Left); - double top = touched.Min(w => w.Bounds.Top); - double right = touched.Max(w => w.Bounds.Right); - double bottom = touched.Max(w => w.Bounds.Bottom); - - region = new RedactionRegion - { - PageIndex = pageIndex, - X = left, - Y = top, - Width = Math.Max(1, right - left), - Height = Math.Max(1, bottom - top), - Reason = "Manual selection" - }; - - label = string.Join(" ", touched.OrderBy(w => w.Bounds.Left).Select(w => w.Text)); - if (label.Length > 60) label = label.Substring(0, 60) + "…"; - } - else - { - // Minor click/twitch on empty area (<= 5x5) adds nothing - if (displayRect.Width <= 5 && displayRect.Height <= 5) - { - return; - } - - // Dragging a box over images, signatures, or text not yet parsed as words - region = new RedactionRegion - { - PageIndex = pageIndex, - X = pdfX, - Y = pdfY, - Width = pdfWidth, - Height = pdfHeight, - Reason = "Manual selection" - }; - label = "Manual selection"; - } - } - else - { - region = new RedactionRegion - { - PageIndex = pageIndex, - X = pdfX, - Y = pdfY, - Width = pdfWidth, - Height = pdfHeight, - Reason = "Manual selection" - }; - label = "Manual selection"; - } - - SearchStatusMessage = string.Empty; - Marks.Add(new RedactionMarkItem { Region = region, Label = label }); - RecomputeCurrentPageMarks(); - } - - protected override bool ValidateInputs(out string errorMessage) - { - if (!base.ValidateInputs(out errorMessage)) return false; - - if (Marks.Count == 0) - { - errorMessage = "Search for text to redact and review the matches below before running Redact."; - return false; - } - errorMessage = string.Empty; - return true; - } - - protected override async Task ExecuteCoreAsync(IProgress progress, System.Threading.CancellationToken ct) - { - var options = new RedactionToolOptions - { - InputFilePath = PrimaryInputFile, - Regions = Marks.Select(m => m.Region).ToList(), - PermanentScrubText = PermanentScrubText - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.RedactPdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Security/RepairPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Security/RepairPdfToolViewModel.cs deleted file mode 100644 index 855185e..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Security/RepairPdfToolViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Security; - -public partial class RepairPdfToolViewModel : PdfToolViewModelBase -{ - public override bool UsesWorkspaceShell => true; - - public RepairPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new RepairToolOptions - { - InputFilePath = file - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Security/SignPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Security/SignPdfToolViewModel.cs deleted file mode 100644 index c43cbcf..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Security/SignPdfToolViewModel.cs +++ /dev/null @@ -1,66 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Security; - -public partial class SignPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private string _signerName = "Jane Doe"; - - [ObservableProperty] - private SignatureStyle _style = SignatureStyle.CursiveElegance; - - [ObservableProperty] - private string _reason = "Document Approved and Verified"; - - [ObservableProperty] - private string _location = string.Empty; - - [ObservableProperty] - private int _targetPageNumber = 1; - - [ObservableProperty] - private double _x = 100; - - [ObservableProperty] - private double _y = 600; - - [ObservableProperty] - private double _width = 200; - - [ObservableProperty] - private double _height = 70; - - public override bool UsesWorkspaceShell => true; - - public SignPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - var options = new SignToolOptions - { - InputFilePath = PrimaryInputFile, - SignerName = SignerName, - Style = Style, - Reason = Reason, - Location = string.IsNullOrWhiteSpace(Location) ? null : Location, - TargetPageNumber = TargetPageNumber, - X = X, - Y = Y, - Width = Width, - Height = Height - }; - - return await OperationsService.ExecuteToolAsync(PdfToolId.SignPdf, options, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Security/UnlockPdfToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Security/UnlockPdfToolViewModel.cs deleted file mode 100644 index d741763..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Security/UnlockPdfToolViewModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Security; - -public partial class UnlockPdfToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private string _password = string.Empty; - - public override bool UsesWorkspaceShell => true; - - public UnlockPdfToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new UnlockToolOptions - { - InputFilePath = file, - Password = Password - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/Tools/Security/WatermarkToolViewModel.cs b/src/PdfEditorApp/ViewModels/Tools/Security/WatermarkToolViewModel.cs deleted file mode 100644 index 359cbf9..0000000 --- a/src/PdfEditorApp/ViewModels/Tools/Security/WatermarkToolViewModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Threading; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.ViewModels.Tools.Security; - -public partial class WatermarkToolViewModel : PdfToolViewModelBase -{ - [ObservableProperty] - private string _watermarkText = "CONFIDENTIAL"; - - [ObservableProperty] - private double _opacity = 0.35; - - [ObservableProperty] - private double _rotationAngle = -45; - - [ObservableProperty] - private WatermarkPosition _position = WatermarkPosition.Center; - - [ObservableProperty] - private string _colorHex = "#EF4444"; - - [ObservableProperty] - private double _fontSize = 48; - - [ObservableProperty] - private PageTargetSelection _targetPages = PageTargetSelection.AllPages; - - [ObservableProperty] - private string _customRange = string.Empty; - - public override bool UsesWorkspaceShell => true; - - public WatermarkToolViewModel(IPdfDocumentOperationsService operationsService, PdfToolDefinition tool) - : base(operationsService, tool) - { - } - - protected override async Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return await ExecuteBatchAsync(file => new WatermarkToolOptions - { - InputFilePath = file, - Text = WatermarkText, - Opacity = Opacity, - RotationAngle = RotationAngle, - Position = Position, - ColorHex = ColorHex, - FontSize = FontSize, - TargetPages = TargetPages, - CustomRange = CustomRange - }, progress, ct); - } -} diff --git a/src/PdfEditorApp/ViewModels/ViewModelBase.cs b/src/PdfEditorApp/ViewModels/ViewModelBase.cs deleted file mode 100644 index 0d2dc3b..0000000 --- a/src/PdfEditorApp/ViewModels/ViewModelBase.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace PdfEditorApp.ViewModels; - -public abstract class ViewModelBase : ObservableObject -{ -} diff --git a/src/PdfEditorApp/ViewModels/WorkflowBuilderViewModel.cs b/src/PdfEditorApp/ViewModels/WorkflowBuilderViewModel.cs deleted file mode 100644 index 928240c..0000000 --- a/src/PdfEditorApp/ViewModels/WorkflowBuilderViewModel.cs +++ /dev/null @@ -1,296 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Core; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Tools; - -namespace PdfEditorApp.ViewModels; - -public partial class WorkflowBuilderViewModel : ViewModelBase -{ - private readonly IPdfWorkflowEngine _workflowEngine; - private readonly IPdfToolRegistry _toolRegistry; - private CancellationTokenSource? _cts; - - public IStorageProvider? StorageProvider { get; set; } - - [ObservableProperty] - private bool _isOpen; - - [ObservableProperty] - private WorkflowDefinition _currentWorkflow = new(); - - [ObservableProperty] - private WorkflowStepDefinition? _selectedStep; - - [ObservableProperty] - private bool _isRunning; - - [ObservableProperty] - private double _progressPercentage; - - [ObservableProperty] - private string _statusMessage = "Ready"; - - [ObservableProperty] - private bool _hasError; - - [ObservableProperty] - private string _errorMessage = string.Empty; - - [ObservableProperty] - private bool _isComplete; - - [ObservableProperty] - private string _resultSummary = string.Empty; - - public ObservableCollection InputFiles { get; } = new(); - public ObservableCollection Steps { get; } = new(); - public ObservableCollection AvailableTools { get; } = new(); - public ObservableCollection PresetWorkflows { get; } = new(); - - public WorkflowBuilderViewModel(IPdfWorkflowEngine workflowEngine, IPdfToolRegistry toolRegistry) - { - _workflowEngine = workflowEngine; - _toolRegistry = toolRegistry; - - // Populate available tool palette for workflow steps - foreach (var t in _toolRegistry.GetAllTools().Where(x => !x.IsWorkflowBanner)) - { - AvailableTools.Add(t); - } - - // Load Presets - foreach (var p in _workflowEngine.GetPresetWorkflows()) - { - PresetWorkflows.Add(p); - } - - LoadWorkflow(PresetWorkflows.FirstOrDefault() ?? new WorkflowDefinition()); - } - - public void Open() - { - IsOpen = true; - ResetState(); - } - - public void ResetState() - { - IsRunning = false; - IsComplete = false; - HasError = false; - ErrorMessage = string.Empty; - StatusMessage = "Ready"; - ProgressPercentage = 0; - ResultSummary = string.Empty; - } - - public void LoadWorkflow(WorkflowDefinition workflow) - { - CurrentWorkflow = workflow.Clone(); - Steps.Clear(); - foreach (var s in CurrentWorkflow.Steps) - { - Steps.Add(s); - } - SelectedStep = Steps.FirstOrDefault(); - ResetState(); - } - - [RelayCommand] - public void SelectPreset(WorkflowDefinition preset) - { - if (preset != null) - { - LoadWorkflow(preset); - } - } - - [RelayCommand] - public void AddStep(PdfToolDefinition tool) - { - if (tool == null) return; - var step = new WorkflowStepDefinition - { - Id = Guid.NewGuid().ToString("N"), - ToolId = tool.Id, - StepName = tool.Name, - StepDescription = tool.Description, - IsEnabled = true - }; - Steps.Add(step); - CurrentWorkflow.Steps.Add(step); - SelectedStep = step; - } - - [RelayCommand] - public void RemoveStep(WorkflowStepDefinition step) - { - if (step == null) return; - Steps.Remove(step); - CurrentWorkflow.Steps.Remove(step); - SelectedStep = Steps.FirstOrDefault(); - } - - [RelayCommand] - public void MoveStepUp(WorkflowStepDefinition step) - { - if (step == null) return; - int idx = Steps.IndexOf(step); - if (idx > 0) - { - Steps.Move(idx, idx - 1); - CurrentWorkflow.Steps.Clear(); - CurrentWorkflow.Steps.AddRange(Steps); - SelectedStep = step; - } - } - - [RelayCommand] - public void MoveStepDown(WorkflowStepDefinition step) - { - if (step == null) return; - int idx = Steps.IndexOf(step); - if (idx >= 0 && idx < Steps.Count - 1) - { - Steps.Move(idx, idx + 1); - CurrentWorkflow.Steps.Clear(); - CurrentWorkflow.Steps.AddRange(Steps); - SelectedStep = step; - } - } - - [RelayCommand] - public async Task AddInputFilesAsync() - { - if (StorageProvider == null) return; - - var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Select Input PDF Files for Pipeline", - AllowMultiple = true, - FileTypeFilter = new[] - { - new FilePickerFileType("PDF Documents (*.pdf)") - { - Patterns = new[] { "*.pdf" } - } - } - }); - - if (files != null && files.Count > 0) - { - foreach (var f in files) - { - string path = f.Path.LocalPath; - if (!InputFiles.Contains(path)) InputFiles.Add(path); - } - } - } - - [RelayCommand] - public void RemoveInputFile(string filePath) - { - InputFiles.Remove(filePath); - } - - [RelayCommand] - public void ClearInputFiles() - { - InputFiles.Clear(); - } - - [RelayCommand] - public async Task RunWorkflowAsync() - { - if (InputFiles.Count == 0) - { - HasError = true; - ErrorMessage = "Please add at least one input PDF file to process."; - return; - } - - if (Steps.Count == 0 || !Steps.Any(s => s.IsEnabled)) - { - HasError = true; - ErrorMessage = "Please add at least one enabled step to the pipeline."; - return; - } - - IsRunning = true; - IsComplete = false; - HasError = false; - ErrorMessage = string.Empty; - StatusMessage = "Starting pipeline..."; - ProgressPercentage = 0; - - _cts = new CancellationTokenSource(); - var progress = new Progress(p => - { - ProgressPercentage = p.OverallProgressPercentage; - StatusMessage = p.StatusMessage; - }); - - try - { - CurrentWorkflow.Steps = Steps.ToList(); - var result = await _workflowEngine.ExecuteWorkflowAsync(CurrentWorkflow, InputFiles, progress, _cts.Token); - - if (result.Success) - { - IsComplete = true; - ResultSummary = result.Message ?? "Workflow pipeline executed successfully."; - StatusMessage = "Workflow Complete!"; - ProgressPercentage = 100.0; - } - else - { - HasError = true; - ErrorMessage = result.ErrorMessage ?? "Workflow execution failed."; - StatusMessage = "Failed"; - } - } - catch (OperationCanceledException) - { - StatusMessage = "Workflow cancelled."; - } - catch (Exception ex) - { - HasError = true; - ErrorMessage = $"Execution error: {ex.Message}"; - StatusMessage = "Error"; - } - finally - { - IsRunning = false; - _cts = null; - } - } - - [RelayCommand] - public void CancelWorkflow() - { - if (_cts != null && !_cts.IsCancellationRequested) - { - _cts.Cancel(); - StatusMessage = "Cancelling pipeline..."; - } - } - - [RelayCommand] - public void Close() - { - CancelWorkflow(); - IsOpen = false; - } -} diff --git a/src/PdfEditorApp/Views/AiAssistantSidebarView.axaml b/src/PdfEditorApp/Views/AiAssistantSidebarView.axaml deleted file mode 100644 index 62887b5..0000000 --- a/src/PdfEditorApp/Views/AiAssistantSidebarView.axaml +++ /dev/null @@ -1,641 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/AiAssistantSidebarView.axaml.cs b/src/PdfEditorApp/Views/AiAssistantSidebarView.axaml.cs deleted file mode 100644 index d8b3b93..0000000 --- a/src/PdfEditorApp/Views/AiAssistantSidebarView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class AiAssistantSidebarView : UserControl -{ - public AiAssistantSidebarView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/CanvasRulerControl.cs b/src/PdfEditorApp/Views/CanvasRulerControl.cs deleted file mode 100644 index d0028e7..0000000 --- a/src/PdfEditorApp/Views/CanvasRulerControl.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Globalization; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Layout; -using Avalonia.Media; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; - -namespace PdfEditorApp.Views; - -public class CanvasRulerControl : Control -{ - public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); - - public static readonly StyledProperty UnitProperty = - AvaloniaProperty.Register(nameof(Unit), RulerUnit.Points); - - public static readonly StyledProperty ZoomLevelProperty = - AvaloniaProperty.Register(nameof(ZoomLevel), 1.0); - - public static readonly StyledProperty CursorPositionProperty = - AvaloniaProperty.Register(nameof(CursorPosition), -1); - - public static readonly StyledProperty PageOffsetProperty = - AvaloniaProperty.Register(nameof(PageOffset), 0.0); - - public static readonly StyledProperty PageDimensionProperty = - AvaloniaProperty.Register(nameof(PageDimension), 800.0); - - public Orientation Orientation - { - get => GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - public RulerUnit Unit - { - get => GetValue(UnitProperty); - set => SetValue(UnitProperty, value); - } - - public double ZoomLevel - { - get => GetValue(ZoomLevelProperty); - set => SetValue(ZoomLevelProperty, value); - } - - public double CursorPosition - { - get => GetValue(CursorPositionProperty); - set => SetValue(CursorPositionProperty, value); - } - - public double PageOffset - { - get => GetValue(PageOffsetProperty); - set => SetValue(PageOffsetProperty, value); - } - - public double PageDimension - { - get => GetValue(PageDimensionProperty); - set => SetValue(PageDimensionProperty, value); - } - - // Default Fallback Resources - private static readonly IBrush s_defBgBrush = new SolidColorBrush(Color.Parse("#F8FAFC")); - private static readonly IPen s_defBorderPen = new Pen(new SolidColorBrush(Color.Parse("#E2E8F0")), 1); - private static readonly IPen s_defTickPen = new Pen(new SolidColorBrush(Color.Parse("#94A3B8")), 1); - private static readonly IPen s_defMajorTickPen = new Pen(new SolidColorBrush(Color.Parse("#64748B")), 1); - private static readonly IPen s_defPageBorderPen = new Pen(new SolidColorBrush(Color.Parse("#CBD5E1")), 1.5); - private static readonly IPen s_cursorPen = new Pen(new SolidColorBrush(Color.Parse("#0F6CBD")), 1.5); - private static readonly IBrush s_defPageShadeBrush = new SolidColorBrush(Color.Parse("#FFFFFF")); - private static readonly IBrush s_defLabelBrush = new SolidColorBrush(Color.Parse("#64748B")); - private static readonly Typeface s_typeface = new("Segoe UI, -apple-system, sans-serif"); - - static CanvasRulerControl() - { - AffectsRender( - OrientationProperty, - UnitProperty, - ZoomLevelProperty, - CursorPositionProperty, - PageOffsetProperty, - PageDimensionProperty); - } - - public override void Render(DrawingContext context) - { - base.Render(context); - - var bounds = Bounds; - if (bounds.Width <= 0 || bounds.Height <= 0) return; - - bool isHorizontal = Orientation == Orientation.Horizontal; - - // Resolve Dynamic Theme Brushes - var bgBrush = (this.TryFindResource("WinInputBgBrush", out var bgObj) && bgObj is IBrush b1) ? b1 : s_defBgBrush; - var borderBrush = (this.TryFindResource("WinBorderBrush", out var bdrObj) && bdrObj is IBrush b2) ? b2 : s_defBorderPen.Brush; - var borderPen = new Pen(borderBrush, 1); - var tickBrush = (this.TryFindResource("WinSubtleBrush", out var tObj) && tObj is IBrush b3) ? b3 : s_defTickPen.Brush; - var tickPen = new Pen(tickBrush, 1); - var majorTickBrush = (this.TryFindResource("WinMutedBrush", out var mtObj) && mtObj is IBrush b4) ? b4 : s_defMajorTickPen.Brush; - var majorTickPen = new Pen(majorTickBrush, 1); - var pageShadeBrush = (this.TryFindResource("WinPanelBrush", out var psObj) && psObj is IBrush b5) ? b5 : s_defPageShadeBrush; - var labelBrush = majorTickBrush ?? s_defLabelBrush; - - // Draw ruler background - context.FillRectangle(bgBrush, new Rect(0, 0, bounds.Width, bounds.Height)); - - if (isHorizontal) - { - // Bottom edge border - context.DrawLine(borderPen, new Point(0, bounds.Height), new Point(bounds.Width, bounds.Height)); - - double zoom = Math.Max(0.1, ZoomLevel); - double stepPts = Unit == RulerUnit.Inches ? 72.0 : (Unit == RulerUnit.Millimeters ? 72.0 / 2.54 : 50.0); - double minorStepPts = stepPts / 5.0; - - double startX = PageOffset; - double endX = startX + (PageDimension * zoom); - - // Draw page range shade - context.FillRectangle(pageShadeBrush, new Rect(Math.Max(0, startX), 0, Math.Max(0, endX - Math.Max(0, startX)), bounds.Height)); - context.DrawLine(borderPen, new Point(startX, 0), new Point(startX, bounds.Height)); - context.DrawLine(borderPen, new Point(endX, 0), new Point(endX, bounds.Height)); - - // Draw ticks across canvas - double maxPts = PageDimension + 200; - for (double pt = 0; pt <= maxPts; pt += minorStepPts) - { - double x = startX + (pt * zoom); - if (x < 0 || x > bounds.Width) continue; - - bool isMajor = Math.Abs(pt % stepPts) < 0.01; - double tickH = isMajor ? bounds.Height * 0.55 : bounds.Height * 0.25; - - context.DrawLine(isMajor ? majorTickPen : tickPen, new Point(x, bounds.Height - tickH), new Point(x, bounds.Height)); - - if (isMajor) - { - string label = Unit switch - { - RulerUnit.Inches => $"{pt / 72.0:0}\"", - RulerUnit.Millimeters => $"{pt * 25.4 / 72.0:0}", - _ => $"{pt:0}" - }; - - var formattedText = new FormattedText( - label, - CultureInfo.InvariantCulture, - FlowDirection.LeftToRight, - s_typeface, - 8.5, - labelBrush - ); - - context.DrawText(formattedText, new Point(x + 2, 2)); - } - } - - // Draw cursor tracker - if (CursorPosition >= 0) - { - double cursorX = startX + (CursorPosition * zoom); - if (cursorX >= 0 && cursorX <= bounds.Width) - { - context.DrawLine(s_cursorPen, new Point(cursorX, 0), new Point(cursorX, bounds.Height)); - } - } - } - else - { - // Right edge border - context.DrawLine(borderPen, new Point(bounds.Width, 0), new Point(bounds.Width, bounds.Height)); - - double zoom = Math.Max(0.1, ZoomLevel); - double stepPts = Unit == RulerUnit.Inches ? 72.0 : (Unit == RulerUnit.Millimeters ? 72.0 / 2.54 : 50.0); - double minorStepPts = stepPts / 5.0; - - double startY = PageOffset; - double endY = startY + (PageDimension * zoom); - - // Draw page range shade - context.FillRectangle(pageShadeBrush, new Rect(0, Math.Max(0, startY), bounds.Width, Math.Max(0, endY - Math.Max(0, startY)))); - context.DrawLine(borderPen, new Point(0, startY), new Point(bounds.Width, startY)); - context.DrawLine(borderPen, new Point(0, endY), new Point(bounds.Width, endY)); - - // Draw ticks across canvas - double maxPts = PageDimension + 200; - for (double pt = 0; pt <= maxPts; pt += minorStepPts) - { - double y = startY + (pt * zoom); - if (y < 0 || y > bounds.Height) continue; - - bool isMajor = Math.Abs(pt % stepPts) < 0.01; - double tickW = isMajor ? bounds.Width * 0.55 : bounds.Width * 0.25; - - context.DrawLine(isMajor ? majorTickPen : tickPen, new Point(bounds.Width - tickW, y), new Point(bounds.Width, y)); - - if (isMajor) - { - string label = Unit switch - { - RulerUnit.Inches => $"{pt / 72.0:0}\"", - RulerUnit.Millimeters => $"{pt * 25.4 / 72.0:0}", - _ => $"{pt:0}" - }; - - var formattedText = new FormattedText( - label, - CultureInfo.InvariantCulture, - FlowDirection.LeftToRight, - s_typeface, - 8.0, - labelBrush - ); - - context.DrawText(formattedText, new Point(2, y + 2)); - } - } - - // Draw cursor tracker - if (CursorPosition >= 0) - { - double cursorY = startY + (CursorPosition * zoom); - if (cursorY >= 0 && cursorY <= bounds.Height) - { - context.DrawLine(s_cursorPen, new Point(0, cursorY), new Point(bounds.Width, cursorY)); - } - } - } - } -} diff --git a/src/PdfEditorApp/Views/CanvasTextHudView.axaml b/src/PdfEditorApp/Views/CanvasTextHudView.axaml deleted file mode 100644 index 6f2b027..0000000 --- a/src/PdfEditorApp/Views/CanvasTextHudView.axaml +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/CanvasTextHudView.axaml.cs b/src/PdfEditorApp/Views/CanvasTextHudView.axaml.cs deleted file mode 100644 index 668bc29..0000000 --- a/src/PdfEditorApp/Views/CanvasTextHudView.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; - -namespace PdfEditorApp.Views; - -public partial class CanvasTextHudView : UserControl -{ - public CanvasTextHudView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Controls/AdvancedTextControl.cs b/src/PdfEditorApp/Views/Controls/AdvancedTextControl.cs deleted file mode 100644 index 60c3771..0000000 --- a/src/PdfEditorApp/Views/Controls/AdvancedTextControl.cs +++ /dev/null @@ -1,692 +0,0 @@ -using System; -using System.Globalization; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Media; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Typography; - -namespace PdfEditorApp.Views.Controls; - -public class AdvancedTextControl : Control -{ - public static readonly StyledProperty TextProperty = - AvaloniaProperty.Register(nameof(Text), defaultValue: "Enter text here"); - - public static readonly StyledProperty FontFamilyNameProperty = - AvaloniaProperty.Register(nameof(FontFamilyName), defaultValue: "Segoe UI"); - - public static readonly StyledProperty FontSizeProperty = - AvaloniaProperty.Register(nameof(FontSize), defaultValue: 14.0); - - public static readonly StyledProperty IsBoldProperty = - AvaloniaProperty.Register(nameof(IsBold), defaultValue: false); - - public static readonly StyledProperty IsItalicProperty = - AvaloniaProperty.Register(nameof(IsItalic), defaultValue: false); - - public static readonly StyledProperty IsUnderlineProperty = - AvaloniaProperty.Register(nameof(IsUnderline), defaultValue: false); - - public static readonly StyledProperty IsDoubleUnderlineProperty = - AvaloniaProperty.Register(nameof(IsDoubleUnderline), defaultValue: false); - - public static readonly StyledProperty IsStrikethroughProperty = - AvaloniaProperty.Register(nameof(IsStrikethrough), defaultValue: false); - - public static readonly StyledProperty TextBrushProperty = - AvaloniaProperty.Register(nameof(TextBrush), defaultValue: Brushes.Black); - - public static readonly StyledProperty TextOpacityProperty = - AvaloniaProperty.Register(nameof(TextOpacity), defaultValue: 1.0); - - public static readonly StyledProperty AlignmentProperty = - AvaloniaProperty.Register(nameof(Alignment), defaultValue: TextAlignmentMode.Left); - - public static readonly StyledProperty TextVerticalAlignmentProperty = - AvaloniaProperty.Register(nameof(TextVerticalAlignment), defaultValue: TextVerticalAlignment.Top); - - public static readonly StyledProperty LineHeightProperty = - AvaloniaProperty.Register(nameof(LineHeight), defaultValue: 1.4); - - public static readonly StyledProperty CharacterSpacingProperty = - AvaloniaProperty.Register(nameof(CharacterSpacing), defaultValue: 0.0); - - public static readonly StyledProperty WordSpacingProperty = - AvaloniaProperty.Register(nameof(WordSpacing), defaultValue: 0.0); - - public static readonly StyledProperty ParagraphSpacingProperty = - AvaloniaProperty.Register(nameof(ParagraphSpacing), defaultValue: 0.0); - - public static readonly StyledProperty TextWrapProperty = - AvaloniaProperty.Register(nameof(TextWrap), defaultValue: true); - - // Stroke / Outline - public static readonly StyledProperty HasStrokeProperty = - AvaloniaProperty.Register(nameof(HasStroke), defaultValue: false); - - public static readonly StyledProperty StrokeBrushProperty = - AvaloniaProperty.Register(nameof(StrokeBrush), defaultValue: Brushes.Black); - - public static readonly StyledProperty StrokeWidthProperty = - AvaloniaProperty.Register(nameof(StrokeWidth), defaultValue: 1.0); - - // Shadow & Glow - public static readonly StyledProperty HasShadowProperty = - AvaloniaProperty.Register(nameof(HasShadow), defaultValue: false); - - public static readonly StyledProperty ShadowBrushProperty = - AvaloniaProperty.Register(nameof(ShadowBrush), defaultValue: Brushes.DarkGray); - - public static readonly StyledProperty ShadowOffsetXProperty = - AvaloniaProperty.Register(nameof(ShadowOffsetX), defaultValue: 2.0); - - public static readonly StyledProperty ShadowOffsetYProperty = - AvaloniaProperty.Register(nameof(ShadowOffsetY), defaultValue: 2.0); - - public static readonly StyledProperty ShadowBlurRadiusProperty = - AvaloniaProperty.Register(nameof(ShadowBlurRadius), defaultValue: 4.0); - - public static readonly StyledProperty ShadowOpacityProperty = - AvaloniaProperty.Register(nameof(ShadowOpacity), defaultValue: 0.5); - - // Box Background, Border, Padding - public static readonly StyledProperty BackgroundBrushProperty = - AvaloniaProperty.Register(nameof(BackgroundBrush), defaultValue: Brushes.Transparent); - - public static readonly StyledProperty BorderBrushProperty = - AvaloniaProperty.Register(nameof(BorderBrush), defaultValue: Brushes.Transparent); - - public static readonly StyledProperty BorderThicknessProperty = - AvaloniaProperty.Register(nameof(BorderThickness), defaultValue: 0.0); - - public static readonly StyledProperty CornerRadiusProperty = - AvaloniaProperty.Register(nameof(CornerRadius), defaultValue: 0.0); - - public static readonly StyledProperty PaddingProperty = - AvaloniaProperty.Register(nameof(Padding), defaultValue: 0.0); - - // Shape / Curved / Circular Typography - public static readonly StyledProperty ShapeModeProperty = - AvaloniaProperty.Register(nameof(ShapeMode), defaultValue: TextShapeMode.Normal); - - public static readonly StyledProperty CurveRadiusProperty = - AvaloniaProperty.Register(nameof(CurveRadius), defaultValue: 120.0); - - public static readonly StyledProperty CurveArcAngleProperty = - AvaloniaProperty.Register(nameof(CurveArcAngle), defaultValue: 180.0); - - public static readonly StyledProperty CurveStartAngleProperty = - AvaloniaProperty.Register(nameof(CurveStartAngle), defaultValue: 0.0); - - public static readonly StyledProperty CurveClockwiseProperty = - AvaloniaProperty.Register(nameof(CurveClockwise), defaultValue: true); - - public static readonly StyledProperty CurveInvertProperty = - AvaloniaProperty.Register(nameof(CurveInvert), defaultValue: false); - - public static readonly StyledProperty CircularPlacementProperty = - AvaloniaProperty.Register(nameof(CircularPlacement), defaultValue: CircularTextPlacement.TopArc); - - // Transformations - public static readonly StyledProperty BaselineShiftProperty = - AvaloniaProperty.Register(nameof(BaselineShift), defaultValue: 0.0); - - public static readonly StyledProperty CharacterRotationProperty = - AvaloniaProperty.Register(nameof(CharacterRotation), defaultValue: 0.0); - - public static readonly StyledProperty ScaleXProperty = - AvaloniaProperty.Register(nameof(ScaleX), defaultValue: 1.0); - - public static readonly StyledProperty ScaleYProperty = - AvaloniaProperty.Register(nameof(ScaleY), defaultValue: 1.0); - - public static readonly StyledProperty FlipXProperty = - AvaloniaProperty.Register(nameof(FlipX), defaultValue: false); - - public static readonly StyledProperty FlipYProperty = - AvaloniaProperty.Register(nameof(FlipY), defaultValue: false); - - // Bézier Curve Typography - public static readonly StyledProperty BezierPresetProperty = - AvaloniaProperty.Register(nameof(BezierPreset), defaultValue: BezierCurvePreset.Wave); - - public static readonly StyledProperty BezierP0XProperty = - AvaloniaProperty.Register(nameof(BezierP0X), defaultValue: 0.0); - - public static readonly StyledProperty BezierP0YProperty = - AvaloniaProperty.Register(nameof(BezierP0Y), defaultValue: 0.5); - - public static readonly StyledProperty BezierP1XProperty = - AvaloniaProperty.Register(nameof(BezierP1X), defaultValue: 0.33); - - public static readonly StyledProperty BezierP1YProperty = - AvaloniaProperty.Register(nameof(BezierP1Y), defaultValue: 0.10); - - public static readonly StyledProperty BezierP2XProperty = - AvaloniaProperty.Register(nameof(BezierP2X), defaultValue: 0.67); - - public static readonly StyledProperty BezierP2YProperty = - AvaloniaProperty.Register(nameof(BezierP2Y), defaultValue: 0.90); - - public static readonly StyledProperty BezierP3XProperty = - AvaloniaProperty.Register(nameof(BezierP3X), defaultValue: 1.0); - - public static readonly StyledProperty BezierP3YProperty = - AvaloniaProperty.Register(nameof(BezierP3Y), defaultValue: 0.5); - - public static readonly StyledProperty?> SpansProperty = - AvaloniaProperty.Register?>(nameof(Spans), defaultValue: null); - - static AdvancedTextControl() - { - AffectsRender( - TextProperty, SpansProperty, FontFamilyNameProperty, FontSizeProperty, IsBoldProperty, IsItalicProperty, - IsUnderlineProperty, IsDoubleUnderlineProperty, IsStrikethroughProperty, TextBrushProperty, - TextOpacityProperty, AlignmentProperty, TextVerticalAlignmentProperty, LineHeightProperty, - CharacterSpacingProperty, WordSpacingProperty, ParagraphSpacingProperty, TextWrapProperty, - HasStrokeProperty, StrokeBrushProperty, StrokeWidthProperty, HasShadowProperty, - ShadowBrushProperty, ShadowOffsetXProperty, ShadowOffsetYProperty, ShadowBlurRadiusProperty, - ShadowOpacityProperty, BackgroundBrushProperty, BorderBrushProperty, BorderThicknessProperty, - CornerRadiusProperty, PaddingProperty, ShapeModeProperty, CurveRadiusProperty, - CurveArcAngleProperty, CurveStartAngleProperty, CurveClockwiseProperty, CurveInvertProperty, - CircularPlacementProperty, BaselineShiftProperty, CharacterRotationProperty, ScaleXProperty, - ScaleYProperty, FlipXProperty, FlipYProperty, - BezierPresetProperty, BezierP0XProperty, BezierP0YProperty, BezierP1XProperty, BezierP1YProperty, - BezierP2XProperty, BezierP2YProperty, BezierP3XProperty, BezierP3YProperty - ); - } - - #region Properties Accessors - public System.Collections.Generic.IReadOnlyList? Spans { get => GetValue(SpansProperty); set => SetValue(SpansProperty, value); } - public string Text { get => GetValue(TextProperty); set => SetValue(TextProperty, value); } - public string FontFamilyName { get => GetValue(FontFamilyNameProperty); set => SetValue(FontFamilyNameProperty, value); } - public double FontSize { get => GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); } - public bool IsBold { get => GetValue(IsBoldProperty); set => SetValue(IsBoldProperty, value); } - public bool IsItalic { get => GetValue(IsItalicProperty); set => SetValue(IsItalicProperty, value); } - public bool IsUnderline { get => GetValue(IsUnderlineProperty); set => SetValue(IsUnderlineProperty, value); } - public bool IsDoubleUnderline { get => GetValue(IsDoubleUnderlineProperty); set => SetValue(IsDoubleUnderlineProperty, value); } - public bool IsStrikethrough { get => GetValue(IsStrikethroughProperty); set => SetValue(IsStrikethroughProperty, value); } - public IBrush? TextBrush { get => GetValue(TextBrushProperty); set => SetValue(TextBrushProperty, value); } - public double TextOpacity { get => GetValue(TextOpacityProperty); set => SetValue(TextOpacityProperty, value); } - public TextAlignmentMode Alignment { get => GetValue(AlignmentProperty); set => SetValue(AlignmentProperty, value); } - public TextVerticalAlignment TextVerticalAlignment { get => GetValue(TextVerticalAlignmentProperty); set => SetValue(TextVerticalAlignmentProperty, value); } - public double LineHeight { get => GetValue(LineHeightProperty); set => SetValue(LineHeightProperty, value); } - public double CharacterSpacing { get => GetValue(CharacterSpacingProperty); set => SetValue(CharacterSpacingProperty, value); } - public double WordSpacing { get => GetValue(WordSpacingProperty); set => SetValue(WordSpacingProperty, value); } - public double ParagraphSpacing { get => GetValue(ParagraphSpacingProperty); set => SetValue(ParagraphSpacingProperty, value); } - public bool TextWrap { get => GetValue(TextWrapProperty); set => SetValue(TextWrapProperty, value); } - - public bool HasStroke { get => GetValue(HasStrokeProperty); set => SetValue(HasStrokeProperty, value); } - public IBrush? StrokeBrush { get => GetValue(StrokeBrushProperty); set => SetValue(StrokeBrushProperty, value); } - public double StrokeWidth { get => GetValue(StrokeWidthProperty); set => SetValue(StrokeWidthProperty, value); } - - public bool HasShadow { get => GetValue(HasShadowProperty); set => SetValue(HasShadowProperty, value); } - public IBrush? ShadowBrush { get => GetValue(ShadowBrushProperty); set => SetValue(ShadowBrushProperty, value); } - public double ShadowOffsetX { get => GetValue(ShadowOffsetXProperty); set => SetValue(ShadowOffsetXProperty, value); } - public double ShadowOffsetY { get => GetValue(ShadowOffsetYProperty); set => SetValue(ShadowOffsetYProperty, value); } - public double ShadowBlurRadius { get => GetValue(ShadowBlurRadiusProperty); set => SetValue(ShadowBlurRadiusProperty, value); } - public double ShadowOpacity { get => GetValue(ShadowOpacityProperty); set => SetValue(ShadowOpacityProperty, value); } - - public IBrush? BackgroundBrush { get => GetValue(BackgroundBrushProperty); set => SetValue(BackgroundBrushProperty, value); } - public IBrush? BorderBrush { get => GetValue(BorderBrushProperty); set => SetValue(BorderBrushProperty, value); } - public double BorderThickness { get => GetValue(BorderThicknessProperty); set => SetValue(BorderThicknessProperty, value); } - public double CornerRadius { get => GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } - public double Padding { get => GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); } - - public TextShapeMode ShapeMode { get => GetValue(ShapeModeProperty); set => SetValue(ShapeModeProperty, value); } - public double CurveRadius { get => GetValue(CurveRadiusProperty); set => SetValue(CurveRadiusProperty, value); } - public double CurveArcAngle { get => GetValue(CurveArcAngleProperty); set => SetValue(CurveArcAngleProperty, value); } - public double CurveStartAngle { get => GetValue(CurveStartAngleProperty); set => SetValue(CurveStartAngleProperty, value); } - public bool CurveClockwise { get => GetValue(CurveClockwiseProperty); set => SetValue(CurveClockwiseProperty, value); } - public bool CurveInvert { get => GetValue(CurveInvertProperty); set => SetValue(CurveInvertProperty, value); } - public CircularTextPlacement CircularPlacement { get => GetValue(CircularPlacementProperty); set => SetValue(CircularPlacementProperty, value); } - - public double BaselineShift { get => GetValue(BaselineShiftProperty); set => SetValue(BaselineShiftProperty, value); } - public double CharacterRotation { get => GetValue(CharacterRotationProperty); set => SetValue(CharacterRotationProperty, value); } - public double ScaleX { get => GetValue(ScaleXProperty); set => SetValue(ScaleXProperty, value); } - public double ScaleY { get => GetValue(ScaleYProperty); set => SetValue(ScaleYProperty, value); } - public bool FlipX { get => GetValue(FlipXProperty); set => SetValue(FlipXProperty, value); } - public bool FlipY { get => GetValue(FlipYProperty); set => SetValue(FlipYProperty, value); } - - public BezierCurvePreset BezierPreset { get => GetValue(BezierPresetProperty); set => SetValue(BezierPresetProperty, value); } - public double BezierP0X { get => GetValue(BezierP0XProperty); set => SetValue(BezierP0XProperty, value); } - public double BezierP0Y { get => GetValue(BezierP0YProperty); set => SetValue(BezierP0YProperty, value); } - public double BezierP1X { get => GetValue(BezierP1XProperty); set => SetValue(BezierP1XProperty, value); } - public double BezierP1Y { get => GetValue(BezierP1YProperty); set => SetValue(BezierP1YProperty, value); } - public double BezierP2X { get => GetValue(BezierP2XProperty); set => SetValue(BezierP2XProperty, value); } - public double BezierP2Y { get => GetValue(BezierP2YProperty); set => SetValue(BezierP2YProperty, value); } - public double BezierP3X { get => GetValue(BezierP3XProperty); set => SetValue(BezierP3XProperty, value); } - public double BezierP3Y { get => GetValue(BezierP3YProperty); set => SetValue(BezierP3YProperty, value); } - #endregion - - public override void Render(DrawingContext context) - { - base.Render(context); - - double w = Bounds.Width; - double h = Bounds.Height; - if (w <= 0 || h <= 0) return; - - // Apply Opacity - using var opacityScope = TextOpacity < 0.99 ? context.PushOpacity(TextOpacity) : default; - - // Draw Background and Border - if (BackgroundBrush != null || (BorderBrush != null && BorderThickness > 0)) - { - var bgPen = (BorderBrush != null && BorderThickness > 0) ? new Pen(BorderBrush, BorderThickness) : null; - var rect = new RoundedRect(new Rect(0, 0, w, h), CornerRadius); - context.DrawRectangle(BackgroundBrush, bgPen, rect); - } - - // Apply Scaling & Flipping Matrix around center - bool hasCustomTransform = FlipX || FlipY || Math.Abs(ScaleX - 1.0) > 0.001 || Math.Abs(ScaleY - 1.0) > 0.001; - IDisposable? transformScope = null; - if (hasCustomTransform) - { - double sx = (FlipX ? -1.0 : 1.0) * (ScaleX != 0 ? ScaleX : 1.0); - double sy = (FlipY ? -1.0 : 1.0) * (ScaleY != 0 ? ScaleY : 1.0); - var center = new Point(w / 2.0, h / 2.0); - var transform = Matrix.CreateTranslation(-center.X, -center.Y) * - Matrix.CreateScale(sx, sy) * - Matrix.CreateTranslation(center.X, center.Y); - transformScope = context.PushTransform(transform); - } - - try - { - var avaloniaFamily = FontHelper.CreateFontFamily(FontFamilyName); - var typeface = new Typeface( - avaloniaFamily, - IsItalic ? FontStyle.Italic : FontStyle.Normal, - IsBold ? FontWeight.Bold : FontWeight.Normal); - - var fillBrush = TextBrush ?? Brushes.Black; - Pen? strokePen = (HasStroke && StrokeWidth > 0 && StrokeBrush != null) ? new Pen(StrokeBrush, StrokeWidth) : null; - IBrush? shadowBrush = (HasShadow && ShadowBrush != null) ? ShadowBrush : null; - - if (ShapeMode == TextShapeMode.Normal) - { - RenderNormalText(context, typeface, fillBrush, strokePen, shadowBrush, w, h); - } - else if (ShapeMode == TextShapeMode.BezierCurve) - { - RenderBezierText(context, typeface, fillBrush, strokePen, shadowBrush, w, h); - } - else - { - RenderCurvedText(context, typeface, fillBrush, strokePen, shadowBrush, w, h); - } - } - finally - { - transformScope?.Dispose(); - } - } - - private void RenderNormalText( - DrawingContext context, - Typeface typeface, - IBrush fillBrush, - Pen? strokePen, - IBrush? shadowBrush, - double w, - double h) - { - var layout = TextLayoutEngine.CalculateNormalLayout( - Text, - FontFamilyName, - FontSize, - IsBold, - IsItalic, - w, - LineHeight, - CharacterSpacing, - WordSpacing, - ParagraphSpacing, - Alignment, - TextVerticalAlignment, - h, - TextWrap, - Padding - ); - - int docIndex = 0; - string docText = Text ?? ""; - - foreach (var line in layout.Lines) - { - if (string.IsNullOrEmpty(line.Text)) - { - docIndex++; - continue; - } - - int lineOffset = docText.IndexOf(line.Text, Math.Min(docIndex, docText.Length), StringComparison.Ordinal); - if (lineOffset < 0) lineOffset = docIndex; - docIndex = lineOffset + line.Text.Length; - - // Draw Shadow - if (HasShadow && shadowBrush != null) - { - using (context.PushOpacity(ShadowOpacity)) - { - var shadowFt = new FormattedText( - line.Text, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - shadowBrush); - ApplySpanTypography(shadowFt, line.Text, shadowBrush, lineOffset); - context.DrawText(shadowFt, new Point(line.X + ShadowOffsetX, line.Y + ShadowOffsetY)); - } - } - - var ft = new FormattedText( - line.Text, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - fillBrush); - ApplySpanTypography(ft, line.Text, null, lineOffset); - - // Draw Stroke Outline - if (strokePen != null) - { - var geom = ft.BuildGeometry(new Point(line.X, line.Y)); - if (geom != null) - { - context.DrawGeometry(null, strokePen, geom); - } - } - - // Draw Fill Text - context.DrawText(ft, new Point(line.X, line.Y)); - - // Double Underline - if (IsDoubleUnderline) - { - double uY1 = line.BaselineY + 2; - double uY2 = line.BaselineY + 5; - var uPen = new Pen(fillBrush, 1.0); - context.DrawLine(uPen, new Point(line.X, uY1), new Point(line.X + line.Width, uY1)); - context.DrawLine(uPen, new Point(line.X, uY2), new Point(line.X + line.Width, uY2)); - } - } - } - - private void ApplySpanTypography(FormattedText ft, string lineText, IBrush? forceBrush, int lineOffset) - { - if (Spans == null || Spans.Count == 0 || string.IsNullOrEmpty(lineText)) return; - - int spanStartInDoc = 0; - int lineEndInDoc = lineOffset + lineText.Length; - - foreach (var span in Spans) - { - if (string.IsNullOrEmpty(span.Text)) continue; - int spanLength = span.Text.Length; - int spanEndInDoc = spanStartInDoc + spanLength; - - int overlapStartInDoc = Math.Max(spanStartInDoc, lineOffset); - int overlapEndInDoc = Math.Min(spanEndInDoc, lineEndInDoc); - - if (overlapEndInDoc > overlapStartInDoc) - { - int localIndex = overlapStartInDoc - lineOffset; - int localLength = overlapEndInDoc - overlapStartInDoc; - - // 1. Font family, weight, italic - bool spanBold = span.IsBold ?? IsBold; - bool spanItalic = span.IsItalic ?? IsItalic; - string spanFamily = span.FontFamily ?? FontFamilyName; - - if (spanBold != IsBold || spanItalic != IsItalic || spanFamily != FontFamilyName) - { - if (spanFamily != FontFamilyName) - { - ft.SetFontFamily(FontHelper.CreateFontFamily(spanFamily), localIndex, localLength); - } - if (spanBold != IsBold) - { - ft.SetFontWeight(spanBold ? FontWeight.Bold : FontWeight.Normal, localIndex, localLength); - } - if (spanItalic != IsItalic) - { - ft.SetFontStyle(spanItalic ? FontStyle.Italic : FontStyle.Normal, localIndex, localLength); - } - } - - // 2. Font size / script scaling - double spanSize = span.FontSize ?? FontSize; - if (span.Script == TextScriptMode.Superscript || span.Script == TextScriptMode.Subscript) - { - spanSize = Math.Max(6, spanSize * 0.75); - } - - if (Math.Abs(spanSize - FontSize) > 0.01) - { - ft.SetFontSize(spanSize, localIndex, localLength); - } - - // 3. Foreground Brush (if not overriding with shadow brush) - if (forceBrush == null && !string.IsNullOrEmpty(span.TextColorHex)) - { - if (Color.TryParse(span.TextColorHex, out var col)) - { - ft.SetForegroundBrush(new SolidColorBrush(col), localIndex, localLength); - } - } - - // 4. Text decorations (underline / strikethrough) - bool spanUnderline = span.IsUnderline ?? IsUnderline; - bool spanStrikethrough = span.IsStrikethrough ?? IsStrikethrough; - if (spanUnderline || spanStrikethrough) - { - var decs = new TextDecorationCollection(); - if (spanUnderline) foreach (var d in Avalonia.Media.TextDecorations.Underline) decs.Add(d); - if (spanStrikethrough) foreach (var d in Avalonia.Media.TextDecorations.Strikethrough) decs.Add(d); - ft.SetTextDecorations(decs, localIndex, localLength); - } - } - - spanStartInDoc = spanEndInDoc; - } - } - - private void RenderCurvedText( - DrawingContext context, - Typeface typeface, - IBrush fillBrush, - Pen? strokePen, - IBrush? shadowBrush, - double w, - double h) - { - var curvedLayout = TextLayoutEngine.CalculateCurvedGlyphs( - Text, - FontFamilyName, - FontSize, - IsBold, - IsItalic, - w, - h, - CurveRadius, - CurveArcAngle, - CurveStartAngle, - CurveClockwise, - CurveInvert, - CharacterSpacing, - CircularPlacement, - ShapeMode, - BaselineShift - ); - - var decorations = new TextDecorationCollection(); - if (IsUnderline) foreach (var d in TextDecorations.Underline) decorations.Add(d); - if (IsStrikethrough) foreach (var d in TextDecorations.Strikethrough) decorations.Add(d); - - foreach (var g in curvedLayout.Glyphs) - { - if (char.IsWhiteSpace(g.Character)) continue; - - double angleDeg = g.TangentAngleDeg + CharacterRotation; - double angleRad = (angleDeg * Math.PI) / 180.0; - - // Rotate around glyph center - var glyphCenter = new Point(g.X, g.Y); - var transform = Matrix.CreateTranslation(-glyphCenter.X, -glyphCenter.Y) * - Matrix.CreateRotation(angleRad) * - Matrix.CreateTranslation(glyphCenter.X, glyphCenter.Y); - - using (context.PushTransform(transform)) - { - // Shadow - if (HasShadow && shadowBrush != null) - { - using (context.PushOpacity(ShadowOpacity)) - { - var sFt = new FormattedText( - g.Text, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - shadowBrush); - context.DrawText(sFt, new Point(g.X - (g.Width / 2.0) + ShadowOffsetX, g.Y - g.BaselineOffset + ShadowOffsetY)); - } - } - - var ft = new FormattedText( - g.Text, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - fillBrush); - - var origin = new Point(g.X - (g.Width / 2.0), g.Y - g.BaselineOffset); - - // Stroke Outline - if (strokePen != null) - { - var geom = ft.BuildGeometry(origin); - if (geom != null) - { - context.DrawGeometry(null, strokePen, geom); - } - } - - // Fill - context.DrawText(ft, origin); - } - } - } - - private void RenderBezierText( - DrawingContext context, - Typeface typeface, - IBrush fillBrush, - Pen? strokePen, - IBrush? shadowBrush, - double w, - double h) - { - var layout = TextLayoutEngine.CalculateBezierGlyphs( - Text, - FontFamilyName, - FontSize, - IsBold, - IsItalic, - w, - h, - new Point(BezierP0X, BezierP0Y), - new Point(BezierP1X, BezierP1Y), - new Point(BezierP2X, BezierP2Y), - new Point(BezierP3X, BezierP3Y), - CurveInvert, - CharacterSpacing, - BaselineShift - ); - - foreach (var g in layout.Glyphs) - { - if (string.IsNullOrEmpty(g.Text) || char.IsWhiteSpace(g.Character)) continue; - - double totalAngle = g.TangentAngleDeg + CharacterRotation; - double angleRad = (totalAngle * Math.PI) / 180.0; - - var transform = Matrix.CreateTranslation(-g.X, -g.Y) * - Matrix.CreateRotation(angleRad) * - Matrix.CreateTranslation(g.X, g.Y); - - using (context.PushTransform(transform)) - { - // Shadow - if (HasShadow && shadowBrush != null) - { - using (context.PushOpacity(ShadowOpacity)) - { - var sFt = new FormattedText( - g.Text, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - shadowBrush); - context.DrawText(sFt, new Point(g.X - (g.Width / 2.0) + ShadowOffsetX, g.Y - g.BaselineOffset + ShadowOffsetY)); - } - } - - var ft = new FormattedText( - g.Text, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - fillBrush); - - var origin = new Point(g.X - (g.Width / 2.0), g.Y - g.BaselineOffset); - - // Stroke Outline - if (strokePen != null) - { - var geom = ft.BuildGeometry(origin); - if (geom != null) - { - context.DrawGeometry(null, strokePen, geom); - } - } - - // Fill Text - context.DrawText(ft, origin); - - // Underline & Strikethrough along character local axis - if (IsUnderline || IsDoubleUnderline) - { - var uPen = new Pen(fillBrush, Math.Max(1.0, FontSize * 0.07)); - double uY = g.Y + (FontSize * 0.45); - context.DrawLine(uPen, new Point(g.X - (g.Width / 2.0), uY), new Point(g.X + (g.Width / 2.0), uY)); - - if (IsDoubleUnderline) - { - double uY2 = uY + Math.Max(2.0, FontSize * 0.12); - context.DrawLine(uPen, new Point(g.X - (g.Width / 2.0), uY2), new Point(g.X + (g.Width / 2.0), uY2)); - } - } - - if (IsStrikethrough) - { - var sPen = new Pen(fillBrush, Math.Max(1.0, FontSize * 0.07)); - double sY = g.Y; - context.DrawLine(sPen, new Point(g.X - (g.Width / 2.0), sY), new Point(g.X + (g.Width / 2.0), sY)); - } - } - } - } -} diff --git a/src/PdfEditorApp/Views/Controls/AskAiButton.cs b/src/PdfEditorApp/Views/Controls/AskAiButton.cs deleted file mode 100644 index 5287c98..0000000 --- a/src/PdfEditorApp/Views/Controls/AskAiButton.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.VisualTree; -using Material.Icons; -using Material.Icons.Avalonia; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views.Controls; - -/// -/// Display variant style for the . -/// -public enum AskAiButtonVariant -{ - /// - /// Vibrant purple floating pill badge positioned above element selection borders. - /// - FloatingPill, - - /// - /// Soft lilac themed badge designed for properties inspectors and toolbars. - /// - Subtle -} - -/// -/// Reusable, context-aware "Ask AI" button control for FryPDF Studio. -/// Can be dropped anywhere in the canvas or sidebar UI via <controls:AskAiButton />. -/// Automatically resolves the target element from DataContext/CommandParameter and executes -/// . -/// -public class AskAiButton : Button -{ - public static readonly StyledProperty VariantProperty = - AvaloniaProperty.Register(nameof(Variant), AskAiButtonVariant.FloatingPill); - - public static readonly StyledProperty ButtonTextProperty = - AvaloniaProperty.Register(nameof(ButtonText), "Ask AI"); - - public static readonly StyledProperty ToolTipTextProperty = - AvaloniaProperty.Register(nameof(ToolTipText), "Ask AI to Modify (Ctrl+I / ⌘I)"); - - public AskAiButtonVariant Variant - { - get => GetValue(VariantProperty); - set => SetValue(VariantProperty, value); - } - - public string ButtonText - { - get => GetValue(ButtonTextProperty); - set => SetValue(ButtonTextProperty, value); - } - - public string ToolTipText - { - get => GetValue(ToolTipTextProperty); - set => SetValue(ToolTipTextProperty, value); - } - - protected override Type StyleKeyOverride => typeof(Button); - - private readonly MaterialIcon _icon; - private readonly TextBlock _textBlock; - - public AskAiButton() - { - try - { - Cursor = new Cursor(StandardCursorType.Hand); - } - catch - { - // Headless / mock test runner without platform cursor service - } - - _icon = new MaterialIcon - { - VerticalAlignment = VerticalAlignment.Center - }; - - try - { - _icon.Kind = MaterialIconKind.AutoFixHigh; - } - catch - { - // Headless unit test environment without Avalonia rendering interface - } - - _textBlock = new TextBlock - { - FontWeight = FontWeight.SemiBold, - VerticalAlignment = VerticalAlignment.Center - }; - - var stack = new StackPanel - { - Orientation = Orientation.Horizontal, - Spacing = 4, - VerticalAlignment = VerticalAlignment.Center, - Children = { _icon, _textBlock } - }; - - Content = stack; - - ApplyVariant(Variant); - UpdateTextAndToolTip(); - } - - static AskAiButton() - { - VariantProperty.Changed.AddClassHandler((btn, e) => - { - if (e.NewValue is AskAiButtonVariant v) btn.ApplyVariant(v); - }); - - ButtonTextProperty.Changed.AddClassHandler((btn, e) => - { - btn.UpdateTextAndToolTip(); - }); - - ToolTipTextProperty.Changed.AddClassHandler((btn, e) => - { - btn.UpdateTextAndToolTip(); - }); - } - - private void UpdateTextAndToolTip() - { - _textBlock.Text = ButtonText; - ToolTip.SetTip(this, ToolTipText); - } - - private void ApplyVariant(AskAiButtonVariant variant) - { - if (variant == AskAiButtonVariant.FloatingPill) - { - HorizontalAlignment = HorizontalAlignment.Right; - VerticalAlignment = VerticalAlignment.Top; - Margin = new Thickness(0, -24, 0, 0); - Height = 22; - Padding = new Thickness(6, 2); - Background = Brush.Parse("#7C3AED"); - BorderBrush = Brush.Parse("#6D28D9"); - BorderThickness = new Thickness(1); - CornerRadius = new CornerRadius(11); - - _icon.Width = 12; - _icon.Height = 12; - _icon.Foreground = Brushes.White; - - _textBlock.FontSize = 10; - _textBlock.Foreground = Brushes.White; - } - else // Subtle - { - HorizontalAlignment = HorizontalAlignment.Left; - VerticalAlignment = VerticalAlignment.Center; - Margin = new Thickness(0); - Height = 24; - Padding = new Thickness(6, 2); - Background = Brush.Parse("#EDE9FE"); - BorderBrush = Brush.Parse("#C4B5FD"); - BorderThickness = new Thickness(1); - CornerRadius = new CornerRadius(5); - - _icon.Width = 13; - _icon.Height = 13; - _icon.Foreground = Brush.Parse("#7C3AED"); - - _textBlock.FontSize = 10.5; - _textBlock.Foreground = Brush.Parse("#7C3AED"); - } - } - - protected override void OnPointerEntered(PointerEventArgs e) - { - base.OnPointerEntered(e); - if (Variant == AskAiButtonVariant.FloatingPill) - { - Background = Brush.Parse("#6D28D9"); - BorderBrush = Brush.Parse("#5B21B6"); - } - else - { - Background = Brush.Parse("#DDD6FE"); - BorderBrush = Brush.Parse("#A78BFA"); - } - } - - protected override void OnPointerExited(PointerEventArgs e) - { - base.OnPointerExited(e); - if (Variant == AskAiButtonVariant.FloatingPill) - { - Background = Brush.Parse("#7C3AED"); - BorderBrush = Brush.Parse("#6D28D9"); - } - else - { - Background = Brush.Parse("#EDE9FE"); - BorderBrush = Brush.Parse("#C4B5FD"); - } - } - - protected override void OnClick() - { - if (Command != null) - { - base.OnClick(); - return; - } - - // Automatic smart invocation: - // 1. Resolve target element (from CommandParameter, or from DataContext) - var targetParam = CommandParameter ?? DataContext; - - // 2. Discover MainViewModel in visual ancestor hierarchy - var mainVm = this.FindAncestorOfType()?.DataContext as MainViewModel - ?? this.FindAncestorOfType()?.DataContext as MainViewModel - ?? (TopLevel.GetTopLevel(this) as Window)?.DataContext as MainViewModel; - - if (mainVm != null) - { - if (mainVm.AskAiToModifyCommand.CanExecute(targetParam)) - { - mainVm.AskAiToModifyCommand.Execute(targetParam); - } - return; - } - - base.OnClick(); - } -} diff --git a/src/PdfEditorApp/Views/Controls/PdfTextOverlayControl.cs b/src/PdfEditorApp/Views/Controls/PdfTextOverlayControl.cs deleted file mode 100644 index 3ea719e..0000000 --- a/src/PdfEditorApp/Views/Controls/PdfTextOverlayControl.cs +++ /dev/null @@ -1,666 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Media; -using Avalonia.VisualTree; -using Material.Icons; -using Material.Icons.Avalonia; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views.Controls; - -/// -/// High-performance, pixel-perfect text selection and highlight markup overlay for PDF Viewer pages. -/// Provides continuous mouse drag selection, multi-click word/line selection, I-beam hover cursor, -/// Acrobat-style vibrant visual selection highlights, permanent vector highlight annotations, -/// and rich right-click context menu actions (Copy, Citation, Highlight, Note, Search). -/// -public class PdfTextOverlayControl : Control -{ - public static readonly StyledProperty PageProperty = - AvaloniaProperty.Register(nameof(Page)); - - public static readonly StyledProperty ZoomLevelProperty = - AvaloniaProperty.Register(nameof(ZoomLevel), defaultValue: 1.0); - - public static readonly StyledProperty ViewerViewModelProperty = - AvaloniaProperty.Register(nameof(ViewerViewModel)); - - public PdfViewerPageItem? Page - { - get => GetValue(PageProperty); - set => SetValue(PageProperty, value); - } - - public double ZoomLevel - { - get => GetValue(ZoomLevelProperty); - set => SetValue(ZoomLevelProperty, value); - } - - public PdfViewerViewModel? ViewerViewModel - { - get => GetValue(ViewerViewModelProperty); - set => SetValue(ViewerViewModelProperty, value); - } - - // Interaction state - private bool _isPointerDown; - private Point _dragStartPoint; - private bool _hasMovedSinceDown; - private PdfViewerPageItem? _subscribedPage; - private bool _isAreaSelecting; - private Rect _currentMarqueeRect; - private Point? _lastAnchorPoint; - private bool _isTargetedOcrRunning; - - // Visual brushes (cached for maximum performance) - private static readonly IBrush SelectionFillBrush = new SolidColorBrush(Color.FromArgb(95, 59, 130, 246)); - private static readonly IPen SelectionBorderPen = new Pen(new SolidColorBrush(Color.FromArgb(160, 37, 99, 235)), 1.0); - private static readonly IBrush MarqueeFillBrush = new SolidColorBrush(Color.FromArgb(45, 15, 108, 189)); - private static readonly IPen MarqueeBorderPen = new Pen(new SolidColorBrush(Color.FromArgb(200, 15, 108, 189)), 1.5, DashStyle.Dash); - private static readonly IBrush HandleFillBrush = new SolidColorBrush(Colors.White); - private static readonly IPen HandleBorderPen = new Pen(new SolidColorBrush(Color.FromArgb(220, 15, 108, 189)), 1.0); - - static PdfTextOverlayControl() - { - AffectsRender(PageProperty, ZoomLevelProperty); - } - - public PdfTextOverlayControl() - { - ClipToBounds = true; - IsHitTestVisible = true; - ContextMenu = CreateSelectionContextMenu(); - } - - private PdfViewerViewModel? ResolveViewModel() - { - if (ViewerViewModel != null) return ViewerViewModel; - if (DataContext is PdfViewerViewModel vm) return vm; - var parent = this.FindAncestorOfType(); - return parent?.DataContext as PdfViewerViewModel; - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == PageProperty) - { - if (_subscribedPage != null) - { - _subscribedPage.SelectionChanged -= OnPageSelectionChanged; - } - - _subscribedPage = change.GetNewValue(); - - if (_subscribedPage != null) - { - _subscribedPage.SelectionChanged += OnPageSelectionChanged; - } - - InvalidateVisual(); - } - else if (change.Property == ZoomLevelProperty) - { - InvalidateVisual(); - } - } - - private void OnPageSelectionChanged() - { - InvalidateVisual(); - } - - private (double scaleX, double scaleY) GetEffectiveScales(PdfViewerPageItem page) - { - double fallbackZoom = Math.Max(0.1, ZoomLevel > 0 ? ZoomLevel : 1.0); - double sx = (page.WidthPoints > 0 && Bounds.Width > 0) ? Bounds.Width / page.WidthPoints : fallbackZoom; - double sy = (page.HeightPoints > 0 && Bounds.Height > 0) ? Bounds.Height / page.HeightPoints : fallbackZoom; - return (sx, sy); - } - - protected override void OnPointerMoved(PointerEventArgs e) - { - base.OnPointerMoved(e); - - var page = Page; - if (page == null) return; - - var vm = ResolveViewModel(); - if (page.Words.Count == 0 && vm != null) - { - vm.EnsurePageGeometry(page); - } - - var (scaleX, scaleY) = GetEffectiveScales(page); - var pos = e.GetPosition(this); - var unscaledPos = new Point(pos.X / scaleX, pos.Y / scaleY); - - if (_isPointerDown) - { - _hasMovedSinceDown = true; - - if (_isAreaSelecting) - { - double minX = Math.Min(_dragStartPoint.X, unscaledPos.X); - double minY = Math.Min(_dragStartPoint.Y, unscaledPos.Y); - double maxX = Math.Max(_dragStartPoint.X, unscaledPos.X); - double maxY = Math.Max(_dragStartPoint.Y, unscaledPos.Y); - _currentMarqueeRect = new Rect(minX, minY, Math.Max(1, maxX - minX), Math.Max(1, maxY - minY)); - - page.SelectionRects.Clear(); - page.SelectionRects.Add(_currentMarqueeRect); - - if (vm != null) - { - vm.LastSelectedAreaRect = _currentMarqueeRect; - } - - InvalidateVisual(); - e.Handled = true; - } - else - { - page.SetSelectionRange(_dragStartPoint, unscaledPos); - - if (vm != null) - { - vm.ActiveSelectedText = page.SelectedText; - vm.ActiveSelectedPageNumber = page.PageNumber; - vm.HasTextSelection = !string.IsNullOrEmpty(page.SelectedText); - } - - InvalidateVisual(); - e.Handled = true; - } - } - else - { - // Dynamic Cursor - bool isAreaMode = (vm != null && vm.SelectionMode == PdfViewerSelectionMode.Area) || e.KeyModifiers.HasFlag(KeyModifiers.Alt); - if (isAreaMode) - { - Cursor = new Cursor(StandardCursorType.Cross); - } - else - { - bool isOverText = page.Words != null && page.Words.Any(w => w.Bounds.Contains(unscaledPos)); - if (isOverText) - { - Cursor = new Cursor(StandardCursorType.Ibeam); - } - else if (page.Words == null || page.Words.Count == 0) - { - Cursor = new Cursor(StandardCursorType.Cross); - } - else - { - Cursor = Cursor.Default; - } - } - } - } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - base.OnPointerPressed(e); - - var page = Page; - if (page == null) return; - - var vm = ResolveViewModel(); - if (page.Words.Count == 0 && vm != null) - { - vm.EnsurePageGeometry(page); - } - - var (scaleX, scaleY) = GetEffectiveScales(page); - var currentPoint = e.GetCurrentPoint(this); - var pos = e.GetPosition(this); - var unscaledPos = new Point(pos.X / scaleX, pos.Y / scaleY); - - if (currentPoint.Properties.IsLeftButtonPressed) - { - int clickCount = e.ClickCount; - bool isShift = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - bool isAlt = e.KeyModifiers.HasFlag(KeyModifiers.Alt); - bool isAreaMode = (vm != null && vm.SelectionMode == PdfViewerSelectionMode.Area) || isAlt || (page.Words.Count == 0); - - if (clickCount == 1) - { - // Synchronize active page and thumbnail selection on click - if (vm != null) - { - vm.SelectPage(page); - if (vm.ActiveSelectedPageNumber != page.PageNumber) - { - vm.ClearSelection(); - } - } - - if (isShift && _lastAnchorPoint.HasValue && !isAreaMode && page.Words.Count > 0) - { - // Shift + Click: Extend selection from previous anchor to clicked point - page.SetSelectionRange(_lastAnchorPoint.Value, unscaledPos); - SyncSelectionToViewModel(page, vm); - InvalidateVisual(); - e.Handled = true; - return; - } - - _isPointerDown = true; - _dragStartPoint = unscaledPos; - _lastAnchorPoint = unscaledPos; - _hasMovedSinceDown = false; - _isAreaSelecting = isAreaMode; - _currentMarqueeRect = default; - - e.Pointer.Capture(this); - e.Handled = true; - } - else if (clickCount == 2 && !isAreaMode) - { - // Double-click: Select word under cursor - var word = page.Words?.FirstOrDefault(w => w.Bounds.Contains(unscaledPos)) - ?? page.Words?.OrderBy(w => Math.Abs(w.Bounds.Center.X - unscaledPos.X) + Math.Abs(w.Bounds.Center.Y - unscaledPos.Y) * 2).FirstOrDefault(); - if (word != null) - { - page.SelectWord(word); - _lastAnchorPoint = word.Bounds.TopLeft; - SyncSelectionToViewModel(page, vm); - InvalidateVisual(); - e.Handled = true; - } - } - else if (clickCount >= 3 && !isAreaMode) - { - // Triple-click: Select entire line under cursor - var line = page.TextLines?.FirstOrDefault(l => l.Bounds.Contains(unscaledPos)) - ?? page.TextLines?.OrderBy(l => Math.Abs(l.Bounds.Center.Y - unscaledPos.Y)).FirstOrDefault(); - if (line != null) - { - page.SelectLine(line); - _lastAnchorPoint = line.Bounds.TopLeft; - SyncSelectionToViewModel(page, vm); - InvalidateVisual(); - e.Handled = true; - } - } - } - else if (currentPoint.Properties.IsRightButtonPressed) - { - // Right click: If clicking inside existing selection, keep it; otherwise select word under cursor - bool isInsideSelection = page.SelectionRects != null && page.SelectionRects.Any(r => r.Contains(unscaledPos)); - if (!isInsideSelection) - { - var word = page.Words?.FirstOrDefault(w => w.Bounds.Contains(unscaledPos)); - if (word != null) - { - page.SelectWord(word); - SyncSelectionToViewModel(page, vm); - InvalidateVisual(); - } - } - } - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - base.OnPointerReleased(e); - - if (_isPointerDown) - { - _isPointerDown = false; - e.Pointer.Capture(null); - - var page = Page; - var vm = ResolveViewModel(); - if (page != null) - { - if (!_hasMovedSinceDown && e.InitialPressMouseButton == MouseButton.Left) - { - // Single click with no drag on blank space: clear selection - var (scaleX, scaleY) = GetEffectiveScales(page); - var pos = e.GetPosition(this); - var unscaledPos = new Point(pos.X / scaleX, pos.Y / scaleY); - bool hitWord = page.Words != null && page.Words.Any(w => w.Bounds.Contains(unscaledPos)); - - if (!hitWord) - { - page.ClearSelection(); - vm?.ClearSelection(); - _currentMarqueeRect = default; - } - else - { - var word = page.Words!.First(w => w.Bounds.Contains(unscaledPos)); - page.SelectWord(word); - _lastAnchorPoint = word.Bounds.TopLeft; - SyncSelectionToViewModel(page, vm); - } - } - else if (_isAreaSelecting && _hasMovedSinceDown) - { - if (vm != null) - { - vm.LastSelectedAreaRect = _currentMarqueeRect; - } - - List selectedWords = new(); - if (page.Words != null && page.Words.Count > 0) - { - selectedWords = page.Words - .Where(w => w.Bounds.Intersects(_currentMarqueeRect)) - .OrderBy(w => w.Bounds.Top) - .ThenBy(w => w.Bounds.Left) - .ToList(); - } - - bool wordsAreGarbled = selectedWords.Count > 0 && selectedWords.All(w => PdfViewerViewModel.IsGarbledText(w.Text)); - - if (selectedWords.Count > 0 && !wordsAreGarbled) - { - page.SelectedText = string.Join(" ", selectedWords.Select(w => w.Text)); - SyncSelectionToViewModel(page, vm); - } - else if (vm != null && _currentMarqueeRect.Width > 5 && _currentMarqueeRect.Height > 5) - { - // Scanned page (zero words) or garbled words in area: perform targeted OCR on cropped rect - _isTargetedOcrRunning = true; - InvalidateVisual(); - var rectToOcr = _currentMarqueeRect; - - _ = Task.Run(async () => - { - try - { - string text = await vm.ExtractOcrTextFromRegionAsync(page, rectToOcr, forceOcr: true); - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - _isTargetedOcrRunning = false; - if (!string.IsNullOrWhiteSpace(text)) - { - page.SelectedText = text; - SyncSelectionToViewModel(page, vm); - } - InvalidateVisual(); - }); - } - catch - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - _isTargetedOcrRunning = false; - InvalidateVisual(); - }); - } - }); - } - } - else - { - SyncSelectionToViewModel(page, vm); - } - } - - InvalidateVisual(); - e.Handled = true; - } - } - - private void SyncSelectionToViewModel(PdfViewerPageItem page, PdfViewerViewModel? vm) - { - if (vm != null) - { - vm.ActiveSelectedText = page.SelectedText; - vm.ActiveSelectedPageNumber = page.PageNumber; - vm.HasTextSelection = !string.IsNullOrEmpty(page.SelectedText); - } - } - - public override void Render(DrawingContext context) - { - base.Render(context); - - // 1. Fill transparent bounds to guarantee 100% reliable mouse hit-testing across entire page card - context.FillRectangle(Brushes.Transparent, new Rect(0, 0, Math.Max(1, Bounds.Width), Math.Max(1, Bounds.Height))); - - var page = Page; - if (page == null) return; - - var (scaleX, scaleY) = GetEffectiveScales(page); - - // 2. Draw Permanent Highlight Annotations on this page - if (page.PageAnnotations != null && page.PageAnnotations.Count > 0) - { - foreach (var ann in page.PageAnnotations) - { - if (ann.Type == "Highlight" && ann.HighlightRects != null && ann.HighlightRects.Count > 0) - { - IBrush highlightBrush = CreateHighlightBrush(ann.ColorHex); - foreach (var rect in ann.HighlightRects) - { - var scaledRect = new Rect( - rect.X * scaleX, - rect.Y * scaleY, - Math.Max(2, rect.Width * scaleX), - Math.Max(2, rect.Height * scaleY)); - - context.FillRectangle(highlightBrush, scaledRect, 2.0f); - } - } - } - } - - // 3. Draw Active User Text Selection - if (page.SelectionRects != null && page.SelectionRects.Count > 0) - { - var vm = ResolveViewModel(); - bool isMarquee = _isAreaSelecting || (vm != null && vm.SelectionMode == PdfViewerSelectionMode.Area) || (page.Words.Count == 0); - - foreach (var rect in page.SelectionRects) - { - var scaledRect = new Rect( - rect.X * scaleX, - rect.Y * scaleY, - Math.Max(2, rect.Width * scaleX), - Math.Max(2, rect.Height * scaleY)); - - if (isMarquee) - { - // Draw Area / Marquee Box - context.FillRectangle(MarqueeFillBrush, scaledRect, 2.0f); - context.DrawRectangle(MarqueeBorderPen, scaledRect, 2.0f); - - // Draw 4 corner handles - const double handleSize = 6.0; - const double halfHandle = handleSize / 2.0; - var corners = new[] - { - new Point(scaledRect.Left, scaledRect.Top), - new Point(scaledRect.Right, scaledRect.Top), - new Point(scaledRect.Left, scaledRect.Bottom), - new Point(scaledRect.Right, scaledRect.Bottom) - }; - - foreach (var corner in corners) - { - var handleRect = new Rect(corner.X - halfHandle, corner.Y - halfHandle, handleSize, handleSize); - context.FillRectangle(HandleFillBrush, handleRect); - context.DrawRectangle(HandleBorderPen, handleRect); - } - - if (_isTargetedOcrRunning) - { - // Draw "⚡ OCR..." badge - var badgeRect = new Rect(scaledRect.Left + 4, Math.Max(0, scaledRect.Top - 22), 65, 18); - context.FillRectangle(new SolidColorBrush(Color.FromArgb(230, 15, 108, 189)), badgeRect, 4.0f); - var typeface = new Typeface(FontFamily.Default, FontStyle.Normal, FontWeight.SemiBold); - var formattedText = new FormattedText("⚡ OCR...", System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, 10.5, Brushes.White); - context.DrawText(formattedText, new Point(badgeRect.Left + 6, badgeRect.Top + 2)); - } - } - else - { - context.FillRectangle(SelectionFillBrush, scaledRect, 2.0f); - context.DrawRectangle(SelectionBorderPen, scaledRect, 2.0f); - } - } - } - } - - private static IBrush CreateHighlightBrush(string colorHex) - { - try - { - if (Color.TryParse(colorHex, out var parsed)) - { - return new SolidColorBrush(Color.FromArgb(98, parsed.R, parsed.G, parsed.B)); - } - } - catch { } - - return new SolidColorBrush(Color.FromArgb(98, 254, 240, 138)); // Default yellow - } - - private ContextMenu CreateSelectionContextMenu() - { - var menu = new ContextMenu(); - - // 1. Copy - var copyItem = new MenuItem - { - Header = "Copy Text", - InputGesture = new KeyGesture(Key.C, KeyModifiers.Control | KeyModifiers.Meta) - }; - copyItem.Icon = new MaterialIcon { Kind = MaterialIconKind.ContentCopy, Width = 15, Height = 15 }; - copyItem.Click += (s, e) => ResolveViewModel()?.CopySelectedTextCommand.Execute(null); - menu.Items.Add(copyItem); - - // 2. Copy with Citation - var citationItem = new MenuItem - { - Header = "Copy with Page Reference", - InputGesture = new KeyGesture(Key.C, KeyModifiers.Control | KeyModifiers.Shift) - }; - citationItem.Icon = new MaterialIcon { Kind = MaterialIconKind.FormatQuoteClose, Width = 15, Height = 15 }; - citationItem.Click += (s, e) => ResolveViewModel()?.CopySelectedCitationCommand.Execute(null); - menu.Items.Add(citationItem); - - // 3. Copy Area Snapshot Image - var copySnapshotItem = new MenuItem - { - Header = "Copy Area Snapshot", - InputGesture = new KeyGesture(Key.S, KeyModifiers.Control | KeyModifiers.Shift) - }; - copySnapshotItem.Icon = new MaterialIcon { Kind = MaterialIconKind.CameraOutline, Width = 15, Height = 15, Foreground = new SolidColorBrush(Color.Parse("#0284C7")) }; - copySnapshotItem.Click += (s, e) => ResolveViewModel()?.CopySelectedAreaImageCommand.Execute(null); - menu.Items.Add(copySnapshotItem); - - // 4. Recognize Selection with OCR - var ocrSelectionItem = new MenuItem - { - Header = "Recognize Selection with OCR", - InputGesture = new KeyGesture(Key.R, KeyModifiers.Control | KeyModifiers.Shift) - }; - ocrSelectionItem.Icon = new MaterialIcon { Kind = MaterialIconKind.ScanHelper, Width = 15, Height = 15, Foreground = new SolidColorBrush(Color.Parse("#0F6CBD")) }; - ocrSelectionItem.Click += (s, e) => ResolveViewModel()?.RecognizeSelectedTextOcrCommand.Execute(null); - menu.Items.Add(ocrSelectionItem); - - // 5. Recognize Entire Page (OCR) - var ocrItem = new MenuItem - { - Header = "Recognize Page (OCR)" - }; - ocrItem.Icon = new MaterialIcon { Kind = MaterialIconKind.TextRecognition, Width = 15, Height = 15, Foreground = new SolidColorBrush(Color.Parse("#0F6CBD")) }; - ocrItem.Click += (s, e) => ResolveViewModel()?.RecognizeActivePageOcrCommand.Execute(null); - menu.Items.Add(ocrItem); - - menu.Items.Add(new Separator()); - - // 3. Highlight submenu - var highlightMenu = new MenuItem - { - Header = "Highlight Selection" - }; - highlightMenu.Icon = new MaterialIcon { Kind = MaterialIconKind.FormatColorHighlight, Width = 15, Height = 15, Foreground = new SolidColorBrush(Color.Parse("#CA8A04")) }; - - var yellowHl = new MenuItem { Header = "Yellow Highlight" }; - yellowHl.Icon = new Border { Width = 12, Height = 12, CornerRadius = new CornerRadius(2), Background = new SolidColorBrush(Color.Parse("#FEF08A")), BorderBrush = new SolidColorBrush(Color.Parse("#CA8A04")), BorderThickness = new Thickness(1) }; - yellowHl.Click += (s, e) => ResolveViewModel()?.HighlightSelectedTextCommand.Execute("#FEF08A"); - highlightMenu.Items.Add(yellowHl); - - var greenHl = new MenuItem { Header = "Green Highlight" }; - greenHl.Icon = new Border { Width = 12, Height = 12, CornerRadius = new CornerRadius(2), Background = new SolidColorBrush(Color.Parse("#A7F3D0")), BorderBrush = new SolidColorBrush(Color.Parse("#059669")), BorderThickness = new Thickness(1) }; - greenHl.Click += (s, e) => ResolveViewModel()?.HighlightSelectedTextCommand.Execute("#A7F3D0"); - highlightMenu.Items.Add(greenHl); - - var blueHl = new MenuItem { Header = "Blue Highlight" }; - blueHl.Icon = new Border { Width = 12, Height = 12, CornerRadius = new CornerRadius(2), Background = new SolidColorBrush(Color.Parse("#BAE6FD")), BorderBrush = new SolidColorBrush(Color.Parse("#0284C7")), BorderThickness = new Thickness(1) }; - blueHl.Click += (s, e) => ResolveViewModel()?.HighlightSelectedTextCommand.Execute("#BAE6FD"); - highlightMenu.Items.Add(blueHl); - - var pinkHl = new MenuItem { Header = "Pink Highlight" }; - pinkHl.Icon = new Border { Width = 12, Height = 12, CornerRadius = new CornerRadius(2), Background = new SolidColorBrush(Color.Parse("#FBCFE8")), BorderBrush = new SolidColorBrush(Color.Parse("#DB2777")), BorderThickness = new Thickness(1) }; - pinkHl.Click += (s, e) => ResolveViewModel()?.HighlightSelectedTextCommand.Execute("#FBCFE8"); - highlightMenu.Items.Add(pinkHl); - - var orangeHl = new MenuItem { Header = "Orange Highlight" }; - orangeHl.Icon = new Border { Width = 12, Height = 12, CornerRadius = new CornerRadius(2), Background = new SolidColorBrush(Color.Parse("#FED7AA")), BorderBrush = new SolidColorBrush(Color.Parse("#EA580C")), BorderThickness = new Thickness(1) }; - orangeHl.Click += (s, e) => ResolveViewModel()?.HighlightSelectedTextCommand.Execute("#FED7AA"); - highlightMenu.Items.Add(orangeHl); - - menu.Items.Add(highlightMenu); - - // 4. Add Sticky Note from selection - var noteItem = new MenuItem - { - Header = "Add Note on Selection...", - InputGesture = new KeyGesture(Key.N, KeyModifiers.Control | KeyModifiers.Meta) - }; - noteItem.Icon = new MaterialIcon { Kind = MaterialIconKind.CommentTextOutline, Width = 15, Height = 15, Foreground = new SolidColorBrush(Color.Parse("#0284C7")) }; - noteItem.Click += (s, e) => ResolveViewModel()?.AddNoteFromSelectionCommand.Execute(null); - menu.Items.Add(noteItem); - - menu.Items.Add(new Separator()); - - // 5. Search in document - var searchItem = new MenuItem - { - Header = "Find in PDF", - InputGesture = new KeyGesture(Key.F, KeyModifiers.Control | KeyModifiers.Meta) - }; - searchItem.Icon = new MaterialIcon { Kind = MaterialIconKind.Magnify, Width = 15, Height = 15 }; - searchItem.Click += (s, e) => ResolveViewModel()?.SearchSelectedTextCommand.Execute(null); - menu.Items.Add(searchItem); - - // 6. Search on web - var webSearchItem = new MenuItem - { - Header = "Search with Google" - }; - webSearchItem.Icon = new MaterialIcon { Kind = MaterialIconKind.Web, Width = 15, Height = 15, Foreground = new SolidColorBrush(Color.Parse("#2563EB")) }; - webSearchItem.Click += (s, e) => ResolveViewModel()?.SearchWebSelectedTextCommand.Execute(null); - menu.Items.Add(webSearchItem); - - menu.Items.Add(new Separator()); - - // 7. Select All - var selectAllItem = new MenuItem - { - Header = "Select All on Page", - InputGesture = new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Meta) - }; - selectAllItem.Icon = new MaterialIcon { Kind = MaterialIconKind.SelectAll, Width = 15, Height = 15 }; - selectAllItem.Click += (s, e) => ResolveViewModel()?.SelectAllPageTextCommand.Execute(null); - menu.Items.Add(selectAllItem); - - return menu; - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/AboutDialog.axaml b/src/PdfEditorApp/Views/Dialogs/AboutDialog.axaml deleted file mode 100644 index 7e24def..0000000 --- a/src/PdfEditorApp/Views/Dialogs/AboutDialog.axaml +++ /dev/null @@ -1,510 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/AboutDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/AboutDialog.axaml.cs deleted file mode 100644 index 0f10446..0000000 --- a/src/PdfEditorApp/Views/Dialogs/AboutDialog.axaml.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Input; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class AboutDialog : UserControl -{ - public AboutDialog() - { - InitializeComponent(); - } - - private void OnBackdropPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (DataContext is MainViewModel vm) - { - vm.CloseAboutDialog(); - } - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (e.Key == Key.Escape && DataContext is MainViewModel vm) - { - vm.CloseAboutDialog(); - e.Handled = true; - } - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/AiAssistantDialog.axaml b/src/PdfEditorApp/Views/Dialogs/AiAssistantDialog.axaml deleted file mode 100644 index e59e085..0000000 --- a/src/PdfEditorApp/Views/Dialogs/AiAssistantDialog.axaml +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/AiAssistantDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/AiAssistantDialog.axaml.cs deleted file mode 100644 index 7fa8758..0000000 --- a/src/PdfEditorApp/Views/Dialogs/AiAssistantDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class AiAssistantDialog : UserControl -{ - public AiAssistantDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/BatchGenerationDialog.axaml b/src/PdfEditorApp/Views/Dialogs/BatchGenerationDialog.axaml deleted file mode 100644 index d316dbb..0000000 --- a/src/PdfEditorApp/Views/Dialogs/BatchGenerationDialog.axaml +++ /dev/null @@ -1,532 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/BatchGenerationDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/BatchGenerationDialog.axaml.cs deleted file mode 100644 index 91570a1..0000000 --- a/src/PdfEditorApp/Views/Dialogs/BatchGenerationDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class BatchGenerationDialog : UserControl -{ - public BatchGenerationDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/BatesNumberingDialog.axaml b/src/PdfEditorApp/Views/Dialogs/BatesNumberingDialog.axaml deleted file mode 100644 index bc28343..0000000 --- a/src/PdfEditorApp/Views/Dialogs/BatesNumberingDialog.axaml +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/BatesNumberingDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/BatesNumberingDialog.axaml.cs deleted file mode 100644 index 56b2791..0000000 --- a/src/PdfEditorApp/Views/Dialogs/BatesNumberingDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class BatesNumberingDialog : UserControl -{ - public BatesNumberingDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/CommandPaletteDialog.axaml b/src/PdfEditorApp/Views/Dialogs/CommandPaletteDialog.axaml deleted file mode 100644 index 8f9419b..0000000 --- a/src/PdfEditorApp/Views/Dialogs/CommandPaletteDialog.axaml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/CommandPaletteDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/CommandPaletteDialog.axaml.cs deleted file mode 100644 index 413ec35..0000000 --- a/src/PdfEditorApp/Views/Dialogs/CommandPaletteDialog.axaml.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class CommandPaletteDialog : UserControl -{ - public CommandPaletteDialog() - { - InitializeComponent(); - PropertyChanged += OnDialogPropertyChanged; - } - - private void OnDialogPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == IsVisibleProperty && e.NewValue is true) - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - SearchInputTextBox?.Focus(); - SearchInputTextBox?.SelectAll(); - }, Avalonia.Threading.DispatcherPriority.Loaded); - } - } - - private void OnBackdropPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (DataContext is MainViewModel vm) - { - vm.CloseCommandPalette(); - } - } - - private void OnSearchInputKeyDown(object? sender, KeyEventArgs e) - { - if (DataContext is not MainViewModel vm) return; - - switch (e.Key) - { - case Key.Escape: - vm.CloseCommandPalette(); - e.Handled = true; - break; - case Key.Enter: - vm.ExecuteSelectedPaletteCommand(); - e.Handled = true; - break; - case Key.Down: - vm.SelectNextPaletteCommand(); - e.Handled = true; - break; - case Key.Up: - vm.SelectPreviousPaletteCommand(); - e.Handled = true; - break; - } - } - - private void OnCommandItemPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is Control control && control.DataContext is CommandPaletteItem item) - { - if (DataContext is MainViewModel vm) - { - vm.CloseCommandPalette(); - item.Action?.Invoke(); - e.Handled = true; - } - } - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/CompareDocumentsDialog.axaml b/src/PdfEditorApp/Views/Dialogs/CompareDocumentsDialog.axaml deleted file mode 100644 index 1012cac..0000000 --- a/src/PdfEditorApp/Views/Dialogs/CompareDocumentsDialog.axaml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/CompareDocumentsDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/CompareDocumentsDialog.axaml.cs deleted file mode 100644 index e2f6566..0000000 --- a/src/PdfEditorApp/Views/Dialogs/CompareDocumentsDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class CompareDocumentsDialog : UserControl -{ - public CompareDocumentsDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/CustomStampDialog.axaml b/src/PdfEditorApp/Views/Dialogs/CustomStampDialog.axaml deleted file mode 100644 index c36ed8a..0000000 --- a/src/PdfEditorApp/Views/Dialogs/CustomStampDialog.axaml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/CustomStampDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/CustomStampDialog.axaml.cs deleted file mode 100644 index e29e618..0000000 --- a/src/PdfEditorApp/Views/Dialogs/CustomStampDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class CustomStampDialog : UserControl -{ - public CustomStampDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/DataStudioDialog.axaml b/src/PdfEditorApp/Views/Dialogs/DataStudioDialog.axaml deleted file mode 100644 index 454f984..0000000 --- a/src/PdfEditorApp/Views/Dialogs/DataStudioDialog.axaml +++ /dev/null @@ -1,346 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/DataStudioDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/DataStudioDialog.axaml.cs deleted file mode 100644 index bd974aa..0000000 --- a/src/PdfEditorApp/Views/Dialogs/DataStudioDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class DataStudioDialog : UserControl -{ - public DataStudioDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/DeleteConfirmationDialog.axaml b/src/PdfEditorApp/Views/Dialogs/DeleteConfirmationDialog.axaml deleted file mode 100644 index 16804e7..0000000 --- a/src/PdfEditorApp/Views/Dialogs/DeleteConfirmationDialog.axaml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/DocumentSecurityDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/DocumentSecurityDialog.axaml.cs deleted file mode 100644 index 1e0e75c..0000000 --- a/src/PdfEditorApp/Views/Dialogs/DocumentSecurityDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class DocumentSecurityDialog : UserControl -{ - public DocumentSecurityDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/ExportSuccessDialog.axaml b/src/PdfEditorApp/Views/Dialogs/ExportSuccessDialog.axaml deleted file mode 100644 index 66797aa..0000000 --- a/src/PdfEditorApp/Views/Dialogs/ExportSuccessDialog.axaml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/HeaderFooterDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/HeaderFooterDialog.axaml.cs deleted file mode 100644 index b1f8cb2..0000000 --- a/src/PdfEditorApp/Views/Dialogs/HeaderFooterDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class HeaderFooterDialog : UserControl -{ - public HeaderFooterDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/MathEquationStudioDialog.axaml b/src/PdfEditorApp/Views/Dialogs/MathEquationStudioDialog.axaml deleted file mode 100644 index 38f9390..0000000 --- a/src/PdfEditorApp/Views/Dialogs/MathEquationStudioDialog.axaml +++ /dev/null @@ -1,317 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/MathEquationStudioDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/MathEquationStudioDialog.axaml.cs deleted file mode 100644 index c292e93..0000000 --- a/src/PdfEditorApp/Views/Dialogs/MathEquationStudioDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class MathEquationStudioDialog : UserControl -{ - public MathEquationStudioDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/NewDocumentDialog.axaml b/src/PdfEditorApp/Views/Dialogs/NewDocumentDialog.axaml deleted file mode 100644 index b268fc5..0000000 --- a/src/PdfEditorApp/Views/Dialogs/NewDocumentDialog.axaml +++ /dev/null @@ -1,561 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/PdfToolRunnerDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/PdfToolRunnerDialog.axaml.cs deleted file mode 100644 index 3dd9da2..0000000 --- a/src/PdfEditorApp/Views/Dialogs/PdfToolRunnerDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class PdfToolRunnerDialog : UserControl -{ - public PdfToolRunnerDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/PluginsDialog.axaml b/src/PdfEditorApp/Views/Dialogs/PluginsDialog.axaml deleted file mode 100644 index 54dd180..0000000 --- a/src/PdfEditorApp/Views/Dialogs/PluginsDialog.axaml +++ /dev/null @@ -1,424 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/PluginsDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/PluginsDialog.axaml.cs deleted file mode 100644 index 85112df..0000000 --- a/src/PdfEditorApp/Views/Dialogs/PluginsDialog.axaml.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Input; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class PluginsDialog : UserControl -{ - public PluginsDialog() - { - InitializeComponent(); - DragDrop.SetAllowDrop(this, true); - AddHandler(DragDrop.DragOverEvent, OnDragOver); - AddHandler(DragDrop.DropEvent, OnDrop); - } - - private void OnDragOver(object? sender, DragEventArgs e) - { - if (e.DataTransfer.Formats.Any(f => f == DataFormat.File)) - { - e.DragEffects = DragDropEffects.Copy; - } - else - { - e.DragEffects = DragDropEffects.None; - } - e.Handled = true; - } - - private async void OnDrop(object? sender, DragEventArgs e) - { - if (DataContext is MainViewModel vm && e.DataTransfer.Formats.Any(f => f == DataFormat.File)) - { - var files = e.DataTransfer.TryGetFiles(); - if (files != null) - { - foreach (var file in files) - { - var path = file.Path.LocalPath; - if (path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || - path.EndsWith(".fryplugin", StringComparison.OrdinalIgnoreCase)) - { - await vm.InstallAndMountPluginPathAsync(path); - } - } - } - } - e.Handled = true; - } - - private void OnBackdropPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (DataContext is MainViewModel vm) - { - vm.ClosePluginsDialog(); - } - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/PreflightDiagnosticsDialog.axaml b/src/PdfEditorApp/Views/Dialogs/PreflightDiagnosticsDialog.axaml deleted file mode 100644 index ad95159..0000000 --- a/src/PdfEditorApp/Views/Dialogs/PreflightDiagnosticsDialog.axaml +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/PreflightDiagnosticsDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/PreflightDiagnosticsDialog.axaml.cs deleted file mode 100644 index 809b9c8..0000000 --- a/src/PdfEditorApp/Views/Dialogs/PreflightDiagnosticsDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class PreflightDiagnosticsDialog : UserControl -{ - public PreflightDiagnosticsDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/RenameDocumentDialog.axaml b/src/PdfEditorApp/Views/Dialogs/RenameDocumentDialog.axaml deleted file mode 100644 index 039d1eb..0000000 --- a/src/PdfEditorApp/Views/Dialogs/RenameDocumentDialog.axaml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/SearchRedactDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/SearchRedactDialog.axaml.cs deleted file mode 100644 index f4b05ee..0000000 --- a/src/PdfEditorApp/Views/Dialogs/SearchRedactDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class SearchRedactDialog : UserControl -{ - public SearchRedactDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/ShortcutsHelpDialog.axaml b/src/PdfEditorApp/Views/Dialogs/ShortcutsHelpDialog.axaml deleted file mode 100644 index 0a0ff77..0000000 --- a/src/PdfEditorApp/Views/Dialogs/ShortcutsHelpDialog.axaml +++ /dev/null @@ -1,320 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/ShortcutsHelpDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/ShortcutsHelpDialog.axaml.cs deleted file mode 100644 index e18d2b5..0000000 --- a/src/PdfEditorApp/Views/Dialogs/ShortcutsHelpDialog.axaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Input; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class ShortcutsHelpDialog : UserControl -{ - public ShortcutsHelpDialog() - { - InitializeComponent(); - } - - private void OnBackdropPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (DataContext is MainViewModel vm) - { - vm.CloseShortcutsHelp(); - } - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/SignatureStudioDialog.axaml b/src/PdfEditorApp/Views/Dialogs/SignatureStudioDialog.axaml deleted file mode 100644 index eb22136..0000000 --- a/src/PdfEditorApp/Views/Dialogs/SignatureStudioDialog.axaml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/SignatureStudioDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/SignatureStudioDialog.axaml.cs deleted file mode 100644 index a7c25ba..0000000 --- a/src/PdfEditorApp/Views/Dialogs/SignatureStudioDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class SignatureStudioDialog : UserControl -{ - public SignatureStudioDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/SplitExtractDialog.axaml b/src/PdfEditorApp/Views/Dialogs/SplitExtractDialog.axaml deleted file mode 100644 index 592b60b..0000000 --- a/src/PdfEditorApp/Views/Dialogs/SplitExtractDialog.axaml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/SplitExtractDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/SplitExtractDialog.axaml.cs deleted file mode 100644 index 0ec7dfe..0000000 --- a/src/PdfEditorApp/Views/Dialogs/SplitExtractDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class SplitExtractDialog : UserControl -{ - public SplitExtractDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/WatermarkManagerDialog.axaml b/src/PdfEditorApp/Views/Dialogs/WatermarkManagerDialog.axaml deleted file mode 100644 index 1c21e82..0000000 --- a/src/PdfEditorApp/Views/Dialogs/WatermarkManagerDialog.axaml +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/WatermarkManagerDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/WatermarkManagerDialog.axaml.cs deleted file mode 100644 index 99e01af..0000000 --- a/src/PdfEditorApp/Views/Dialogs/WatermarkManagerDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class WatermarkManagerDialog : UserControl -{ - public WatermarkManagerDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Dialogs/WorkflowBuilderDialog.axaml b/src/PdfEditorApp/Views/Dialogs/WorkflowBuilderDialog.axaml deleted file mode 100644 index 64a0a7e..0000000 --- a/src/PdfEditorApp/Views/Dialogs/WorkflowBuilderDialog.axaml +++ /dev/null @@ -1,303 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Dialogs/WorkflowBuilderDialog.axaml.cs b/src/PdfEditorApp/Views/Dialogs/WorkflowBuilderDialog.axaml.cs deleted file mode 100644 index 261d4bd..0000000 --- a/src/PdfEditorApp/Views/Dialogs/WorkflowBuilderDialog.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Dialogs; - -public partial class WorkflowBuilderDialog : UserControl -{ - public WorkflowBuilderDialog() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/DocumentCanvasView.axaml b/src/PdfEditorApp/Views/DocumentCanvasView.axaml deleted file mode 100644 index d113caa..0000000 --- a/src/PdfEditorApp/Views/DocumentCanvasView.axaml +++ /dev/null @@ -1,1163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/DocumentCanvasView.axaml.cs b/src/PdfEditorApp/Views/DocumentCanvasView.axaml.cs deleted file mode 100644 index 166eb6e..0000000 --- a/src/PdfEditorApp/Views/DocumentCanvasView.axaml.cs +++ /dev/null @@ -1,1414 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.VisualTree; -using PdfEditorApp.Core.Models; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Views; - -public partial class DocumentCanvasView : UserControl -{ - private bool _isDraggingElement; - private bool _isResizingHandle; - private bool _isMarqueeSelecting; - private Point _marqueeStartPoint; - private DateTime _lastMarqueeSelectionUpdate = DateTime.MinValue; - private string? _activeResizeHandle; - private Point _lastPointerPosition; - private ElementViewModelBase? _draggedElement; - private List _draggedElements = new(); - - // Potential Drag Tracking (for reliable click vs drag detection) - private bool _isPotentialDrag; - private ElementViewModelBase? _potentialDragElement; - private Point _potentialDragStartPos; - private bool _wasAlreadySelectedOnPress; - - // In-place text editing baseline tracking - private readonly Dictionary _initialEditContents = new(); - - // Movement & Resize Undo Tracking - private double _dragStartElementX; - private double _dragStartElementY; - private List<(ElementViewModelBase Element, double X, double Y)> _dragStartPositions = new(); - private double _resizeStartX; - private double _resizeStartY; - private double _resizeStartW; - private double _resizeStartH; - - // Pan state - private bool _isSpacePressed; - private bool _isPanning; - private Point _panStart; - private Vector _scrollStart; - - // Pinch Gesture state - private bool _isPinching; - private double _pinchStartZoom = 1.0; - - // Zoom commit throttle — wheel/trackpad/pinch events can fire far faster than the - // LayoutTransformControl-driven canvas can afford to re-measure/re-arrange for. The delta - // math below still runs on every raw event (so a fast flick still compounds correctly and - // the cursor-anchor point stays accurate); only the actual write to ZoomLevel/Offset — - // the part that triggers an expensive layout pass — is coalesced to roughly once per frame. - private CancellationTokenSource? _zoomThrottleCts; - private double _pendingZoomLevel; - private Vector _pendingCanvasOffset; - - private double EffectiveZoomLevel => _zoomThrottleCts != null - ? _pendingZoomLevel - : (ViewModel != null && ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0); - - private Vector EffectiveCanvasOffset => _zoomThrottleCts != null - ? _pendingCanvasOffset - : (CanvasScrollViewer?.Offset ?? default); - - private void RequestZoomLevelChange(double newZoom, Vector newOffset) - { - if (ViewModel == null) return; - - _pendingZoomLevel = newZoom; - _pendingCanvasOffset = newOffset; - - if (_zoomThrottleCts != null) return; // a commit is already scheduled; it will pick up these latest values - - _zoomThrottleCts = new CancellationTokenSource(); - var token = _zoomThrottleCts.Token; - - _ = Task.Run(async () => - { - try - { - await Task.Delay(16, token); - } - catch (TaskCanceledException) - { - return; - } - if (token.IsCancellationRequested) return; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => CommitPendingZoom(token)); - }, token); - } - - private void CommitPendingZoom(CancellationToken token) - { - if (token.IsCancellationRequested) return; - _zoomThrottleCts = null; - var targetOffset = _pendingCanvasOffset; - if (ViewModel != null) ViewModel.ZoomLevel = _pendingZoomLevel; - if (CanvasScrollViewer != null) - { - CanvasScrollViewer.Offset = targetOffset; - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - if (CanvasScrollViewer != null) - { - CanvasScrollViewer.Offset = targetOffset; - } - }, Avalonia.Threading.DispatcherPriority.Loaded); - } - } - - /// Applies the pending zoom immediately instead of waiting out the throttle — - /// used when a gesture explicitly ends, so the canvas settles crisply. - private void FlushPendingZoom() - { - if (_zoomThrottleCts == null) return; - _zoomThrottleCts.Cancel(); - _zoomThrottleCts = null; - var targetOffset = _pendingCanvasOffset; - if (ViewModel != null) ViewModel.ZoomLevel = _pendingZoomLevel; - if (CanvasScrollViewer != null) - { - CanvasScrollViewer.Offset = targetOffset; - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - if (CanvasScrollViewer != null) - { - CanvasScrollViewer.Offset = targetOffset; - } - }, Avalonia.Threading.DispatcherPriority.Loaded); - } - } - - public DocumentCanvasView() - { - InitializeComponent(); - - GestureRecognizers.Add(new PinchGestureRecognizer()); - - AddHandler(PointerMovedEvent, OnGlobalPointerMoved, RoutingStrategies.Tunnel); - AddHandler(PointerReleasedEvent, OnGlobalPointerReleased, RoutingStrategies.Bubble); - AddHandler(PointerWheelChangedEvent, OnCanvasPointerWheelChanged, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PointerTouchPadGestureMagnifyEvent, OnCanvasTouchPadGestureMagnify, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PinchEvent, OnCanvasPinch, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PinchEndedEvent, OnCanvasPinchEnded, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(DoubleTappedEvent, OnCanvasDoubleTapped, RoutingStrategies.Tunnel); - AddHandler(KeyDownEvent, OnCanvasKeyDown, RoutingStrategies.Tunnel); - AddHandler(KeyUpEvent, OnCanvasKeyUp, RoutingStrategies.Tunnel); - - InspectorViewModel.OnActiveTextFormattingApplied = textVm => - { - if (ActiveInPlaceTextBox != null && ActiveInPlaceTextBox.DataContext == textVm) - { - ActiveInPlaceTextBox.Text = textVm.Text; - ActiveInPlaceTextBox.SelectionStart = textVm.ActiveSelectionStart; - ActiveInPlaceTextBox.SelectionEnd = textVm.ActiveSelectionStart + textVm.ActiveSelectionLength; - ActiveInPlaceTextBox.Focus(); - } - }; - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - if (CanvasScrollViewer != null) - { - CanvasScrollViewer.PropertyChanged += (s, ev) => - { - if (ev.Property == ScrollViewer.OffsetProperty || ev.Property == ScrollViewer.ViewportProperty) - { - UpdateViewportOnPlacementService(); - } - }; - } - } - - private void UpdateViewportOnPlacementService() - { - if (CanvasScrollViewer == null || PageElementsCanvas == null || ViewModel?.CurrentPage == null) return; - - double zoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - var scrollCenter = new Point(CanvasScrollViewer.Viewport.Width / 2.0, CanvasScrollViewer.Viewport.Height / 2.0); - var canvasCenter = CanvasScrollViewer.TranslatePoint(scrollCenter, PageElementsCanvas); - - if (canvasCenter.HasValue) - { - double pageCenterX = canvasCenter.Value.X / zoom; - double pageCenterY = canvasCenter.Value.Y / zoom; - double visibleWidth = CanvasScrollViewer.Viewport.Width / zoom; - double visibleHeight = CanvasScrollViewer.Viewport.Height / zoom; - - ViewModel.SmartPlacement.UpdateViewport(pageCenterX, pageCenterY, visibleWidth, visibleHeight); - } - } - - private MainViewModel? ViewModel => DataContext as MainViewModel; - - private void OnCanvasPointerWheelChanged(object? sender, PointerWheelEventArgs e) - { - // Support Cmd (Meta), Ctrl, and Option (Alt) modifier keys as well as Zoom tool mode - bool isZoomModifier = e.KeyModifiers.HasFlag(KeyModifiers.Control) || - e.KeyModifiers.HasFlag(KeyModifiers.Meta) || - e.KeyModifiers.HasFlag(KeyModifiers.Alt); - bool isZoomTool = ViewModel?.ActiveToolMode == ToolMode.Zoom; - - if ((isZoomModifier || isZoomTool) && ViewModel != null && CanvasScrollViewer != null) - { - double oldZoom = EffectiveZoomLevel; - double effectiveDelta = Math.Abs(e.Delta.Y) >= Math.Abs(e.Delta.X) ? e.Delta.Y : e.Delta.X; - - // Proportional smooth zoom supporting both discrete mouse wheel ticks and continuous trackpad pinch gestures - double zoomDeltaFactor; - if (Math.Abs(effectiveDelta) >= 1.0) - { - // Discrete mouse wheel tick (e.g. standard external wheel) - zoomDeltaFactor = effectiveDelta > 0 ? 1.15 : (1.0 / 1.15); - } - else if (Math.Abs(effectiveDelta) > 0.0001) - { - // High-precision smooth trackpad pinch / scroll gesture - zoomDeltaFactor = Math.Pow(1.002, effectiveDelta * 100); - } - else - { - zoomDeltaFactor = 1.0; - } - - double newZoom = Math.Clamp(Math.Round(oldZoom * zoomDeltaFactor, 3), 0.1, 5.0); - - if (Math.Abs(newZoom - oldZoom) > 0.001) - { - var mouseInViewer = e.GetPosition(CanvasScrollViewer); - double ratio = newZoom / oldZoom; - var baseOffset = EffectiveCanvasOffset; - double targetOffsetX = (baseOffset.X + mouseInViewer.X) * ratio - mouseInViewer.X; - double targetOffsetY = (baseOffset.Y + mouseInViewer.Y) * ratio - mouseInViewer.Y; - - RequestZoomLevelChange(newZoom, new Vector(Math.Max(0, targetOffsetX), Math.Max(0, targetOffsetY))); - } - - UpdateViewportOnPlacementService(); - e.Handled = true; - } - else if (e.KeyModifiers.HasFlag(KeyModifiers.Shift) && CanvasScrollViewer != null) - { - // Shift + Wheel -> Horizontal scroll - double delta = e.Delta.Y != 0 ? e.Delta.Y : e.Delta.X; - CanvasScrollViewer.Offset = new Vector( - Math.Max(0, CanvasScrollViewer.Offset.X - (delta * 40)), - CanvasScrollViewer.Offset.Y); - e.Handled = true; - } - } - - private void OnCanvasTouchPadGestureMagnify(object? sender, PointerDeltaEventArgs e) - { - if (ViewModel != null && CanvasScrollViewer != null) - { - double oldZoom = EffectiveZoomLevel; - double delta = Math.Abs(e.Delta.Y) >= Math.Abs(e.Delta.X) ? e.Delta.Y : e.Delta.X; - if (delta == 0 && (e.Delta.X != 0 || e.Delta.Y != 0)) - { - delta = e.Delta.X != 0 ? e.Delta.X : e.Delta.Y; - } - - double zoomFactor = 1.0 + delta; - double newZoom = Math.Clamp(Math.Round(oldZoom * zoomFactor, 3), 0.1, 5.0); - - if (Math.Abs(newZoom - oldZoom) > 0.001) - { - var mouseInViewer = e.GetPosition(CanvasScrollViewer); - double ratio = newZoom / oldZoom; - var baseOffset = EffectiveCanvasOffset; - double targetOffsetX = (baseOffset.X + mouseInViewer.X) * ratio - mouseInViewer.X; - double targetOffsetY = (baseOffset.Y + mouseInViewer.Y) * ratio - mouseInViewer.Y; - - RequestZoomLevelChange(newZoom, new Vector(Math.Max(0, targetOffsetX), Math.Max(0, targetOffsetY))); - } - - UpdateViewportOnPlacementService(); - e.Handled = true; - } - } - - private void OnCanvasPinch(object? sender, PinchEventArgs e) - { - if (ViewModel != null && CanvasScrollViewer != null) - { - if (!_isPinching) - { - _isPinching = true; - _pinchStartZoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - } - - // e.Scale is the total cumulative scale of the pinch gesture since start (starts at 1.0) - double targetZoom = Math.Clamp(Math.Round(_pinchStartZoom * e.Scale, 3), 0.1, 5.0); - double oldZ = EffectiveZoomLevel; - if (Math.Abs(targetZoom - oldZ) > 0.002 && oldZ > 0) - { - var origin = e.ScaleOrigin; - double ratio = targetZoom / oldZ; - var baseOffset = EffectiveCanvasOffset; - double newOffsetX = (baseOffset.X + origin.X) * ratio - origin.X; - double newOffsetY = (baseOffset.Y + origin.Y) * ratio - origin.Y; - RequestZoomLevelChange(targetZoom, new Vector(Math.Max(0, newOffsetX), Math.Max(0, newOffsetY))); - } - e.Handled = true; - } - } - - private void OnCanvasPinchEnded(object? sender, PinchEndedEventArgs e) - { - _isPinching = false; - FlushPendingZoom(); - UpdateViewportOnPlacementService(); - e.Handled = true; - } - - private void OnCanvasDoubleTapped(object? sender, TappedEventArgs e) - { - if (ViewModel == null || CanvasScrollViewer == null) return; - - if (ViewModel.ActiveToolMode == ToolMode.Pan || _isSpacePressed) - { - ViewModel.FitToPageDynamic(CanvasScrollViewer.Viewport.Width, CanvasScrollViewer.Viewport.Height); - e.Handled = true; - return; - } - else if (ViewModel.ActiveToolMode == ToolMode.Zoom) - { - ViewModel.ResetZoom(); - e.Handled = true; - return; - } - - // Ignore double-taps originating inside overlays (CanvasTextHudView, FindReplaceBarView) - if (e.Source is Visual sourceVisual && (sourceVisual.FindAncestorOfType() != null || sourceVisual.FindAncestorOfType() != null)) - { - return; - } - - // Direct in-place editing trigger on double-click - if (ViewModel.CurrentPage == null || PageElementsCanvas == null) return; - - // 1. Check if the double-tap happened directly on an element visual or its ancestor - ElementViewModelBase? targetElement = null; - if (e.Source is Visual visual) - { - targetElement = (visual as Control)?.DataContext as ElementViewModelBase ?? - visual.FindAncestorOfType()?.DataContext as ElementViewModelBase; - } - - // 2. If not found from visual source, check geometric coordinates on the canvas - if (targetElement == null) - { - var canvasPos = e.GetPosition(PageElementsCanvas); - double zoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - double docX = canvasPos.X / zoom; - double docY = canvasPos.Y / zoom; - - targetElement = ViewModel.CurrentPage.Elements - .Where(el => !el.IsLocked && docX >= el.X && docX <= el.X + el.Width && docY >= el.Y && docY <= el.Y + el.Height) - .OrderByDescending(el => el.ZIndex) - .FirstOrDefault(); - } - - if (targetElement == null) - { - targetElement = ViewModel.CurrentPage.SelectedElement; - } - - if (targetElement != null && !targetElement.IsLocked) - { - ViewModel.CurrentPage.SelectElement(targetElement); - - if (targetElement is TextElementViewModel textVm) - { - textVm.IsInEditMode = true; - e.Handled = true; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - FocusInPlaceTextBox(textVm); - }, Avalonia.Threading.DispatcherPriority.Input); - } - else if (targetElement is ShapeElementViewModel shapeVm) - { - shapeVm.IsInEditMode = true; - e.Handled = true; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - FocusInPlaceTextBox(shapeVm); - }, Avalonia.Threading.DispatcherPriority.Input); - } - else if (targetElement is StickyNoteElementViewModel stickyVm) - { - stickyVm.IsInEditMode = true; - e.Handled = true; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - FocusInPlaceTextBox(stickyVm); - }, Avalonia.Threading.DispatcherPriority.Loaded); - } - else if (targetElement is MathElementViewModel mathVm) - { - mathVm.IsInEditMode = true; - e.Handled = true; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - FocusInPlaceTextBox(mathVm); - }, Avalonia.Threading.DispatcherPriority.Loaded); - } - } - } - - private void OnCanvasBackgroundPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (ViewModel?.CurrentPage == null || PageElementsCanvas == null) return; - Focus(); - - var pointerPoint = e.GetCurrentPoint(this); - - // Middle button click, Spacebar held, or Pan Tool mode => Pan mode - if (_isSpacePressed || pointerPoint.Properties.IsMiddleButtonPressed || ViewModel.ActiveToolMode == ToolMode.Pan) - { - _isPanning = true; - _panStart = e.GetPosition(this); - if (CanvasScrollViewer != null) - { - _scrollStart = new Vector(CanvasScrollViewer.Offset.X, CanvasScrollViewer.Offset.Y); - } - Cursor = new Cursor(StandardCursorType.Hand); - e.Handled = true; - return; - } - - // Zoom Tool mode => Click to Zoom In / Alt-Click to Zoom Out - if (ViewModel.ActiveToolMode == ToolMode.Zoom) - { - if (pointerPoint.Properties.IsLeftButtonPressed) - { - bool isAlt = e.KeyModifiers.HasFlag(KeyModifiers.Alt); - if (isAlt) - { - ViewModel.ZoomOut(); - } - else - { - ViewModel.ZoomIn(); - } - UpdateViewportOnPlacementService(); - e.Handled = true; - return; - } - } - - var pos = e.GetPosition(PageElementsCanvas); - double zoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - double canvasX = Math.Max(0, pos.X / zoom); - double canvasY = Math.Max(0, pos.Y / zoom); - - ViewModel.LastCanvasClickPoint = (canvasX, canvasY); - - // Register right-click point for context-menu placement and AI Studio point targeting - if (pointerPoint.Properties.IsRightButtonPressed) - { - ViewModel.SmartPlacement.SetContextMenuPointer(canvasX, canvasY); - } - else if (ViewModel.AiAssistant.IsPointTargetMode) - { - ViewModel.AiAssistant.TargetPointOnPage(canvasX, canvasY, ViewModel.CurrentPage); - } - - if (ViewModel.ActiveToolMode == ToolMode.Draw || ViewModel.ActiveToolMode == ToolMode.Highlight) - { - bool isHighlighter = ViewModel.ActiveToolMode == ToolMode.Highlight; - var ink = new InkElementViewModel - { - X = Math.Max(0, canvasX), - Y = Math.Max(0, canvasY), - Width = 20, - Height = isHighlighter ? 18 : 6, - StrokeColorHex = isHighlighter ? "#FEF08A" : "#0F6CBD", - StrokeThickness = isHighlighter ? 14.0 : 3.0, - Opacity = isHighlighter ? 0.45 : 1.0, - IsHighlighter = isHighlighter - }; - - ViewModel.AddElementWithUndo(ink, isHighlighter ? "Added Highlighter Stroke" : "Added Ink Stroke"); - _isResizingHandle = true; - _activeResizeHandle = "bottomright"; - _draggedElement = ink; - _resizeStartX = ink.X; - _resizeStartY = ink.Y; - _resizeStartW = ink.Width; - _resizeStartH = ink.Height; - _lastPointerPosition = pos; - e.Pointer.Capture(this); - e.Handled = true; - return; - } - - // Marquee Selection Drag on background - bool isToggle = e.KeyModifiers.HasFlag(KeyModifiers.Shift) || e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Meta); - if (!isToggle) - { - ViewModel.CurrentPage.ClearSelection(); - } - - if (pointerPoint.Properties.IsLeftButtonPressed && e.Pointer.Type != PointerType.Touch) - { - _isMarqueeSelecting = true; - _marqueeStartPoint = pos; - if (MarqueeSelectionBox != null) - { - Canvas.SetLeft(MarqueeSelectionBox, pos.X); - Canvas.SetTop(MarqueeSelectionBox, pos.Y); - MarqueeSelectionBox.Width = 0; - MarqueeSelectionBox.Height = 0; - MarqueeSelectionBox.IsVisible = true; - } - e.Pointer.Capture(this); - e.Handled = true; - } - } - - private void OnElementPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (_isSpacePressed || e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed) - { - _isPanning = true; - _panStart = e.GetPosition(this); - if (CanvasScrollViewer != null) - { - _scrollStart = new Vector(CanvasScrollViewer.Offset.X, CanvasScrollViewer.Offset.Y); - } - e.Pointer.Capture(this); - e.Handled = true; - return; - } - - if (sender is Control control && control.DataContext is ElementViewModelBase elementVm) - { - // If the clicked element is locked (e.g. background watermark or full-page frame), - // check if there is an unlocked element under the cursor to select and edit instead. - if (elementVm.IsLocked && PageElementsCanvas != null && ViewModel?.CurrentPage != null) - { - var canvasPos = e.GetPosition(PageElementsCanvas); - double zoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - double docX = canvasPos.X / zoom; - double docY = canvasPos.Y / zoom; - - var topUnlocked = ViewModel.CurrentPage.Elements - .Where(el => !el.IsLocked && docX >= el.X && docX <= el.X + el.Width && docY >= el.Y && docY <= el.Y + el.Height) - .OrderByDescending(el => el.ZIndex) - .FirstOrDefault(); - - if (topUnlocked != null) - { - elementVm = topUnlocked; - } - } - - bool wasAlreadySelected = elementVm.IsSelected; - bool isToggle = e.KeyModifiers.HasFlag(KeyModifiers.Shift) || e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Meta); - - if (isToggle) - { - ViewModel?.CurrentPage?.ToggleElementSelection(elementVm); - } - else if (!elementVm.IsSelected) - { - ViewModel?.CurrentPage?.SelectElement(elementVm); - } - - if (!elementVm.IsInEditMode) - { - Focus(); - } - - if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed && PageElementsCanvas != null && ViewModel != null) - { - var pos = e.GetPosition(PageElementsCanvas); - double zoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - ViewModel.SmartPlacement.SetContextMenuPointer(pos.X / zoom, pos.Y / zoom); - } - - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && ViewModel?.CurrentPage != null && !elementVm.IsLocked) - { - if (elementVm.IsInEditMode) - { - return; - } - - _isPotentialDrag = true; - _potentialDragElement = elementVm; - _potentialDragStartPos = e.GetPosition(PageElementsCanvas); - _wasAlreadySelectedOnPress = wasAlreadySelected; - _lastPointerPosition = _potentialDragStartPos; - e.Handled = true; - } - } - } - - private void OnHandlePointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is Control control && control.Tag is string handleName && control.DataContext is ElementViewModelBase elementVm) - { - Focus(); - _isResizingHandle = true; - _activeResizeHandle = handleName; - _draggedElement = elementVm; - _resizeStartX = elementVm.X; - _resizeStartY = elementVm.Y; - _resizeStartW = elementVm.Width; - _resizeStartH = elementVm.Height; - _lastPointerPosition = e.GetPosition(PageElementsCanvas); - e.Pointer.Capture(this); - e.Handled = true; - } - } - - private void FocusInPlaceTextBox(ElementViewModelBase elementVm) - { - if (PageElementsCanvas == null) return; - - var textBox = PageElementsCanvas.GetVisualDescendants() - .OfType() - .FirstOrDefault(tb => tb.DataContext == elementVm && tb.IsVisible); - - if (textBox != null) - { - textBox.Focus(); - textBox.CaretIndex = textBox.Text?.Length ?? 0; - textBox.SelectAll(); - } - } - - public static TextBox? ActiveInPlaceTextBox { get; private set; } - - private static void UpdateSelectionFromTextBox(TextBox textBox) - { - if (textBox.DataContext is TextElementViewModel textVm) - { - int start = Math.Min(textBox.SelectionStart, textBox.SelectionEnd); - int end = Math.Max(textBox.SelectionStart, textBox.SelectionEnd); - int len = end - start; - - // If TextBox temporarily lost focus and reports 0 length while ViewModel already has a selection, - // preserve the selection so toolbar/HUD commands can format it! - if (!textBox.IsFocused && len == 0 && textVm.HasTextSelection) - { - return; - } - - string text = textBox.Text ?? ""; - string sel = len > 0 && start + len <= text.Length - ? text.Substring(start, len) - : string.Empty; - - textVm.UpdateTextSelection(start, len, sel); - } - } - - private void OnInPlaceTextBoxAttached(object? sender, VisualTreeAttachmentEventArgs e) - { - if (sender is TextBox textBox) - { - textBox.GotFocus += (s, args) => - { - ActiveInPlaceTextBox = textBox; - UpdateSelectionFromTextBox(textBox); - if (textBox.DataContext is ElementViewModelBase el) - { - _initialEditContents[el.Id] = textBox.Text ?? ""; - } - }; - - textBox.LostFocus += (s, args) => - { - if (textBox.DataContext is TextElementViewModel textVm && textVm.IsInEditMode) - { - // User is still in edit mode (e.g. interacting with HUD, Ribbon, or Sidebar). - // Keep ActiveInPlaceTextBox assigned so inline formatting commands continue working! - return; - } - - if (ActiveInPlaceTextBox == textBox) - { - ActiveInPlaceTextBox = null; - } - }; - - textBox.PropertyChanged += (s, args) => - { - if (args.Property == Visual.IsVisibleProperty && textBox.IsVisible) - { - ActiveInPlaceTextBox = textBox; - if (textBox.DataContext is ElementViewModelBase el) - { - _initialEditContents[el.Id] = textBox.Text ?? ""; - } - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - textBox.Focus(); - textBox.CaretIndex = textBox.Text?.Length ?? 0; - textBox.SelectAll(); - UpdateSelectionFromTextBox(textBox); - }, Avalonia.Threading.DispatcherPriority.Input); - } - else if (args.Property == TextBox.SelectionStartProperty || args.Property == TextBox.SelectionEndProperty || args.Property == TextBox.TextProperty) - { - UpdateSelectionFromTextBox(textBox); - } - }; - - textBox.PointerReleased += (s, args) => - { - UpdateSelectionFromTextBox(textBox); - }; - - textBox.KeyUp += (s, args) => - { - UpdateSelectionFromTextBox(textBox); - }; - - if (textBox.IsVisible) - { - ActiveInPlaceTextBox = textBox; - if (textBox.DataContext is ElementViewModelBase el) - { - _initialEditContents[el.Id] = textBox.Text ?? ""; - } - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - textBox.Focus(); - textBox.CaretIndex = textBox.Text?.Length ?? 0; - textBox.SelectAll(); - UpdateSelectionFromTextBox(textBox); - }, Avalonia.Threading.DispatcherPriority.Input); - } - } - } - - private void OnTextElementDoubleTapped(object? sender, TappedEventArgs e) - { - if (sender is Control control && control.DataContext is TextElementViewModel textVm) - { - if (textVm.IsLocked) return; - - if (textVm.Spans != null && textVm.Spans.Count > 0) - { - textVm.Text = textVm.GetMarkdownText(); - } - - textVm.IsInEditMode = true; - e.Handled = true; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - FocusInPlaceTextBox(textVm); - }, Avalonia.Threading.DispatcherPriority.Input); - } - } - - private void OnTextBoxLostFocus(object? sender, RoutedEventArgs e) - { - if (sender is Control control && control.DataContext is TextElementViewModel textVm) - { - // If the element is still in edit mode (e.g. interacting with HUD, Ribbon, or Sidebar), - // DO NOT exit edit mode or wipe spans! - if (textVm.IsInEditMode) - { - return; - } - - _initialEditContents.TryGetValue(textVm.Id, out var oldTxt); - string finalPlain = textVm.Text ?? ""; - var finalSpans = textVm.Spans?.Select(s => s.Clone()).ToList(); - - if (finalPlain != oldTxt) - { - _initialEditContents[textVm.Id] = finalPlain; - var savedSpans = finalSpans; - ViewModel?.UndoRedo.RecordAction( - "Edit Text", - () => { - textVm.Text = oldTxt ?? ""; - textVm.Spans = null; - }, - () => { - textVm.Text = finalPlain; - textVm.Spans = savedSpans != null ? savedSpans.Select(s => s.Clone()).ToList() : null; - } - ); - } - } - } - - private void OnShapeElementDoubleTapped(object? sender, TappedEventArgs e) - { - if (sender is Control control && control.DataContext is ShapeElementViewModel shapeVm) - { - shapeVm.IsInEditMode = true; - e.Handled = true; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - FocusInPlaceTextBox(shapeVm); - }, Avalonia.Threading.DispatcherPriority.Input); - } - } - - private void OnShapeTextBoxLostFocus(object? sender, RoutedEventArgs e) - { - if (sender is Control control && control.DataContext is ShapeElementViewModel shapeVm) - { - string currentLabel = shapeVm.Label ?? ""; - if (_initialEditContents.TryGetValue(shapeVm.Id, out var oldTxt) && currentLabel != oldTxt) - { - _initialEditContents[shapeVm.Id] = currentLabel; - ViewModel?.UndoRedo.RecordAction( - "Edit Shape Label", - () => shapeVm.Label = oldTxt, - () => shapeVm.Label = currentLabel - ); - } - } - } - - private void OnTableCellLostFocus(object? sender, RoutedEventArgs e) - { - // Table cells have two-way binding directly to TableHeaderItem/TableCellItem - } - - private void OnStickyNoteTextBoxLostFocus(object? sender, RoutedEventArgs e) - { - // Sticky note text has two-way binding directly to NoteText - } - - private void OnMathElementDoubleTapped(object? sender, TappedEventArgs e) - { - if (sender is Control control && control.DataContext is MathElementViewModel mathVm) - { - ViewModel?.OpenMathStudioCommand.Execute(mathVm); - e.Handled = true; - } - } - - private void OnMathTextBoxLostFocus(object? sender, RoutedEventArgs e) - { - if (sender is Control control && control.DataContext is MathElementViewModel mathVm) - { - if (_initialEditContents.TryGetValue(mathVm.Id, out var oldFormula) && mathVm.Formula != oldFormula) - { - string newFormula = mathVm.Formula; - _initialEditContents[mathVm.Id] = newFormula; - ViewModel?.UndoRedo.RecordAction( - "Edit Formula", - () => { mathVm.Formula = oldFormula; mathVm.RenderSvg(); }, - () => { mathVm.Formula = newFormula; mathVm.RenderSvg(); } - ); - } - mathVm.IsInEditMode = false; - mathVm.RenderSvg(); - } - } - - private void OnGlobalPointerMoved(object? sender, PointerEventArgs e) - { - if (_isPanning && CanvasScrollViewer != null) - { - var curPos = e.GetPosition(this); - var delta = curPos - _panStart; - CanvasScrollViewer.Offset = new Vector( - Math.Max(0, _scrollStart.X - delta.X), - Math.Max(0, _scrollStart.Y - delta.Y)); - e.Handled = true; - return; - } - - if (PageElementsCanvas != null && ViewModel != null) - { - var pPos = e.GetPosition(PageElementsCanvas); - double z = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - ViewModel.CursorCanvasX = Math.Max(0, pPos.X / z); - ViewModel.CursorCanvasY = Math.Max(0, pPos.Y / z); - } - - if (_isMarqueeSelecting && PageElementsCanvas != null && ViewModel?.CurrentPage != null) - { - var curPos = e.GetPosition(PageElementsCanvas); - double minX = Math.Min(_marqueeStartPoint.X, curPos.X); - double minY = Math.Min(_marqueeStartPoint.Y, curPos.Y); - double w = Math.Abs(curPos.X - _marqueeStartPoint.X); - double h = Math.Abs(curPos.Y - _marqueeStartPoint.Y); - - // The box itself tracks the cursor on every move (cheap, needs to feel precise). - // Recomputing which elements intersect it is an O(n) scan over the whole page - // followed by another O(n) pass inside SelectElements, so that part is throttled — - // OnGlobalPointerReleased always does one final, unthrottled pass so the committed - // selection is never stale. - if (MarqueeSelectionBox != null) - { - Canvas.SetLeft(MarqueeSelectionBox, minX); - Canvas.SetTop(MarqueeSelectionBox, minY); - MarqueeSelectionBox.Width = w; - MarqueeSelectionBox.Height = h; - } - - var now = DateTime.UtcNow; - if ((now - _lastMarqueeSelectionUpdate).TotalMilliseconds >= 16) - { - _lastMarqueeSelectionUpdate = now; - UpdateMarqueeSelection(minX, minY, w, h); - } - - e.Handled = true; - return; - } - - if (_isPotentialDrag && !_isDraggingElement && PageElementsCanvas != null && ViewModel?.CurrentPage != null) - { - var curPos = e.GetPosition(PageElementsCanvas); - double distSq = Math.Pow(curPos.X - _potentialDragStartPos.X, 2) + Math.Pow(curPos.Y - _potentialDragStartPos.Y, 2); - if (distSq > 9.0) // 3px drag threshold - { - _isDraggingElement = true; - _draggedElement = _potentialDragElement; - _draggedElements = ViewModel.CurrentPage.SelectedElements.Where(el => !el.IsLocked).ToList(); - if (_draggedElement != null && !_draggedElements.Contains(_draggedElement)) _draggedElements.Add(_draggedElement); - - if (_draggedElement != null && !string.IsNullOrEmpty(_draggedElement.GroupId)) - { - var groupMembers = ViewModel.CurrentPage.Elements.Where(el => el.GroupId == _draggedElement.GroupId && !el.IsLocked); - foreach (var gm in groupMembers) - { - if (!_draggedElements.Contains(gm)) _draggedElements.Add(gm); - } - } - - _dragStartPositions = _draggedElements.Select(el => (Element: el, el.X, el.Y)).ToList(); - if (_draggedElement != null) - { - _dragStartElementX = _draggedElement.X; - _dragStartElementY = _draggedElement.Y; - } - _lastPointerPosition = curPos; - e.Pointer.Capture(this); - } - } - - if (PageElementsCanvas == null || ViewModel?.CurrentPage == null) return; - if (!_isResizingHandle && !_isDraggingElement) return; - - var currentPos = e.GetPosition(PageElementsCanvas); - double deltaX = currentPos.X - _lastPointerPosition.X; - double deltaY = currentPos.Y - _lastPointerPosition.Y; - - double zoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - deltaX /= zoom; - deltaY /= zoom; - - if (_isResizingHandle && _draggedElement != null && !string.IsNullOrEmpty(_activeResizeHandle)) - { - _draggedElement.Resize(_activeResizeHandle, deltaX, deltaY); - _lastPointerPosition = currentPos; - ViewModel.CurrentPage.UpdateSelectionBoundingBox(); - } - else if (_isDraggingElement && _draggedElements.Count > 0) - { - foreach (var el in _draggedElements) - { - el.MoveBy(deltaX, deltaY, ViewModel.CurrentPage.Width, ViewModel.CurrentPage.Height); - } - _lastPointerPosition = currentPos; - ViewModel.CurrentPage.UpdateSelectionBoundingBox(); - } - } - - private void OnCanvasPointerMoved(object? sender, PointerEventArgs e) - { - // Handled by tunnel/bubble or direct event - } - - private void UpdateMarqueeSelection(double boxLeft, double boxTop, double boxWidth, double boxHeight) - { - if (ViewModel?.CurrentPage == null) return; - - double z = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - var marqueeRect = new Rect(boxLeft / z, boxTop / z, Math.Max(1, boxWidth / z), Math.Max(1, boxHeight / z)); - - var intersecting = ViewModel.CurrentPage.Elements - .Where(el => marqueeRect.Intersects(new Rect(el.X, el.Y, Math.Max(1, el.Width), Math.Max(1, el.Height)))) - .ToList(); - - ViewModel.CurrentPage.SelectElements(intersecting); - } - - private void OnGlobalPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (_isPotentialDrag && !_isDraggingElement && !_isResizingHandle && !_isMarqueeSelecting) - { - // Click without drag occurred! - if (_wasAlreadySelectedOnPress && _potentialDragElement != null && !_potentialDragElement.IsInEditMode) - { - // Second click on already selected element enters in-place edit mode! - if (_potentialDragElement is TextElementViewModel textVm) - { - textVm.IsInEditMode = true; - Avalonia.Threading.Dispatcher.UIThread.Post(() => FocusInPlaceTextBox(textVm), Avalonia.Threading.DispatcherPriority.Input); - } - else if (_potentialDragElement is ShapeElementViewModel shapeVm) - { - shapeVm.IsInEditMode = true; - Avalonia.Threading.Dispatcher.UIThread.Post(() => FocusInPlaceTextBox(shapeVm), Avalonia.Threading.DispatcherPriority.Input); - } - } - } - _isPotentialDrag = false; - _potentialDragElement = null; - - if (_isMarqueeSelecting) - { - // Final, unthrottled pass — guarantees the committed selection matches exactly - // where the box ended up, regardless of the move handler's throttle timing. Only - // for an actual drag: a plain click (no move at all, box still at its 0x0 press - // size) must leave selection untouched, matching the previous behavior — otherwise - // a shift/ctrl-click with no drag would wipe out the existing selection. - if (MarqueeSelectionBox != null) - { - if (MarqueeSelectionBox.Width > 2 || MarqueeSelectionBox.Height > 2) - { - UpdateMarqueeSelection(Canvas.GetLeft(MarqueeSelectionBox), Canvas.GetTop(MarqueeSelectionBox), MarqueeSelectionBox.Width, MarqueeSelectionBox.Height); - } - MarqueeSelectionBox.IsVisible = false; - } - _isMarqueeSelecting = false; - } - else if (_isResizingHandle && _draggedElement != null) - { - var el = _draggedElement; - if (ViewModel != null && ViewModel.SnapToGrid) - { - double snap = (double)(int)ViewModel.GridSnapSize; - if (snap > 1) - { - el.X = Math.Round(el.X / snap) * snap; - el.Y = Math.Round(el.Y / snap) * snap; - el.Width = Math.Max(snap, Math.Round(el.Width / snap) * snap); - el.Height = Math.Max(snap, Math.Round(el.Height / snap) * snap); - } - } - - double fromX = _resizeStartX, fromY = _resizeStartY, fromW = _resizeStartW, fromH = _resizeStartH; - double toX = el.X, toY = el.Y, toW = el.Width, toH = el.Height; - if (Math.Abs(toW - fromW) > 0.5 || Math.Abs(toH - fromH) > 0.5 || Math.Abs(toX - fromX) > 0.5 || Math.Abs(toY - fromY) > 0.5) - { - ViewModel?.UndoRedo.RecordAction( - $"Resize {el.DisplayName}", - () => { el.X = fromX; el.Y = fromY; el.Width = fromW; el.Height = fromH; ViewModel?.CurrentPage?.UpdateSelectionBoundingBox(); }, - () => { el.X = toX; el.Y = toY; el.Width = toW; el.Height = toH; ViewModel?.CurrentPage?.UpdateSelectionBoundingBox(); } - ); - } - } - else if (_isDraggingElement && _draggedElements.Count > 0) - { - if (ViewModel != null && ViewModel.SnapToGrid) - { - double snap = (double)(int)ViewModel.GridSnapSize; - if (snap > 1) - { - foreach (var el in _draggedElements) - { - el.X = Math.Round(el.X / snap) * snap; - el.Y = Math.Round(el.Y / snap) * snap; - } - } - } - - var initialPositions = _dragStartPositions.ToList(); - var finalPositions = _draggedElements.Select(el => (Element: el, el.X, el.Y)).ToList(); - - bool anyMoved = false; - for (int i = 0; i < initialPositions.Count; i++) - { - if (Math.Abs(initialPositions[i].X - finalPositions[i].X) > 0.5 || Math.Abs(initialPositions[i].Y - finalPositions[i].Y) > 0.5) - { - anyMoved = true; - break; - } - } - - if (anyMoved) - { - string desc = _draggedElements.Count == 1 ? $"Move {_draggedElements[0].DisplayName}" : $"Move {_draggedElements.Count} Elements"; - ViewModel?.UndoRedo.RecordAction( - desc, - () => { - foreach (var p in initialPositions) { p.Element.X = p.X; p.Element.Y = p.Y; } - ViewModel?.CurrentPage?.UpdateSelectionBoundingBox(); - }, - () => { - foreach (var p in finalPositions) { p.Element.X = p.X; p.Element.Y = p.Y; } - ViewModel?.CurrentPage?.UpdateSelectionBoundingBox(); - } - ); - } - } - - if (e.Pointer.Captured == this) - { - e.Pointer.Capture(null); - } - _isDraggingElement = false; - _isResizingHandle = false; - _activeResizeHandle = null; - _draggedElement = null; - _draggedElements.Clear(); - _dragStartPositions.Clear(); - _isPanning = false; - } - - protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) - { - base.OnPointerCaptureLost(e); - if (MarqueeSelectionBox != null) MarqueeSelectionBox.IsVisible = false; - _isMarqueeSelecting = false; - _isDraggingElement = false; - _isResizingHandle = false; - _activeResizeHandle = null; - _draggedElement = null; - _draggedElements.Clear(); - _dragStartPositions.Clear(); - _isPanning = false; - } - - private void OnCanvasKeyUp(object? sender, KeyEventArgs e) - { - if (e.Key == Key.Space) - { - _isSpacePressed = false; - Cursor = Cursor.Default; - } - } - - private void OnCanvasKeyDown(object? sender, KeyEventArgs e) - { - if (ViewModel?.CurrentPage == null) return; - - var selected = ViewModel.CurrentPage.SelectedElement; - - // Check if user is actively typing inside any TextBox / TextPresenter - var topLevel = TopLevel.GetTopLevel(this); - var focused = topLevel?.FocusManager?.GetFocusedElement(); - bool isFocusedInTextBox = focused is TextBox || - focused is Avalonia.Controls.Presenters.TextPresenter || - (focused is Control ctrl && ctrl.FindAncestorOfType() != null); - - bool isSourceTextBox = e.Source is TextBox || - e.Source is Avalonia.Controls.Presenters.TextPresenter || - (e.Source is Control sCtrl && sCtrl.FindAncestorOfType() != null); - - bool isEditingText = isFocusedInTextBox || isSourceTextBox || (selected != null && selected.IsInEditMode); - - if (isEditingText) - { - if (e.Key == Key.Escape) - { - if (selected != null) - { - selected.IsInEditMode = false; - } - e.Handled = true; - } - else if (e.Key == Key.Enter && (e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Meta))) - { - if (selected != null) - { - selected.IsInEditMode = false; - } - e.Handled = true; - } - // Allow all typing, backspace, delete, arrows, and shortcuts to flow to the TextBox - return; - } - - if (e.Key == Key.Space && !_isSpacePressed) - { - _isSpacePressed = true; - Cursor = new Cursor(StandardCursorType.Hand); - return; - } - - bool isCtrlOrCmd = e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Meta); - double step = e.KeyModifiers.HasFlag(KeyModifiers.Shift) ? 10.0 : 1.0; - - if (isCtrlOrCmd) - { - switch (e.Key) - { - case Key.D0: - case Key.NumPad0: - ViewModel.ResetZoom(); - e.Handled = true; - break; - case Key.D1: - case Key.NumPad1: - ViewModel.FitToWidthDynamic(CanvasScrollViewer?.Viewport.Width ?? 800); - e.Handled = true; - break; - case Key.D9: - case Key.NumPad9: - ViewModel.FitToPageDynamic(CanvasScrollViewer?.Viewport.Width ?? 800, CanvasScrollViewer?.Viewport.Height ?? 800); - e.Handled = true; - break; - case Key.D2: - case Key.NumPad2: - ViewModel.FitToWidthDynamic(CanvasScrollViewer?.Viewport.Width ?? 800); - e.Handled = true; - break; - case Key.OemPlus: - case Key.Add: - ViewModel.ZoomIn(); - e.Handled = true; - break; - case Key.OemMinus: - case Key.Subtract: - ViewModel.ZoomOut(); - e.Handled = true; - break; - case Key.A: - ViewModel.CurrentPage?.SelectAll(); - e.Handled = true; - break; - case Key.C: - ViewModel.CopyCommand.Execute(null); - e.Handled = true; - break; - case Key.X: - ViewModel.CutCommand.Execute(null); - e.Handled = true; - break; - case Key.V: - ViewModel.PasteCommand.Execute(null); - e.Handled = true; - break; - case Key.D: - ViewModel.DuplicateCommand.Execute(null); - e.Handled = true; - break; - case Key.Z: - ViewModel.UndoCommand.Execute(null); - e.Handled = true; - break; - case Key.Y: - ViewModel.RedoCommand.Execute(null); - e.Handled = true; - break; - case Key.S: - ViewModel.SaveProjectCommand.Execute(null); - e.Handled = true; - break; - case Key.OemCloseBrackets: - ViewModel.Inspector.BringToFrontCommand.Execute(null); - e.Handled = true; - break; - case Key.OemOpenBrackets: - ViewModel.Inspector.SendToBackCommand.Execute(null); - e.Handled = true; - break; - } - } - else if (selected != null || (ViewModel.CurrentPage?.SelectedElements.Count ?? 0) > 0) - { - // If user starts typing any alphanumeric or punctuation character while a text element is selected, - // immediately enter in-place edit mode and route focus to the TextBox! - if (selected is TextElementViewModel textEl && !textEl.IsInEditMode && !isCtrlOrCmd && !e.KeyModifiers.HasFlag(KeyModifiers.Alt)) - { - bool isPrintable = (e.Key >= Key.A && e.Key <= Key.Z) || - (e.Key >= Key.D0 && e.Key <= Key.D9) || - (e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9) || - e.Key == Key.Space || e.Key == Key.Enter || e.Key == Key.F2 || - e.Key == Key.OemPeriod || e.Key == Key.OemComma || e.Key == Key.OemMinus || e.Key == Key.OemPlus; - - if (isPrintable) - { - textEl.IsInEditMode = true; - - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - FocusInPlaceTextBox(textEl); - }, Avalonia.Threading.DispatcherPriority.Input); - - if (e.Key == Key.Enter || e.Key == Key.F2) - { - e.Handled = true; - return; - } - return; - } - } - else if (selected is ShapeElementViewModel shapeEl && !shapeEl.IsInEditMode && !isCtrlOrCmd && !e.KeyModifiers.HasFlag(KeyModifiers.Alt)) - { - if (e.Key == Key.Enter || e.Key == Key.F2) - { - shapeEl.IsInEditMode = true; - e.Handled = true; - Avalonia.Threading.Dispatcher.UIThread.Post(() => FocusInPlaceTextBox(shapeEl), Avalonia.Threading.DispatcherPriority.Input); - return; - } - } - - switch (e.Key) - { - case Key.Enter: - case Key.F2: - break; - case Key.Delete: - case Key.Back: - ViewModel.Inspector.DeleteSelectedElementCommand.Execute(null); - e.Handled = true; - break; - case Key.Escape: - ViewModel.CurrentPage?.ClearSelection(); - e.Handled = true; - break; - case Key.Left: - case Key.Right: - case Key.Up: - case Key.Down: - if (selected != null && ViewModel?.CurrentPage is { } page) - { - double oldX = selected.X; - double oldY = selected.Y; - - if (e.Key == Key.Left) selected.X = Math.Max(0, selected.X - step); - else if (e.Key == Key.Right) selected.X = Math.Min(page.Width - selected.Width, selected.X + step); - else if (e.Key == Key.Up) selected.Y = Math.Max(0, selected.Y - step); - else if (e.Key == Key.Down) selected.Y = Math.Min(page.Height - selected.Height, selected.Y + step); - - double newX = selected.X; - double newY = selected.Y; - var el = selected; - ViewModel.UndoRedo.RecordAction( - $"Nudge {el.DisplayName}", - () => { el.X = oldX; el.Y = oldY; }, - () => { el.X = newX; el.Y = newY; } - ); - e.Handled = true; - } - break; - case Key.OemCloseBrackets: - ViewModel.Inspector.BringToFrontCommand.Execute(null); - e.Handled = true; - break; - case Key.OemOpenBrackets: - ViewModel.Inspector.SendToBackCommand.Execute(null); - e.Handled = true; - break; - } - } - else - { - // Global quick single-key tool switches (when no element is selected) - switch (e.Key) - { - case Key.V: - ViewModel.SetToolModeCommand.Execute("Select"); - e.Handled = true; - break; - case Key.H: - ViewModel.SetToolModeCommand.Execute("Pan"); - e.Handled = true; - break; - case Key.Z: - ViewModel.SetToolModeCommand.Execute("Zoom"); - e.Handled = true; - break; - case Key.T: - ViewModel.AddTextElementCommand.Execute(null); - e.Handled = true; - break; - case Key.R: - ViewModel.AddShapeElementCommand.Execute("Rectangle"); - e.Handled = true; - break; - case Key.N: - ViewModel.AddStickyNoteElementCommand.Execute(null); - e.Handled = true; - break; - case Key.D: - ViewModel.AddInkElementCommand.Execute(false); - e.Handled = true; - break; - } - } - } -} diff --git a/src/PdfEditorApp/Views/FindReplaceBarView.axaml b/src/PdfEditorApp/Views/FindReplaceBarView.axaml deleted file mode 100644 index df4d4fe..0000000 --- a/src/PdfEditorApp/Views/FindReplaceBarView.axaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/FindReplaceBarView.axaml.cs b/src/PdfEditorApp/Views/FindReplaceBarView.axaml.cs deleted file mode 100644 index 880ea77..0000000 --- a/src/PdfEditorApp/Views/FindReplaceBarView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class FindReplaceBarView : UserControl -{ - public FindReplaceBarView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/FontManagerPageView.axaml b/src/PdfEditorApp/Views/FontManagerPageView.axaml deleted file mode 100644 index 91325fa..0000000 --- a/src/PdfEditorApp/Views/FontManagerPageView.axaml +++ /dev/null @@ -1,412 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/FontManagerPageView.axaml.cs b/src/PdfEditorApp/Views/FontManagerPageView.axaml.cs deleted file mode 100644 index 7492942..0000000 --- a/src/PdfEditorApp/Views/FontManagerPageView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class FontManagerPageView : UserControl -{ - public FontManagerPageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/FryPdfViewerView.axaml b/src/PdfEditorApp/Views/FryPdfViewerView.axaml deleted file mode 100644 index 4e4c389..0000000 --- a/src/PdfEditorApp/Views/FryPdfViewerView.axaml +++ /dev/null @@ -1,1035 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/FryPdfViewerView.axaml.cs b/src/PdfEditorApp/Views/FryPdfViewerView.axaml.cs deleted file mode 100644 index d77a8e1..0000000 --- a/src/PdfEditorApp/Views/FryPdfViewerView.axaml.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Markup.Xaml; -using PdfEditorApp.ViewModels.FryPdfViewer; - -namespace PdfEditorApp.Views; - -public partial class FryPdfViewerView : UserControl -{ - public FryPdfViewerView() - { - InitializeComponent(); - Loaded += (s, e) => Focus(); - PointerPressed += (s, e) => Focus(); - - var scrollViewer = this.FindControl("ViewportScrollViewer"); - if (scrollViewer != null) - { - scrollViewer.SizeChanged += (s, e) => - { - if (DataContext is FryPdfViewerViewModel vm && vm.IsPresentationMode) - { - vm.FitToViewport(e.NewSize.Width, e.NewSize.Height); - } - }; - } - - DataContextChanged += (s, e) => - { - if (DataContext is FryPdfViewerViewModel vm) - { - vm.PropertyChanged += (sender, args) => - { - if (args.PropertyName == nameof(FryPdfViewerViewModel.IsPresentationMode)) - { - if (vm.IsPresentationMode && scrollViewer != null) - { - vm.FitToViewport(scrollViewer.Bounds.Width, scrollViewer.Bounds.Height); - } - } - }; - } - }; - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - - if (DataContext is not FryPdfViewerViewModel vm) - return; - - // If focus is currently inside a text input or search bar, don't hijack typing - if (TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() is TextBox) - return; - - if (e.Key == Key.Right || e.Key == Key.Down || e.Key == Key.PageDown || e.Key == Key.Space) - { - if (vm.CanGoNextPage) - { - vm.NextPage(); - e.Handled = true; - } - } - else if (e.Key == Key.Left || e.Key == Key.Up || e.Key == Key.PageUp || e.Key == Key.Back) - { - if (vm.CanGoPreviousPage) - { - vm.PreviousPage(); - e.Handled = true; - } - } - else if (e.Key == Key.Home) - { - vm.FirstPage(); - e.Handled = true; - } - else if (e.Key == Key.End) - { - vm.LastPage(); - e.Handled = true; - } - else if (e.Key == Key.Escape && vm.IsPresentationMode) - { - vm.IsPresentationMode = false; - e.Handled = true; - } - } -} diff --git a/src/PdfEditorApp/Views/HelpGuidePageView.axaml b/src/PdfEditorApp/Views/HelpGuidePageView.axaml deleted file mode 100644 index dd9ec02..0000000 --- a/src/PdfEditorApp/Views/HelpGuidePageView.axaml +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/HelpGuidePageView.axaml.cs b/src/PdfEditorApp/Views/HelpGuidePageView.axaml.cs deleted file mode 100644 index 78fc891..0000000 --- a/src/PdfEditorApp/Views/HelpGuidePageView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class HelpGuidePageView : UserControl -{ - public HelpGuidePageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/HomeDashboardView.axaml b/src/PdfEditorApp/Views/HomeDashboardView.axaml deleted file mode 100644 index b944e4e..0000000 --- a/src/PdfEditorApp/Views/HomeDashboardView.axaml +++ /dev/null @@ -1,761 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/HomeDashboardView.axaml.cs b/src/PdfEditorApp/Views/HomeDashboardView.axaml.cs deleted file mode 100644 index 07fdefc..0000000 --- a/src/PdfEditorApp/Views/HomeDashboardView.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Core; -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class HomeDashboardView : UserControl -{ - public HomeDashboardView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/HomeView.axaml b/src/PdfEditorApp/Views/HomeView.axaml deleted file mode 100644 index 79cf865..0000000 --- a/src/PdfEditorApp/Views/HomeView.axaml +++ /dev/null @@ -1,627 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/HomeView.axaml.cs b/src/PdfEditorApp/Views/HomeView.axaml.cs deleted file mode 100644 index 6ccb105..0000000 --- a/src/PdfEditorApp/Views/HomeView.axaml.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.ComponentModel; -using Avalonia; -using Avalonia.Controls; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views; - -public partial class HomeView : UserControl -{ - public HomeView() - { - InitializeComponent(); - } - - protected override void OnDataContextChanged(EventArgs e) - { - base.OnDataContextChanged(e); - if (DataContext is HomeViewModel vm) - { - vm.PropertyChanged -= OnHomeViewModelPropertyChanged; - vm.PropertyChanged += OnHomeViewModelPropertyChanged; - } - } - - private void OnHomeViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(HomeViewModel.SelectedNavSection)) - { - if (MainContentScrollViewer != null) - { - MainContentScrollViewer.Offset = new Vector(0, 0); - } - } - } -} diff --git a/src/PdfEditorApp/Views/InspectorSidebarView.axaml b/src/PdfEditorApp/Views/InspectorSidebarView.axaml deleted file mode 100644 index e6fe13a..0000000 --- a/src/PdfEditorApp/Views/InspectorSidebarView.axaml +++ /dev/null @@ -1,3319 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/InspectorSidebarView.axaml.cs b/src/PdfEditorApp/Views/InspectorSidebarView.axaml.cs deleted file mode 100644 index d296acc..0000000 --- a/src/PdfEditorApp/Views/InspectorSidebarView.axaml.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Material.Icons; -using Material.Icons.Avalonia; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views; - -public partial class InspectorSidebarView : UserControl -{ - private bool _isResizingTextEditor; - private Point _resizeStartPoint; - private double _resizeStartHeight; - private const double DefaultEditorHeight = 72.0; - private const double ExpandedEditorHeight = 180.0; - private const double MinEditorHeight = 44.0; - private const double MaxEditorHeight = 450.0; - - public InspectorSidebarView() - { - InitializeComponent(); - } - - private void OnSidebarTextBoxAttached(object? sender, VisualTreeAttachmentEventArgs e) - { - if (sender is TextBox textBox) - { - textBox.PropertyChanged += (s, args) => - { - if (args.Property == TextBox.SelectionStartProperty || args.Property == TextBox.SelectionEndProperty || args.Property == TextBox.TextProperty) - { - UpdateSidebarSelection(textBox); - } - }; - - textBox.PointerReleased += (s, args) => UpdateSidebarSelection(textBox); - textBox.KeyUp += (s, args) => UpdateSidebarSelection(textBox); - } - } - - private void OnTextEditorResizeGripPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is Control grip && e.GetCurrentPoint(grip).Properties.IsLeftButtonPressed) - { - _isResizingTextEditor = true; - _resizeStartPoint = e.GetPosition(this); - var textBox = this.FindControl("SidebarTextEditor"); - _resizeStartHeight = textBox?.Bounds.Height > 0 ? textBox.Bounds.Height : (textBox?.Height ?? DefaultEditorHeight); - e.Pointer.Capture(grip); - e.Handled = true; - } - } - - private void OnTextEditorResizeGripPointerMoved(object? sender, PointerEventArgs e) - { - if (_isResizingTextEditor) - { - var currentPoint = e.GetPosition(this); - double deltaY = currentPoint.Y - _resizeStartPoint.Y; - double newHeight = Math.Clamp(_resizeStartHeight + deltaY, MinEditorHeight, MaxEditorHeight); - var textBox = this.FindControl("SidebarTextEditor"); - if (textBox != null) - { - textBox.Height = newHeight; - } - e.Handled = true; - } - } - - private void OnTextEditorResizeGripPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (_isResizingTextEditor) - { - _isResizingTextEditor = false; - e.Pointer.Capture(null); - e.Handled = true; - UpdateExpandIcon(); - } - } - - private void OnTextEditorResizeGripDoubleTapped(object? sender, TappedEventArgs e) - { - ToggleEditorSize(); - e.Handled = true; - } - - private void OnToggleTextEditorExpandClicked(object? sender, RoutedEventArgs e) - { - ToggleEditorSize(); - } - - private void ToggleEditorSize() - { - var textBox = this.FindControl("SidebarTextEditor"); - if (textBox != null) - { - double current = textBox.Height; - if (double.IsNaN(current) || current <= DefaultEditorHeight + 20) - { - textBox.Height = ExpandedEditorHeight; - } - else - { - textBox.Height = DefaultEditorHeight; - } - UpdateExpandIcon(); - } - } - - private void UpdateExpandIcon() - { - var textBox = this.FindControl("SidebarTextEditor"); - var icon = this.FindControl("TextEditorExpandIcon"); - if (textBox != null && icon != null) - { - bool isExpanded = textBox.Height > DefaultEditorHeight + 20; - icon.Kind = isExpanded ? MaterialIconKind.ArrowCollapseVertical : MaterialIconKind.ArrowExpandVertical; - } - } - - private void UpdateSidebarSelection(TextBox textBox) - { - if (DataContext is InspectorViewModel inspector && inspector.TextElement != null) - { - int start = Math.Min(textBox.SelectionStart, textBox.SelectionEnd); - int end = Math.Max(textBox.SelectionStart, textBox.SelectionEnd); - int len = end - start; - - if (!textBox.IsFocused && len == 0 && inspector.TextElement.HasTextSelection) - { - return; - } - - string text = textBox.Text ?? ""; - string sel = len > 0 && start + len <= text.Length ? text.Substring(start, len) : string.Empty; - inspector.TextElement.UpdateTextSelection(start, len, sel); - } - } -} diff --git a/src/PdfEditorApp/Views/LicensingPageView.axaml b/src/PdfEditorApp/Views/LicensingPageView.axaml deleted file mode 100644 index b8ffc4a..0000000 --- a/src/PdfEditorApp/Views/LicensingPageView.axaml +++ /dev/null @@ -1,567 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/LicensingPageView.axaml.cs b/src/PdfEditorApp/Views/LicensingPageView.axaml.cs deleted file mode 100644 index cdba26f..0000000 --- a/src/PdfEditorApp/Views/LicensingPageView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class LicensingPageView : UserControl -{ - public LicensingPageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/MainWindow.axaml b/src/PdfEditorApp/Views/MainWindow.axaml deleted file mode 100644 index 04a14b0..0000000 --- a/src/PdfEditorApp/Views/MainWindow.axaml +++ /dev/null @@ -1,561 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/NewDocumentPageView.axaml.cs b/src/PdfEditorApp/Views/NewDocumentPageView.axaml.cs deleted file mode 100644 index 9c7878e..0000000 --- a/src/PdfEditorApp/Views/NewDocumentPageView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class NewDocumentPageView : UserControl -{ - public NewDocumentPageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Overlays/ShellOverlayHost.axaml b/src/PdfEditorApp/Views/Overlays/ShellOverlayHost.axaml deleted file mode 100644 index 060d9e6..0000000 --- a/src/PdfEditorApp/Views/Overlays/ShellOverlayHost.axaml +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Overlays/ShellOverlayHost.axaml.cs b/src/PdfEditorApp/Views/Overlays/ShellOverlayHost.axaml.cs deleted file mode 100644 index 43cd828..0000000 --- a/src/PdfEditorApp/Views/Overlays/ShellOverlayHost.axaml.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Specialized; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views.Overlays; - -public partial class ShellOverlayHost : UserControl -{ - private MainViewModel? _observedVm; - - public ShellOverlayHost() - { - InitializeComponent(); - } - - protected override void OnDataContextChanged(EventArgs e) - { - base.OnDataContextChanged(e); - - if (_observedVm != null) - { - _observedVm.ActiveOverlays.CollectionChanged -= OnActiveOverlaysChanged; - } - - _observedVm = DataContext as MainViewModel; - - if (_observedVm != null) - { - _observedVm.ActiveOverlays.CollectionChanged += OnActiveOverlaysChanged; - ClampAndPositionOverlays(); - } - } - - private void OnActiveOverlaysChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - ClampAndPositionOverlays(); - } - - protected override void OnSizeChanged(SizeChangedEventArgs e) - { - base.OnSizeChanged(e); - ClampAndPositionOverlays(); - } - - private Point _dragStartPoint; - private bool _isDraggingStandardHeader; - - private enum ResizeMode - { - None, - Right, - Bottom, - BottomRight - } - - private ResizeMode _currentResizeMode = ResizeMode.None; - private Point _resizeStartPointer; - private double _resizeStartWidth; - private double _resizeStartHeight; - private OverlayInstanceViewModel? _resizingVm; - - private void OnOverlayPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is Control ctrl && ctrl.DataContext is OverlayInstanceViewModel vm && _observedVm != null) - { - vm.BringToFront(_observedVm.ActiveOverlays); - } - } - - private void OnStandardHeaderPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - { - _isDraggingStandardHeader = true; - var root = this.VisualRoot as Visual ?? this; - _dragStartPoint = e.GetPosition(root); - e.Pointer.Capture(sender as Control); - - if (sender is Control ctrl && ctrl.DataContext is OverlayInstanceViewModel vm && _observedVm != null) - { - vm.BringToFront(_observedVm.ActiveOverlays); - } - e.Handled = true; - } - } - - private void OnStandardHeaderPointerMoved(object? sender, PointerEventArgs e) - { - if (_isDraggingStandardHeader && sender is Control ctrl && ctrl.DataContext is OverlayInstanceViewModel vm) - { - var root = this.VisualRoot as Visual ?? this; - var currentPoint = e.GetPosition(root); - var deltaX = currentPoint.X - _dragStartPoint.X; - var deltaY = currentPoint.Y - _dragStartPoint.Y; - _dragStartPoint = currentPoint; - - var maxCanvasX = Bounds.Width > 100 ? Math.Max(20, Bounds.Width - 80) : double.PositiveInfinity; - var maxCanvasY = Bounds.Height > 100 ? Math.Max(20, Bounds.Height - 60) : double.PositiveInfinity; - - vm.X = Math.Clamp(vm.X + deltaX, 0, maxCanvasX); - vm.Y = Math.Clamp(vm.Y + deltaY, 0, maxCanvasY); - e.Handled = true; - } - } - - private void OnStandardHeaderPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (_isDraggingStandardHeader) - { - _isDraggingStandardHeader = false; - e.Pointer.Capture(null); - e.Handled = true; - } - } - - private void OnResizeRightPointerPressed(object? sender, PointerPressedEventArgs e) - => StartResize(sender, e, ResizeMode.Right); - - private void OnResizeBottomPointerPressed(object? sender, PointerPressedEventArgs e) - => StartResize(sender, e, ResizeMode.Bottom); - - private void OnResizeBottomRightPointerPressed(object? sender, PointerPressedEventArgs e) - => StartResize(sender, e, ResizeMode.BottomRight); - - private void StartResize(object? sender, PointerPressedEventArgs e, ResizeMode mode) - { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && sender is Control ctrl && ctrl.DataContext is OverlayInstanceViewModel vm) - { - _currentResizeMode = mode; - _resizingVm = vm; - _resizeStartWidth = vm.Width; - _resizeStartHeight = vm.Height; - - var root = this.VisualRoot as Visual ?? this; - _resizeStartPointer = e.GetPosition(root); - - e.Pointer.Capture(ctrl); - - if (_observedVm != null) - { - vm.BringToFront(_observedVm.ActiveOverlays); - } - e.Handled = true; - } - } - - private void OnResizePointerMoved(object? sender, PointerEventArgs e) - { - if (_currentResizeMode != ResizeMode.None && _resizingVm != null) - { - var root = this.VisualRoot as Visual ?? this; - var currentPoint = e.GetPosition(root); - var deltaX = currentPoint.X - _resizeStartPointer.X; - var deltaY = currentPoint.Y - _resizeStartPointer.Y; - - var newWidth = _resizeStartWidth; - var newHeight = _resizeStartHeight; - - if (_currentResizeMode is ResizeMode.Right or ResizeMode.BottomRight) - { - newWidth += deltaX; - } - - if (_currentResizeMode is ResizeMode.Bottom or ResizeMode.BottomRight) - { - newHeight += deltaY; - } - - var maxCanvasWidth = Bounds.Width > 0 ? Math.Max(120, Bounds.Width - _resizingVm.X) : double.PositiveInfinity; - var maxCanvasHeight = Bounds.Height > 0 ? Math.Max(80, Bounds.Height - _resizingVm.Y) : double.PositiveInfinity; - - _resizingVm.Resize(newWidth, newHeight, maxCanvasWidth, maxCanvasHeight); - e.Handled = true; - } - } - - private void OnResizePointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (_currentResizeMode != ResizeMode.None) - { - _currentResizeMode = ResizeMode.None; - _resizingVm = null; - e.Pointer.Capture(null); - e.Handled = true; - } - } - - private void ClampAndPositionOverlays() - { - if (_observedVm?.ActiveOverlays == null) return; - if (Bounds.Width < 200 || Bounds.Height < 200) return; - - foreach (var overlay in _observedVm.ActiveOverlays) - { - // If overlay is unpositioned or placed off-screen, dock it near the top-right / middle-right - if (overlay.X <= 0 || overlay.X + overlay.Width > Bounds.Width) - { - overlay.X = Math.Max(20, Bounds.Width - overlay.Width - 36); - } - - if (overlay.Y <= 0 || overlay.Y + overlay.Height > Bounds.Height) - { - overlay.Y = Math.Max(60, Math.Min(100, Bounds.Height - overlay.Height - 60)); - } - } - } -} diff --git a/src/PdfEditorApp/Views/PdfReaderLandingPageView.axaml b/src/PdfEditorApp/Views/PdfReaderLandingPageView.axaml deleted file mode 100644 index fd2e0a0..0000000 --- a/src/PdfEditorApp/Views/PdfReaderLandingPageView.axaml +++ /dev/null @@ -1,525 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/PdfReaderLandingPageView.axaml.cs b/src/PdfEditorApp/Views/PdfReaderLandingPageView.axaml.cs deleted file mode 100644 index 47b5928..0000000 --- a/src/PdfEditorApp/Views/PdfReaderLandingPageView.axaml.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Markup.Xaml; - -namespace PdfEditorApp.Views; - -public partial class PdfReaderLandingPageView : UserControl -{ - public PdfReaderLandingPageView() - { - InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } -} diff --git a/src/PdfEditorApp/Views/PdfToolPageView.axaml b/src/PdfEditorApp/Views/PdfToolPageView.axaml deleted file mode 100644 index 27ad2ef..0000000 --- a/src/PdfEditorApp/Views/PdfToolPageView.axaml +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/PdfToolPageView.axaml.cs b/src/PdfEditorApp/Views/PdfToolPageView.axaml.cs deleted file mode 100644 index 3eaf397..0000000 --- a/src/PdfEditorApp/Views/PdfToolPageView.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Core; -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class PdfToolPageView : UserControl -{ - public PdfToolPageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/PdfToolsStudioView.axaml b/src/PdfEditorApp/Views/PdfToolsStudioView.axaml deleted file mode 100644 index fb8fe0f..0000000 --- a/src/PdfEditorApp/Views/PdfToolsStudioView.axaml +++ /dev/null @@ -1,282 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/PdfToolsStudioView.axaml.cs b/src/PdfEditorApp/Views/PdfToolsStudioView.axaml.cs deleted file mode 100644 index 3b2d689..0000000 --- a/src/PdfEditorApp/Views/PdfToolsStudioView.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Core; -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class PdfToolsStudioView : UserControl -{ - public PdfToolsStudioView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/PdfViewerView.axaml b/src/PdfEditorApp/Views/PdfViewerView.axaml deleted file mode 100644 index 2d3799c..0000000 --- a/src/PdfEditorApp/Views/PdfViewerView.axaml +++ /dev/null @@ -1,1299 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/PdfViewerView.axaml.cs b/src/PdfEditorApp/Views/PdfViewerView.axaml.cs deleted file mode 100644 index 7dbf619..0000000 --- a/src/PdfEditorApp/Views/PdfViewerView.axaml.cs +++ /dev/null @@ -1,897 +0,0 @@ -using System; -using System.ComponentModel; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; -using Avalonia.Threading; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views; - -public partial class PdfViewerView : UserControl -{ - // Pan state - private bool _isSpacePressed; - private bool _isPanning; - private Point _panStart; - private Vector _scrollStart; - - // Pinch Gesture state - private bool _isPinching; - private double _pinchStartZoom = 1.0; - - // Scroll synchronization flags to prevent feedback loops - private bool _isProgrammaticScroll; - private bool _isUpdatingFromUserScroll; - private DateTime _lastWheelPageTurnTime = DateTime.MinValue; - private PdfViewerViewModel? _subscribedVm; - private IDisposable? _scrollOffsetSubscription; - - public PdfViewerView() - { - InitializeComponent(); - - GestureRecognizers.Add(new PinchGestureRecognizer()); - - AddHandler(PointerWheelChangedEvent, OnViewerPointerWheelChanged, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PointerTouchPadGestureMagnifyEvent, OnViewerTouchPadGestureMagnify, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PinchEvent, OnViewerPinch, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PinchEndedEvent, OnViewerPinchEnded, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PointerPressedEvent, OnViewerPointerPressed, RoutingStrategies.Tunnel); - AddHandler(PointerMovedEvent, OnViewerPointerMoved, RoutingStrategies.Tunnel); - AddHandler(PointerReleasedEvent, OnViewerPointerReleased, RoutingStrategies.Tunnel); - AddHandler(KeyDownEvent, OnViewerKeyDown, RoutingStrategies.Tunnel); - AddHandler(KeyUpEvent, OnViewerKeyUp, RoutingStrategies.Tunnel); - - DataContextChanged += OnDataContextChanged; - Loaded += OnLoaded; - Unloaded += OnUnloaded; - - this.GetObservable(Visual.IsVisibleProperty).Subscribe(new ActionObserver(visible => - { - if (visible) - { - SetupScrollListeners(); - SubscribeViewModel(ViewModel); - Dispatcher.UIThread.Post(OnContinuousScrollOffsetChanged); - } - })); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - public static readonly DirectProperty ViewModelProperty = - AvaloniaProperty.RegisterDirect( - nameof(ViewModel), - o => o.ViewModel); - - public PdfViewerViewModel? ViewModel => DataContext as PdfViewerViewModel; - - private ScrollViewer? ActiveScrollViewer - { - get - { - if (ViewModel == null) return null; - if (ViewModel.IsContinuousScroll) return ContinuousScrollViewer; - if (ViewModel.IsSinglePageMode) return SinglePageScrollViewer; - if (ViewModel.IsTwoPageSpreadMode) return TwoPageSpreadScrollViewer; - return null; - } - } - - public (double ViewportWidth, double ViewportHeight, double HorizontalPadding, double VerticalPadding) GetViewportDimensions() - { - var sv = ActiveScrollViewer; - if (sv != null) - { - double vw = sv.Viewport.Width > 0 ? sv.Viewport.Width : sv.Bounds.Width; - double vh = sv.Viewport.Height > 0 ? sv.Viewport.Height : sv.Bounds.Height; - if (vw > 0 && vh > 0) - { - double hp = sv.Padding.Left + sv.Padding.Right; - double vp = sv.Padding.Top + sv.Padding.Bottom; - return (vw, vh, hp, vp); - } - } - - // If the newly active scrollviewer hasn't had a measure pass yet, check sibling viewers - // which share the exact same grid column bounds - var sibling = ContinuousScrollViewer ?? SinglePageScrollViewer ?? TwoPageSpreadScrollViewer; - if (sibling != null) - { - double svW = sibling.Viewport.Width > 0 ? sibling.Viewport.Width : sibling.Bounds.Width; - double svH = sibling.Viewport.Height > 0 ? sibling.Viewport.Height : sibling.Bounds.Height; - if (svW > 0 && svH > 0) - { - double hp = sibling.Padding.Left + sibling.Padding.Right; - double vp = sibling.Padding.Top + sibling.Padding.Bottom; - return (svW, svH, hp, vp); - } - } - - double w = Bounds.Width; - double h = Bounds.Height; - if (w > 0 && h > 0) - { - if (ViewModel?.IsSidebarOpen == true) - { - w = Math.Max(100, w - 260); - } - return (w, h, 64.0, 64.0); - } - - return (800, 600, 64.0, 64.0); - } - - protected override void OnSizeChanged(SizeChangedEventArgs e) - { - base.OnSizeChanged(e); - if (ViewModel != null && ViewModel.HasDocument && (e.WidthChanged || e.HeightChanged)) - { - if (ViewModel.IsFitToWidthActive) - { - ViewModel.FitToWidth(); - } - else if (ViewModel.IsFitToPageActive) - { - ViewModel.FitToPage(); - } - } - } - - private void OnLoaded(object? sender, RoutedEventArgs e) - { - SetupScrollListeners(); - SubscribeViewModel(ViewModel); - } - - private void OnUnloaded(object? sender, RoutedEventArgs e) - { - _scrollOffsetSubscription?.Dispose(); - _scrollOffsetSubscription = null; - UnsubscribeViewModel(); - } - - private void OnDataContextChanged(object? sender, EventArgs e) - { - var oldVm = _subscribedVm; - var newVm = ViewModel; - RaisePropertyChanged(ViewModelProperty, oldVm, newVm); - SubscribeViewModel(newVm); - SetupScrollListeners(); - } - - private void SubscribeViewModel(PdfViewerViewModel? vm) - { - if (_subscribedVm != null) - { - _subscribedVm.ScrollToPageRequested -= OnScrollToPageRequested; - _subscribedVm.PropertyChanged -= OnViewModelPropertyChanged; - _subscribedVm.ViewportSizeProvider = null; - _subscribedVm = null; - } - - if (vm != null) - { - _subscribedVm = vm; - _subscribedVm.ScrollToPageRequested += OnScrollToPageRequested; - _subscribedVm.PropertyChanged += OnViewModelPropertyChanged; - _subscribedVm.ViewportSizeProvider = GetViewportDimensions; - } - } - - private void UnsubscribeViewModel() - { - _scrollOffsetSubscription?.Dispose(); - _scrollOffsetSubscription = null; - - if (_subscribedVm != null) - { - _subscribedVm.ScrollToPageRequested -= OnScrollToPageRequested; - _subscribedVm.PropertyChanged -= OnViewModelPropertyChanged; - _subscribedVm.ViewportSizeProvider = null; - _subscribedVm = null; - } - } - - private void SetupScrollListeners() - { - var continuousViewer = ContinuousScrollViewer; - if (continuousViewer != null) - { - continuousViewer.PropertyChanged -= OnContinuousViewerPropertyChanged; - continuousViewer.PropertyChanged += OnContinuousViewerPropertyChanged; - - continuousViewer.RemoveHandler(ScrollViewer.ScrollChangedEvent, OnContinuousViewerScrollChanged); - continuousViewer.AddHandler(ScrollViewer.ScrollChangedEvent, OnContinuousViewerScrollChanged); - - _scrollOffsetSubscription?.Dispose(); - _scrollOffsetSubscription = continuousViewer.GetObservable(ScrollViewer.OffsetProperty) - .Subscribe(new ActionObserver(_ => OnContinuousScrollOffsetChanged())); - } - } - - public void OnContinuousViewerScrollChanged(object? sender, ScrollChangedEventArgs e) - { - OnContinuousScrollOffsetChanged(); - } - - private sealed class ActionObserver(Action onNext) : IObserver - { - public void OnCompleted() { } - public void OnError(Exception error) { } - public void OnNext(T value) => onNext(value); - } - - private void OnContinuousViewerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs ev) - { - if (ev.Property == ScrollViewer.OffsetProperty) - { - OnContinuousScrollOffsetChanged(); - } - } - - private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(PdfViewerViewModel.CurrentPageNumber)) - { - if (!_isUpdatingFromUserScroll && ViewModel != null) - { - Dispatcher.UIThread.Post(() => - { - ScrollToPage(ViewModel.CurrentPageNumber); - ScrollThumbnailIntoView(ViewModel.CurrentPageNumber); - }); - } - } - else if (e.PropertyName == nameof(PdfViewerViewModel.SelectedLayoutMode)) - { - if (ViewModel != null) - { - Dispatcher.UIThread.Post(() => - { - ScrollToPage(ViewModel.CurrentPageNumber); - if (ViewModel.IsFitToPageActive) - { - ViewModel.FitToPage(); - } - else if (ViewModel.IsFitToWidthActive) - { - ViewModel.FitToWidth(); - } - }, DispatcherPriority.Loaded); - } - } - else if (e.PropertyName == nameof(PdfViewerViewModel.IsLoading)) - { - // A fresh document just finished loading. The ScrollViewer's offset is likely - // still (0,0) from before, so no Offset-changed event will fire on its own — - // without this, only page 1 (rendered eagerly by the load itself) would show - // anything until the user scrolled. - if (ViewModel != null && !ViewModel.IsLoading && ViewModel.HasDocument) - { - Dispatcher.UIThread.Post(OnContinuousScrollOffsetChanged); - } - } - else if (e.PropertyName == nameof(PdfViewerViewModel.ZoomLevel)) - { - if (ViewModel != null && ViewModel.IsContinuousScroll) - { - Dispatcher.UIThread.Post(OnContinuousScrollOffsetChanged, DispatcherPriority.Background); - } - } - } - - private void OnScrollToPageRequested(int pageNumber) - { - Dispatcher.UIThread.Post(() => - { - ScrollToPage(pageNumber); - ScrollThumbnailIntoView(pageNumber); - }); - } - - public void ScrollToPage(int pageNumber) - { - if (ViewModel == null || pageNumber < 1) return; - - if (ViewModel.IsContinuousScroll) - { - var viewer = ContinuousScrollViewer; - if (viewer == null) return; - - _isProgrammaticScroll = true; - try - { - double zoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - var itemsCtrl = ContinuousItemsControl; - double top = 32.0; - - for (int i = 0; i < pageNumber - 1 && i < ViewModel.Pages.Count; i++) - { - var page = ViewModel.Pages[i]; - double pageH = page.HeightPoints > 0 ? (page.HeightPoints * zoom) : (842.0 * zoom); - double itemTotalH = pageH + 26.0; - top += itemTotalH + 28.0; - } - - double margin = pageNumber == 1 ? 0 : 16.0; - double targetOffsetY = Math.Max(0, top - margin); - viewer.Offset = new Vector(viewer.Offset.X, targetOffsetY); - } - finally - { - _isProgrammaticScroll = false; - } - - // Programmatic scroll updated the offset while _isProgrammaticScroll suppressed - // OnContinuousScrollOffsetChanged. Now that the offset is settled and the flag is cleared, - // immediately trigger visible-page resolution and background rendering for the destination page. - OnContinuousScrollOffsetChanged(); - } - else if (ViewModel.IsSinglePageMode) - { - var singleViewer = SinglePageScrollViewer; - if (singleViewer != null) - { - singleViewer.Offset = new Vector(singleViewer.Offset.X, 0); - } - } - else if (ViewModel.IsTwoPageSpreadMode) - { - var spreadViewer = TwoPageSpreadScrollViewer; - if (spreadViewer != null) - { - spreadViewer.Offset = new Vector(spreadViewer.Offset.X, 0); - } - } - } - - public void ScrollThumbnailIntoView(int pageNumber) - { - var thumbViewer = ThumbnailsScrollViewer; - if (thumbViewer == null || ViewModel == null || pageNumber < 1) return; - - try - { - double viewportH = thumbViewer.Viewport.Height; - if (viewportH <= 0) viewportH = 600; - - double thumbHeight = 270.0; - double targetY = (pageNumber - 1) * thumbHeight; - double currentY = thumbViewer.Offset.Y; - - if (targetY < currentY || targetY + thumbHeight > currentY + viewportH) - { - thumbViewer.Offset = new Vector(0, Math.Max(0, targetY - 20)); - } - } - catch { } - } - - /// - /// Resolves which pages are visible in the viewport and detects which page has the maximum - /// visible vertical overlap on screen. Uses exact, deterministic pixel geometry calculated - /// from ScrollViewer.Offset.Y, page dimensions, zoom level, and spacing. - /// - private bool ResolveVisiblePages(out int firstVisiblePage, out int lastVisiblePage, out int anchorPage) - { - firstVisiblePage = 1; - lastVisiblePage = 1; - anchorPage = 1; - - var viewer = ContinuousScrollViewer; - if (viewer == null || ViewModel == null) return false; - - int count = ViewModel.Pages.Count; - if (count == 0) return false; - - double viewportHeight = viewer.Viewport.Height; - if (viewportHeight <= 0) viewportHeight = viewer.Bounds.Height; - if (viewportHeight <= 0) viewportHeight = 800; - - double zoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - double currentScrollY = viewer.Offset.Y; - - double currentY = 32.0; - int firstIdx = -1; - int lastIdx = -1; - int bestActiveIdx = -1; - double maxVisibleHeight = -1; - - for (int i = 0; i < count; i++) - { - var page = ViewModel.Pages[i]; - double pageH = page.HeightPoints > 0 ? (page.HeightPoints * zoom) : (842.0 * zoom); - double itemTotalH = pageH + 26.0; - - double top = currentY - currentScrollY; - double bottom = top + itemTotalH; - - if (bottom > 0 && top < viewportHeight) - { - if (firstIdx == -1) firstIdx = i; - lastIdx = i; - - double visibleTop = Math.Max(0, top); - double visibleBottom = Math.Min(viewportHeight, bottom); - double visibleH = Math.Max(0, visibleBottom - visibleTop); - if (visibleH > maxVisibleHeight) - { - maxVisibleHeight = visibleH; - bestActiveIdx = i; - } - } - - currentY += itemTotalH + 28.0; - } - - if (firstIdx == -1) - { - if (currentScrollY <= 32.0) - { - firstIdx = 0; - lastIdx = Math.Min(count - 1, 1); - bestActiveIdx = 0; - } - else - { - bestActiveIdx = count - 1; - firstIdx = Math.Max(0, count - 2); - lastIdx = count - 1; - } - } - - firstVisiblePage = ViewModel.Pages[firstIdx].PageNumber; - lastVisiblePage = ViewModel.Pages[lastIdx].PageNumber; - anchorPage = ViewModel.Pages[bestActiveIdx].PageNumber; - return true; - } - - private void OnContinuousScrollOffsetChanged() - { - if (_isProgrammaticScroll || _isUpdatingFromUserScroll) return; - var vm = ViewModel; - if (vm == null || !vm.IsContinuousScroll || vm.Pages.Count == 0) return; - var viewer = ContinuousScrollViewer; - if (viewer == null) return; - - if (!ResolveVisiblePages(out int firstVisiblePage, out int lastVisiblePage, out int detectedPageNum)) return; - - vm.RequestPagesVisible(firstVisiblePage, lastVisiblePage); - - if (detectedPageNum >= 1 && detectedPageNum <= vm.Pages.Count && vm.CurrentPageNumber != detectedPageNum) - { - _isUpdatingFromUserScroll = true; - try - { - vm.CurrentPageNumber = detectedPageNum; - ScrollThumbnailIntoView(detectedPageNum); - } - finally - { - _isUpdatingFromUserScroll = false; - } - } - } - - private void OnViewerPointerWheelChanged(object? sender, PointerWheelEventArgs e) - { - // Support Cmd (Meta), Ctrl, and Option (Alt) modifier keys - bool isZoomModifier = e.KeyModifiers.HasFlag(KeyModifiers.Control) || - e.KeyModifiers.HasFlag(KeyModifiers.Meta) || - e.KeyModifiers.HasFlag(KeyModifiers.Alt); - - if (isZoomModifier && ViewModel != null) - { - double oldZoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - double effectiveDelta = Math.Abs(e.Delta.Y) >= Math.Abs(e.Delta.X) ? e.Delta.Y : e.Delta.X; - - // Proportional smooth zoom supporting discrete mouse wheel ticks and fractional continuous trackpad gestures - double zoomDeltaFactor; - if (Math.Abs(effectiveDelta) >= 1.0) - { - zoomDeltaFactor = effectiveDelta > 0 ? 1.15 : (1.0 / 1.15); - } - else if (Math.Abs(effectiveDelta) > 0.0001) - { - zoomDeltaFactor = Math.Pow(1.002, effectiveDelta * 100); - } - else - { - zoomDeltaFactor = 1.0; - } - - double newZoom = Math.Clamp(Math.Round(oldZoom * zoomDeltaFactor, 3), 0.25, 5.0); - - if (Math.Abs(newZoom - oldZoom) > 0.001) - { - var scrollViewer = ActiveScrollViewer; - if (scrollViewer != null) - { - var mouseInViewer = e.GetPosition(scrollViewer); - double ratio = newZoom / oldZoom; - double targetOffsetX = (scrollViewer.Offset.X + mouseInViewer.X) * ratio - mouseInViewer.X; - double targetOffsetY = (scrollViewer.Offset.Y + mouseInViewer.Y) * ratio - mouseInViewer.Y; - - ViewModel.ZoomLevel = newZoom; - scrollViewer.Offset = new Vector(Math.Max(0, targetOffsetX), Math.Max(0, targetOffsetY)); - } - else - { - ViewModel.ZoomLevel = newZoom; - } - } - - e.Handled = true; - } - else if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - { - var scrollViewer = ActiveScrollViewer; - if (scrollViewer != null) - { - double delta = e.Delta.Y != 0 ? e.Delta.Y : e.Delta.X; - scrollViewer.Offset = new Vector( - Math.Max(0, scrollViewer.Offset.X - (delta * 40)), - scrollViewer.Offset.Y); - e.Handled = true; - } - } - else if (ViewModel != null && (ViewModel.IsSinglePageMode || ViewModel.IsTwoPageSpreadMode)) - { - var scrollViewer = ActiveScrollViewer; - if (scrollViewer != null) - { - double deltaY = e.Delta.Y; - if (Math.Abs(deltaY) > 0.15) - { - bool atBottom = scrollViewer.Offset.Y >= (scrollViewer.Extent.Height - scrollViewer.Viewport.Height - 4.0); - bool atTop = scrollViewer.Offset.Y <= 4.0; - - if (deltaY < 0 && atBottom) - { - if (DateTime.UtcNow - _lastWheelPageTurnTime > TimeSpan.FromMilliseconds(250)) - { - _lastWheelPageTurnTime = DateTime.UtcNow; - ViewModel.NextPageCommand.Execute(null); - scrollViewer.Offset = new Vector(scrollViewer.Offset.X, 0); - e.Handled = true; - } - } - else if (deltaY > 0 && atTop) - { - if (DateTime.UtcNow - _lastWheelPageTurnTime > TimeSpan.FromMilliseconds(250)) - { - _lastWheelPageTurnTime = DateTime.UtcNow; - ViewModel.PreviousPageCommand.Execute(null); - scrollViewer.Offset = new Vector(scrollViewer.Offset.X, Math.Max(0, scrollViewer.Extent.Height - scrollViewer.Viewport.Height)); - e.Handled = true; - } - } - } - } - } - } - - private void OnViewerTouchPadGestureMagnify(object? sender, PointerDeltaEventArgs e) - { - if (ViewModel != null) - { - double oldZoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - double delta = Math.Abs(e.Delta.Y) >= Math.Abs(e.Delta.X) ? e.Delta.Y : e.Delta.X; - if (delta == 0 && (e.Delta.X != 0 || e.Delta.Y != 0)) - { - delta = e.Delta.X != 0 ? e.Delta.X : e.Delta.Y; - } - - double zoomFactor = 1.0 + delta; - double newZoom = Math.Clamp(Math.Round(oldZoom * zoomFactor, 3), 0.25, 5.0); - - if (Math.Abs(newZoom - oldZoom) > 0.001) - { - var scrollViewer = ActiveScrollViewer; - if (scrollViewer != null) - { - var mouseInViewer = e.GetPosition(scrollViewer); - double ratio = newZoom / oldZoom; - double targetOffsetX = (scrollViewer.Offset.X + mouseInViewer.X) * ratio - mouseInViewer.X; - double targetOffsetY = (scrollViewer.Offset.Y + mouseInViewer.Y) * ratio - mouseInViewer.Y; - - ViewModel.ZoomLevel = newZoom; - scrollViewer.Offset = new Vector(Math.Max(0, targetOffsetX), Math.Max(0, targetOffsetY)); - } - else - { - ViewModel.ZoomLevel = newZoom; - } - } - - e.Handled = true; - } - } - - private void OnViewerPinch(object? sender, PinchEventArgs e) - { - if (ViewModel != null) - { - if (!_isPinching) - { - _isPinching = true; - _pinchStartZoom = ViewModel.ZoomLevel > 0 ? ViewModel.ZoomLevel : 1.0; - } - - // e.Scale is the total cumulative scale of the pinch gesture since start (starts at 1.0) - double targetZoom = Math.Clamp(Math.Round(_pinchStartZoom * e.Scale, 3), 0.25, 5.0); - if (Math.Abs(targetZoom - ViewModel.ZoomLevel) > 0.002) - { - double oldZ = ViewModel.ZoomLevel; - var scrollViewer = ActiveScrollViewer; - - ViewModel.ZoomLevel = targetZoom; - - if (oldZ > 0 && scrollViewer != null) - { - var origin = e.ScaleOrigin; - double ratio = targetZoom / oldZ; - double newOffsetX = (scrollViewer.Offset.X + origin.X) * ratio - origin.X; - double newOffsetY = (scrollViewer.Offset.Y + origin.Y) * ratio - origin.Y; - scrollViewer.Offset = new Vector(Math.Max(0, newOffsetX), Math.Max(0, newOffsetY)); - } - } - e.Handled = true; - } - } - - private void OnViewerPinchEnded(object? sender, PinchEndedEventArgs e) - { - _isPinching = false; - e.Handled = true; - } - - private void OnViewerPointerPressed(object? sender, PointerPressedEventArgs e) - { - var pt = e.GetCurrentPoint(this); - if (_isSpacePressed || pt.Properties.IsMiddleButtonPressed) - { - _isPanning = true; - _panStart = e.GetPosition(this); - var scrollViewer = ActiveScrollViewer; - if (scrollViewer != null) - { - _scrollStart = scrollViewer.Offset; - } - e.Pointer.Capture(this); - Cursor = new Cursor(StandardCursorType.Hand); - e.Handled = true; - } - } - - private void OnViewerPointerMoved(object? sender, PointerEventArgs e) - { - if (_isPanning) - { - var scrollViewer = ActiveScrollViewer; - if (scrollViewer != null) - { - var curPos = e.GetPosition(this); - var delta = curPos - _panStart; - scrollViewer.Offset = new Vector( - Math.Max(0, _scrollStart.X - delta.X), - Math.Max(0, _scrollStart.Y - delta.Y)); - e.Handled = true; - } - } - } - - private void OnViewerPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (_isPanning) - { - _isPanning = false; - e.Pointer.Capture(null); - Cursor = _isSpacePressed ? new Cursor(StandardCursorType.Hand) : Cursor.Default; - e.Handled = true; - } - } - - private void OnViewerKeyUp(object? sender, KeyEventArgs e) - { - if (e.Key == Key.Space) - { - _isSpacePressed = false; - if (!_isPanning) - { - Cursor = Cursor.Default; - } - } - } - - private void OnViewerKeyDown(object? sender, KeyEventArgs e) - { - if (ViewModel == null) return; - - // If typing inside a TextBox, handle Enter & Escape specifically - if (e.Source is TextBox textBox) - { - if (e.Key == Key.Enter) - { - if (textBox.Name == "JumpPageTextBox" || textBox == this.FindControl("JumpPageTextBox")) - { - ViewModel.CommitJumpPage(); - e.Handled = true; - } - else if (textBox.Name == "SearchQueryTextBox" || textBox == this.FindControl("SearchQueryTextBox") || ViewModel.IsSearchBarVisible) - { - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - { - ViewModel.PreviousMatchCommand.Execute(null); - } - else - { - ViewModel.NextMatchCommand.Execute(null); - } - e.Handled = true; - } - } - else if (e.Key == Key.Escape) - { - if (ViewModel.IsSearchBarVisible) - { - ViewModel.IsSearchBarVisible = false; - } - this.Focus(); - e.Handled = true; - } - return; - } - - if (e.Key == Key.Space && !_isSpacePressed) - { - _isSpacePressed = true; - Cursor = new Cursor(StandardCursorType.Hand); - return; - } - - bool isCtrlOrCmd = e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Meta); - - if (isCtrlOrCmd) - { - switch (e.Key) - { - case Key.C: - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - { - ViewModel.CopySelectedCitationCommand.Execute(null); - } - else if (ViewModel.HasTextSelection) - { - ViewModel.CopySelectedTextCommand.Execute(null); - } - else - { - ViewModel.CopyPageTextCommand.Execute(null); - } - e.Handled = true; - break; - case Key.A: - ViewModel.SelectAllPageTextCommand.Execute(null); - e.Handled = true; - break; - case Key.D0: - case Key.NumPad0: - ViewModel.ResetZoomCommand.Execute(null); - e.Handled = true; - break; - case Key.D1: - case Key.NumPad1: - ViewModel.FitToWidthCommand.Execute(null); - e.Handled = true; - break; - case Key.D9: - case Key.NumPad9: - ViewModel.FitToPageCommand.Execute(null); - e.Handled = true; - break; - case Key.OemPlus: - case Key.Add: - ViewModel.ZoomInCommand.Execute(null); - e.Handled = true; - break; - case Key.OemMinus: - case Key.Subtract: - ViewModel.ZoomOutCommand.Execute(null); - e.Handled = true; - break; - case Key.F: - if (ViewModel.HasTextSelection) - { - ViewModel.SearchSelectedTextCommand.Execute(null); - } - else - { - ViewModel.ToggleSearchBarCommand.Execute(null); - } - e.Handled = true; - break; - case Key.R: - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - { - ViewModel.RotateCounterClockwiseCommand.Execute(null); - } - else - { - ViewModel.RotateClockwiseCommand.Execute(null); - } - e.Handled = true; - break; - case Key.B: - ViewModel.ToggleSidebarCommand.Execute(null); - e.Handled = true; - break; - } - } - else - { - switch (e.Key) - { - case Key.PageDown: - case Key.Right: - case Key.Down: - ViewModel.NextPageCommand.Execute(null); - e.Handled = true; - break; - case Key.PageUp: - case Key.Left: - case Key.Up: - ViewModel.PreviousPageCommand.Execute(null); - e.Handled = true; - break; - case Key.Home: - ViewModel.FirstPageCommand.Execute(null); - e.Handled = true; - break; - case Key.End: - ViewModel.LastPageCommand.Execute(null); - e.Handled = true; - break; - case Key.F3: - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - { - ViewModel.PreviousMatchCommand.Execute(null); - } - else - { - ViewModel.NextMatchCommand.Execute(null); - } - e.Handled = true; - break; - case Key.F11: - ViewModel.ToggleFullscreenCommand.Execute(null); - e.Handled = true; - break; - case Key.Escape: - if (ViewModel.IsFullscreen) - { - ViewModel.IsFullscreen = false; - e.Handled = true; - } - else if (ViewModel.HasTextSelection) - { - ViewModel.ClearSelection(); - e.Handled = true; - } - else if (ViewModel.IsSearchBarVisible) - { - ViewModel.IsSearchBarVisible = false; - e.Handled = true; - } - break; - } - } - } -} diff --git a/src/PdfEditorApp/Views/PluginsManagerPageView.axaml b/src/PdfEditorApp/Views/PluginsManagerPageView.axaml deleted file mode 100644 index b749574..0000000 --- a/src/PdfEditorApp/Views/PluginsManagerPageView.axaml +++ /dev/null @@ -1,1122 +0,0 @@ - - - - 9999 - 28 - 16 - 12 - 8 - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/PluginsManagerPageView.axaml.cs b/src/PdfEditorApp/Views/PluginsManagerPageView.axaml.cs deleted file mode 100644 index c48e417..0000000 --- a/src/PdfEditorApp/Views/PluginsManagerPageView.axaml.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Input; -using PdfEditorApp.Core.Plugins.Marketplace; -using PdfEditorApp.ViewModels; - -namespace PdfEditorApp.Views; - -public partial class PluginsManagerPageView : UserControl -{ - public PluginsManagerPageView() - { - InitializeComponent(); - AttachedToVisualTree += (s, e) => - { - if (DataContext is PluginsManagerViewModel vm) - { - var topLevel = TopLevel.GetTopLevel(this); - if (topLevel != null) - { - vm.StorageProvider = topLevel.StorageProvider; - vm.Clipboard = topLevel.Clipboard; - } - } - }; - } - - private void OnInstalledCardPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is Border border && border.DataContext is PluginItemViewModel plugin && DataContext is PluginsManagerViewModel vm) - { - vm.SelectedInstalledPlugin = plugin; - } - } - - private void OnMarketplaceCardPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is Border border && border.DataContext is MarketplacePluginItem item && DataContext is PluginsManagerViewModel vm) - { - vm.SelectedMarketplacePlugin = item; - } - } -} diff --git a/src/PdfEditorApp/Views/RibbonView.axaml b/src/PdfEditorApp/Views/RibbonView.axaml deleted file mode 100644 index ec036af..0000000 --- a/src/PdfEditorApp/Views/RibbonView.axaml +++ /dev/null @@ -1,1270 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/RibbonView.axaml.cs b/src/PdfEditorApp/Views/RibbonView.axaml.cs deleted file mode 100644 index 479e1da..0000000 --- a/src/PdfEditorApp/Views/RibbonView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class RibbonView : UserControl -{ - public RibbonView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/SettingsPageView.axaml b/src/PdfEditorApp/Views/SettingsPageView.axaml deleted file mode 100644 index 59aa6ac..0000000 --- a/src/PdfEditorApp/Views/SettingsPageView.axaml +++ /dev/null @@ -1,990 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/SettingsPageView.axaml.cs b/src/PdfEditorApp/Views/SettingsPageView.axaml.cs deleted file mode 100644 index bc5b3ce..0000000 --- a/src/PdfEditorApp/Views/SettingsPageView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class SettingsPageView : UserControl -{ - public SettingsPageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Shell/PdfToolWorkspaceView.axaml b/src/PdfEditorApp/Views/Shell/PdfToolWorkspaceView.axaml deleted file mode 100644 index faf4027..0000000 --- a/src/PdfEditorApp/Views/Shell/PdfToolWorkspaceView.axaml +++ /dev/null @@ -1,368 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Shell/PdfToolWorkspaceView.axaml.cs b/src/PdfEditorApp/Views/Shell/PdfToolWorkspaceView.axaml.cs deleted file mode 100644 index 1cc2aec..0000000 --- a/src/PdfEditorApp/Views/Shell/PdfToolWorkspaceView.axaml.cs +++ /dev/null @@ -1,359 +0,0 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools.Core; - -namespace PdfEditorApp.Views.Shell; - -/// -/// Reader-style shell (toolbar + tabbed left sidebar + live preview canvas) shared by -/// tool screens. Each tool composes it by supplying its own settings content and CTA -/// button through the content-slot properties below; the toolbar, Files/Pages/Info -/// tabs, and preview rendering are shared, backed by . -/// -/// Also mirrors the PDF Reader's () zoom -/// gestures — Ctrl/Cmd/Alt+scroll, trackpad pinch, and space/middle-click pan — over the -/// single preview ScrollViewer here, so every tool gets the same interaction the reader -/// has instead of only the toolbar +/- buttons. Zoom bounds (0.5-3.0) match -/// PdfLivePreviewViewModel's own clamp, since ZoomLevel here is set directly rather than -/// through ZoomInCommand/ZoomOutCommand. -/// -public partial class PdfToolWorkspaceView : UserControl -{ - private const double MinZoom = 0.5; - private const double MaxZoom = 3.0; - - public static readonly StyledProperty OptionsContentProperty = - AvaloniaProperty.Register(nameof(OptionsContent)); - - public static readonly StyledProperty ToolbarActionContentProperty = - AvaloniaProperty.Register(nameof(ToolbarActionContent)); - - public static readonly StyledProperty CanvasOverlayContentProperty = - AvaloniaProperty.Register(nameof(CanvasOverlayContent)); - - public static readonly StyledProperty SideBySideContentProperty = - AvaloniaProperty.Register(nameof(SideBySideContent)); - - public static readonly DirectProperty SideBySideColumnWidthProperty = - AvaloniaProperty.RegisterDirect( - nameof(SideBySideColumnWidth), - o => o.SideBySideColumnWidth, - (o, v) => o.SideBySideColumnWidth = v); - - private GridLength _sideBySideColumnWidth = new GridLength(0, GridUnitType.Pixel); - - public GridLength SideBySideColumnWidth - { - get => _sideBySideColumnWidth; - set => SetAndRaise(SideBySideColumnWidthProperty, ref _sideBySideColumnWidth, value); - } - - public static readonly StyledProperty SelectedTabProperty = - AvaloniaProperty.Register(nameof(SelectedTab), "Options"); - - private readonly ScrollViewer? _previewScrollViewer; - - // Pan state (space-drag or middle-click drag) - private bool _isSpacePressed; - private bool _isPanning; - private Point _panStart; - private Vector _scrollStart; - - // Pinch gesture state - private bool _isPinching; - private double _pinchStartZoom = 1.0; - - /// The tool's own settings/options UI, shown in the sidebar's Options tab (selected by default). - public object? OptionsContent - { - get => GetValue(OptionsContentProperty); - set => SetValue(OptionsContentProperty, value); - } - - /// The tool's primary action button (e.g. "Merge PDFs Now"), shown in the toolbar's right slot. - public object? ToolbarActionContent - { - get => GetValue(ToolbarActionContentProperty); - set => SetValue(ToolbarActionContentProperty, value); - } - - /// Optional interactive layer drawn over the current page (e.g. Redact's mark-drawing canvas). - public object? CanvasOverlayContent - { - get => GetValue(CanvasOverlayContentProperty); - set => SetValue(CanvasOverlayContentProperty, value); - } - - /// Optional side-by-side comparison panel shown next to the preview canvas (e.g. OCR extracted text or side-by-side diff). - public object? SideBySideContent - { - get => GetValue(SideBySideContentProperty); - set => SetValue(SideBySideContentProperty, value); - } - - public string SelectedTab - { - get => GetValue(SelectedTabProperty); - set => SetValue(SelectedTabProperty, value); - } - - /// Typed DataContext accessor so page-card templates (whose own DataContext is the page item) can still reach the tool ViewModel by element name. - public PdfToolViewModelBase? ViewModel => DataContext as PdfToolViewModelBase; - - public PdfToolWorkspaceView() - { - InitializeComponent(); - _previewScrollViewer = this.FindControl("PreviewScrollViewer"); - - GestureRecognizers.Add(new PinchGestureRecognizer()); - - AddHandler(PointerWheelChangedEvent, OnWorkspacePointerWheelChanged, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PointerTouchPadGestureMagnifyEvent, OnWorkspaceTouchPadGestureMagnify, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PinchEvent, OnWorkspacePinch, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PinchEndedEvent, OnWorkspacePinchEnded, RoutingStrategies.Bubble | RoutingStrategies.Tunnel, handledEventsToo: true); - AddHandler(PointerPressedEvent, OnWorkspacePointerPressed, RoutingStrategies.Tunnel); - AddHandler(PointerMovedEvent, OnWorkspacePointerMoved, RoutingStrategies.Tunnel); - AddHandler(PointerReleasedEvent, OnWorkspacePointerReleased, RoutingStrategies.Tunnel); - AddHandler(KeyDownEvent, OnWorkspaceKeyDown, RoutingStrategies.Tunnel); - AddHandler(KeyUpEvent, OnWorkspaceKeyUp, RoutingStrategies.Tunnel); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - if (change.Property == SideBySideContentProperty) - { - SideBySideColumnWidth = change.NewValue != null - ? new GridLength(1, GridUnitType.Star) - : new GridLength(0, GridUnitType.Pixel); - } - } - - private void OnTabButtonClick(object? sender, RoutedEventArgs e) - { - if (sender is Control { Tag: string tabName }) - { - SelectedTab = tabName; - } - } - - private void OnThumbnailPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is Control { DataContext: PdfToolPreviewPage page }) - { - ViewModel?.Preview.SelectPageCommand.Execute(page); - } - } - - private void SetZoomAnchoredAtPointer(double newZoom, double oldZoom, Point pointerInViewer) - { - var preview = ViewModel?.Preview; - if (preview == null) return; - - if (_previewScrollViewer != null && oldZoom > 0) - { - double ratio = newZoom / oldZoom; - double targetOffsetX = (_previewScrollViewer.Offset.X + pointerInViewer.X) * ratio - pointerInViewer.X; - double targetOffsetY = (_previewScrollViewer.Offset.Y + pointerInViewer.Y) * ratio - pointerInViewer.Y; - - preview.ZoomLevel = newZoom; - _previewScrollViewer.Offset = new Vector(Math.Max(0, targetOffsetX), Math.Max(0, targetOffsetY)); - } - else - { - preview.ZoomLevel = newZoom; - } - } - - private void OnWorkspacePointerWheelChanged(object? sender, PointerWheelEventArgs e) - { - var preview = ViewModel?.Preview; - if (preview == null || !preview.HasDocument) return; - - bool isZoomModifier = e.KeyModifiers.HasFlag(KeyModifiers.Control) || - e.KeyModifiers.HasFlag(KeyModifiers.Meta) || - e.KeyModifiers.HasFlag(KeyModifiers.Alt); - if (!isZoomModifier) return; - - double oldZoom = preview.ZoomLevel > 0 ? preview.ZoomLevel : 1.0; - double effectiveDelta = Math.Abs(e.Delta.Y) >= Math.Abs(e.Delta.X) ? e.Delta.Y : e.Delta.X; - - double zoomDeltaFactor; - if (Math.Abs(effectiveDelta) >= 1.0) - { - zoomDeltaFactor = effectiveDelta > 0 ? 1.15 : (1.0 / 1.15); - } - else if (Math.Abs(effectiveDelta) > 0.0001) - { - zoomDeltaFactor = Math.Pow(1.002, effectiveDelta * 100); - } - else - { - zoomDeltaFactor = 1.0; - } - - double newZoom = Math.Clamp(Math.Round(oldZoom * zoomDeltaFactor, 3), MinZoom, MaxZoom); - if (Math.Abs(newZoom - oldZoom) > 0.001 && _previewScrollViewer != null) - { - SetZoomAnchoredAtPointer(newZoom, oldZoom, e.GetPosition(_previewScrollViewer)); - } - - e.Handled = true; - } - - private void OnWorkspaceTouchPadGestureMagnify(object? sender, PointerDeltaEventArgs e) - { - var preview = ViewModel?.Preview; - if (preview == null || !preview.HasDocument || _previewScrollViewer == null) return; - - double oldZoom = preview.ZoomLevel > 0 ? preview.ZoomLevel : 1.0; - double delta = Math.Abs(e.Delta.Y) >= Math.Abs(e.Delta.X) ? e.Delta.Y : e.Delta.X; - if (delta == 0 && (e.Delta.X != 0 || e.Delta.Y != 0)) - { - delta = e.Delta.X != 0 ? e.Delta.X : e.Delta.Y; - } - - double newZoom = Math.Clamp(Math.Round(oldZoom * (1.0 + delta), 3), MinZoom, MaxZoom); - if (Math.Abs(newZoom - oldZoom) > 0.001) - { - SetZoomAnchoredAtPointer(newZoom, oldZoom, e.GetPosition(_previewScrollViewer)); - } - - e.Handled = true; - } - - private void OnWorkspacePinch(object? sender, PinchEventArgs e) - { - var preview = ViewModel?.Preview; - if (preview == null || !preview.HasDocument) return; - - if (!_isPinching) - { - _isPinching = true; - _pinchStartZoom = preview.ZoomLevel > 0 ? preview.ZoomLevel : 1.0; - } - - double targetZoom = Math.Clamp(Math.Round(_pinchStartZoom * e.Scale, 3), MinZoom, MaxZoom); - if (Math.Abs(targetZoom - preview.ZoomLevel) > 0.002) - { - double oldZoom = preview.ZoomLevel; - if (_previewScrollViewer != null && oldZoom > 0) - { - double ratio = targetZoom / oldZoom; - var origin = e.ScaleOrigin; - double newOffsetX = (_previewScrollViewer.Offset.X + origin.X) * ratio - origin.X; - double newOffsetY = (_previewScrollViewer.Offset.Y + origin.Y) * ratio - origin.Y; - - preview.ZoomLevel = targetZoom; - _previewScrollViewer.Offset = new Vector(Math.Max(0, newOffsetX), Math.Max(0, newOffsetY)); - } - else - { - preview.ZoomLevel = targetZoom; - } - } - - e.Handled = true; - } - - private void OnWorkspacePinchEnded(object? sender, PinchEndedEventArgs e) - { - _isPinching = false; - e.Handled = true; - } - - private void OnWorkspacePointerPressed(object? sender, PointerPressedEventArgs e) - { - var pt = e.GetCurrentPoint(this); - if (_isSpacePressed || pt.Properties.IsMiddleButtonPressed) - { - _isPanning = true; - _panStart = e.GetPosition(this); - if (_previewScrollViewer != null) - { - _scrollStart = _previewScrollViewer.Offset; - } - e.Pointer.Capture(this); - Cursor = new Cursor(StandardCursorType.Hand); - e.Handled = true; - } - } - - private void OnWorkspacePointerMoved(object? sender, PointerEventArgs e) - { - if (_isPanning && _previewScrollViewer != null) - { - var curPos = e.GetPosition(this); - var delta = curPos - _panStart; - _previewScrollViewer.Offset = new Vector( - Math.Max(0, _scrollStart.X - delta.X), - Math.Max(0, _scrollStart.Y - delta.Y)); - e.Handled = true; - } - } - - private void OnWorkspacePointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (_isPanning) - { - _isPanning = false; - e.Pointer.Capture(null); - Cursor = _isSpacePressed ? new Cursor(StandardCursorType.Hand) : Cursor.Default; - e.Handled = true; - } - } - - private void OnWorkspaceKeyDown(object? sender, KeyEventArgs e) - { - var preview = ViewModel?.Preview; - if (preview == null) return; - - if (e.Source is TextBox) - { - // Don't hijack typing in the Options tab's own text fields. - return; - } - - if (e.Key == Key.Space && !_isSpacePressed) - { - _isSpacePressed = true; - Cursor = new Cursor(StandardCursorType.Hand); - return; - } - - bool isCtrlOrCmd = e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Meta); - if (!isCtrlOrCmd) return; - - switch (e.Key) - { - case Key.OemPlus: - case Key.Add: - preview.ZoomInCommand.Execute(null); - e.Handled = true; - break; - case Key.OemMinus: - case Key.Subtract: - preview.ZoomOutCommand.Execute(null); - e.Handled = true; - break; - case Key.D0: - case Key.NumPad0: - preview.ResetZoomCommand.Execute(null); - e.Handled = true; - break; - } - } - - private void OnWorkspaceKeyUp(object? sender, KeyEventArgs e) - { - if (e.Key == Key.Space) - { - _isSpacePressed = false; - Cursor = Cursor.Default; - } - } -} diff --git a/src/PdfEditorApp/Views/StarredToolsPageView.axaml b/src/PdfEditorApp/Views/StarredToolsPageView.axaml deleted file mode 100644 index ab18ef9..0000000 --- a/src/PdfEditorApp/Views/StarredToolsPageView.axaml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/StarredToolsPageView.axaml.cs b/src/PdfEditorApp/Views/StarredToolsPageView.axaml.cs deleted file mode 100644 index 54429fe..0000000 --- a/src/PdfEditorApp/Views/StarredToolsPageView.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Core; -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class StarredToolsPageView : UserControl -{ - public StarredToolsPageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/TesseractManagerPageView.axaml b/src/PdfEditorApp/Views/TesseractManagerPageView.axaml deleted file mode 100644 index f46e3f7..0000000 --- a/src/PdfEditorApp/Views/TesseractManagerPageView.axaml +++ /dev/null @@ -1,409 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/TesseractManagerPageView.axaml.cs b/src/PdfEditorApp/Views/TesseractManagerPageView.axaml.cs deleted file mode 100644 index 0ed91eb..0000000 --- a/src/PdfEditorApp/Views/TesseractManagerPageView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class TesseractManagerPageView : UserControl -{ - public TesseractManagerPageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/ThumbnailSidebarView.axaml b/src/PdfEditorApp/Views/ThumbnailSidebarView.axaml deleted file mode 100644 index d1bcce2..0000000 --- a/src/PdfEditorApp/Views/ThumbnailSidebarView.axaml +++ /dev/null @@ -1,617 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/ThumbnailSidebarView.axaml.cs b/src/PdfEditorApp/Views/ThumbnailSidebarView.axaml.cs deleted file mode 100644 index 2e54800..0000000 --- a/src/PdfEditorApp/Views/ThumbnailSidebarView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class ThumbnailSidebarView : UserControl -{ - public ThumbnailSidebarView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/ExcelToPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/ExcelToPdfToolView.axaml deleted file mode 100644 index 622c451..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/ExcelToPdfToolView.axaml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/ExcelToPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/ExcelToPdfToolView.axaml.cs deleted file mode 100644 index a77cfec..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/ExcelToPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class ExcelToPdfToolView : UserControl -{ - public ExcelToPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/HtmlToPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/HtmlToPdfToolView.axaml deleted file mode 100644 index b60f9bb..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/HtmlToPdfToolView.axaml +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/HtmlToPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/HtmlToPdfToolView.axaml.cs deleted file mode 100644 index cf3201c..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/HtmlToPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class HtmlToPdfToolView : UserControl -{ - public HtmlToPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/JpgToPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/JpgToPdfToolView.axaml deleted file mode 100644 index 6e10e6a..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/JpgToPdfToolView.axaml +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/JpgToPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/JpgToPdfToolView.axaml.cs deleted file mode 100644 index 136ebeb..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/JpgToPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class JpgToPdfToolView : UserControl -{ - public JpgToPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToExcelToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/PdfToExcelToolView.axaml deleted file mode 100644 index cdccf54..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToExcelToolView.axaml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToExcelToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/PdfToExcelToolView.axaml.cs deleted file mode 100644 index 940ebb5..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToExcelToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class PdfToExcelToolView : UserControl -{ - public PdfToExcelToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToJpgToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/PdfToJpgToolView.axaml deleted file mode 100644 index d9113a1..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToJpgToolView.axaml +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToJpgToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/PdfToJpgToolView.axaml.cs deleted file mode 100644 index c39df7b..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToJpgToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class PdfToJpgToolView : UserControl -{ - public PdfToJpgToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToMarkdownToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/PdfToMarkdownToolView.axaml deleted file mode 100644 index 1d76d23..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToMarkdownToolView.axaml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToMarkdownToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/PdfToMarkdownToolView.axaml.cs deleted file mode 100644 index 5479cf0..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToMarkdownToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class PdfToMarkdownToolView : UserControl -{ - public PdfToMarkdownToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToPdfAToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/PdfToPdfAToolView.axaml deleted file mode 100644 index 2684dfa..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToPdfAToolView.axaml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToPdfAToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/PdfToPdfAToolView.axaml.cs deleted file mode 100644 index 7fbc2e3..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToPdfAToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class PdfToPdfAToolView : UserControl -{ - public PdfToPdfAToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToPowerPointToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/PdfToPowerPointToolView.axaml deleted file mode 100644 index 32e6b53..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToPowerPointToolView.axaml +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToPowerPointToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/PdfToPowerPointToolView.axaml.cs deleted file mode 100644 index 92d6aaf..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToPowerPointToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class PdfToPowerPointToolView : UserControl -{ - public PdfToPowerPointToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToWordToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/PdfToWordToolView.axaml deleted file mode 100644 index 33486be..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToWordToolView.axaml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PdfToWordToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/PdfToWordToolView.axaml.cs deleted file mode 100644 index 4f1fa14..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PdfToWordToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class PdfToWordToolView : UserControl -{ - public PdfToWordToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PowerPointToPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/PowerPointToPdfToolView.axaml deleted file mode 100644 index 318c2ff..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PowerPointToPdfToolView.axaml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/PowerPointToPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/PowerPointToPdfToolView.axaml.cs deleted file mode 100644 index 3374db7..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/PowerPointToPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class PowerPointToPdfToolView : UserControl -{ - public PowerPointToPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/ScanToPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/ScanToPdfToolView.axaml deleted file mode 100644 index 7390a5d..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/ScanToPdfToolView.axaml +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/ScanToPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/ScanToPdfToolView.axaml.cs deleted file mode 100644 index 98d73ee..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/ScanToPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class ScanToPdfToolView : UserControl -{ - public ScanToPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Conversion/WordToPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Conversion/WordToPdfToolView.axaml deleted file mode 100644 index e16b380..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/WordToPdfToolView.axaml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Conversion/WordToPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Conversion/WordToPdfToolView.axaml.cs deleted file mode 100644 index f997cde..0000000 --- a/src/PdfEditorApp/Views/Tools/Conversion/WordToPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Conversion; - -public partial class WordToPdfToolView : UserControl -{ - public WordToPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/AiSummarizerToolView.axaml b/src/PdfEditorApp/Views/Tools/Intelligence/AiSummarizerToolView.axaml deleted file mode 100644 index 65d1dfb..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/AiSummarizerToolView.axaml +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/AiSummarizerToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Intelligence/AiSummarizerToolView.axaml.cs deleted file mode 100644 index 39097bb..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/AiSummarizerToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Intelligence; - -public partial class AiSummarizerToolView : UserControl -{ - public AiSummarizerToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/ComparePdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Intelligence/ComparePdfToolView.axaml deleted file mode 100644 index 6f0d42c..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/ComparePdfToolView.axaml +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/ComparePdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Intelligence/ComparePdfToolView.axaml.cs deleted file mode 100644 index 5c03fd8..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/ComparePdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Intelligence; - -public partial class ComparePdfToolView : UserControl -{ - public ComparePdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/EditPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Intelligence/EditPdfToolView.axaml deleted file mode 100644 index 649ecd2..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/EditPdfToolView.axaml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/EditPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Intelligence/EditPdfToolView.axaml.cs deleted file mode 100644 index 5ac3eed..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/EditPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Intelligence; - -public partial class EditPdfToolView : UserControl -{ - public EditPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/OcrPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Intelligence/OcrPdfToolView.axaml deleted file mode 100644 index 640bc20..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/OcrPdfToolView.axaml +++ /dev/null @@ -1,315 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/OcrPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Intelligence/OcrPdfToolView.axaml.cs deleted file mode 100644 index 2c447f8..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/OcrPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Intelligence; - -public partial class OcrPdfToolView : UserControl -{ - public OcrPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/PdfFormsToolView.axaml b/src/PdfEditorApp/Views/Tools/Intelligence/PdfFormsToolView.axaml deleted file mode 100644 index f9f8aeb..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/PdfFormsToolView.axaml +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/PdfFormsToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Intelligence/PdfFormsToolView.axaml.cs deleted file mode 100644 index 3abf030..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/PdfFormsToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Intelligence; - -public partial class PdfFormsToolView : UserControl -{ - public PdfFormsToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/TranslatePdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Intelligence/TranslatePdfToolView.axaml deleted file mode 100644 index 2adf853..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/TranslatePdfToolView.axaml +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Intelligence/TranslatePdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Intelligence/TranslatePdfToolView.axaml.cs deleted file mode 100644 index 005aab7..0000000 --- a/src/PdfEditorApp/Views/Tools/Intelligence/TranslatePdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Intelligence; - -public partial class TranslatePdfToolView : UserControl -{ - public TranslatePdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Organize/CropPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Organize/CropPdfToolView.axaml deleted file mode 100644 index ee2791b..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/CropPdfToolView.axaml +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Organize/CropPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Organize/CropPdfToolView.axaml.cs deleted file mode 100644 index 36ab852..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/CropPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Organize; - -public partial class CropPdfToolView : UserControl -{ - public CropPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Organize/MergePdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Organize/MergePdfToolView.axaml deleted file mode 100644 index de196b5..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/MergePdfToolView.axaml +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Organize/MergePdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Organize/MergePdfToolView.axaml.cs deleted file mode 100644 index 0232fa2..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/MergePdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Organize; - -public partial class MergePdfToolView : UserControl -{ - public MergePdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Organize/OrganizePdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Organize/OrganizePdfToolView.axaml deleted file mode 100644 index 28685c3..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/OrganizePdfToolView.axaml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Organize/OrganizePdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Organize/OrganizePdfToolView.axaml.cs deleted file mode 100644 index 1fb639a..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/OrganizePdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Organize; - -public partial class OrganizePdfToolView : UserControl -{ - public OrganizePdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Organize/PageNumbersToolView.axaml b/src/PdfEditorApp/Views/Tools/Organize/PageNumbersToolView.axaml deleted file mode 100644 index d0c0734..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/PageNumbersToolView.axaml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Organize/PageNumbersToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Organize/PageNumbersToolView.axaml.cs deleted file mode 100644 index 25668e2..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/PageNumbersToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Organize; - -public partial class PageNumbersToolView : UserControl -{ - public PageNumbersToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Organize/RotatePdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Organize/RotatePdfToolView.axaml deleted file mode 100644 index 3ca454b..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/RotatePdfToolView.axaml +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Organize/RotatePdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Organize/RotatePdfToolView.axaml.cs deleted file mode 100644 index 1a11cd3..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/RotatePdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Organize; - -public partial class RotatePdfToolView : UserControl -{ - public RotatePdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Organize/SplitPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Organize/SplitPdfToolView.axaml deleted file mode 100644 index ef6df75..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/SplitPdfToolView.axaml +++ /dev/null @@ -1,252 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Organize/SplitPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Organize/SplitPdfToolView.axaml.cs deleted file mode 100644 index 45dbdb7..0000000 --- a/src/PdfEditorApp/Views/Tools/Organize/SplitPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Organize; - -public partial class SplitPdfToolView : UserControl -{ - public SplitPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Security/CompressPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Security/CompressPdfToolView.axaml deleted file mode 100644 index 31d6d6e..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/CompressPdfToolView.axaml +++ /dev/null @@ -1,328 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Security/CompressPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Security/CompressPdfToolView.axaml.cs deleted file mode 100644 index 069aefa..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/CompressPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Security; - -public partial class CompressPdfToolView : UserControl -{ - public CompressPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Security/ProtectPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Security/ProtectPdfToolView.axaml deleted file mode 100644 index e4b616c..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/ProtectPdfToolView.axaml +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Security/ProtectPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Security/ProtectPdfToolView.axaml.cs deleted file mode 100644 index a552b33..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/ProtectPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Security; - -public partial class ProtectPdfToolView : UserControl -{ - public ProtectPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Security/RedactPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Security/RedactPdfToolView.axaml deleted file mode 100644 index 9583a14..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/RedactPdfToolView.axaml +++ /dev/null @@ -1,235 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Security/RedactPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Security/RedactPdfToolView.axaml.cs deleted file mode 100644 index a0eb330..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/RedactPdfToolView.axaml.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using PdfEditorApp.ViewModels.Tools.Security; - -namespace PdfEditorApp.Views.Tools.Security; - -public partial class RedactPdfToolView : UserControl -{ - private readonly Border? _liveSelectionBox; - private bool _isDragging; - private bool _dragForceDrawBox; - private Point _dragStart; - - public RedactPdfToolView() - { - InitializeComponent(); - _liveSelectionBox = this.FindControl("LiveSelectionBox"); - } - - private void OnPreviewPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is not InputElement element) return; - if (!e.GetCurrentPoint(element).Properties.IsLeftButtonPressed) return; - - _isDragging = true; - // Captured once at drag start: holding Alt (Option on macOS) switches this one - // drag to raw-rectangle "draw a box" mode instead of the default text-selection - // snapping — matching how modifier-driven mode switches work in the PDF Reader. - _dragForceDrawBox = e.KeyModifiers.HasFlag(KeyModifiers.Alt); - _dragStart = e.GetPosition(element); - e.Pointer.Capture(element); - - if (_liveSelectionBox != null) - { - Canvas.SetLeft(_liveSelectionBox, _dragStart.X); - Canvas.SetTop(_liveSelectionBox, _dragStart.Y); - _liveSelectionBox.Width = 0; - _liveSelectionBox.Height = 0; - _liveSelectionBox.IsVisible = true; - } - } - - private void OnPreviewPointerMoved(object? sender, PointerEventArgs e) - { - if (!_isDragging || sender is not InputElement element || _liveSelectionBox == null) return; - - var pos = e.GetPosition(element); - double x = System.Math.Min(pos.X, _dragStart.X); - double y = System.Math.Min(pos.Y, _dragStart.Y); - double w = System.Math.Abs(pos.X - _dragStart.X); - double h = System.Math.Abs(pos.Y - _dragStart.Y); - - Canvas.SetLeft(_liveSelectionBox, x); - Canvas.SetTop(_liveSelectionBox, y); - _liveSelectionBox.Width = w; - _liveSelectionBox.Height = h; - } - - private void OnPreviewPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (!_isDragging) return; - _isDragging = false; - e.Pointer.Capture(null); - - if (_liveSelectionBox == null) return; - _liveSelectionBox.IsVisible = false; - - if (DataContext is RedactPdfToolViewModel vm && _liveSelectionBox.Width >= 2 && _liveSelectionBox.Height >= 2) - { - var rect = new Rect( - Canvas.GetLeft(_liveSelectionBox), - Canvas.GetTop(_liveSelectionBox), - _liveSelectionBox.Width, - _liveSelectionBox.Height); - vm.AddManualMark(rect, _dragForceDrawBox); - } - } - - private void OnPreviewPointerWheelChanged(object? sender, PointerWheelEventArgs e) - { - bool zoomModifier = e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Meta); - if (!zoomModifier || DataContext is not RedactPdfToolViewModel vm) return; - - if (e.Delta.Y > 0) vm.Preview.ZoomInCommand.Execute(null); - else if (e.Delta.Y < 0) vm.Preview.ZoomOutCommand.Execute(null); - e.Handled = true; - } -} diff --git a/src/PdfEditorApp/Views/Tools/Security/RepairPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Security/RepairPdfToolView.axaml deleted file mode 100644 index b13a44c..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/RepairPdfToolView.axaml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Security/RepairPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Security/RepairPdfToolView.axaml.cs deleted file mode 100644 index 6c3c1f3..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/RepairPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Security; - -public partial class RepairPdfToolView : UserControl -{ - public RepairPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Security/SignPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Security/SignPdfToolView.axaml deleted file mode 100644 index 78e77cf..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/SignPdfToolView.axaml +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Security/SignPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Security/SignPdfToolView.axaml.cs deleted file mode 100644 index a8af422..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/SignPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Security; - -public partial class SignPdfToolView : UserControl -{ - public SignPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Security/UnlockPdfToolView.axaml b/src/PdfEditorApp/Views/Tools/Security/UnlockPdfToolView.axaml deleted file mode 100644 index f694355..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/UnlockPdfToolView.axaml +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Security/UnlockPdfToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Security/UnlockPdfToolView.axaml.cs deleted file mode 100644 index 85bad9b..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/UnlockPdfToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Security; - -public partial class UnlockPdfToolView : UserControl -{ - public UnlockPdfToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/Tools/Security/WatermarkToolView.axaml b/src/PdfEditorApp/Views/Tools/Security/WatermarkToolView.axaml deleted file mode 100644 index 7ab4ec5..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/WatermarkToolView.axaml +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/Tools/Security/WatermarkToolView.axaml.cs b/src/PdfEditorApp/Views/Tools/Security/WatermarkToolView.axaml.cs deleted file mode 100644 index f001468..0000000 --- a/src/PdfEditorApp/Views/Tools/Security/WatermarkToolView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views.Tools.Security; - -public partial class WatermarkToolView : UserControl -{ - public WatermarkToolView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/Views/TrashCachePageView.axaml b/src/PdfEditorApp/Views/TrashCachePageView.axaml deleted file mode 100644 index f312e6c..0000000 --- a/src/PdfEditorApp/Views/TrashCachePageView.axaml +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/PdfEditorApp/Views/TrashCachePageView.axaml.cs b/src/PdfEditorApp/Views/TrashCachePageView.axaml.cs deleted file mode 100644 index 61c600c..0000000 --- a/src/PdfEditorApp/Views/TrashCachePageView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace PdfEditorApp.Views; - -public partial class TrashCachePageView : UserControl -{ - public TrashCachePageView() - { - InitializeComponent(); - } -} diff --git a/src/PdfEditorApp/app.manifest b/src/PdfEditorApp/app.manifest deleted file mode 100644 index 7b162c6..0000000 --- a/src/PdfEditorApp/app.manifest +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - diff --git a/test b/test deleted file mode 100755 index 275d41d..0000000 --- a/test +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -dotnet clean - -dotnet test tests/PdfEditorApp.Tests "$@" diff --git a/tests/PdfEditorApp.Tests/AiServiceTests.cs b/tests/PdfEditorApp.Tests/AiServiceTests.cs deleted file mode 100644 index 5819eac..0000000 --- a/tests/PdfEditorApp.Tests/AiServiceTests.cs +++ /dev/null @@ -1,1091 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.AI; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class AiServiceTests : IDisposable -{ - private readonly string _tempDir; - private readonly string _tempSettingsPath; - - public AiServiceTests() - { - _tempDir = Path.Combine(Path.GetTempPath(), "FryPdf_AiServiceTests_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempDir); - _tempSettingsPath = Path.Combine(_tempDir, "ui_settings.json"); - } - - public void Dispose() - { - try - { - if (Directory.Exists(_tempDir)) - { - Directory.Delete(_tempDir, true); - } - } - catch - { - // Best effort cleanup - } - } - - [Fact] - public void AiModelInfo_TierAndBadges_CalculatedCorrectly() - { - // 1. Free Local Model - var localModel = new AiModelInfo - { - Id = "llama3.2:latest", - DisplayName = "Llama 3.2", - Provider = AiProviderType.OllamaLocal, - Tier = AiModelTier.FreeLocal, - SizeBytes = 2_000_000_000L - }; - - Assert.True(localModel.IsFree); - Assert.False(localModel.IsCloud); - Assert.Equal("Free / Local", localModel.TierBadgeText); - Assert.Equal("#DCFCE7", localModel.BadgeBackgroundColor); - Assert.Contains("[Free / Local]", localModel.FullTitleWithBadge); - - // 2. Free Cloud Tier Model (e.g. Ollama Cloud Free, Groq Free) - var freeCloudModel = new AiModelInfo - { - Id = "llama-3.1-8b-instant", - DisplayName = "Llama 3.1 8B (Groq Free Cloud)", - Provider = AiProviderType.CustomOpenAiCompatible, - Tier = AiModelTier.FreeCloud - }; - - Assert.True(freeCloudModel.IsFree); - Assert.True(freeCloudModel.IsCloud); - Assert.Equal("Free / Cloud", freeCloudModel.TierBadgeText); - Assert.Equal("#E0F2FE", freeCloudModel.BadgeBackgroundColor); - Assert.Contains("[Free / Cloud]", freeCloudModel.FullTitleWithBadge); - - // 3. Paid Cloud Model - var cloudModel = new AiModelInfo - { - Id = "gpt-4o-mini", - DisplayName = "GPT-4o Mini", - Provider = AiProviderType.OpenAiCloud, - Tier = AiModelTier.PaidCloud - }; - - Assert.False(cloudModel.IsFree); - Assert.True(cloudModel.IsCloud); - Assert.Equal("Paid / Cloud", cloudModel.TierBadgeText); - Assert.Equal("#EDE9FE", cloudModel.BadgeBackgroundColor); - Assert.Contains("[Paid / Cloud]", cloudModel.FullTitleWithBadge); - } - - [Theory] - [InlineData(0, "")] - [InlineData(-100, "")] - [InlineData(500 * 1024, "500.0 KB")] - [InlineData(100 * 1024 * 1024, "100.0 MB")] - [InlineData(2147483648L, "2.0 GB")] - public void AiModelInfo_FormatBytes_FormatsAsExpected(long bytes, string expected) - { - string actual = AiModelInfo.FormatBytes(bytes); - Assert.Equal(expected, actual); - } - - [Fact] - public void AiSettingsModel_DefaultsAndClone_WorkProperly() - { - var settings = new AiSettingsModel(); - - Assert.Equal(AiProviderType.OllamaLocal, settings.SelectedProvider); - Assert.Equal("http://localhost:11434", settings.OllamaEndpoint); - Assert.False(settings.IsOllamaRemote); - Assert.Equal("llama3.2", settings.SelectedModelId); - Assert.Equal(0.7f, settings.Temperature); - - settings.OllamaApiKey = "ollama-cloud-jwt-token"; - settings.OpenAiApiKey = "sk-test-dummy-key"; - settings.CustomBaseUrl = "https://api.groq.com/openai/v1"; - settings.DiscoveredOllamaModels.Add(new AiModelInfo { Id = "test-model", DisplayName = "Test" }); - - var clone = settings.Clone(); - - Assert.NotNull(clone); - Assert.Equal(settings.SelectedProvider, clone.SelectedProvider); - Assert.Equal(settings.OllamaEndpoint, clone.OllamaEndpoint); - Assert.Equal(settings.OllamaApiKey, clone.OllamaApiKey); - Assert.Equal(settings.OpenAiApiKey, clone.OpenAiApiKey); - Assert.Equal(settings.CustomBaseUrl, clone.CustomBaseUrl); - Assert.Single(clone.DiscoveredOllamaModels); - Assert.Equal("test-model", clone.DiscoveredOllamaModels[0].Id); - - // Remote Ollama detection - clone.OllamaEndpoint = "https://ollama.mycompany.cloud"; - Assert.True(clone.IsOllamaRemote); - - // Verify independent instance - clone.DiscoveredOllamaModels.Clear(); - Assert.Single(settings.DiscoveredOllamaModels); - } - - [Fact] - public void AiSettingsModel_JsonSerialization_RoundtripsAccurately() - { - var original = new AiSettingsModel - { - SelectedProvider = AiProviderType.CustomOpenAiCompatible, - OllamaApiKey = "my-token", - CustomBaseUrl = "http://localhost:8080/v1", - CustomModelName = "mistral-small", - Temperature = 0.5f, - SystemInstructions = "Always use blue accent colors" - }; - - string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize(json); - - Assert.NotNull(deserialized); - Assert.Equal(original.SelectedProvider, deserialized.SelectedProvider); - Assert.Equal(original.OllamaApiKey, deserialized.OllamaApiKey); - Assert.Equal(original.CustomBaseUrl, deserialized.CustomBaseUrl); - Assert.Equal(original.CustomModelName, deserialized.CustomModelName); - Assert.Equal(original.Temperature, deserialized.Temperature); - Assert.Equal(original.SystemInstructions, deserialized.SystemInstructions); - } - - [Fact] - public void AiService_GetCloudCatalogModels_ReturnsBothFreeAndPaidTiers() - { - var aiService = new AiService(); - var models = aiService.GetCloudCatalogModels(); - - Assert.NotEmpty(models); - - // Must contain Free Cloud tier models (e.g. Groq free tier, OpenRouter free) - var freeCloud = models.Where(m => m.Tier == AiModelTier.FreeCloud).ToList(); - Assert.NotEmpty(freeCloud); - Assert.All(freeCloud, m => - { - Assert.True(m.IsFree); - Assert.True(m.IsCloud); - Assert.Equal("Free / Cloud", m.TierBadgeText); - }); - - // Must contain Paid Cloud tier models (e.g. OpenAI GPT-4o) - var paidCloud = models.Where(m => m.Tier == AiModelTier.PaidCloud).ToList(); - Assert.NotEmpty(paidCloud); - Assert.All(paidCloud, m => - { - Assert.False(m.IsFree); - Assert.True(m.IsCloud); - Assert.Equal("Paid / Cloud", m.TierBadgeText); - }); - - Assert.Contains(models, m => m.Id == "gpt-4o-mini"); - Assert.Contains(models, m => m.Id == "gpt-4o"); - Assert.Contains(models, m => m.Id == "llama-3.1-8b-instant"); - } - - [Fact] - public void AiService_GetUnifiedModelCatalog_IncludesExpectedModels() - { - var aiService = new AiService(); - var settings = new AiSettingsModel - { - SelectedProvider = AiProviderType.OllamaLocal - }; - settings.DiscoveredOllamaModels.Add(new AiModelInfo - { - Id = "custom-llama:latest", - DisplayName = "Custom Llama", - Provider = AiProviderType.OllamaLocal, - Tier = AiModelTier.FreeLocal - }); - - var catalog = aiService.GetUnifiedModelCatalog(settings); - - Assert.NotEmpty(catalog); - Assert.Contains(catalog, m => m.Id == "custom-llama:latest"); - - // When switching to OpenAI provider, catalog includes cloud models - settings.SelectedProvider = AiProviderType.OpenAiCloud; - var cloudCatalog = aiService.GetUnifiedModelCatalog(settings); - Assert.Contains(cloudCatalog, m => m.Id == "gpt-4o-mini"); - Assert.Contains(cloudCatalog, m => m.Tier == AiModelTier.FreeCloud); - } - - [Fact] - public async Task AiService_DiscoverOllamaModelsAsync_HandlesUnreachableEndpointGracefully() - { - var aiService = new AiService(); - // Use an unassigned port that won't respond - var models = await aiService.DiscoverOllamaModelsAsync("http://127.0.0.1:59999", CancellationToken.None); - - Assert.NotNull(models); - Assert.Empty(models); - } - - [Fact] - public async Task AiService_TestConnectionAsync_ReportsFailureForEmptyCloudApiKey() - { - var aiService = new AiService(); - var settings = new AiSettingsModel - { - SelectedProvider = AiProviderType.OpenAiCloud, - OpenAiApiKey = "" - }; - - var (success, message, _) = await aiService.TestConnectionAsync(settings, CancellationToken.None); - - Assert.False(success); - Assert.Contains("API Key", message, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void AiSettingsViewModel_InitializesAndUpdatesCatalog() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var vm = new AiSettingsViewModel(uiSettings, aiService); - - Assert.NotNull(vm.AvailableModels); - Assert.NotEmpty(vm.AvailableModels); - Assert.NotNull(vm.SelectedModel); - - // Switch to Cloud provider - vm.SelectedProvider = AiProviderType.OpenAiCloud; - Assert.NotEmpty(vm.AvailableModels); - Assert.Contains(vm.AvailableModels, m => !m.IsFree); - Assert.Contains(vm.AvailableModels, m => m.Id == "gpt-4o-mini"); - Assert.Contains(vm.AvailableModels, m => m.Tier == AiModelTier.FreeCloud); - } - - [Fact] - public void AiAssistantViewModel_InitialStateAndPrompts_AreValid() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - - Assert.False(vm.IsOpen); - Assert.False(vm.IsGenerating); - Assert.NotEmpty(vm.SuggestedPrompts); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("Invoice", StringComparison.OrdinalIgnoreCase)); - - vm.Open(); - Assert.True(vm.IsOpen); - - vm.Close(); - Assert.False(vm.IsOpen); - } - - [Fact] - public async Task PdfStudioAgentService_ExecutePromptAsync_ValidatesInput() - { - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var settings = new AiSettingsModel(); - - // Empty prompt test - var emptyResult = await agentService.ExecutePromptAsync("", null!, settings); - Assert.False(emptyResult.Success); - Assert.Contains("empty", emptyResult.Message, StringComparison.OrdinalIgnoreCase); - - // Null target page test - var nullPageResult = await agentService.ExecutePromptAsync("Create heading", null!, settings); - Assert.False(nullPageResult.Success); - Assert.Contains("required", nullPageResult.Message, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void ParseOllamaLibraryHtml_ExtractsModelsAndCloudBadgesAccurately() - { - string sampleHtml = """ -
  • - -
    -

    - qwen3.8-flash-next -

    -

    This experimental preview of the architecture that will underpin Qwen4.

    -
    -
    -
    - vision - tools - thinking -
    -

    - 69.7K -

    -
    -
    -
  • -
  • - -
    -

    - glm-5.3-flash -

    -

    Z.ai's first natively multimodal model with 18B active parameters.

    -
    -
    -
    - vision - tools - thinking - cloud -
    -
    -
    -
  • - """; - - var models = AiService.ParseOllamaLibraryHtml(sampleHtml); - - Assert.Equal(2, models.Count); - - // Model 1: Local - var qwen = models.First(m => m.Id == "qwen3.8-flash-next"); - Assert.Equal(AiModelTier.FreeLocal, qwen.Tier); - Assert.Contains("Vision", qwen.Capabilities); - Assert.Contains("Tools", qwen.Capabilities); - Assert.Contains("Thinking", qwen.Capabilities); - Assert.Contains("69.7K pulls", qwen.FormattedSize); - - // Model 2: Cloud - var glm = models.First(m => m.Id == "glm-5.3-flash"); - Assert.Equal(AiModelTier.FreeCloud, glm.Tier); - Assert.Equal("Ollama Cloud", glm.FormattedSize); - Assert.Equal("18B", glm.ParameterSize); - } - - [Fact] - public void AiSettingsViewModel_CustomModelTyping_SynthesizesNonEmptyMetadata() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var vm = new AiSettingsViewModel(uiSettings, aiService); - - // Type an unlisted custom model - vm.SelectedModelId = "custom-nemotron-70b-preview"; - - var activeInfo = vm.ActiveModelInfo; - Assert.NotNull(activeInfo); - Assert.Equal("custom-nemotron-70b-preview", activeInfo.Id); - Assert.Equal("custom-nemotron-70b-preview", activeInfo.DisplayName); - Assert.Equal(AiModelTier.FreeLocal, activeInfo.Tier); - Assert.False(string.IsNullOrWhiteSpace(activeInfo.TierBadgeText)); - } - - [Theory] - [InlineData("https://api.groq.com/openai/v1/chat/completions", "https://api.groq.com/openai/v1")] - [InlineData("https://api.groq.com/openai/v1/chat/completions/", "https://api.groq.com/openai/v1")] - [InlineData("https://api.groq.com/openai/v1", "https://api.groq.com/openai/v1")] - [InlineData("https://api.groq.com/openai/v1/", "https://api.groq.com/openai/v1")] - [InlineData("https://openrouter.ai/api/v1/chat/completions", "https://openrouter.ai/api/v1")] - [InlineData(null, "https://api.openai.com/v1")] - [InlineData("", "https://api.openai.com/v1")] - [InlineData(" ", "https://api.openai.com/v1")] - public void AiService_NormalizeCustomOpenAiBaseUrl_SanitizesEndpoints(string? input, string expected) - { - string actual = AiService.NormalizeCustomOpenAiBaseUrl(input); - Assert.Equal(expected, actual); - } - - [Fact] - public void AiSettingsViewModel_CustomModelName_SynchronizesWithSelectedModelId() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var vm = new AiSettingsViewModel(uiSettings, aiService) - { - SelectedProvider = AiProviderType.CustomOpenAiCompatible - }; - - // User types custom model into CustomModelName field - vm.CustomModelName = "qwen/qwen3.6-27b"; - - Assert.Equal("qwen/qwen3.6-27b", vm.SelectedModelId); - Assert.Equal("qwen/qwen3.6-27b", vm.ActiveModelInfo.Id); - - // Selecting a model from the list updates CustomModelName as well - vm.SelectedModelId = "openai/gpt-oss-120b"; - Assert.Equal("openai/gpt-oss-120b", vm.CustomModelName); - } - - [Theory] - [InlineData("groq", "https://api.groq.com/openai/v1", "openai/gpt-oss-120b")] - [InlineData("openrouter", "https://openrouter.ai/api/v1", "meta-llama/llama-3.2-3b-instruct:free")] - [InlineData("together", "https://api.together.xyz/v1", "meta-llama/Llama-3.3-70B-Instruct-Turbo")] - [InlineData("lmstudio", "http://localhost:1234/v1", "local-model")] - public void AiSettingsViewModel_ApplyPreset_ConfiguresProviderAndModelsCorrectly(string preset, string expectedUrl, string expectedModel) - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var vm = new AiSettingsViewModel(uiSettings, aiService) - { - SelectedProvider = AiProviderType.CustomOpenAiCompatible - }; - - vm.ApplyPreset(preset); - - Assert.Equal(expectedUrl, vm.CustomBaseUrl); - Assert.Equal(expectedModel, vm.SelectedModelId); - Assert.Equal(expectedModel, vm.CustomModelName); - } - - [Fact] - public void AiSettingsViewModel_CustomCompatibleMode_ExcludesOllamaModelsAndProvidesDynamicHistory() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - uiSettings.UpdateSettings(s => - { - s.AiSettings.DiscoveredOllamaModels.Clear(); - s.AiSettings.DiscoveredOllamaModels.Add(new AiModelInfo { Id = "ollama-local-exclusive", DisplayName = "Ollama Local Only", Provider = AiProviderType.OllamaLocal }); - }); - - var aiService = new AiService(); - var vm = new AiSettingsViewModel(uiSettings, aiService) - { - SelectedProvider = AiProviderType.CustomOpenAiCompatible, - CustomBaseUrl = "https://api.groq.com/openai/v1" - }; - - // Assert Ollama model is NOT present in AvailableModels - Assert.DoesNotContain(vm.AvailableModels, m => m.Id == "ollama-local-exclusive"); - - // Assert Groq/cloud models are present - Assert.Contains(vm.AvailableModels, m => m.Id == "openai/gpt-oss-120b"); - - // Assert dynamic subtitle and placeholder - Assert.Contains("custom model identifier", vm.ModelSectionSubtitle, StringComparison.OrdinalIgnoreCase); - Assert.Contains("ANY", vm.ModelInputPlaceholder); - } - - [Fact] - public void AiSettingsViewModel_SaveCustomModel_FutureProofWithoutCodeChanges() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var vm = new AiSettingsViewModel(uiSettings, aiService) - { - SelectedProvider = AiProviderType.CustomOpenAiCompatible, - CustomBaseUrl = "https://api.groq.com/openai/v1" - }; - - // User enters a brand new future model not known in the codebase - string futureModelId = "meta-llama/llama-4-scintilla-400b"; - vm.SelectedModelId = futureModelId; - vm.SaveCustomModel(futureModelId); - - // Model is immediately remembered in history - Assert.Contains(futureModelId, vm.CustomModelHistory); - - // Model is in AvailableModels catalog - Assert.Contains(vm.AvailableModels, m => m.Id == futureModelId); - - // ActiveModelInfo reflects the future model - Assert.Equal(futureModelId, vm.ActiveModelInfo.Id); - - // Future model is persisted in settings storage - Assert.Contains(futureModelId, uiSettings.Settings.AiSettings.CustomModelHistory); - } - - [Fact] - public async Task ModifyElementAsync_Chart_ModifiesChartTypeAndPalette() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - - var chartVm = new PdfEditorApp.ViewModels.ElementViewModels.ChartElementViewModel - { - Title = "FY 2026 Quarterly Revenue Trajectory ($ Billions)", - ChartType = ChartType.BarColumn, - Palette = ChartPalette.CorporateBlue - }; - chartVm.Bars.Add(new PdfEditorApp.ViewModels.ElementViewModels.ChartBarItem { Category = "Q1", Value = 0.65, ValueLabel = "$0.65B" }); - chartVm.Bars.Add(new PdfEditorApp.ViewModels.ElementViewModels.ChartBarItem { Category = "Q2", Value = 0.74, ValueLabel = "$0.74B" }); - chartVm.Bars.Add(new PdfEditorApp.ViewModels.ElementViewModels.ChartBarItem { Category = "Q3", Value = 0.82, ValueLabel = "$0.82B" }); - chartVm.Bars.Add(new PdfEditorApp.ViewModels.ElementViewModels.ChartBarItem { Category = "Q4", Value = 0.89, ValueLabel = "$0.89B" }); - - var settings = new AiSettingsModel(); - - // Prompt requesting Line chart, emerald palette, and 2027 projection - var result = await agent.ModifyElementAsync( - chartVm, - "Change to a smooth Line chart with emerald green palette and add 2027 projection", - settings); - - Assert.True(result.Success); - Assert.Equal(ChartType.SmoothLine, chartVm.ChartType); - Assert.Equal(ChartPalette.EmeraldGreen, chartVm.Palette); - Assert.True(chartVm.Bars.Count >= 5); - Assert.Contains(chartVm.Bars, b => b.Category.Contains("2027")); - } - - [Fact] - public async Task ModifyElementAsync_Text_ModifiesTypographyAndContent() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - - var textVm = new PdfEditorApp.ViewModels.ElementViewModels.TextElementViewModel - { - Text = "Net profit margins expanded by 4.2% year-over-year.", - FontSize = 12, - IsBold = false, - TextColorHex = "#201F1E" - }; - - var settings = new AiSettingsModel(); - - var result = await agent.ModifyElementAsync( - textVm, - "Make bold, 18pt, blue color, center aligned, and executive tone", - settings); - - Assert.True(result.Success); - Assert.True(textVm.IsBold); - Assert.Equal(18, textVm.FontSize); - Assert.Equal("#0F6CBD", textVm.TextColorHex); - Assert.Equal(TextAlignmentMode.Center, textVm.Alignment); - Assert.Contains("Executive Summary:", textVm.Text); - } - - [Fact] - public async Task ModifyElementAsync_Table_AddsSummaryRow() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - - var tableVm = new PdfEditorApp.ViewModels.ElementViewModels.TableElementViewModel(); - tableVm.Headers.Clear(); - tableVm.Headers.Add(new PdfEditorApp.ViewModels.ElementViewModels.TableHeaderItem("Department")); - tableVm.Headers.Add(new PdfEditorApp.ViewModels.ElementViewModels.TableHeaderItem("Headcount")); - tableVm.Headers.Add(new PdfEditorApp.ViewModels.ElementViewModels.TableHeaderItem("Budget")); - - tableVm.Rows.Clear(); - tableVm.Rows.Add(new PdfEditorApp.ViewModels.ElementViewModels.TableRowItem(new[] { "Engineering", "45", "$450,000" })); - tableVm.Rows.Add(new PdfEditorApp.ViewModels.ElementViewModels.TableRowItem(new[] { "Product", "12", "$120,000" })); - - var settings = new AiSettingsModel(); - - var result = await agent.ModifyElementAsync( - tableVm, - "Add a total row and use emerald green header", - settings); - - Assert.True(result.Success); - Assert.Equal(3, tableVm.Rows.Count); - Assert.Equal("Total", tableVm.Rows[2].Cells[0].Text); - Assert.Equal("#047857", tableVm.HeaderBackgroundHex); - } - - [Fact] - public async Task ModifyElementAsync_UndoRedo_RestoresOriginalState() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - - var chartVm = new PdfEditorApp.ViewModels.ElementViewModels.ChartElementViewModel - { - Title = "Original Bar Chart", - ChartType = ChartType.BarColumn, - Palette = ChartPalette.CorporateBlue - }; - - var settings = new AiSettingsModel(); - - var result = await agent.ModifyElementAsync( - chartVm, - "Switch to Line chart with sunset orange palette", - settings); - - Assert.True(result.Success); - Assert.Equal(ChartType.Line, chartVm.ChartType); - Assert.Equal(ChartPalette.SunsetOrange, chartVm.Palette); - Assert.True(undoRedo.CanUndo); - - // Perform Undo - undoRedo.Undo(); - - // State is completely restored to original - Assert.Equal("Original Bar Chart", chartVm.Title); - Assert.Equal(ChartType.BarColumn, chartVm.ChartType); - Assert.Equal(ChartPalette.CorporateBlue, chartVm.Palette); - - // Perform Redo - Assert.True(undoRedo.CanRedo); - undoRedo.Redo(); - - Assert.Equal(ChartType.Line, chartVm.ChartType); - Assert.Equal(ChartPalette.SunsetOrange, chartVm.Palette); - } - - [Fact] - public void AiAssistantViewModel_OpenForElement_SwitchesToModifyModeAndPopulatesElementSuggestions() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - var uiSettings = new UiSettingsService(_tempSettingsPath); - - var vm = new AiAssistantViewModel(agent, uiSettings, aiService); - - var chartVm = new PdfEditorApp.ViewModels.ElementViewModels.ChartElementViewModel - { - Title = "FY 2026 Revenue Trajectory", - ChartType = ChartType.BarColumn - }; - - // Open targeting chart - vm.OpenForElement(chartVm); - - Assert.True(vm.IsOpen); - Assert.True(vm.IsModifyMode); - Assert.True(vm.HasTargetElement); - Assert.Same(chartVm, vm.TargetElement); - Assert.Equal("FY 2026 Revenue Trajectory", vm.TargetElementTitle); - Assert.Equal("Chart", vm.TargetElementKindBadge); - - // Verify contextual suggestions were generated for chart - Assert.NotEmpty(vm.SuggestedPrompts); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("Line chart", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("Emerald Green", StringComparison.OrdinalIgnoreCase)); - - // Switch to create mode - vm.SwitchToCreateMode(); - Assert.False(vm.IsModifyMode); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("invoice header", StringComparison.OrdinalIgnoreCase)); - - // Switch back to modify mode - vm.SwitchToModifyMode(); - Assert.True(vm.IsModifyMode); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("Line chart", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void MainViewModel_AskAiToModify_OpensAiAssistantForSelectedElement() - { - var mainVm = new MainViewModel(); - Assert.NotNull(mainVm.CurrentPage); - - var chartVm = new PdfEditorApp.ViewModels.ElementViewModels.ChartElementViewModel - { - Title = "FY 2026 Quarterly Revenue Trajectory ($ Billions)" - }; - - // Select the chart on the current page - mainVm.CurrentPage.AddElement(chartVm); - mainVm.CurrentPage.SelectElement(chartVm); - - // Execute AskAiToModifyCommand - mainVm.AskAiToModifyCommand.Execute(null); - - // Assert AiAssistant opened in modify mode targeting the chart - Assert.True(mainVm.AiAssistant.IsOpen); - Assert.True(mainVm.AiAssistant.IsModifyMode); - Assert.Same(chartVm, mainVm.AiAssistant.TargetElement); - Assert.Equal("FY 2026 Quarterly Revenue Trajectory ($ Billions)", mainVm.AiAssistant.TargetElementTitle); - } - - [Fact] - public async Task PdfStudioAgentService_ModifyTextElement_WithBulletsOrCheckmarks_TransformsContentCorrectly() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - - var textVm = new TextElementViewModel - { - Text = "Acquisition of TechNova AI\nEnterprise Customer Expansion\nMultiplatform Engine Upgrade", - FontSize = 14, - IsBold = false, - TextColorHex = "#201F1E" - }; - - var settings = new AiSettingsModel(); - var result = await agent.ModifyElementAsync(textVm, "add checkmarks and make bold in emerald green", settings); - - Assert.True(result.Success); - Assert.Contains("✔", textVm.Text); - Assert.True(textVm.IsBold); - Assert.Equal("#16A34A", textVm.TextColorHex); - Assert.True(undoRedo.CanUndo); - - // Verify Undo restores original text and styling - undoRedo.Undo(); - Assert.False(textVm.IsBold); - Assert.DoesNotContain("✔", textVm.Text); - Assert.Equal("#201F1E", textVm.TextColorHex); - - // Verify Redo reapplies modifications - undoRedo.Redo(); - Assert.Contains("✔", textVm.Text); - Assert.True(textVm.IsBold); - Assert.Equal("#16A34A", textVm.TextColorHex); - } - - [Fact] - public async Task PdfStudioAgentService_ModifyMathFormula_TransformsLatexAndSize() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - var settings = new AiSettingsModel(); - - var mathVm = new MathElementViewModel - { - Formula = "x + y = z", - FontSize = 16, - TextColorHex = "#333333" - }; - - var result = await agent.ModifyElementAsync(mathVm, "change to pythagorean theorem with font size 24 and navy text", settings); - - Assert.True(result.Success); - Assert.Equal("a^2 + b^2 = c^2", mathVm.Formula); - Assert.Equal(24, mathVm.FontSize); - Assert.Equal("#0F172A", mathVm.TextColorHex); - - // Test Quadratic formula prompt - var result2 = await agent.ModifyElementAsync(mathVm, "change to quadratic formula with emerald text", settings); - Assert.True(result2.Success); - Assert.Contains(@"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}", mathVm.Formula); - Assert.Equal("#059669", mathVm.TextColorHex); - } - - [Fact] - public async Task PdfStudioAgentService_ModifyImageAndQrCodeElements_UpdatesProperties() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - var settings = new AiSettingsModel(); - - // 1. Image Element - var imgVm = new ImageElementViewModel - { - Opacity = 1.0, - CornerRadius = 0 - }; - - var imgResult = await agent.ModifyElementAsync(imgVm, "set opacity to 50% and rounded corners", settings); - Assert.True(imgResult.Success); - Assert.Equal(0.5, imgVm.Opacity, 2); - Assert.Equal(8, imgVm.CornerRadius); - - // 2. QR Code Element - var qrVm = new QrCodeElementViewModel - { - Content = "https://example.org/old", - DarkColorHex = "#000000" - }; - - var qrResult = await agent.ModifyElementAsync(qrVm, "change url to https://frypdf.com/verify and dark color navy", settings); - Assert.True(qrResult.Success); - Assert.Equal("https://frypdf.com/verify", qrVm.Content); - Assert.Equal("#1E293B", qrVm.DarkColorHex); - } - - [Fact] - public void AiAssistantViewModel_OpenForDifferentElementTypes_ConfiguresIconTitleAndSuggestions() - { - var aiService = new AiService(); - var undoRedo = new UndoRedoService(); - var agent = new PdfStudioAgentService(aiService, undoRedo); - var uiSettings = new UiSettingsService(_tempSettingsPath); - var vm = new AiAssistantViewModel(agent, uiSettings, aiService); - - // Text element - var textVm = new TextElementViewModel { Text = "Acquisition of TechNova AI" }; - vm.OpenForElement(textVm); - Assert.True(vm.IsOpen); - Assert.Equal("Text", vm.TargetElementKindBadge); - Assert.Equal("Acquisition of TechNova AI", vm.TargetElementTitle); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("checkmark", StringComparison.OrdinalIgnoreCase) || p.Contains("check", StringComparison.OrdinalIgnoreCase)); - - // Math element - var mathVm = new MathElementViewModel { Formula = "x^2 + y^2 = r^2", PresetName = "Circle Equation" }; - vm.OpenForElement(mathVm); - Assert.Equal("Math", vm.TargetElementKindBadge); - Assert.Contains("Circle", vm.TargetElementTitle); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("Quadratic", StringComparison.OrdinalIgnoreCase)); - - // QR Code element - var qrVm = new QrCodeElementViewModel { Content = "https://frypdf.com/verify" }; - vm.OpenForElement(qrVm); - Assert.Equal("QrCode", vm.TargetElementKindBadge); - Assert.Contains("https://frypdf.com/verify", vm.TargetElementTitle); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("URL", StringComparison.OrdinalIgnoreCase)); - - // Form Field element - var formVm = new FormFieldElementViewModel { Label = "Authorized Signatory Name" }; - vm.OpenForElement(formVm); - Assert.Equal("FormField", vm.TargetElementKindBadge); - Assert.Equal("Field: Authorized Signatory Name", vm.TargetElementTitle); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("required", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void AskAiButton_InstantiatesWithDefaults_AndSupportsVariants() - { - var btn = new PdfEditorApp.Views.Controls.AskAiButton(); - - Assert.Equal(PdfEditorApp.Views.Controls.AskAiButtonVariant.FloatingPill, btn.Variant); - Assert.Equal("Ask AI", btn.ButtonText); - Assert.Equal("Ask AI to Modify (Ctrl+I / ⌘I)", btn.ToolTipText); - Assert.Equal(22, btn.Height); - Assert.Equal(Avalonia.Layout.HorizontalAlignment.Right, btn.HorizontalAlignment); - Assert.Equal(Avalonia.Layout.VerticalAlignment.Top, btn.VerticalAlignment); - - // Switch to subtle variant - btn.Variant = PdfEditorApp.Views.Controls.AskAiButtonVariant.Subtle; - Assert.Equal(PdfEditorApp.Views.Controls.AskAiButtonVariant.Subtle, btn.Variant); - Assert.Equal(24, btn.Height); - Assert.Equal(Avalonia.Layout.HorizontalAlignment.Left, btn.HorizontalAlignment); - } - - [Fact] - public void AiAssistantViewModel_NewChat_CreatesFreshSession() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - - Assert.NotEmpty(vm.Sessions); - Assert.NotNull(vm.CurrentSession); - var initialSession = vm.CurrentSession; - - vm.NewChatCommand.Execute(null); - - Assert.NotSame(initialSession, vm.CurrentSession); - Assert.Equal("New Chat", vm.CurrentSession.Title); - Assert.Empty(vm.CurrentSession.Messages); - Assert.Equal(2, vm.Sessions.Count); - } - - [Fact] - public void AiAssistantViewModel_SwitchSession_ChangesCurrentMessages() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - - var session1 = vm.CurrentSession; - Assert.NotNull(session1); - session1.Messages.Add(new AiChatMessage { Content = "Hello from session 1" }); - - vm.NewChatCommand.Execute(null); - var session2 = vm.CurrentSession; - Assert.NotNull(session2); - session2.Messages.Add(new AiChatMessage { Content = "Hello from session 2" }); - - Assert.NotNull(vm.CurrentSession); - Assert.Single(vm.CurrentSession.Messages); - Assert.Equal("Hello from session 2", vm.CurrentSession.Messages[0].Content); - - // Switch back to session 1 - vm.SwitchSessionCommand.Execute(session1); - Assert.Same(session1, vm.CurrentSession); - Assert.Single(vm.CurrentSession.Messages); - Assert.Equal("Hello from session 1", vm.CurrentSession.Messages[0].Content); - } - - [Fact] - public void AiAssistantViewModel_DeleteSession_RemovesSessionAndMaintainsActive() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - - var session1 = vm.CurrentSession; - Assert.NotNull(session1); - vm.NewChatCommand.Execute(null); - var session2 = vm.CurrentSession; - Assert.NotNull(session2); - - Assert.Equal(2, vm.Sessions.Count); - - // Delete active session2 - vm.DeleteSessionCommand.Execute(session2); - - Assert.Single(vm.Sessions); - Assert.Same(session1, vm.CurrentSession); - } - - [Fact] - public void AiAssistantViewModel_ToggleHistoryDrawer_TogglesDrawerState() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - - Assert.False(vm.IsHistoryDrawerOpen); - vm.ToggleHistoryDrawerCommand.Execute(null); - Assert.True(vm.IsHistoryDrawerOpen); - vm.ToggleHistoryDrawerCommand.Execute(null); - Assert.False(vm.IsHistoryDrawerOpen); - } - - [Fact] - public void AiAssistantViewModel_TargetEntirePage_SetsEntirePageScopeAndSuggestions() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - var page = new PageViewModel { PageNumber = 2, Width = 800, Height = 1131 }; - - vm.TargetEntirePage(page); - - Assert.Equal(AiTargetKind.EntirePage, vm.TargetKind); - Assert.True(vm.IsPageTargetMode); - Assert.False(vm.IsPointTargetMode); - Assert.False(vm.HasTargetPoint); - Assert.False(vm.HasTargetElement); - Assert.Equal(2, vm.TargetPageNumber); - Assert.Contains("Page 2", vm.TargetDescription); - Assert.Contains("800 × 1131", vm.TargetDescription); - Assert.NotEmpty(vm.SuggestedPrompts); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("invoice", StringComparison.OrdinalIgnoreCase) || p.Contains("page", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void AiAssistantViewModel_TargetPointOnPage_SetsCoordinatesAndSuggestions() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - var page = new PageViewModel { PageNumber = 3, Width = 800, Height = 1131 }; - - vm.TargetPointOnPage(185.5, 340.2, page); - - Assert.Equal(AiTargetKind.PointOnPage, vm.TargetKind); - Assert.True(vm.IsPointTargetMode); - Assert.False(vm.IsPageTargetMode); - Assert.True(vm.HasTargetPoint); - Assert.NotNull(vm.TargetPoint); - Assert.Equal(185.5, vm.TargetPointX); - Assert.Equal(340.2, vm.TargetPointY); - Assert.Contains("186", vm.TargetPointDisplay); - Assert.Contains("340", vm.TargetPointDisplay); - Assert.Contains("Point at", vm.TargetDescription); - Assert.Contains("Page 3", vm.TargetDescription); - Assert.NotEmpty(vm.SuggestedPrompts); - Assert.Contains(vm.SuggestedPrompts, p => p.Contains("here", StringComparison.OrdinalIgnoreCase) || p.Contains("point", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void AiAssistantViewModel_ClearTargetPoint_RevertsToEntirePage() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - var page = new PageViewModel { PageNumber = 1, Width = 800, Height = 1131 }; - - vm.TargetPointOnPage(200, 400, page); - Assert.True(vm.HasTargetPoint); - - vm.ClearTargetPointCommand.Execute(null); - - Assert.False(vm.HasTargetPoint); - Assert.Null(vm.TargetPoint); - Assert.Equal(AiTargetKind.EntirePage, vm.TargetKind); - Assert.True(vm.IsPageTargetMode); - } - - [Fact] - public void AiAssistantViewModel_SwitchModes_UpdatesScopeAndSuggestions() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - var page = new PageViewModel { PageNumber = 1, Width = 800, Height = 1131 }; - vm.GetCurrentPage = () => page; - - // Start in Entire Page mode - vm.SwitchToEntirePageModeCommand.Execute(null); - Assert.Equal(AiTargetKind.EntirePage, vm.TargetKind); - - // Switch to Point Target mode - vm.SwitchToPointTargetModeCommand.Execute(null); - Assert.Equal(AiTargetKind.PointOnPage, vm.TargetKind); - Assert.True(vm.HasTargetPoint); - - // Switch back to Entire Page mode - vm.SwitchToEntirePageModeCommand.Execute(null); - Assert.Equal(AiTargetKind.EntirePage, vm.TargetKind); - Assert.False(vm.HasTargetPoint); - } - - [Fact] - public void AiAssistantViewModel_UpdateTargetElement_OverridesPointAndPage() - { - var uiSettings = new UiSettingsService(_tempSettingsPath); - var aiService = new AiService(); - var agentService = new PdfStudioAgentService(aiService); - var vm = new AiAssistantViewModel(agentService, uiSettings, aiService); - var page = new PageViewModel { PageNumber = 1 }; - vm.GetCurrentPage = () => page; - - vm.TargetPointOnPage(100, 200, page); - Assert.True(vm.HasTargetPoint); - - var textElement = new PdfEditorApp.ViewModels.ElementViewModels.TextElementViewModel { Text = "Sample Heading" }; - vm.UpdateTargetElement(textElement); - - Assert.Equal(AiTargetKind.Element, vm.TargetKind); - Assert.True(vm.HasTargetElement); - Assert.False(vm.HasTargetPoint); - Assert.Contains("Sample Heading", vm.TargetElementTitle); - - // Deselect element - vm.UpdateTargetElement(null); - Assert.False(vm.HasTargetElement); - Assert.Equal(AiTargetKind.EntirePage, vm.TargetKind); - } - - [Fact] - public void AiChatMessage_Undo_TriggersUndoCallback() - { - bool undoCalled = false; - var msg = new AiChatMessage - { - Role = AiChatRole.Assistant, - CanUndo = true, - UndoAction = () => undoCalled = true - }; - - Assert.True(msg.CanUndo); - msg.UndoCommand.Execute(null); - - Assert.True(undoCalled); - Assert.False(msg.CanUndo); - } -} diff --git a/tests/PdfEditorApp.Tests/BatchPdfGeneratorTests.cs b/tests/PdfEditorApp.Tests/BatchPdfGeneratorTests.cs deleted file mode 100644 index 8195069..0000000 --- a/tests/PdfEditorApp.Tests/BatchPdfGeneratorTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Data; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class BatchPdfGeneratorTests -{ - private readonly IDataMergeEngine _mergeEngine = new DataMergeEngine(); - private readonly IPdfExportService _exportService = new PdfExportService(); - - private PdfDocumentModel CreateSampleTemplate() - { - var doc = new PdfDocumentModel - { - Title = "Payslip_{{EmployeeId}}.pdf" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Width = 600, - Height = 800, - Elements = new List - { - new PdfTextElement - { - X = 50, - Y = 50, - Width = 400, - Height = 30, - Text = "Payslip for {{EmployeeName}}", - FontSize = 14 - }, - new PdfTextElement - { - X = 50, - Y = 90, - Width = 400, - Height = 25, - Text = "Net Salary: {{NetSalary:currency:INR}}", - FontSize = 12 - }, - new PdfQrCodeElement - { - X = 50, - Y = 130, - Width = 80, - Height = 80, - Content = "https://verify.codefrydev.in?emp={{EmployeeId}}" - } - } - }; - - doc.Pages.Add(page); - return doc; - } - - private DataMatrix CreateSampleMatrix() - { - var headers = new List { "EmployeeId", "EmployeeName", "NetSalary" }; - var rows = new List> - { - new() { "EMP-101", "John Doe", "75000" }, - new() { "EMP-102", "Jane Doe", "82000" }, - new() { "EMP-103", "Alex Doe", "68000" } - }; - - return new DataMatrix(headers, rows); - } - - [Fact] - public async Task GenerateBatch_SeparateFilesMode_CreatesIndividualPdfs() - { - var generator = new BatchPdfGeneratorService(_mergeEngine, _exportService); - var template = CreateSampleTemplate(); - var matrix = CreateSampleMatrix(); - - string tempDir = Path.Combine(Path.GetTempPath(), "FryPdf_Test_BatchSeparate_" + Guid.NewGuid().ToString("N")); - - try - { - var config = new BatchGenerationConfig - { - OutputMode = BatchOutputMode.SeparateFiles, - OutputDirectory = tempDir, - FilenamePattern = "Payslip_{{EmployeeId}}_{{EmployeeName}}.pdf" - }; - - var progressReports = new List(); - var progress = new Progress(p => progressReports.Add(p)); - - var result = await generator.GenerateBatchAsync(template, matrix, null, config, progress); - - Assert.True(result.IsSuccess); - Assert.Equal(3, result.SuccessfulCount); - Assert.Equal(3, result.GeneratedFiles.Count); - - foreach (var file in result.GeneratedFiles) - { - Assert.True(File.Exists(file)); - byte[] bytes = File.ReadAllBytes(file); - Assert.True(bytes.Length > 100); - } - } - finally - { - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } - } - - [Fact] - public async Task GenerateBatch_SingleMergedPdfMode_CreatesMultiPagePdf() - { - var generator = new BatchPdfGeneratorService(_mergeEngine, _exportService); - var template = CreateSampleTemplate(); - var matrix = CreateSampleMatrix(); - - string tempFile = Path.Combine(Path.GetTempPath(), "FryPdf_Test_Merged_" + Guid.NewGuid().ToString("N") + ".pdf"); - - try - { - var config = new BatchGenerationConfig - { - OutputMode = BatchOutputMode.SingleMergedPdf, - OutputFilePath = tempFile - }; - - var result = await generator.GenerateBatchAsync(template, matrix, null, config); - - Assert.True(result.IsSuccess); - Assert.Equal(3, result.SuccessfulCount); - Assert.True(File.Exists(tempFile)); - - byte[] bytes = File.ReadAllBytes(tempFile); - Assert.True(bytes.Length > 200); - } - finally - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - } - } - - [Fact] - public async Task GenerateBatch_ZipArchiveMode_CreatesZipWithAllPdfs() - { - var generator = new BatchPdfGeneratorService(_mergeEngine, _exportService); - var template = CreateSampleTemplate(); - var matrix = CreateSampleMatrix(); - - string tempZip = Path.Combine(Path.GetTempPath(), "FryPdf_Test_Zip_" + Guid.NewGuid().ToString("N") + ".zip"); - - try - { - var config = new BatchGenerationConfig - { - OutputMode = BatchOutputMode.ZipArchive, - OutputFilePath = tempZip, - FilenamePattern = "Payslip_{{EmployeeId}}.pdf" - }; - - var result = await generator.GenerateBatchAsync(template, matrix, null, config); - - Assert.True(result.IsSuccess); - Assert.Equal(3, result.SuccessfulCount); - Assert.True(File.Exists(tempZip)); - - // Verify Zip Entries - using (var zip = ZipFile.OpenRead(tempZip)) - { - Assert.Equal(3, zip.Entries.Count); - Assert.NotNull(zip.GetEntry("Payslip_EMP-101.pdf")); - Assert.NotNull(zip.GetEntry("Payslip_EMP-102.pdf")); - Assert.NotNull(zip.GetEntry("Payslip_EMP-103.pdf")); - } - } - finally - { - if (File.Exists(tempZip)) - { - File.Delete(tempZip); - } - } - } - - [Fact] - public async Task GenerateBatch_Cancellation_StopsExecutionGracefully() - { - var generator = new BatchPdfGeneratorService(_mergeEngine, _exportService); - var template = CreateSampleTemplate(); - var matrix = CreateSampleMatrix(); - - var cts = new CancellationTokenSource(); - cts.Cancel(); // Cancel immediately - - var config = new BatchGenerationConfig - { - OutputMode = BatchOutputMode.SeparateFiles, - OutputDirectory = Path.GetTempPath() - }; - - await Assert.ThrowsAnyAsync(async () => - { - await generator.GenerateBatchAsync(template, matrix, null, config, null, cts.Token); - }); - } -} diff --git a/tests/PdfEditorApp.Tests/BezierShapeAndLineEngineTests.cs b/tests/PdfEditorApp.Tests/BezierShapeAndLineEngineTests.cs deleted file mode 100644 index f768bef..0000000 --- a/tests/PdfEditorApp.Tests/BezierShapeAndLineEngineTests.cs +++ /dev/null @@ -1,410 +0,0 @@ -using System; -using System.IO; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Templates.Events; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class BezierShapeAndLineEngineTests -{ - [Theory] - [InlineData(ShapeType.BezierCurve)] - [InlineData(ShapeType.CurvedArrow)] - [InlineData(ShapeType.SCurveConnector)] - [InlineData(ShapeType.WaveLine)] - [InlineData(ShapeType.ArcLine)] - [InlineData(ShapeType.CurlyBrace)] - [InlineData(ShapeType.CurvedCallout)] - [InlineData(ShapeType.Teardrop)] - [InlineData(ShapeType.WaveRibbon)] - [InlineData(ShapeType.OrganicBlob)] - public void GetVectorPath_GeneratesValidSvgPath_ForBezierShapes(ShapeType shapeType) - { - string path = SvgShapeHelper.GetVectorPath(shapeType, 200, 100, 4); - - Assert.False(string.IsNullOrWhiteSpace(path)); - Assert.StartsWith("M ", path); - Assert.True(path.Contains("C ") || path.Contains("Q ") || path.Contains("L ") || path.Contains("A ")); - } - - [Theory] - [InlineData(LineDashStyle.Solid, null)] - [InlineData(LineDashStyle.Dashed, "7.0,4.0")] - [InlineData(LineDashStyle.Dotted, "2.0,4.0")] - [InlineData(LineDashStyle.DashDot, "8.0,4.0,2.0,4.0")] - public void GetDashArray_ReturnsCorrectDashString(LineDashStyle style, string? expectedDash) - { - string? result = SvgShapeHelper.GetDashArray(style, 2.0); - Assert.Equal(expectedDash, result); - } - - [Theory] - [InlineData(DividerStyle.Straight)] - [InlineData(DividerStyle.Wave)] - [InlineData(DividerStyle.SCurve)] - [InlineData(DividerStyle.Arch)] - [InlineData(DividerStyle.DoubleWave)] - [InlineData(DividerStyle.CalligraphicFlourish)] - public void GenerateDividerSvgPath_HorizontalAndVertical_ProducesValidPaths(DividerStyle style) - { - string hPath = SvgShapeHelper.GenerateDividerSvgPath(style, 400, 24, 6.0, 4.0, false); - Assert.False(string.IsNullOrWhiteSpace(hPath)); - Assert.StartsWith("M 0,", hPath); - - string vPath = SvgShapeHelper.GenerateDividerSvgPath(style, 24, 400, 6.0, 4.0, true); - Assert.False(string.IsNullOrWhiteSpace(vPath)); - Assert.StartsWith("M ", vPath); - } - - [Fact] - public void GenerateDividerSvgMarkup_GeneratesFullValidSvgDocument() - { - var divEl = new PdfDividerElement - { - Width = 300, - Height = 20, - Thickness = 2.5, - ColorHex = "#2563EB", - Style = DividerStyle.Wave, - WaveAmplitude = 8.0, - WaveFrequency = 5.0, - DashStyle = LineDashStyle.Dashed - }; - - string svg = SvgShapeHelper.GenerateDividerSvgMarkup(divEl); - - Assert.Contains("", svg); - } - - [Fact] - public void GenerateSmoothInkSvgPath_ConvertsRawPointsToCubicBezierSpline() - { - string points = "10,10 50,40 100,20 180,60 220,30"; - - string smoothPath = SvgShapeHelper.GenerateSmoothInkSvgPath(points, isSmoothSpline: true); - Assert.StartsWith("M 10.0,10.0", smoothPath); - Assert.Contains(" C ", smoothPath); - - string linearPath = SvgShapeHelper.GenerateSmoothInkSvgPath(points, isSmoothSpline: false); - Assert.StartsWith("M 10.0,10.0", linearPath); - Assert.Contains(" L ", linearPath); - Assert.DoesNotContain(" C ", linearPath); - } - - [Fact] - public void GenerateSmoothInkSvgPath_GracefullyHandlesDegeneratePoints() - { - Assert.Equal("M 0,0", SvgShapeHelper.GenerateSmoothInkSvgPath("")); - Assert.StartsWith("M 10.0,20.0", SvgShapeHelper.GenerateSmoothInkSvgPath("10,20")); - Assert.StartsWith("M 10.0,20.0 L 30.0,40.0", SvgShapeHelper.GenerateSmoothInkSvgPath("10,20 30,40")); - } - - [Fact] - public void GenerateInkSvgMarkup_GeneratesValidSvgWithOpacityAndSmoothGeometry() - { - var ink = new PdfInkElement - { - Width = 200, - Height = 60, - StrokeColorHex = "#7C3AED", - StrokeThickness = 3.5, - Opacity = 0.85, - PointsData = "10,10 40,30 80,15 140,50", - IsSmoothSpline = true - }; - - string svg = SvgShapeHelper.GenerateInkSvgMarkup(ink); - - Assert.Contains("", svg); - } - - [Fact] - public void ViewModels_ToModelAndLoadFromModel_PreservesBezierProperties() - { - // 1. ShapeElementViewModel - var shapeVm = new ShapeElementViewModel - { - ShapeType = ShapeType.BezierCurve, - StrokeColorHex = "#D97706", - StrokeThickness = 3.0, - DashStyle = LineDashStyle.DashDot, - StartCap = LineEndCap.Circle, - EndCap = LineEndCap.Arrow, - WaveFrequency = 3.5, - CurvatureDepth = 55.0 - }; - - var shapeModel = (PdfShapeElement)shapeVm.ToModel(); - Assert.Equal(ShapeType.BezierCurve, shapeModel.ShapeType); - Assert.Equal(LineDashStyle.DashDot, shapeModel.DashStyle); - Assert.Equal(LineEndCap.Circle, shapeModel.StartCap); - Assert.Equal(LineEndCap.Arrow, shapeModel.EndCap); - - var restoredShapeVm = new ShapeElementViewModel(); - restoredShapeVm.LoadFromModel(shapeModel); - Assert.Equal(ShapeType.BezierCurve, restoredShapeVm.ShapeType); - Assert.Equal(LineDashStyle.DashDot, restoredShapeVm.DashStyle); - Assert.Equal(LineEndCap.Circle, restoredShapeVm.StartCap); - Assert.Equal(LineEndCap.Arrow, restoredShapeVm.EndCap); - - // 2. DividerElementViewModel - var divVm = new DividerElementViewModel - { - Style = DividerStyle.CalligraphicFlourish, - WaveAmplitude = 9.0, - WaveFrequency = 6.0, - DashStyle = LineDashStyle.Dotted, - Thickness = 2.5 - }; - - var divModel = (PdfDividerElement)divVm.ToModel(); - Assert.Equal(DividerStyle.CalligraphicFlourish, divModel.Style); - Assert.Equal(9.0, divModel.WaveAmplitude); - Assert.Equal(LineDashStyle.Dotted, divModel.DashStyle); - - var restoredDivVm = new DividerElementViewModel(); - restoredDivVm.LoadFromModel(divModel); - Assert.Equal(DividerStyle.CalligraphicFlourish, restoredDivVm.Style); - Assert.Equal(9.0, restoredDivVm.WaveAmplitude); - Assert.Equal(LineDashStyle.Dotted, restoredDivVm.DashStyle); - - // 3. InkElementViewModel - var inkVm = new InkElementViewModel - { - PointsData = "5,5 25,15 60,35", - IsSmoothSpline = true, - StrokeThickness = 4.0 - }; - - var inkModel = (PdfInkElement)inkVm.ToModel(); - Assert.True(inkModel.IsSmoothSpline); - - var restoredInkVm = new InkElementViewModel(); - restoredInkVm.LoadFromModel(inkModel); - Assert.True(restoredInkVm.IsSmoothSpline); - } - - [Fact] - public async Task PdfExportService_RendersDocumentWithBezierShapesDividersAndInk_Successfully() - { - var doc = new PdfDocumentModel - { - Title = "Bézier Vector Features Verification Document", - Author = "Professional PDF Creator Engine" - }; - - var page = new PdfPageModel - { - Width = 595.28, - Height = 841.89, - PageNumber = 1 - }; - - // Add Bézier curve shape - page.Elements.Add(new PdfShapeElement - { - X = 50, - Y = 50, - Width = 300, - Height = 100, - ShapeType = ShapeType.BezierCurve, - FillColorHex = "#00000000", - StrokeColorHex = "#2563EB", - StrokeThickness = 3.0, - DashStyle = LineDashStyle.DashDot - }); - - // Add Curved Arrow - page.Elements.Add(new PdfShapeElement - { - X = 50, - Y = 170, - Width = 250, - Height = 80, - ShapeType = ShapeType.CurvedArrow, - FillColorHex = "#00000000", - StrokeColorHex = "#7C3AED", - StrokeThickness = 2.5 - }); - - // Add Decorative Wave Divider - page.Elements.Add(new PdfDividerElement - { - X = 50, - Y = 270, - Width = 495, - Height = 24, - Style = DividerStyle.Wave, - WaveAmplitude = 8.0, - WaveFrequency = 5.0, - Thickness = 2.0, - ColorHex = "#059669" - }); - - // Add Calligraphic Flourish Divider - page.Elements.Add(new PdfDividerElement - { - X = 50, - Y = 320, - Width = 495, - Height = 24, - Style = DividerStyle.CalligraphicFlourish, - WaveAmplitude = 7.0, - Thickness = 2.0, - ColorHex = "#D97706" - }); - - // Add Freehand Ink Drawing with Catmull-Rom to Cubic Bézier Spline - page.Elements.Add(new PdfInkElement - { - X = 50, - Y = 380, - Width = 350, - Height = 90, - PointsData = "10,20 60,70 120,30 200,80 280,40 340,75", - StrokeColorHex = "#DC2626", - StrokeThickness = 3.0, - Opacity = 0.9, - IsSmoothSpline = true - }); - - // Add Curved Callout and Curly Brace - page.Elements.Add(new PdfShapeElement - { - X = 50, - Y = 500, - Width = 200, - Height = 100, - ShapeType = ShapeType.CurvedCallout, - FillColorHex = "#EFF6FF", - StrokeColorHex = "#3B82F6", - StrokeThickness = 2.0, - Label = "Bézier Callout" - }); - - page.Elements.Add(new PdfShapeElement - { - X = 300, - Y = 500, - Width = 40, - Height = 150, - ShapeType = ShapeType.CurlyBrace, - FillColorHex = "#00000000", - StrokeColorHex = "#475569", - StrokeThickness = 2.0 - }); - - doc.Pages.Add(page); - - var exportService = new PdfExportService(); - string tempPdfPath = Path.Combine(Path.GetTempPath(), $"bezier_shapes_test_{Guid.NewGuid():N}.pdf"); - - try - { - await exportService.ExportToFileAsync(doc, tempPdfPath); - Assert.True(File.Exists(tempPdfPath)); - var fileInfo = new FileInfo(tempPdfPath); - Assert.True(fileInfo.Length > 1000); - - byte[] pdfBytes = await File.ReadAllBytesAsync(tempPdfPath); - // PDF header magic bytes %PDF- - Assert.Equal(0x25, pdfBytes[0]); // % - Assert.Equal(0x50, pdfBytes[1]); // P - Assert.Equal(0x44, pdfBytes[2]); // D - Assert.Equal(0x46, pdfBytes[3]); // F - } - finally - { - if (File.Exists(tempPdfPath)) - { - File.Delete(tempPdfPath); - } - } - } - - [Fact] - public void CreativeTypographyShowcaseTemplate_GeneratesValidDocumentWithAllBezierAndTextFeatures() - { - var template = new CreativeTypographyShowcaseTemplate(); - Assert.Equal("typographyshowcase", template.Id); - Assert.Equal("Design & Creative", template.Category); - - var doc = template.Create(); - Assert.NotNull(doc); - Assert.Single(doc.Pages); - - var page = doc.Pages[0]; - Assert.True(page.Elements.Count >= 20, "Showcase template should have comprehensive elements"); - - // Verify Bézier curve text elements exist - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.ShapeMode == TextShapeMode.BezierCurve && text.BezierPreset == BezierCurvePreset.Wave); - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.ShapeMode == TextShapeMode.BezierCurve && text.BezierPreset == BezierCurvePreset.Bridge); - - // Verify Text Outline, Drop Shadow, and Background badges exist - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.HasShadow && text.ShadowBlurRadius > 0); - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.HasStroke && text.StrokeWidth > 0); - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.BorderColorHex != "#00000000"); - - // Verify Decorative Dividers exist - Assert.Contains(page.Elements, e => e is PdfDividerElement div && div.Style == DividerStyle.CalligraphicFlourish); - Assert.Contains(page.Elements, e => e is PdfDividerElement div && div.Style == DividerStyle.DoubleWave); - Assert.Contains(page.Elements, e => e is PdfDividerElement div && div.Style == DividerStyle.Arch); - - // Verify Bézier Shapes & Connectors exist - Assert.Contains(page.Elements, e => e is PdfShapeElement shape && shape.ShapeType == ShapeType.MedalRibbonBadge); - Assert.Contains(page.Elements, e => e is PdfShapeElement shape && shape.ShapeType == ShapeType.CurvedCallout); - Assert.Contains(page.Elements, e => e is PdfShapeElement shape && shape.ShapeType == ShapeType.CurvedArrow); - Assert.Contains(page.Elements, e => e is PdfShapeElement shape && shape.ShapeType == ShapeType.SCurveConnector); - Assert.Contains(page.Elements, e => e is PdfShapeElement shape && shape.ShapeType == ShapeType.CurlyBrace); - - // Verify Smooth Bézier Ink Signatures exist - Assert.Contains(page.Elements, e => e is PdfInkElement ink && ink.IsSmoothSpline && ink.StrokeThickness > 0); - } - - [Fact] - public async Task CreativeTypographyShowcaseTemplate_ExportsToValidPdfFile() - { - var templateService = new TemplateService(); - var doc = templateService.CreateTypographyShowcaseTemplate(); - Assert.NotNull(doc); - - var exportService = new PdfExportService(); - string tempPdfPath = Path.Combine(Path.GetTempPath(), $"showcase_template_test_{Guid.NewGuid():N}.pdf"); - - try - { - await exportService.ExportToFileAsync(doc, tempPdfPath); - Assert.True(File.Exists(tempPdfPath)); - var fileInfo = new FileInfo(tempPdfPath); - Assert.True(fileInfo.Length > 2000, "Exported PDF with rich vector assets should be substantial in size"); - - byte[] pdfBytes = await File.ReadAllBytesAsync(tempPdfPath); - Assert.Equal(0x25, pdfBytes[0]); // % - Assert.Equal(0x50, pdfBytes[1]); // P - Assert.Equal(0x44, pdfBytes[2]); // D - Assert.Equal(0x46, pdfBytes[3]); // F - } - finally - { - if (File.Exists(tempPdfPath)) - { - File.Delete(tempPdfPath); - } - } - } -} diff --git a/tests/PdfEditorApp.Tests/CanvasEditingEngineTests.cs b/tests/PdfEditorApp.Tests/CanvasEditingEngineTests.cs deleted file mode 100644 index a24311d..0000000 --- a/tests/PdfEditorApp.Tests/CanvasEditingEngineTests.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System; -using System.Linq; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class CanvasEditingEngineTests -{ - [Fact] - public void PageViewModel_MultiSelection_TogglesAndCalculatesBoundingBoxCorrectly() - { - // Arrange - var page = new PageViewModel { Width = 595.28, Height = 841.89 }; - var el1 = new TextElementViewModel { X = 50, Y = 100, Width = 150, Height = 40, Text = "Title 1" }; - var el2 = new TextElementViewModel { X = 250, Y = 200, Width = 100, Height = 60, Text = "Title 2" }; - page.AddElement(el1); - page.AddElement(el2); - - // Act - Single select - page.SelectElement(el1); - Assert.Single(page.SelectedElements); - Assert.Same(el1, page.SelectedElement); - Assert.False(page.HasMultiSelection); - Assert.Equal(1, page.SelectionCount); - - // Act - Multi select (toggle second) - page.ToggleElementSelection(el2); - Assert.Equal(2, page.SelectedElements.Count); - Assert.True(page.HasMultiSelection); - Assert.Equal(2, page.SelectionCount); - - // Assert - Bounding box spans both elements - // Min X = 50, Min Y = 100, Max X = 250+100=350, Max Y = 200+60=260 - // Width = 350-50 = 300, Height = 260-100 = 160 - var bbox = page.SelectionBoundingBox; - Assert.Equal(50, bbox.X); - Assert.Equal(100, bbox.Y); - Assert.Equal(300, bbox.Width); - Assert.Equal(160, bbox.Height); - - // Act - Select All - page.SelectAll(); - Assert.Equal(2, page.SelectedElements.Count); - - // Act - Clear Selection - page.ClearSelection(); - Assert.Empty(page.SelectedElements); - Assert.Null(page.SelectedElement); - Assert.False(page.HasMultiSelection); - } - - [Fact] - public void InspectorViewModel_MathematicalAlignment_AlignsMultiSelectionCorrectly() - { - // Arrange - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel { Width = 600, Height = 800 }; - - var el1 = new TextElementViewModel { X = 100, Y = 100, Width = 100, Height = 50 }; - var el2 = new TextElementViewModel { X = 200, Y = 200, Width = 120, Height = 40 }; - var el3 = new TextElementViewModel { X = 50, Y = 300, Width = 80, Height = 60 }; - - page.AddElement(el1); - page.AddElement(el2); - page.AddElement(el3); - page.SelectElements(new[] { el1, el2, el3 }); - inspector.UpdateSelection(el1, page); - - // Act - Align Left (should all align to min X = 50) - inspector.AlignLeft(); - Assert.Equal(50, el1.X); - Assert.Equal(50, el2.X); - Assert.Equal(50, el3.X); - - // Undo Align Left - undoRedo.Undo(); - Assert.Equal(100, el1.X); - Assert.Equal(200, el2.X); - Assert.Equal(50, el3.X); - - // Act - Align Right (Max right edge is max(100+100, 200+120, 50+80) = 320) - inspector.AlignRight(); - Assert.Equal(320 - 100, el1.X); // 220 - Assert.Equal(320 - 120, el2.X); // 200 - Assert.Equal(320 - 80, el3.X); // 240 - - // Act - Align Top (Min Y = 100) - inspector.AlignTop(); - Assert.Equal(100, el1.Y); - Assert.Equal(100, el2.Y); - Assert.Equal(100, el3.Y); - } - - [Fact] - public void InspectorViewModel_DistributeHorizontallyAndVertically_SpacesElementsEqually() - { - // Arrange - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel { Width = 1000, Height = 1000 }; - - // 3 elements of width 50 - // Leftmost X = 100, Rightmost X = 500 (Right edge = 550) - // Middle element initially at X = 200 - // Span = 550 - 100 = 450. Total element widths = 150. Total gaps = 300. - // Gap per space (2 spaces) = 300 / 2 = 150. - // Expected X positions: El1: 100, El2: 100 + 50 + 150 = 300, El3: 500 - var el1 = new TextElementViewModel { X = 100, Y = 50, Width = 50, Height = 30 }; - var el2 = new TextElementViewModel { X = 200, Y = 50, Width = 50, Height = 30 }; - var el3 = new TextElementViewModel { X = 500, Y = 50, Width = 50, Height = 30 }; - - page.AddElement(el1); - page.AddElement(el2); - page.AddElement(el3); - page.SelectElements(new[] { el1, el2, el3 }); - inspector.UpdateSelection(el1, page); - - // Act - Distribute Horizontally - inspector.DistributeHorizontally(); - - Assert.Equal(100, el1.X); - Assert.Equal(300, el2.X); - Assert.Equal(500, el3.X); - - // Act - Undo - undoRedo.Undo(); - Assert.Equal(200, el2.X); - - // Act - Redo - undoRedo.Redo(); - Assert.Equal(300, el2.X); - } - - [Fact] - public void InspectorViewModel_BatchDeleteAndDuplicate_MaintainsAtomicUndoRedo() - { - // Arrange - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel { Width = 600, Height = 800 }; - - var el1 = new TextElementViewModel { X = 50, Y = 50, Width = 100, Height = 30, Text = "Elem 1" }; - var el2 = new TextElementViewModel { X = 50, Y = 100, Width = 100, Height = 30, Text = "Elem 2" }; - - page.AddElement(el1); - page.AddElement(el2); - page.SelectElements(new[] { el1, el2 }); - inspector.UpdateSelection(el1, page); - - // Act - Duplicate Selected Elements (Batch) - inspector.DuplicateSelectedElement(); - Assert.Equal(4, page.Elements.Count); - Assert.Equal(2, page.SelectedElements.Count); // Newly duplicated are selected - - // Undo duplication - undoRedo.Undo(); - Assert.Equal(2, page.Elements.Count); - - // Redo duplication - undoRedo.Redo(); - Assert.Equal(4, page.Elements.Count); - - // Act - Delete All 4 elements - page.SelectAll(); - inspector.UpdateSelection(page.SelectedElement, page); - inspector.DeleteSelectedElement(); - Assert.Empty(page.Elements); - - // Undo deletion - undoRedo.Undo(); - Assert.Equal(4, page.Elements.Count); - } - - [Fact] - public void InspectorViewModel_SingleElementAlignment_AlignsRelativeToPage() - { - // Arrange - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel { Width = 600, Height = 800 }; - var el = new TextElementViewModel { X = 250, Y = 350, Width = 100, Height = 50 }; - page.AddElement(el); - page.SelectElement(el); - inspector.UpdateSelection(el, page); - - // Align Left (60 margin) - inspector.AlignLeft(); - Assert.Equal(60, el.X); - - // Align Right (600 - 100 - 60 = 440) - inspector.AlignRight(); - Assert.Equal(440, el.X); - - // Align Center ((600 - 100) / 2 = 250) - inspector.AlignCenter(); - Assert.Equal(250, el.X); - - // Align Top (60 margin) - inspector.AlignTop(); - Assert.Equal(60, el.Y); - - // Align Bottom (800 - 50 - 60 = 690) - inspector.AlignBottom(); - Assert.Equal(690, el.Y); - - // Align Middle ((800 - 50) / 2 = 375) - inspector.AlignMiddle(); - Assert.Equal(375, el.Y); - } - - [Fact] - public void InspectorViewModel_BatchLockToggle_LocksAndUnlocksAllSelected() - { - // Arrange - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel { Width = 600, Height = 800 }; - var el1 = new TextElementViewModel { X = 10, Y = 10, Width = 50, Height = 20, IsLocked = false }; - var el2 = new TextElementViewModel { X = 20, Y = 20, Width = 50, Height = 20, IsLocked = false }; - page.AddElement(el1); - page.AddElement(el2); - page.SelectElements(new[] { el1, el2 }); - inspector.UpdateSelection(el1, page); - - // Act - Lock All - inspector.ToggleLock(); - Assert.True(el1.IsLocked); - Assert.True(el2.IsLocked); - - // Undo - undoRedo.Undo(); - Assert.False(el1.IsLocked); - Assert.False(el2.IsLocked); - - // Redo - undoRedo.Redo(); - Assert.True(el1.IsLocked); - Assert.True(el2.IsLocked); - } - - [Fact] - public void MainViewModel_DynamicZoomAndFit_CalculatesAccurately() - { - // Arrange - var vm = new MainViewModel(); - - // Standard A4 page width = 595.28 pt, height = 841.89 pt - Assert.NotNull(vm.CurrentPage); - - // Act - Zoom in and out - vm.ZoomLevel = 1.0; - vm.ZoomIn(); - Assert.True(vm.ZoomLevel > 1.0); - - vm.ZoomOut(); - Assert.Equal(1.0, vm.ZoomLevel); - - // Act - Fit to Width with viewport width = 2.0x - double targetWidth = vm.CurrentPage.Width * 2.0 + 64.0; - vm.FitToWidthDynamic(targetWidth); - Assert.Equal(2.0, vm.ZoomLevel); - - // Act - Fit to Page with viewport = 1.0x - double targetPageW = vm.CurrentPage.Width * 1.0 + 64.0; - double targetPageH = vm.CurrentPage.Height * 1.0 + 64.0; - vm.FitToPageDynamic(targetPageW, targetPageH); - Assert.Equal(1.0, vm.ZoomLevel); - - // Act - Reset zoom - vm.ResetZoom(); - Assert.Equal(1.0, vm.ZoomLevel); - } - - [Fact] - public void PdfTextElement_And_ViewModel_DefaultPadding_IsZero() - { - var model = new PdfTextElement(); - Assert.Equal(0.0, model.Padding); - - var vm = new TextElementViewModel(); - Assert.Equal(0.0, vm.Padding); - } - - [Fact] - public void TextElementViewModel_CalculateRequiredHeight_ProvidesAdequateHeight() - { - var vm = new TextElementViewModel - { - Text = "I. INTRODUCTION & EXPERIMENTAL SETUP", - FontSize = 11, - FontFamily = "Segoe UI", - Width = 330, - Height = 15 // Deliberately tight/too small - }; - - double requiredHeight = vm.CalculateRequiredHeight(); - Assert.True(requiredHeight >= 18.0, $"Required height should be >= 18 for 11pt font, was {requiredHeight}"); - - vm.AutoFitHeight(); - Assert.True(vm.Height >= 18.0); - } - - [Fact] - public void InspectorViewModel_AutoFitTextHeight_ExpandsSelectedElementsWithUndoRedo() - { - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel { Width = 600, Height = 800 }; - var textEl = new TextElementViewModel - { - Text = "Multi-Line\nAffiliation and Department\nUniversity of Science", - FontSize = 10, - Width = 200, - Height = 15 // Clipped - }; - page.AddElement(textEl); - page.SelectElement(textEl); - inspector.UpdateSelection(textEl, page); - - inspector.AutoFitTextHeight(); - Assert.True(textEl.Height > 30.0, $"Height should expand for 3 lines of text, but was {textEl.Height}"); - - // Test Undo - undoRedo.Undo(); - Assert.Equal(15.0, textEl.Height); - - // Test Redo - undoRedo.Redo(); - Assert.True(textEl.Height > 30.0); - } - - [Fact] - public void AllTemplates_CreateValidDocuments_WithZeroPaddingAndValidTextBounds() - { - var templateService = new TemplateService(); - var templates = templateService.GetAllTemplates(); - - Assert.NotEmpty(templates); - Assert.True(templates.Count() >= 18, $"Expected at least 18 templates, found {templates.Count()}"); - - foreach (var def in templates) - { - var doc = def.Create(); - Assert.NotNull(doc); - Assert.NotEmpty(doc.Pages); - - foreach (var p in doc.Pages) - { - Assert.True(p.Width > 0); - Assert.True(p.Height > 0); - Assert.NotEmpty(p.Elements); - - foreach (var el in p.Elements.OfType()) - { - Assert.True(el.Width > 0, $"Text element '{el.Text}' in template '{def.Name}' has non-positive width"); - Assert.True(el.Height >= 18, $"Text element '{el.Text}' in template '{def.Name}' has height {el.Height} which is too small and might clip"); - } - } - } - } -} diff --git a/tests/PdfEditorApp.Tests/CollapseExpandWorkspaceTests.cs b/tests/PdfEditorApp.Tests/CollapseExpandWorkspaceTests.cs deleted file mode 100644 index de701cc..0000000 --- a/tests/PdfEditorApp.Tests/CollapseExpandWorkspaceTests.cs +++ /dev/null @@ -1,336 +0,0 @@ -using System.Linq; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class CollapseExpandWorkspaceTests -{ - [Fact] - public void MainViewModel_ToggleRibbonCollapse_TogglesStateCorrectly() - { - var vm = new MainViewModel(); - Assert.False(vm.IsRibbonCollapsed); - - vm.ToggleRibbonCollapseCommand.Execute(null); - Assert.True(vm.IsRibbonCollapsed); - - vm.ToggleRibbonCollapseCommand.Execute(null); - Assert.False(vm.IsRibbonCollapsed); - } - - [Fact] - public void MainViewModel_SelectRibbonTab_AutoExpandsWhenCollapsed() - { - var vm = new MainViewModel(); - vm.IsRibbonCollapsed = true; - Assert.True(vm.IsRibbonCollapsed); - - // Selecting a ribbon tab should automatically expand the ribbon - vm.SelectRibbonTabCommand.Execute(RibbonTabKind.Insert); - - Assert.Equal(RibbonTabKind.Insert, vm.ActiveRibbonTab); - Assert.False(vm.IsRibbonCollapsed); - } - - [Fact] - public void MainViewModel_ToggleLeftSidebar_TogglesStateCorrectly() - { - var vm = new MainViewModel(); - Assert.False(vm.IsLeftSidebarCollapsed); - - vm.ToggleLeftSidebarCommand.Execute(null); - Assert.True(vm.IsLeftSidebarCollapsed); - - vm.ToggleLeftSidebarCommand.Execute(null); - Assert.False(vm.IsLeftSidebarCollapsed); - } - - [Fact] - public void MainViewModel_SelectSidebarTab_AutoExpandsWhenCollapsed() - { - var vm = new MainViewModel(); - vm.IsLeftSidebarCollapsed = true; - Assert.True(vm.IsLeftSidebarCollapsed); - - // Selecting a sidebar tab should automatically expand the sidebar - vm.SelectSidebarTabCommand.Execute(SidebarTabKind.Outline); - - Assert.Equal(SidebarTabKind.Outline, vm.ActiveSidebarTab); - Assert.False(vm.IsLeftSidebarCollapsed); - } - - [Fact] - public void MainViewModel_ToggleInspectorCollapse_TogglesStateCorrectly() - { - var vm = new MainViewModel(); - Assert.False(vm.IsInspectorCollapsed); - - vm.ToggleInspectorCollapseCommand.Execute(null); - Assert.True(vm.IsInspectorCollapsed); - - vm.ToggleInspectorCollapseCommand.Execute(null); - Assert.False(vm.IsInspectorCollapsed); - } - - [Fact] - public void MainViewModel_ExpandAllPanels_ExpandsAllThreePanels() - { - var vm = new MainViewModel(); - vm.IsRibbonCollapsed = true; - vm.IsLeftSidebarCollapsed = true; - vm.IsInspectorCollapsed = true; - - vm.ExpandAllPanelsCommand.Execute(null); - - Assert.False(vm.IsRibbonCollapsed); - Assert.False(vm.IsLeftSidebarCollapsed); - Assert.False(vm.IsInspectorCollapsed); - } - - [Fact] - public void MainViewModel_CommandPalette_ContainsCollapseAndExpandEntries() - { - var vm = new MainViewModel(); - var palette = vm.AllPaletteCommands; - - Assert.Contains(palette, c => c.Title.Contains("Toggle Ribbon Toolbar")); - Assert.Contains(palette, c => c.Title.Contains("Toggle Pages Sidebar")); - Assert.Contains(palette, c => c.Title.Contains("Toggle Properties Inspector")); - Assert.Contains(palette, c => c.Title.Contains("Expand All Panels")); - } - - [Fact] - public void InspectorViewModel_AccordionSections_ToggleIndividually() - { - var inspector = new InspectorViewModel(); - - // Default state: all expanded - Assert.True(inspector.IsPresetGeometryExpanded); - Assert.True(inspector.IsLineStrokePatternExpanded); - Assert.True(inspector.IsColorsExpanded); - Assert.True(inspector.IsDimensionsExpanded); - Assert.True(inspector.IsTypographyExpanded); - Assert.True(inspector.IsParagraphExpanded); - Assert.True(inspector.IsTransformExpanded); - Assert.True(inspector.IsShadowExpanded); - - // Toggle Preset Geometry - inspector.TogglePresetGeometryExpandedCommand.Execute(null); - Assert.False(inspector.IsPresetGeometryExpanded); - inspector.TogglePresetGeometryExpandedCommand.Execute(null); - Assert.True(inspector.IsPresetGeometryExpanded); - - // Toggle Line Stroke Pattern - inspector.ToggleLineStrokePatternExpandedCommand.Execute(null); - Assert.False(inspector.IsLineStrokePatternExpanded); - inspector.ToggleLineStrokePatternExpandedCommand.Execute(null); - Assert.True(inspector.IsLineStrokePatternExpanded); - - // Toggle Colors - inspector.ToggleColorsExpandedCommand.Execute(null); - Assert.False(inspector.IsColorsExpanded); - inspector.ToggleColorsExpandedCommand.Execute(null); - Assert.True(inspector.IsColorsExpanded); - - // Toggle Dimensions - inspector.ToggleDimensionsExpandedCommand.Execute(null); - Assert.False(inspector.IsDimensionsExpanded); - inspector.ToggleDimensionsExpandedCommand.Execute(null); - Assert.True(inspector.IsDimensionsExpanded); - - // Toggle Typography - inspector.ToggleTypographyExpandedCommand.Execute(null); - Assert.False(inspector.IsTypographyExpanded); - inspector.ToggleTypographyExpandedCommand.Execute(null); - Assert.True(inspector.IsTypographyExpanded); - - // Toggle Paragraph - inspector.ToggleParagraphExpandedCommand.Execute(null); - Assert.False(inspector.IsParagraphExpanded); - inspector.ToggleParagraphExpandedCommand.Execute(null); - Assert.True(inspector.IsParagraphExpanded); - - // Toggle Transform - inspector.ToggleTransformExpandedCommand.Execute(null); - Assert.False(inspector.IsTransformExpanded); - inspector.ToggleTransformExpandedCommand.Execute(null); - Assert.True(inspector.IsTransformExpanded); - - // Toggle Shadow - inspector.ToggleShadowExpandedCommand.Execute(null); - Assert.False(inspector.IsShadowExpanded); - inspector.ToggleShadowExpandedCommand.Execute(null); - Assert.True(inspector.IsShadowExpanded); - - // Toggle Page Setup - inspector.TogglePageSetupExpandedCommand.Execute(null); - Assert.False(inspector.IsPageSetupExpanded); - inspector.TogglePageSetupExpandedCommand.Execute(null); - Assert.True(inspector.IsPageSetupExpanded); - - // Toggle Image Adjustments - inspector.ToggleImageAdjustmentsExpandedCommand.Execute(null); - Assert.False(inspector.IsImageAdjustmentsExpanded); - inspector.ToggleImageAdjustmentsExpandedCommand.Execute(null); - Assert.True(inspector.IsImageAdjustmentsExpanded); - - // Toggle Table Properties - inspector.ToggleTablePropertiesExpandedCommand.Execute(null); - Assert.False(inspector.IsTablePropertiesExpanded); - inspector.ToggleTablePropertiesExpandedCommand.Execute(null); - Assert.True(inspector.IsTablePropertiesExpanded); - - // Toggle Math Formula - inspector.ToggleMathFormulaExpandedCommand.Execute(null); - Assert.False(inspector.IsMathFormulaExpanded); - inspector.ToggleMathFormulaExpandedCommand.Execute(null); - Assert.True(inspector.IsMathFormulaExpanded); - } - - [Fact] - public void InspectorViewModel_ExpandAndCollapseAllSections_ControlsAllSubSections() - { - var inspector = new InspectorViewModel(); - - inspector.CollapseAllSectionsCommand.Execute(null); - - Assert.False(inspector.IsPageSetupExpanded); - Assert.False(inspector.IsPresetGeometryExpanded); - Assert.False(inspector.IsLineStrokePatternExpanded); - Assert.False(inspector.IsColorsExpanded); - Assert.False(inspector.IsDimensionsExpanded); - Assert.False(inspector.IsTypographyExpanded); - Assert.False(inspector.IsParagraphExpanded); - Assert.False(inspector.IsTransformExpanded); - Assert.False(inspector.IsShadowExpanded); - Assert.False(inspector.IsImageAdjustmentsExpanded); - Assert.False(inspector.IsTablePropertiesExpanded); - Assert.False(inspector.IsMathFormulaExpanded); - - inspector.ExpandAllSectionsCommand.Execute(null); - - Assert.True(inspector.IsPageSetupExpanded); - Assert.True(inspector.IsPresetGeometryExpanded); - Assert.True(inspector.IsLineStrokePatternExpanded); - Assert.True(inspector.IsColorsExpanded); - Assert.True(inspector.IsDimensionsExpanded); - Assert.True(inspector.IsTypographyExpanded); - Assert.True(inspector.IsParagraphExpanded); - Assert.True(inspector.IsTransformExpanded); - Assert.True(inspector.IsShadowExpanded); - Assert.True(inspector.IsImageAdjustmentsExpanded); - Assert.True(inspector.IsTablePropertiesExpanded); - Assert.True(inspector.IsMathFormulaExpanded); - } - - [Fact] - public void MainViewModel_TogglePanels_ShowsToastMessage() - { - var vm = new MainViewModel(); - - vm.ToggleRibbonCollapseCommand.Execute(null); - Assert.True(vm.IsToastVisible); - Assert.Contains("Ribbon", vm.ToastMessage); - - vm.ToggleLeftSidebarCommand.Execute(null); - Assert.True(vm.IsToastVisible); - Assert.Contains("Sidebar", vm.ToastMessage); - - vm.ToggleInspectorCollapseCommand.Execute(null); - Assert.True(vm.IsToastVisible); - Assert.Contains("Inspector", vm.ToastMessage); - - vm.ExpandAllPanelsCommand.Execute(null); - Assert.True(vm.IsToastVisible); - Assert.Contains("All Workspace Panels Expanded", vm.ToastMessage); - } - - [Fact] - public void LeftSidebar_ResizingAndCollapseExpand_MaintainsWidth() - { - var vm = new MainViewModel(); - Assert.False(vm.IsLeftSidebarCollapsed); - Assert.True(vm.IsLeftSidebarResizable); - Assert.Equal(340, vm.LeftSidebarGridLength.Value); - - // Simulate user dragging GridSplitter to 450px - vm.LeftSidebarGridLength = new Avalonia.Controls.GridLength(450, Avalonia.Controls.GridUnitType.Pixel); - Assert.Equal(450, vm.LeftSidebarGridLength.Value); - - // Collapse left sidebar (e.g. ⌘B) - vm.ToggleLeftSidebarCommand.Execute(null); - Assert.True(vm.IsLeftSidebarCollapsed); - Assert.False(vm.IsLeftSidebarResizable); - Assert.Equal(38, vm.LeftSidebarGridLength.Value); - - // Expand left sidebar again (⌘B) -> should restore 450px - vm.ToggleLeftSidebarCommand.Execute(null); - Assert.False(vm.IsLeftSidebarCollapsed); - Assert.True(vm.IsLeftSidebarResizable); - Assert.Equal(450, vm.LeftSidebarGridLength.Value); - } - - [Fact] - public void OpenAiAssistant_ExpandsSidebar_SelectsAiTab_AndEnsuresComfortableWidth() - { - var vm = new MainViewModel(); - vm.IsLeftSidebarCollapsed = true; - Assert.True(vm.IsLeftSidebarCollapsed); - - vm.OpenAiAssistantCommand.Execute(null); - - Assert.False(vm.IsLeftSidebarCollapsed); - Assert.Equal(SidebarTabKind.AiAssistant, vm.ActiveSidebarTab); - Assert.True(vm.LeftSidebarGridLength.Value >= 340); - Assert.True(vm.AiAssistant.IsOpen); - } - - [Fact] - public void CanvasSelectionChanged_WhenAiSidebarActive_LiveUpdatesTargetElement() - { - var vm = new MainViewModel(); - Assert.NotNull(vm.CurrentPage); - - var chartVm = new PdfEditorApp.ViewModels.ElementViewModels.ChartElementViewModel - { - Title = "Live Performance Metrics" - }; - vm.CurrentPage.AddElement(chartVm); - - // Open AI in sidebar - vm.OpenAiAssistantCommand.Execute(null); - Assert.Equal(SidebarTabKind.AiAssistant, vm.ActiveSidebarTab); - - // Click element on canvas - vm.CurrentPage.SelectElement(chartVm); - - // Assert AI Assistant updated live - Assert.True(vm.AiAssistant.IsModifyMode); - Assert.Same(chartVm, vm.AiAssistant.TargetElement); - Assert.Equal("Live Performance Metrics", vm.AiAssistant.TargetElementTitle); - - // Deselect element - vm.CurrentPage.SelectElement(null); - Assert.False(vm.AiAssistant.IsModifyMode); - Assert.Null(vm.AiAssistant.TargetElement); - } - - [Fact] - public void AiAssistant_CloseOrDone_ReturnsToThumbnails() - { - var vm = new MainViewModel(); - vm.OpenAiAssistantCommand.Execute(null); - Assert.Equal(SidebarTabKind.AiAssistant, vm.ActiveSidebarTab); - Assert.True(vm.AiAssistant.IsOpen); - - // User clicks Close or Done - vm.AiAssistant.CloseCommand.Execute(null); - - Assert.False(vm.AiAssistant.IsOpen); - Assert.Equal(SidebarTabKind.Thumbnails, vm.ActiveSidebarTab); - } -} - diff --git a/tests/PdfEditorApp.Tests/ConverterTests.cs b/tests/PdfEditorApp.Tests/ConverterTests.cs deleted file mode 100644 index b7c0122..0000000 --- a/tests/PdfEditorApp.Tests/ConverterTests.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using Avalonia.Media; -using PdfEditorApp.Converters; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class ConverterTests -{ - [Fact] - public void ByteSizeToStringConverter_FormatsAndParsesByteSizesAccurately() - { - var conv = ByteSizeToStringConverter.Instance; - var culture = CultureInfo.InvariantCulture; - - // Conversion tests - Assert.Equal("0 B", conv.Convert(0L, typeof(string), null, culture)); - Assert.Equal("0 B", conv.Convert(null, typeof(string), null, culture)); - Assert.Equal("512 B", conv.Convert(512L, typeof(string), null, culture)); - Assert.Equal("1.0 KB", conv.Convert(1024L, typeof(string), null, culture)); - Assert.Equal("1.5 KB", conv.Convert(1536L, typeof(string), null, culture)); - Assert.Equal("2.00 MB", conv.Convert(2 * 1024 * 1024L, typeof(string), "2", culture)); - Assert.Equal("1.5 GB", conv.Convert((long)(1.5 * 1024 * 1024 * 1024), typeof(string), null, culture)); - - // ConvertBack tests - Assert.Equal(1024L, conv.ConvertBack("1 KB", typeof(long), null, culture)); - Assert.Equal(2097152L, conv.ConvertBack("2 MB", typeof(long), null, culture)); - Assert.Equal(0L, conv.ConvertBack("invalid", typeof(long), null, culture)); - } - - [Fact] - public void DateTimeFormatConverter_FormatsCustomAndRelativeDates() - { - var conv = DateTimeFormatConverter.Instance; - var culture = CultureInfo.InvariantCulture; - - var dt = new DateTime(2026, 8, 15, 14, 30, 0); - Assert.Equal("2026-08-15", conv.Convert(dt, typeof(string), "yyyy-MM-dd", culture)); - Assert.Equal("Aug 15, 2026", conv.Convert(dt, typeof(string), "MMM dd, yyyy", culture)); - - // Relative formatting for recent time - var recent = DateTime.Now.AddSeconds(-10); - Assert.Equal("Just now", conv.Convert(recent, typeof(string), "Relative", culture)); - - // ConvertBack - var parsed = conv.ConvertBack("2026-08-15", typeof(DateTime), null, culture); - Assert.NotNull(parsed); - Assert.Equal(new DateTime(2026, 8, 15), (DateTime)parsed!); - } - - [Fact] - public void TimeSpanFormatConverter_FormatsDurations() - { - var conv = TimeSpanFormatConverter.Instance; - var culture = CultureInfo.InvariantCulture; - - Assert.Equal("1:30", conv.Convert(TimeSpan.FromSeconds(90), typeof(string), null, culture)); - Assert.Equal("1:30", conv.Convert(90.0, typeof(string), null, culture)); - Assert.Equal("1:15:30", conv.Convert(TimeSpan.FromSeconds(4530), typeof(string), null, culture)); - Assert.Equal("1h 15m", conv.Convert(TimeSpan.FromSeconds(4530), typeof(string), "words", culture)); - Assert.Equal("45s", conv.Convert(TimeSpan.FromSeconds(45), typeof(string), "words", culture)); - } - - [Fact] - public void MathConverters_PerformArithmeticAndRoundtrips() - { - var culture = CultureInfo.InvariantCulture; - - // MathAddConverter - Assert.Equal(15.0, MathAddConverter.Instance.Convert(10.0, typeof(double), "5", culture)); - Assert.Equal(10.0, MathAddConverter.Instance.ConvertBack(15.0, typeof(double), "5", culture)); - - // MathSubtractConverter - Assert.Equal(5.0, MathSubtractConverter.Instance.Convert(10.0, typeof(double), "5", culture)); - Assert.Equal(10.0, MathSubtractConverter.Instance.ConvertBack(5.0, typeof(double), "5", culture)); - - // MathMultiplyConverter - Assert.Equal(50.0, MathMultiplyConverter.Instance.Convert(10.0, typeof(double), "5", culture)); - Assert.Equal(10.0, MathMultiplyConverter.Instance.ConvertBack(50.0, typeof(double), "5", culture)); - - // MathDivideConverter - Assert.Equal(2.0, MathDivideConverter.Instance.Convert(10.0, typeof(double), "5", culture)); - Assert.Equal(10.0, MathDivideConverter.Instance.ConvertBack(2.0, typeof(double), "5", culture)); - } - - [Fact] - public void PercentageConverter_FormatsAndParsesPercentages() - { - var conv = PercentageConverter.Instance; - var culture = CultureInfo.InvariantCulture; - - // 0..1 scale - Assert.Equal("75%", conv.Convert(0.75, typeof(string), null, culture)); - Assert.Equal(0.75, (double)(conv.ConvertBack("75%", typeof(double), null, culture) ?? 0.0), 2); - - // 0..100 scale - Assert.Equal("85%", conv.Convert(85.0, typeof(string), "0-100", culture)); - Assert.Equal(85.0, (double)(conv.ConvertBack("85%", typeof(double), "0-100", culture) ?? 0.0), 2); - } - - [Fact] - public void UnitConverters_PointsToMmInchesAndPixels() - { - var culture = CultureInfo.InvariantCulture; - - // 72 points = 1.0 inch = 25.4 mm - double pt = 72.0; - - // Points to MM - var mm = PointsToMillimetersConverter.Instance.Convert(pt, typeof(double), null, culture); - Assert.Equal(25.4, (double)(mm ?? 0.0), 1); - var ptFromMm = PointsToMillimetersConverter.Instance.ConvertBack(25.4, typeof(double), null, culture); - Assert.Equal(72.0, (double)(ptFromMm ?? 0.0), 1); - - // Formatted MM - Assert.Equal("25.4 mm", PointsToMillimetersConverter.Instance.Convert(pt, typeof(string), "format", culture)); - - // Points to Inches - var inches = PointsToInchesConverter.Instance.Convert(pt, typeof(double), null, culture); - Assert.Equal(1.0, (double)(inches ?? 0.0), 2); - var ptFromIn = PointsToInchesConverter.Instance.ConvertBack(1.0, typeof(double), null, culture); - Assert.Equal(72.0, (double)(ptFromIn ?? 0.0), 1); - - // Points to Pixels (96 DPI default -> 72pt * 96/72 = 96px) - var px = PointsToPixelsConverter.Instance.Convert(pt, typeof(double), null, culture); - Assert.Equal(96.0, (double)(px ?? 0.0), 1); - } - - [Fact] - public void BooleanAndLogicConverters_EvaluateConditionsAccurately() - { - var culture = CultureInfo.InvariantCulture; - - // InverseBooleanConverter - Assert.False((bool)(InverseBooleanConverter.Instance.Convert(true, typeof(bool), null, culture) ?? true)); - Assert.True((bool)(InverseBooleanConverter.Instance.Convert(false, typeof(bool), null, culture) ?? false)); - Assert.False((bool)(InverseBooleanConverter.Instance.ConvertBack(true, typeof(bool), null, culture) ?? true)); - - // CountToBooleanConverter - var list = new List { 1, 2, 3 }; - Assert.True((bool)(CountToBooleanConverter.Instance.Convert(list, typeof(bool), null, culture) ?? false)); - Assert.False((bool)(CountToBooleanConverter.Instance.Convert(new List(), typeof(bool), null, culture) ?? true)); - Assert.False((bool)(CountToBooleanConverter.Instance.Convert(list, typeof(bool), "invert", culture) ?? true)); - Assert.True((bool)(CountToBooleanConverter.Instance.Convert(5, typeof(bool), "2", culture) ?? false)); // 5 > 2 - - // NullOrEmptyToBooleanConverter - Assert.True((bool)(NullOrEmptyToBooleanConverter.Instance.Convert("", typeof(bool), null, culture) ?? false)); - Assert.True((bool)(NullOrEmptyToBooleanConverter.Instance.Convert(null, typeof(bool), null, culture) ?? false)); - Assert.False((bool)(NullOrEmptyToBooleanConverter.Instance.Convert("hello", typeof(bool), null, culture) ?? true)); - Assert.True((bool)(NullOrEmptyToBooleanConverter.Instance.Convert("hello", typeof(bool), "invert", culture) ?? false)); - - // ComparisonToBooleanConverter - Assert.True((bool)(ComparisonToBooleanConverter.Instance.Convert(10, typeof(bool), ">5", culture) ?? false)); - Assert.False((bool)(ComparisonToBooleanConverter.Instance.Convert(3, typeof(bool), ">=5", culture) ?? true)); - Assert.True((bool)(ComparisonToBooleanConverter.Instance.Convert(5, typeof(bool), "<=5", culture) ?? false)); - Assert.True((bool)(ComparisonToBooleanConverter.Instance.Convert(42, typeof(bool), "==42", culture) ?? false)); - Assert.True((bool)(ComparisonToBooleanConverter.Instance.Convert(42, typeof(bool), "!=0", culture) ?? false)); - - // CountToStringConverter - Assert.Equal("1 page", CountToStringConverter.Instance.Convert(1, typeof(string), "page|pages", culture)); - Assert.Equal("5 pages", CountToStringConverter.Instance.Convert(5, typeof(string), "page|pages", culture)); - Assert.Equal("0 pages", CountToStringConverter.Instance.Convert(0, typeof(string), "page|pages", culture)); - } - - [Fact] - public void GraphicsAndTransformConverters_CreateVisualElements() - { - var culture = CultureInfo.InvariantCulture; - - // ColorToBrushConverter - var brush = ColorToBrushConverter.Instance.Convert(Colors.Red, typeof(IBrush), null, culture); - Assert.NotNull(brush); - var scb = Assert.IsType(brush); - Assert.Equal(Colors.Red, scb.Color); - - // ScaleToTransformConverter - var st = ScaleToTransformConverter.Instance.Convert(1.5, typeof(ITransform), null, culture); - Assert.NotNull(st); - var scale = Assert.IsType(st); - Assert.Equal(1.5, scale.ScaleX); - Assert.Equal(1.5, scale.ScaleY); - Assert.Null(ScaleToTransformConverter.Instance.Convert(1.0, typeof(ITransform), null, culture)); // Identity returns null - - // TranslateToTransformConverter - var tt = TranslateToTransformConverter.Instance.Convert("10,20", typeof(ITransform), null, culture); - Assert.NotNull(tt); - var trans = Assert.IsType(tt); - Assert.Equal(10.0, trans.X); - Assert.Equal(20.0, trans.Y); - - // StringToGeometryConverter - Assert.Null(StringToGeometryConverter.Instance.Convert(null, typeof(Geometry), null, culture)); - Assert.Null(StringToGeometryConverter.Instance.Convert("", typeof(Geometry), null, culture)); - // If an already parsed Geometry is passed, it returns it directly - // Test ConvertBack - Assert.Null(StringToGeometryConverter.Instance.ConvertBack(null, typeof(string), null, culture)); - } - - [Fact] - public void StringAndTextConverters_FormatStringsCorrectly() - { - var culture = CultureInfo.InvariantCulture; - - // StringCaseConverter - Assert.Equal("HELLO WORLD", StringCaseConverter.Instance.Convert("hello world", typeof(string), "upper", culture)); - Assert.Equal("hello world", StringCaseConverter.Instance.Convert("HELLO WORLD", typeof(string), "lower", culture)); - Assert.Equal("Hello World", StringCaseConverter.Instance.Convert("hello world", typeof(string), "title", culture)); - Assert.Equal("Hello", StringCaseConverter.Instance.Convert("hello", typeof(string), "capitalize", culture)); - - // StringTruncateConverter - Assert.Equal("This is a...", StringTruncateConverter.Instance.Convert("This is a very long string that should be truncated", typeof(string), "12", culture)); - Assert.Equal("Short", StringTruncateConverter.Instance.Convert("Short", typeof(string), "20", culture)); - - // StringPrefixSuffixConverter - Assert.Equal("Page 5", StringPrefixSuffixConverter.Instance.Convert(5, typeof(string), "Page {0}", culture)); - Assert.Equal("72 pt", StringPrefixSuffixConverter.Instance.Convert(72, typeof(string), "{0} pt", culture)); - - // EnumToDescriptionConverter - Assert.Equal("Left", EnumToDescriptionConverter.Instance.Convert(TextAlignmentMode.Left, typeof(string), null, culture)); - Assert.Equal("Top Arc", EnumToDescriptionConverter.Instance.Convert(CircularTextPlacement.TopArc, typeof(string), null, culture)); - Assert.Equal("A4", EnumToDescriptionConverter.Instance.Convert(PageFormat.A4, typeof(string), null, culture)); - Assert.Equal("Corporate Blue", EnumToDescriptionConverter.Instance.Convert(ChartPalette.CorporateBlue, typeof(string), null, culture)); - } - - [Fact] - public void MultiValueConverters_EvaluateAggregationsCorrectly() - { - var culture = CultureInfo.InvariantCulture; - - // AllTrueMultiConverter - Assert.True((bool)(AllTrueMultiConverter.Instance.Convert(new object?[] { true, true, true }, typeof(bool), null, culture) ?? false)); - Assert.False((bool)(AllTrueMultiConverter.Instance.Convert(new object?[] { true, false, true }, typeof(bool), null, culture) ?? true)); - - // AnyTrueMultiConverter - Assert.True((bool)(AnyTrueMultiConverter.Instance.Convert(new object?[] { false, true, false }, typeof(bool), null, culture) ?? false)); - Assert.False((bool)(AnyTrueMultiConverter.Instance.Convert(new object?[] { false, false, false }, typeof(bool), null, culture) ?? true)); - - // StringConcatMultiConverter - Assert.Equal("Page 1 of 10", StringConcatMultiConverter.Instance.Convert(new object?[] { "Page", " 1 ", "of", " 10" }, typeof(string), null, culture)); - Assert.Equal("A-B-C", StringConcatMultiConverter.Instance.Convert(new object?[] { "A", "B", "C" }, typeof(string), "-", culture)); - - // MathMaxMultiConverter & MathMinMultiConverter - Assert.Equal(42.0, (double)(MathMaxMultiConverter.Instance.Convert(new object?[] { 10.0, 42.0, 5.0 }, typeof(double), null, culture) ?? 0.0)); - Assert.Equal(5.0, (double)(MathMinMultiConverter.Instance.Convert(new object?[] { 10.0, 42.0, 5.0 }, typeof(double), null, culture) ?? 0.0)); - } -} diff --git a/tests/PdfEditorApp.Tests/DataMergeEngineTests.cs b/tests/PdfEditorApp.Tests/DataMergeEngineTests.cs deleted file mode 100644 index 23d04d7..0000000 --- a/tests/PdfEditorApp.Tests/DataMergeEngineTests.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using PdfEditorApp.Core.Data; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DataMergeEngineTests -{ - private readonly IDataMergeEngine _engine = new DataMergeEngine(); - - [Fact] - public void EvaluateText_SimplePlaceholder_ReplacesValue() - { - var record = new Dictionary - { - ["EmployeeName"] = "Johnathan Doe", - ["EmployeeId"] = "EMP-0842" - }; - - string template = "Employee: {{EmployeeName}} (ID: {{EmployeeId}})"; - string result = _engine.EvaluateText(template, record); - - Assert.Equal("Employee: Johnathan Doe (ID: EMP-0842)", result); - } - - [Fact] - public void EvaluateText_CaseInsensitiveLookup_MatchesDifferentCasing() - { - var record = new Dictionary - { - ["EMPLOYEE_NAME"] = "Sophia Martinez" - }; - - string template = "Hello, {{EmployeeName}}!"; - string result = _engine.EvaluateText(template, record, new DataMergeOptions { CaseInsensitiveLookup = true }); - - Assert.Equal("Hello, Sophia Martinez!", result); - } - - [Fact] - public void EvaluateText_FallbackValue_UsedWhenKeyIsMissingOrEmpty() - { - var record = new Dictionary - { - ["EmployeeName"] = "David Kim" - }; - - string template = "Dept: {{Department ?? Engineering}}, Level: {{Level ?? L5}}"; - string result = _engine.EvaluateText(template, record); - - Assert.Equal("Dept: Engineering, Level: L5", result); - } - - [Fact] - public void EvaluateText_CurrencyFormatting_FormatsCorrectly() - { - var record = new Dictionary - { - ["BasicSalary"] = "8500", - ["InrSalary"] = "125000", - ["EurSalary"] = "6200", - ["UsdSalary"] = "10500" - }; - - string templateInr = "Salary: {{InrSalary:currency:INR}}"; - string templateEur = "Salary: {{EurSalary:currency:EUR}}"; - string templateUsd = "Salary: {{UsdSalary:currency:USD}}"; - - string resultInr = _engine.EvaluateText(templateInr, record); - string resultEur = _engine.EvaluateText(templateEur, record); - string resultUsd = _engine.EvaluateText(templateUsd, record); - - Assert.Contains("₹1,25,000.00", resultInr); - Assert.Contains("€6,200.00", resultEur); - Assert.Contains("$10,500.00", resultUsd); - } - - [Theory] - [InlineData("en-US")] - [InlineData("en-GB")] - [InlineData("de-DE")] - [InlineData("fr-FR")] - [InlineData("ja-JP")] - public void EvaluateText_CurrencyFormatting_IsCultureInvariantAcrossLocales(string cultureName) - { - var prevCulture = CultureInfo.CurrentCulture; - try - { - CultureInfo.CurrentCulture = new CultureInfo(cultureName); - - var record = new Dictionary - { - ["InrSalary"] = "125000", - ["EurSalary"] = "6200" - }; - - string resultInr = _engine.EvaluateText("Salary: {{InrSalary:currency:INR}}", record); - string resultEur = _engine.EvaluateText("Salary: {{EurSalary:currency:EUR}}", record); - - Assert.Contains("₹1,25,000.00", resultInr); - Assert.Contains("€6,200.00", resultEur); - } - finally - { - CultureInfo.CurrentCulture = prevCulture; - } - } - - [Fact] - public void EvaluateText_NumericAndDateFormatting_FormatsCorrectly() - { - var record = new Dictionary - { - ["GrossAmount"] = "12345.6789", - ["JoiningDate"] = "2022-04-15" - }; - - string templateNum = "Amount: {{GrossAmount:N2}}"; - string templateDate = "Joined: {{JoiningDate:yyyy/MM/dd}}"; - - string resultNum = _engine.EvaluateText(templateNum, record); - string resultDate = _engine.EvaluateText(templateDate, record); - - Assert.Equal("Amount: 12,345.68", resultNum); - Assert.Equal("Joined: 2022/04/15", resultDate); - } - - [Fact] - public void EvaluateText_CaseTransforms_UppercaseLowercaseTitleCase() - { - var record = new Dictionary - { - ["Designation"] = "senior cloud engineer" - }; - - string templateUpper = "{{Designation:upper}}"; - string templateTitle = "{{Designation:title}}"; - - Assert.Equal("SENIOR CLOUD ENGINEER", _engine.EvaluateText(templateUpper, record)); - Assert.Equal("Senior Cloud Engineer", _engine.EvaluateText(templateTitle, record)); - } - - [Fact] - public void DetectPlaceholders_ExtractsAllTagsAcrossDocumentElements() - { - var doc = new PdfDocumentModel - { - Title = "Payslip for {{EmployeeName}}", - Author = "{{Company}}" - }; - - var page = new PdfPageModel - { - HeaderLeft = "Confidential - {{Department}}", - FooterRight = "Generated for {{EmployeeId}}", - Watermark = new PdfWatermarkElement { Text = "{{WatermarkStatus}}" }, - Elements = new List - { - new PdfTextElement { Text = "Gross: {{GrossSalary:C}}" }, - new PdfQrCodeElement { Content = "https://verify.com?id={{EmployeeId}}" }, - new PdfBarcodeElement { CodeValue = "{{BarcodeValue}}" }, - new PdfTableElement - { - Headers = new List { "{{Header1}}", "Amount" }, - Rows = new List> - { - new() { "Basic", "{{BasicSalary}}" } - } - } - } - }; - - doc.Pages.Add(page); - - var detected = _engine.DetectPlaceholders(doc); - - Assert.Contains("EmployeeName", detected); - Assert.Contains("Company", detected); - Assert.Contains("Department", detected); - Assert.Contains("EmployeeId", detected); - Assert.Contains("WatermarkStatus", detected); - Assert.Contains("GrossSalary", detected); - Assert.Contains("BarcodeValue", detected); - Assert.Contains("Header1", detected); - Assert.Contains("BasicSalary", detected); - } - - [Fact] - public void HydrateDocument_PopulatesAllElementsInClonedDocument() - { - var doc = new PdfDocumentModel - { - Title = "Payslip - {{EmployeeName}}" - }; - - var page = new PdfPageModel - { - HeaderLeft = "Dept: {{Department}}", - Elements = new List - { - new PdfTextElement { Text = "Welcome {{EmployeeName}}, Salary: {{Salary:currency:INR}}" }, - new PdfQrCodeElement { Content = "https://verify.corp.com/emp/{{EmployeeId}}" }, - new PdfBarcodeElement { CodeValue = "{{EmployeeId}}" }, - new PdfTableElement - { - Headers = new List { "Item", "Value" }, - Rows = new List> - { - new() { "Base", "{{Salary:currency:INR}}" } - } - } - } - }; - doc.Pages.Add(page); - - var record = new Dictionary - { - ["EmployeeName"] = "Amina Al-Mansoor", - ["Department"] = "Human Resources", - ["EmployeeId"] = "EMP-1315", - ["Salary"] = "92000" - }; - - var hydrated = _engine.HydrateDocument(doc, record); - - // Original template remains untouched - Assert.Equal("Payslip - {{EmployeeName}}", doc.Title); - - // Hydrated clone has values replaced - Assert.Equal("Payslip - Amina Al-Mansoor", hydrated.Title); - Assert.Equal("Dept: Human Resources", hydrated.Pages[0].HeaderLeft); - - var textEl = (PdfTextElement)hydrated.Pages[0].Elements[0]; - Assert.Contains("Welcome Amina Al-Mansoor", textEl.Text); - Assert.Contains("₹92,000.00", textEl.Text); - - var qrEl = (PdfQrCodeElement)hydrated.Pages[0].Elements[1]; - Assert.Equal("https://verify.corp.com/emp/EMP-1315", qrEl.Content); - - var barEl = (PdfBarcodeElement)hydrated.Pages[0].Elements[2]; - Assert.Equal("EMP-1315", barEl.CodeValue); - - var tableEl = (PdfTableElement)hydrated.Pages[0].Elements[3]; - Assert.Equal("₹92,000.00", tableEl.Rows[0][1]); - } -} diff --git a/tests/PdfEditorApp.Tests/DataStudioTests.cs b/tests/PdfEditorApp.Tests/DataStudioTests.cs deleted file mode 100644 index 4d8f90e..0000000 --- a/tests/PdfEditorApp.Tests/DataStudioTests.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using DocumentFormat.OpenXml; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Spreadsheet; -using PdfEditorApp.Core.Data; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.DataStudio; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DataStudioTests -{ - [Fact] - public void DataMatrix_BasicOperations_AndTypeInference() - { - var headers = new List { "Product", "Revenue", "Margin", "Launch Date" }; - var rows = new List> - { - new() { "Laptop Pro", "$1,200.50", "25.5%", "2024-01-15" }, - new() { "Desk Monitor", "$450.00", "30.0%", "2024-02-20" }, - new() { "Mechanical Keyboard", "$120.00", "42.1%", "2024-03-05" } - }; - - var matrix = new DataMatrix(headers, rows); - - Assert.Equal(3, matrix.RowCount); - Assert.Equal(4, matrix.ColumnCount); - Assert.Equal("$1,200.50", matrix.GetCell(0, 1)); - Assert.Equal("Mechanical Keyboard", matrix.GetCell(2, 0)); - - // Test Type Inference - Assert.Equal(DataColumnType.Text, matrix.InferColumnType(0)); - Assert.Equal(DataColumnType.Currency, matrix.InferColumnType(1)); - Assert.Equal(DataColumnType.Percentage, matrix.InferColumnType(2)); - Assert.Equal(DataColumnType.Date, matrix.InferColumnType(3)); - - // Row/Column mutations - matrix.AddRow(new[] { "Wireless Mouse", "$60.00", "50.0%", "2024-04-10" }); - Assert.Equal(4, matrix.RowCount); - - matrix.RemoveRow(3); - Assert.Equal(3, matrix.RowCount); - - matrix.AddColumn("Stock Units", "100"); - Assert.Equal(5, matrix.ColumnCount); - Assert.Equal("100", matrix.GetCell(0, 4)); - - matrix.RemoveColumn(4); - Assert.Equal(4, matrix.ColumnCount); - } - - [Fact] - public void DataSourceService_CsvParser_HandlesRFC4180AndDelimiters() - { - var service = new DataSourceService(); - - // 1. Standard CSV with quotes, commas, and embedded newlines - string csvData = "Name,Description,Amount,Status\n" + - "\"Alpha Widget\",\"High quality, durable\",1500,Active\n" + - "\"Beta Device\",\"Multi-line\r\nSpecs Included\",3200,Pending"; - - var matrix = service.ParseCsv(csvData, ',', true); - - Assert.Equal(2, matrix.RowCount); - Assert.Equal(4, matrix.ColumnCount); - Assert.Equal("Name", matrix.Headers[0]); - Assert.Equal("Alpha Widget", matrix.GetCell(0, 0)); - Assert.Equal("High quality, durable", matrix.GetCell(0, 1)); - Assert.Equal("1500", matrix.GetCell(0, 2)); - Assert.Contains("Multi-line", matrix.GetCell(1, 1)); - - // 2. Semicolon-delimited European CSV - string semiCsv = "Category;Q1;Q2;Q3\nEurope;100;120;140\nAsia;200;220;250"; - var semiMatrix = service.ParseCsv(semiCsv, ';', true); - Assert.Equal(2, semiMatrix.RowCount); - Assert.Equal(4, semiMatrix.ColumnCount); - Assert.Equal("Europe", semiMatrix.GetCell(0, 0)); - - // 3. Tab-separated data (TSV) - string tsvData = "Dept\tHeadcount\tBudget\nEngineering\t45\t$500k\nSales\t30\t$400k"; - var tsvMatrix = service.ParseTsv(tsvData, true); - Assert.Equal(2, tsvMatrix.RowCount); - Assert.Equal(3, tsvMatrix.ColumnCount); - Assert.Equal("Engineering", tsvMatrix.GetCell(0, 0)); - } - - [Fact] - public void DataSourceService_JsonParser_SupportsArraysAndObjects() - { - var service = new DataSourceService(); - - // 1. Array of flat objects - string jsonObjects = @"[ - { ""id"": 1, ""product"": ""Cloud Storage"", ""price"": 99.99, ""tier"": ""Standard"" }, - { ""id"": 2, ""product"": ""Dedicated VM"", ""price"": 299.00, ""tier"": ""Enterprise"" } - ]"; - - var matrix = service.ParseJson(jsonObjects); - Assert.Equal(2, matrix.RowCount); - Assert.True(matrix.ColumnCount >= 4); - Assert.Contains("product", matrix.Headers); - Assert.Equal("Cloud Storage", matrix.GetCell(0, matrix.Headers.IndexOf("product"))); - - // 2. Nested JSON with path (e.g. { data: { metrics: [...] } }) - string nestedJson = @"{ - ""status"": ""success"", - ""data"": { - ""metrics"": [ - { ""quarter"": ""Q1"", ""sales"": 500, ""profit"": 80 }, - { ""quarter"": ""Q2"", ""sales"": 650, ""profit"": 110 } - ] - } - }"; - - var nestedMatrix = service.ParseJson(nestedJson, "data.metrics"); - Assert.Equal(2, nestedMatrix.RowCount); - Assert.Equal("Q1", nestedMatrix.GetCell(0, nestedMatrix.Headers.IndexOf("quarter"))); - - // 3. 2D array format - string json2D = @"[ - [""Region"", ""2024"", ""2025""], - [""North"", 100, 130], - [""South"", 80, 95] - ]"; - var matrix2D = service.ParseJson(json2D); - Assert.Equal(2, matrix2D.RowCount); - Assert.Equal(3, matrix2D.ColumnCount); - Assert.Equal("Region", matrix2D.Headers[0]); - Assert.Equal("North", matrix2D.GetCell(0, 0)); - } - - [Fact] - public void DataSourceService_ExcelParser_ReadsOpenXmlSpreadsheet() - { - var service = new DataSourceService(); - - // Generate in-memory .xlsx file using OpenXml - using var ms = new MemoryStream(); - using (var doc = SpreadsheetDocument.Create(ms, SpreadsheetDocumentType.Workbook, true)) - { - var workbookPart = doc.AddWorkbookPart(); - workbookPart.Workbook = new Workbook(); - - var worksheetPart = workbookPart.AddNewPart(); - var sheetData = new SheetData(); - - // Row 1: Headers - var headerRow = new Row { RowIndex = 1 }; - headerRow.Append( - new Cell { CellReference = "A1", DataType = CellValues.String, CellValue = new CellValue("Quarter") }, - new Cell { CellReference = "B1", DataType = CellValues.String, CellValue = new CellValue("Revenue") }, - new Cell { CellReference = "C1", DataType = CellValues.String, CellValue = new CellValue("Expenses") } - ); - sheetData.Append(headerRow); - - // Row 2: Data - var dataRow1 = new Row { RowIndex = 2 }; - dataRow1.Append( - new Cell { CellReference = "A2", DataType = CellValues.String, CellValue = new CellValue("Q1") }, - new Cell { CellReference = "B2", DataType = CellValues.Number, CellValue = new CellValue("150") }, - new Cell { CellReference = "C2", DataType = CellValues.Number, CellValue = new CellValue("90") } - ); - sheetData.Append(dataRow1); - - // Row 3: Data - var dataRow2 = new Row { RowIndex = 3 }; - dataRow2.Append( - new Cell { CellReference = "A3", DataType = CellValues.String, CellValue = new CellValue("Q2") }, - new Cell { CellReference = "B3", DataType = CellValues.Number, CellValue = new CellValue("220") }, - new Cell { CellReference = "C3", DataType = CellValues.Number, CellValue = new CellValue("110") } - ); - sheetData.Append(dataRow2); - - worksheetPart.Worksheet = new Worksheet(sheetData); - - var sheets = doc.WorkbookPart!.Workbook!.AppendChild(new Sheets()); - sheets.Append(new Sheet - { - Id = doc.WorkbookPart.GetIdOfPart(worksheetPart), - SheetId = 1, - Name = "FinancialSummary" - }); - - workbookPart.Workbook.Save(); - } - - ms.Position = 0; - var sheetNames = service.GetExcelSheetNames(ms); - Assert.Single(sheetNames); - Assert.Equal("FinancialSummary", sheetNames[0]); - - ms.Position = 0; - var matrix = service.ParseExcel(ms, "FinancialSummary", true); - Assert.Equal(2, matrix.RowCount); - Assert.Equal(3, matrix.ColumnCount); - Assert.Equal("Quarter", matrix.Headers[0]); - Assert.Equal("Q1", matrix.GetCell(0, 0)); - Assert.Equal("150", matrix.GetCell(0, 1)); - Assert.Equal("Q2", matrix.GetCell(1, 0)); - Assert.Equal("220", matrix.GetCell(1, 1)); - } - - [Fact] - public void DataBindingService_ChartAndTableMapping_AndBidirectionalConversion() - { - var bindingService = new DataBindingService(); - - var headers = new List { "Period", "Gross Sales", "Net Profit" }; - var rows = new List> - { - new() { "2023", "500", "120" }, - new() { "2024", "750", "210" }, - new() { "2025", "1100", "380" } - }; - var matrix = new DataMatrix(headers, rows); - - // 1. Apply to Chart (Multi-Series) - var chart = new PdfChartElement { ChartType = ChartType.BarColumn }; - bindingService.ApplyToChart(matrix, chart, 0, new List { 1, 2 }); - - Assert.Equal(3, chart.Categories.Count); - Assert.Equal("2023", chart.Categories[0]); - Assert.Equal(2, chart.MultiSeries.Count); - Assert.Equal("Gross Sales", chart.MultiSeries[0].Name); - Assert.Equal(500, chart.MultiSeries[0].Values[0]); - Assert.Equal("Net Profit", chart.MultiSeries[1].Name); - Assert.Equal(120, chart.MultiSeries[1].Values[0]); - - // 2. Extract DataMatrix from Chart - var extractedFromChart = bindingService.ExtractFromChart(chart); - Assert.Equal(3, extractedFromChart.RowCount); - Assert.Equal(3, extractedFromChart.ColumnCount); - Assert.Equal("2023", extractedFromChart.GetCell(0, 0)); - Assert.Equal("500", extractedFromChart.GetCell(0, 1)); - Assert.Equal("120", extractedFromChart.GetCell(0, 2)); - - // 3. Apply to Table - var table = new PdfTableElement(); - bindingService.ApplyToTable(matrix, table); - Assert.Equal(3, table.Headers.Count); - Assert.Equal(3, table.Rows.Count); - Assert.Equal("Period", table.Headers[0]); - Assert.Equal("1100", table.Rows[2][1]); - - // 4. Convert Table to Chart - var convertedChart = bindingService.ConvertTableToChart(table, ChartType.SmoothLine); - Assert.NotNull(convertedChart); - Assert.Equal(ChartType.SmoothLine, convertedChart.ChartType); - Assert.Equal(3, convertedChart.Categories.Count); - - // 5. Convert Chart to Table - var convertedTable = bindingService.ConvertChartToTable(convertedChart); - Assert.NotNull(convertedTable); - Assert.Equal(3, convertedTable.Rows.Count); - } - - [Fact] - public void DataStudioViewModel_ManualEditing_AndElementCreation() - { - var dataSourceService = new DataSourceService(); - var dataBindingService = new DataBindingService(); - var vm = new DataStudioViewModel(dataSourceService, dataBindingService); - - // Open for new Chart - var page = new PageViewModel { PageNumber = 1, Width = 595, Height = 842 }; - vm.OpenForNew("NewChart", page); - - Assert.True(vm.IsOpen); - Assert.True(vm.IsChartTarget); - - // Set raw text input and parse - vm.RawTextInput = "Division\tTarget\nAmericas\t500\nEMEA\t400\nAPAC\t650"; - vm.ParseRawTextInput(); - - Assert.Equal(3, vm.CurrentMatrix.RowCount); - Assert.Equal(2, vm.CurrentMatrix.ColumnCount); - Assert.Equal("Division", vm.MatrixHeaders[0]); - Assert.Equal("Target", vm.MatrixHeaders[1]); - - // Add a row - vm.AddRow(); - Assert.Equal(4, vm.CurrentMatrix.RowCount); - - // Apply Data and verify event callback - ElementViewModelBase? createdElement = null; - vm.OnElementCreated += (el, desc) => createdElement = el; - - vm.ApplyData(); - - Assert.False(vm.IsOpen); - Assert.NotNull(createdElement); - Assert.IsType(createdElement); - - var chartVm = (ChartElementViewModel)createdElement; - Assert.Equal(4, chartVm.Bars.Count); - Assert.Equal("Americas", chartVm.Bars[0].Category); - Assert.Equal(500, chartVm.Bars[0].Value); - } -} diff --git a/tests/PdfEditorApp.Tests/DirectEditingTests.cs b/tests/PdfEditorApp.Tests/DirectEditingTests.cs deleted file mode 100644 index 3d9a43a..0000000 --- a/tests/PdfEditorApp.Tests/DirectEditingTests.cs +++ /dev/null @@ -1,371 +0,0 @@ -using System; -using System.Linq; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DirectEditingTests -{ - [Fact] - public void TextElement_EditMode_TransitionsCorrectly() - { - var textEl = new TextElementViewModel - { - Text = "Original Content", - FontSize = 14, - IsInEditMode = false - }; - - Assert.False(textEl.IsInEditMode); - - // Enter edit mode - textEl.IsInEditMode = true; - Assert.True(textEl.IsInEditMode); - - // Edit text - textEl.Text = "Updated Direct Canvas Text"; - Assert.Equal("Updated Direct Canvas Text", textEl.Text); - - // Exit edit mode - textEl.IsInEditMode = false; - Assert.False(textEl.IsInEditMode); - } - - [Fact] - public void InspectorViewModel_FontSize_IncreaseAndDecreaseCommands() - { - var inspector = new InspectorViewModel(); - var page = new PageViewModel(); - var textEl = new TextElementViewModel - { - Text = "Headline Text", - FontSize = 16 - }; - page.AddElement(textEl); - inspector.UpdateSelection(textEl, page); - - Assert.True(inspector.IsTextElement); - Assert.Equal(16, textEl.FontSize); - - // Increase font size - inspector.IncreaseFontSizeCommand.Execute(null); - Assert.Equal(17, textEl.FontSize); - - // Increase again - inspector.IncreaseFontSizeCommand.Execute(null); - Assert.Equal(18, textEl.FontSize); - - // Decrease font size - inspector.DecreaseFontSizeCommand.Execute(null); - Assert.Equal(17, textEl.FontSize); - - // Test bounds and finish edit mode - textEl.IsInEditMode = false; - Assert.False(textEl.IsInEditMode); - - inspector.StartEditModeCommand.Execute(null); - Assert.True(textEl.IsInEditMode); - - inspector.ToggleEditModeCommand.Execute(null); - Assert.False(textEl.IsInEditMode); - - inspector.ToggleEditModeCommand.Execute(null); - Assert.True(textEl.IsInEditMode); - - inspector.FinishEditModeCommand.Execute(null); - Assert.False(textEl.IsInEditMode); - } - - [Fact] - public void InspectorViewModel_TextFormattingCommands_WorkDirectly() - { - var inspector = new InspectorViewModel(); - var page = new PageViewModel(); - var textEl = new TextElementViewModel - { - Text = "Sample Paragraph", - FontSize = 12, - IsBold = false, - IsItalic = false, - IsUnderline = false, - Alignment = TextAlignmentMode.Left - }; - page.AddElement(textEl); - inspector.UpdateSelection(textEl, page); - - // Toggle Bold - inspector.ToggleBoldCommand.Execute(null); - Assert.True(textEl.IsBold); - - // Toggle Italic - inspector.ToggleItalicCommand.Execute(null); - Assert.True(textEl.IsItalic); - - // Toggle Underline - inspector.ToggleUnderlineCommand.Execute(null); - Assert.True(textEl.IsUnderline); - - // Change Alignment - inspector.SetAlignmentCommand.Execute("Center"); - Assert.Equal(TextAlignmentMode.Center, textEl.Alignment); - - // Change Color - inspector.SetTextColorCommand.Execute("#0F6CBD"); - Assert.Equal("#0F6CBD", textEl.TextColorHex); - - // Auto-Fit Text - inspector.AutoFitTextBothCommand.Execute(null); - Assert.True(textEl.Width > 0); - Assert.True(textEl.Height > 0); - } - - [Fact] - public void ShapeElement_InPlaceLabelEditing_WorksCorrectly() - { - var shape = new ShapeElementViewModel - { - ShapeType = ShapeType.RoundedRectangle, - Label = "CONFIDENTIAL", - LabelFontSize = 14, - IsInEditMode = false - }; - - Assert.False(shape.IsInEditMode); - Assert.Equal("CONFIDENTIAL", shape.Label); - - // Enter label edit mode - shape.IsInEditMode = true; - Assert.True(shape.IsInEditMode); - - // Edit label directly - shape.Label = "APPROVED"; - Assert.Equal("APPROVED", shape.Label); - - // Exit edit mode - shape.IsInEditMode = false; - Assert.False(shape.IsInEditMode); - } - - [Fact] - public void TableElement_DirectCellAndHeaderEditing_RoundTripsModel() - { - var table = new TableElementViewModel(); - Assert.Equal(4, table.Headers.Count); - Assert.Equal(3, table.Rows.Count); - - // Direct edit header - table.Headers[0].Text = "Service Description"; - Assert.Equal("Service Description", table.Headers[0].Text); - - // Direct edit cell - table.Rows[0].Cells[0].Text = "Enterprise Architecture Consulting"; - table.Rows[0].Cells[3].Text = "$12,500.00"; - - Assert.Equal("Enterprise Architecture Consulting", table.Rows[0].Cells[0].Text); - Assert.Equal("$12,500.00", table.Rows[0].Cells[3].Text); - - // Convert to Model and back - var model = (PdfTableElement)table.ToModel(); - Assert.Equal("Service Description", model.Headers[0]); - Assert.Equal("Enterprise Architecture Consulting", model.Rows[0][0]); - Assert.Equal("$12,500.00", model.Rows[0][3]); - - var restoredTable = new TableElementViewModel(); - restoredTable.LoadFromModel(model); - - Assert.Equal("Service Description", restoredTable.Headers[0].Text); - Assert.Equal("Enterprise Architecture Consulting", restoredTable.Rows[0].Cells[0].Text); - Assert.Equal("$12,500.00", restoredTable.Rows[0].Cells[3].Text); - } - - [Fact] - public void StickyNoteElement_DirectTextEditing_WorksCorrectly() - { - var note = new StickyNoteElementViewModel - { - Author = "Reviewer", - NoteText = "Please update section 3" - }; - - note.NoteText = "Section 3 approved with revisions."; - var model = (PdfStickyNoteElement)note.ToModel(); - Assert.Equal("Section 3 approved with revisions.", model.NoteText); - } - - [Fact] - public void PageViewModel_SelectionChange_DeactivatesEditModeOnOtherElements() - { - var page = new PageViewModel(); - var el1 = new TextElementViewModel { Text = "Element 1", IsInEditMode = true }; - var el2 = new TextElementViewModel { Text = "Element 2", IsInEditMode = false }; - page.AddElement(el1); - page.AddElement(el2); - - page.SelectElement(el1); - el1.IsInEditMode = true; - Assert.True(el1.IsSelected); - Assert.True(el1.IsInEditMode); - - // Selecting el2 must automatically deactivate edit mode on el1 - page.SelectElement(el2); - Assert.False(el1.IsSelected); - Assert.False(el1.IsInEditMode); - Assert.True(el2.IsSelected); - - // Clearing selection must deactivate edit mode on all elements - el2.IsInEditMode = true; - Assert.True(el2.IsInEditMode); - page.ClearSelection(); - Assert.False(el2.IsSelected); - Assert.False(el2.IsInEditMode); - } - - [Fact] - public void DeleteSelectedElement_SingleElement_DeletesAndSupportsUndo() - { - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel(); - var textEl = new TextElementViewModel { Text = "Heading to delete" }; - page.AddElement(textEl); - page.SelectElement(textEl); - inspector.UpdateSelection(textEl, page); - - Assert.Single(page.Elements); - Assert.Equal(textEl, page.SelectedElement); - - // Execute Delete - inspector.DeleteSelectedElementCommand.Execute(null); - - Assert.Empty(page.Elements); - Assert.Null(page.SelectedElement); - Assert.Empty(page.SelectedElements); - Assert.True(undoRedo.CanUndo); - - // Undo Delete - undoRedo.Undo(); - Assert.Single(page.Elements); - Assert.Equal("Heading to delete", ((TextElementViewModel)page.Elements[0]).Text); - } - - [Fact] - public void DeleteSelectedElement_MultiElement_DeletesAllAndSupportsUndo() - { - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel(); - var el1 = new TextElementViewModel { Text = "Heading 1" }; - var el2 = new TextElementViewModel { Text = "Paragraph 1" }; - var el3 = new ShapeElementViewModel { Width = 100, Height = 50 }; - page.AddElement(el1); - page.AddElement(el2); - page.AddElement(el3); - - page.SelectElements(new[] { el1, el2 }); - inspector.UpdateSelection(el2, page); - - Assert.Equal(3, page.Elements.Count); - Assert.Equal(2, page.SelectedElements.Count); - - // Execute Delete - inspector.DeleteSelectedElementCommand.Execute(null); - - Assert.Single(page.Elements); - Assert.Contains(el3, page.Elements); - Assert.Empty(page.SelectedElements); - Assert.True(undoRedo.CanUndo); - - // Undo Delete - undoRedo.Undo(); - Assert.Equal(3, page.Elements.Count); - Assert.Contains(el1, page.Elements); - Assert.Contains(el2, page.Elements); - Assert.Contains(el3, page.Elements); - } - - [Fact] - public void InspectorViewModel_ToggleBold_WithTextSelection_FormatsOnlySelection_AndSupportsUndo() - { - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel(); - var textEl = new TextElementViewModel - { - Text = "normative period of rec\nmacro environment,\nd top-line revenue" - }; - page.AddElement(textEl); - inspector.UpdateSelection(textEl, page); - - // Enter edit mode and select "macro environment," - textEl.IsInEditMode = true; - textEl.UpdateTextSelection(24, 18, "macro environment,"); - Assert.True(textEl.HasTextSelection); - - // Execute ToggleBold - inspector.ToggleBoldCommand.Execute(null); - - Assert.Equal("normative period of rec\n**macro environment,**\nd top-line revenue", textEl.Text); - Assert.False(textEl.IsBold); // Element-wide property remains false! - Assert.NotNull(textEl.Spans); // Spans are generated - Assert.True(undoRedo.CanUndo); - - // Undo - undoRedo.Undo(); - Assert.Equal("normative period of rec\nmacro environment,\nd top-line revenue", textEl.Text); - - // Redo - undoRedo.Redo(); - Assert.Equal("normative period of rec\n**macro environment,**\nd top-line revenue", textEl.Text); - } - - [Fact] - public void InspectorViewModel_ToggleBold_WithoutTextSelection_TogglesElementProperty() - { - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel(); - var textEl = new TextElementViewModel { Text = "Sample Heading", IsBold = false }; - page.AddElement(textEl); - inspector.UpdateSelection(textEl, page); - - Assert.False(textEl.IsBold); - Assert.False(textEl.HasTextSelection); - - // Execute ToggleBold - inspector.ToggleBoldCommand.Execute(null); - Assert.True(textEl.IsBold); - - // Toggle again - inspector.ToggleBoldCommand.Execute(null); - Assert.False(textEl.IsBold); - } - - [Fact] - public void InspectorViewModel_SetTextColor_WithTextSelection_FormatsOnlySelection() - { - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var page = new PageViewModel(); - var textEl = new TextElementViewModel { Text = "Hello World" }; - page.AddElement(textEl); - inspector.UpdateSelection(textEl, page); - - textEl.IsInEditMode = true; - textEl.UpdateTextSelection(6, 5, "World"); - - inspector.SetTextColorCommand.Execute("#0F6CBD"); - - Assert.Equal("Hello World", textEl.Text); - Assert.NotNull(textEl.Spans); - var coloredSpan = textEl.Spans.FirstOrDefault(s => s.TextColorHex == "#0F6CBD"); - Assert.NotNull(coloredSpan); - Assert.Equal("World", coloredSpan.Text); - } -} - diff --git a/tests/PdfEditorApp.Tests/DocumentAuditServiceTests.cs b/tests/PdfEditorApp.Tests/DocumentAuditServiceTests.cs deleted file mode 100644 index 35eb426..0000000 --- a/tests/PdfEditorApp.Tests/DocumentAuditServiceTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DocumentAuditServiceTests -{ - [Fact] - public void Wcag21_RelativeLuminanceAndContrastRatio_CalculatesAccurately() - { - // Black luminance is 0.0 - double blackLum = DocumentAuditService.CalculateRelativeLuminance("#000000"); - Assert.Equal(0.0, blackLum, 3); - - // White luminance is 1.0 - double whiteLum = DocumentAuditService.CalculateRelativeLuminance("#FFFFFF"); - Assert.Equal(1.0, whiteLum, 3); - - // Black on White contrast ratio is 21.0:1 - double maxContrast = DocumentAuditService.CalculateContrastRatio("#000000", "#FFFFFF"); - Assert.Equal(21.0, maxContrast); - - // White on White contrast ratio is 1.0:1 - double minContrast = DocumentAuditService.CalculateContrastRatio("#FFFFFF", "#FFFFFF"); - Assert.Equal(1.0, minContrast); - - // Gray #777777 on White #FFFFFF is ~4.47:1 (fails normal AA 4.5:1, passes large text AA 3.0:1) - double grayContrast = DocumentAuditService.CalculateContrastRatio("#777777", "#FFFFFF"); - Assert.True(grayContrast >= 4.4 && grayContrast <= 4.55); - - // Fluent Brand Blue #0F6CBD on White #FFFFFF passes AA - double brandBlueContrast = DocumentAuditService.CalculateContrastRatio("#0F6CBD", "#FFFFFF"); - Assert.True(brandBlueContrast >= 4.5); - } - - [Fact] - public void DocumentAuditService_DetectsLowContrastAndAutoFixes() - { - // Arrange - var auditService = new DocumentAuditService(); - var doc = new PdfDocumentModel { Title = "Quarterly Compliance Report", Author = "Financial Audit Team" }; - var page = new PdfPageModel { Width = 595, Height = 842, BackgroundColorHex = "#FFFFFF" }; - - // Low contrast text element (white text on white page) - var lowContrastText = new PdfTextElement - { - Id = "txt-low-contrast", - X = 50, - Y = 50, - Width = 200, - Height = 30, - Text = "Hidden Low Contrast Disclaimer", - FontSize = 10, - TextColorHex = "#FFFFFF", - BackgroundColorHex = "Transparent" - }; - page.Elements.Add(lowContrastText); - doc.Pages.Add(page); - - // Act - Initial Audit - var report1 = auditService.RunAudit(doc); - - // Assert - Error detected - var contrastIssue = report1.Issues.FirstOrDefault(i => i.Category == "Accessibility" && i.Severity == "Error"); - Assert.NotNull(contrastIssue); - Assert.Contains("WCAG 2.1 AA Contrast Failure", contrastIssue.Title); - Assert.True(contrastIssue.CanAutoFix); - - // Act - Auto-Fix - int fixedCount = auditService.AutoFixContrastIssues(doc); - Assert.Equal(1, fixedCount); - Assert.Equal("#0F172A", lowContrastText.TextColorHex); // Dark high-contrast replacement - - // Act - Re-Audit - var report2 = auditService.RunAudit(doc); - var remainingErrors = report2.Issues.Where(i => i.Category == "Accessibility" && i.Severity == "Error"); - Assert.Empty(remainingErrors); - Assert.True(report2.HealthScore > report1.HealthScore); - } - - [Fact] - public void DocumentAuditService_ValidatesPdfAMetadataAndAutoFixes() - { - // Arrange - var auditService = new DocumentAuditService(); - var doc = new PdfDocumentModel - { - Title = "", // Missing Title - Author = "" // Missing Author - }; - var page = new PdfPageModel { Width = 595, Height = 842 }; - page.Elements.Add(new PdfTextElement { X = 50, Y = 50, Text = "Valid Content", FontSize = 12, TextColorHex = "#000000" }); - doc.Pages.Add(page); - - // Act - var report1 = auditService.RunAudit(doc); - - // Assert - Missing title issue found - var titleIssue = report1.Issues.FirstOrDefault(i => i.Title == "Missing Document Title"); - Assert.NotNull(titleIssue); - Assert.True(report1.PdfAComplianceScore < 100); - - // Act - Auto Fix Metadata - int fixedCount = auditService.AutoFixMetadataIssues(doc); - Assert.True(fixedCount >= 1); - Assert.False(string.IsNullOrWhiteSpace(doc.Title)); - - // Act - Re-Audit - var report2 = auditService.RunAudit(doc); - Assert.Null(report2.Issues.FirstOrDefault(i => i.Title == "Missing Document Title")); - Assert.True(report2.PdfAComplianceScore > report1.PdfAComplianceScore); - } - - [Fact] - public void DocumentAuditService_AutoFixMissingAltText_PopulatesFigureLabels() - { - // Arrange - var auditService = new DocumentAuditService(); - var doc = new PdfDocumentModel { Title = "Catalog", Author = "Design Dept" }; - var page = new PdfPageModel { Width = 595, Height = 842 }; - - var img1 = new PdfImageElement { X = 50, Y = 100, Width = 200, Height = 150, AltText = "" }; - var img2 = new PdfImageElement { X = 50, Y = 300, Width = 200, Height = 150, AltText = "" }; - page.Elements.Add(img1); - page.Elements.Add(img2); - doc.Pages.Add(page); - - // Act - Auto Fix Alt Text - int fixedCount = auditService.AutoFixMissingAltText(doc); - Assert.Equal(2, fixedCount); - Assert.Equal("Document Figure 1", img1.AltText); - Assert.Equal("Document Figure 2", img2.AltText); - } -} diff --git a/tests/PdfEditorApp.Tests/DocumentCompareServiceTests.cs b/tests/PdfEditorApp.Tests/DocumentCompareServiceTests.cs deleted file mode 100644 index 5060606..0000000 --- a/tests/PdfEditorApp.Tests/DocumentCompareServiceTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Linq; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DocumentCompareServiceTests -{ - [Fact] - public void CompareDocuments_IdenticalDocs_ReportsZeroDifferences() - { - // Arrange - var service = new DocumentCompareService(); - var doc1 = new PdfDocumentModel { Title = "Budget 2026", Author = "Finance Team" }; - var page1 = new PdfPageModel { PageNumber = 1, Width = 600, Height = 800 }; - page1.Elements.Add(new PdfTextElement { Id = "t1", Text = "Revenue $1M", X = 50, Y = 50, Width = 200, Height = 30 }); - doc1.Pages.Add(page1); - - var doc2 = new PdfDocumentModel { Title = "Budget 2026", Author = "Finance Team" }; - var page2 = new PdfPageModel { PageNumber = 1, Width = 600, Height = 800 }; - page2.Elements.Add(new PdfTextElement { Id = "t1", Text = "Revenue $1M", X = 50, Y = 50, Width = 200, Height = 30 }); - doc2.Pages.Add(page2); - - // Act - var report = service.CompareDocuments(doc1, doc2); - - // Assert - Assert.Equal(0, report.TotalDifferencesCount); - Assert.Equal(0, report.AdditionsCount); - Assert.Equal(0, report.DeletionsCount); - Assert.Equal(0, report.ModificationsCount); - } - - [Fact] - public void CompareDocuments_DetectsMetadataAndSecurityChanges() - { - // Arrange - var service = new DocumentCompareService(); - var doc1 = new PdfDocumentModel - { - Title = "Draft V1.pdf", - Author = "Intern", - SecuritySettings = new PdfSecuritySettings { IsPasswordProtected = false } - }; - doc1.Pages.Add(new PdfPageModel { PageNumber = 1 }); - - var doc2 = new PdfDocumentModel - { - Title = "Final V2.pdf", - Author = "Lead Director", - SecuritySettings = new PdfSecuritySettings { IsPasswordProtected = true, OpenPassword = "123" } - }; - doc2.Pages.Add(new PdfPageModel { PageNumber = 1 }); - - // Act - var report = service.CompareDocuments(doc1, doc2); - - // Assert - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.MetadataModified && d.Description.Contains("Title")); - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.MetadataModified && d.Description.Contains("Author")); - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.SecurityModified); - } - - [Fact] - public void CompareDocuments_DetectsTextModificationsAndFormattingChanges() - { - // Arrange - var service = new DocumentCompareService(); - var doc1 = new PdfDocumentModel { Title = "Memo" }; - var page1 = new PdfPageModel { PageNumber = 1 }; - page1.Elements.Add(new PdfTextElement - { - Id = "txt-content", - Text = "Draft policy v1.0", - FontSize = 12, - TextColorHex = "#000000" - }); - doc1.Pages.Add(page1); - - var doc2 = new PdfDocumentModel { Title = "Memo" }; - var page2 = new PdfPageModel { PageNumber = 1 }; - page2.Elements.Add(new PdfTextElement - { - Id = "txt-content", - Text = "Approved policy v2.0", - FontSize = 14, - TextColorHex = "#0F6CBD" - }); - doc2.Pages.Add(page2); - - // Act - var report = service.CompareDocuments(doc1, doc2); - - // Assert - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.TextModified && d.OldValue == "Draft policy v1.0" && d.NewValue == "Approved policy v2.0"); - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.FormattingModified); - } - - [Fact] - public void CompareDocuments_DetectsPageAdditionsAndDeletions() - { - // Arrange - var service = new DocumentCompareService(); - var doc1 = new PdfDocumentModel { Title = "Report" }; - doc1.Pages.Add(new PdfPageModel { PageNumber = 1 }); - doc1.Pages.Add(new PdfPageModel { PageNumber = 2 }); - - var doc2 = new PdfDocumentModel { Title = "Report" }; - doc2.Pages.Add(new PdfPageModel { PageNumber = 1 }); - doc2.Pages.Add(new PdfPageModel { PageNumber = 2 }); - doc2.Pages.Add(new PdfPageModel { PageNumber = 3 }); // Added 3rd page - - // Act - var report = service.CompareDocuments(doc1, doc2); - - // Assert - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.PageCountChanged); - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.ElementAdded && d.PageNumber == 3); - } -} diff --git a/tests/PdfEditorApp.Tests/DocumentExportAndPersistenceTests.cs b/tests/PdfEditorApp.Tests/DocumentExportAndPersistenceTests.cs deleted file mode 100644 index 4f32b2a..0000000 --- a/tests/PdfEditorApp.Tests/DocumentExportAndPersistenceTests.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DocumentExportAndPersistenceTests -{ - [Fact] - public async Task PdfExportService_GeneratesValidPdfBytes_WithElements() - { - // Arrange - var exportService = new PdfExportService(); - var doc = new PdfDocumentModel - { - Title = "Export Test Document", - Author = "FryPDF Test Suite" - }; - - var page = new PdfPageModel - { - Width = 595.28, - Height = 841.89, - PageNumber = 1, - BackgroundColorHex = "#FFFFFF" - }; - - page.Elements.Add(new PdfTextElement - { - X = 50, - Y = 50, - Width = 300, - Height = 40, - Text = "Executive Summary", - FontSize = 22, - IsBold = true, - TextColorHex = "#0F172A" - }); - - page.Elements.Add(new PdfShapeElement - { - X = 50, - Y = 100, - Width = 495, - Height = 2, - ShapeType = ShapeType.Line, - StrokeColorHex = "#0F6CBD", - StrokeThickness = 2 - }); - - page.Elements.Add(new PdfQrCodeElement - { - X = 50, - Y = 120, - Width = 100, - Height = 100, - Content = "https://github.com/PrashantUnity/PDFCreator", - Label = "Scan to Verify" - }); - - page.Elements.Add(new PdfBarcodeElement - { - X = 180, - Y = 120, - Width = 200, - Height = 60, - CodeValue = "FRY-2026-X99", - ShowText = true - }); - - page.Elements.Add(new PdfTableElement - { - X = 50, - Y = 240, - Width = 495, - Height = 120, - Headers = new() { "Item", "Category", "Amount ($)" }, - Rows = new() - { - new() { "Cloud Architecture", "Engineering", "12,500.00" }, - new() { "Security Preflight", "Audit", "4,200.00" } - } - }); - - doc.Pages.Add(page); - - // Act - File export - string tempPdf = Path.Combine(Path.GetTempPath(), $"FryPDF_Export_{Guid.NewGuid():N}.pdf"); - try - { - await exportService.ExportToFileAsync(doc, tempPdf); - - // Assert - Assert.True(File.Exists(tempPdf)); - byte[] pdfBytes = await File.ReadAllBytesAsync(tempPdf); - Assert.True(pdfBytes.Length > 100); - - // PDF files must begin with '%PDF-' - string header = Encoding.ASCII.GetString(pdfBytes.AsSpan(0, 5)); - Assert.Equal("%PDF-", header); - } - finally - { - if (File.Exists(tempPdf)) File.Delete(tempPdf); - } - } - - [Fact] - public async Task ProjectPersistenceService_AtomicSaveAndLoad_RoundTripsSuccessfully() - { - // Arrange - var persistenceService = new ProjectPersistenceService(); - string tempFile = Path.Combine(Path.GetTempPath(), $"FryPDF_Test_{Guid.NewGuid():N}.frypdf"); - - try - { - var doc = new PdfDocumentModel - { - Title = "Roundtrip Test", - Author = "Principal Architect", - Subject = "Architecture Review" - }; - - var page = new PdfPageModel { Width = 612, Height = 792, PageNumber = 1 }; - page.Elements.Add(new PdfTextElement { Text = "Test Note", X = 20, Y = 30, Width = 150, Height = 40 }); - page.Elements.Add(new PdfQrCodeElement { Content = "https://codefrydev.in", X = 100, Y = 100, Width = 80, Height = 80 }); - doc.Pages.Add(page); - - // Act - Save - await persistenceService.SaveProjectAsync(doc, tempFile); - - // Assert file exists and temp file was cleaned up - Assert.True(File.Exists(tempFile)); - Assert.False(File.Exists(tempFile + ".tmp")); - - // Act - Load - var loaded = await persistenceService.LoadProjectAsync(tempFile); - - // Assert loaded model matches - Assert.NotNull(loaded); - Assert.Equal("Roundtrip Test", loaded.Title); - Assert.Equal("Principal Architect", loaded.Author); - Assert.Single(loaded.Pages); - Assert.Equal(2, loaded.Pages[0].Elements.Count); - Assert.IsType(loaded.Pages[0].Elements[0]); - Assert.IsType(loaded.Pages[0].Elements[1]); - } - finally - { - if (File.Exists(tempFile)) File.Delete(tempFile); - if (File.Exists(tempFile + ".tmp")) File.Delete(tempFile + ".tmp"); - } - } - - [Fact] - public void UndoRedoService_CapsHistoryAt100_AndEvictsOldest() - { - // Arrange - var undoRedo = new UndoRedoService(); - int counter = 0; - - // Act - push 120 actions - for (int i = 1; i <= 120; i++) - { - int val = i; - undoRedo.RecordAction($"Action {val}", () => { counter -= val; }, () => { counter += val; }); - } - - // Assert - NextUndoDescription is Action 120 - Assert.True(undoRedo.CanUndo); - Assert.Equal("Action 120", undoRedo.NextUndoDescription); - - // Perform undo operations until stack is exhausted - int undoCount = 0; - while (undoRedo.CanUndo) - { - undoRedo.Undo(); - undoCount++; - } - - // Must have capped at exactly 100 operations - Assert.Equal(100, undoCount); - Assert.False(undoRedo.CanUndo); - Assert.True(undoRedo.CanRedo); - } - - [Fact] - public async Task PdfExportService_PermanentRedaction_SanitizesCoveredText() - { - // Arrange - var exportService = new PdfExportService(); - var doc = new PdfDocumentModel { Title = "Classified Report" }; - var page = new PdfPageModel { Width = 600, Height = 800, PageNumber = 1 }; - - // Confidential text at (100, 100, 200, 30) - var secretText = new PdfTextElement - { - Text = "TOP SECRET SSN 000-11-2222", - X = 100, - Y = 100, - Width = 200, - Height = 30 - }; - - // Redaction box completely covering the secret text: (90, 90, 220, 50) - var redaction = new PdfRedactionElement - { - X = 90, - Y = 90, - Width = 220, - Height = 50, - ExemptionCode = "(b)(6)" - }; - - // Non-redacted text at (100, 300, 200, 30) - var publicText = new PdfTextElement - { - Text = "Public Notice Section", - X = 100, - Y = 300, - Width = 200, - Height = 30 - }; - - page.Elements.Add(secretText); - page.Elements.Add(redaction); - page.Elements.Add(publicText); - doc.Pages.Add(page); - - // Act - byte[] pdfBytes = await exportService.ExportToBytesAsync(doc); - - // Assert - PDF generated successfully - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 100); - string header = Encoding.ASCII.GetString(pdfBytes.AsSpan(0, 5)); - Assert.Equal("%PDF-", header); - } - - [Fact] - public async Task ProjectPersistenceService_AutoSaveAndRecovery_SavesAndCleans() - { - // Arrange - var persistenceService = new ProjectPersistenceService(); - string tempProjectFile = Path.Combine(Path.GetTempPath(), $"FryPDF_AutoSave_Test_{Guid.NewGuid():N}.frypdf"); - - try - { - var doc = new PdfDocumentModel { Title = "Unsaved Work in Progress", Author = "UX Specialist" }; - var page = new PdfPageModel { PageNumber = 1 }; - page.Elements.Add(new PdfTextElement { Text = "Crucial Unshelved Notes", X = 10, Y = 20 }); - doc.Pages.Add(page); - - // Act - AutoSave - await persistenceService.SaveAutoSaveAsync(doc, tempProjectFile); - - // Assert - Recoverable - bool hasAutoSave = persistenceService.HasRecoverableAutoSave(tempProjectFile, out string autoSavePath); - Assert.True(hasAutoSave); - Assert.True(File.Exists(autoSavePath)); - - // Load autosave - var recovered = await persistenceService.LoadAutoSaveAsync(autoSavePath); - Assert.NotNull(recovered); - Assert.Equal("Unsaved Work in Progress", recovered.Title); - - // Act - Clean - persistenceService.CleanAutoSave(tempProjectFile); - Assert.False(File.Exists(autoSavePath)); - } - finally - { - persistenceService.CleanAutoSave(tempProjectFile); - if (File.Exists(tempProjectFile)) File.Delete(tempProjectFile); - } - } -} diff --git a/tests/PdfEditorApp.Tests/DocumentFileOperationsTests.cs b/tests/PdfEditorApp.Tests/DocumentFileOperationsTests.cs deleted file mode 100644 index bf99685..0000000 --- a/tests/PdfEditorApp.Tests/DocumentFileOperationsTests.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DocumentFileOperationsTests : IDisposable -{ - private readonly string _tempTestDir; - - public DocumentFileOperationsTests() - { - _tempTestDir = Path.Combine(Path.GetTempPath(), "FryPdf_Test_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempTestDir); - } - - public void Dispose() - { - try - { - if (Directory.Exists(_tempTestDir)) - { - Directory.Delete(_tempTestDir, recursive: true); - } - } - catch { } - } - - [Theory] - [InlineData("ValidName", true)] - [InlineData("Report_2026", true)] - [InlineData("Document-Final v2", true)] - [InlineData("", false)] - [InlineData(" ", false)] - [InlineData(".", false)] - [InlineData("..", false)] - [InlineData("Bad/Name", false)] - [InlineData("Bad\\Name", false)] - [InlineData("Bad:Name", false)] - [InlineData("Bad*Name", false)] - [InlineData("Bad?Name", false)] - [InlineData("Bad\"Name", false)] - [InlineData("BadName", false)] - [InlineData("Bad|Name", false)] - public void ValidateFileName_EvaluatesCorrectly(string candidate, bool expectedValid) - { - var isValid = FileOperationHelper.ValidateFileName(candidate, out var error); - Assert.Equal(expectedValid, isValid); - if (!expectedValid) - { - Assert.False(string.IsNullOrWhiteSpace(error)); - } - } - - [Fact] - public void RenameFile_RenamesExistingFileSuccessfully() - { - // Arrange - var originalPath = Path.Combine(_tempTestDir, "original_sample.pdf"); - File.WriteAllText(originalPath, "PDF content mock"); - - // Act - var success = FileOperationHelper.RenameFile(originalPath, "renamed_sample", out var newPath, out var error); - - // Assert - Assert.True(success); - Assert.Null(error); - Assert.NotNull(newPath); - Assert.False(File.Exists(originalPath)); - Assert.True(File.Exists(newPath)); - Assert.Equal("renamed_sample.pdf", Path.GetFileName(newPath)); - Assert.Equal("PDF content mock", File.ReadAllText(newPath)); - } - - [Fact] - public void RenameFile_PreservesOrAppendsExtension() - { - // Arrange - var originalPath = Path.Combine(_tempTestDir, "test_ext.frypdf"); - File.WriteAllText(originalPath, "Project mock"); - - // Act - user types "my_new_project.frypdf" explicitly - var success = FileOperationHelper.RenameFile(originalPath, "my_new_project.frypdf", out var newPath, out _); - - // Assert - Assert.True(success); - Assert.NotNull(newPath); - Assert.True(File.Exists(newPath)); - Assert.Equal("my_new_project.frypdf", Path.GetFileName(newPath)); - } - - [Fact] - public void RenameFile_FailsIfTargetAlreadyExists() - { - // Arrange - var fileA = Path.Combine(_tempTestDir, "file_alpha.pdf"); - var fileB = Path.Combine(_tempTestDir, "file_beta.pdf"); - File.WriteAllText(fileA, "Alpha"); - File.WriteAllText(fileB, "Beta"); - - // Act - var success = FileOperationHelper.RenameFile(fileA, "file_beta", out var newPath, out var error); - - // Assert - Assert.False(success); - Assert.NotNull(error); - Assert.Contains("already exists", error, StringComparison.OrdinalIgnoreCase); - Assert.True(File.Exists(fileA)); - Assert.True(File.Exists(fileB)); - } - - [Fact] - public void DuplicateFile_CreatesIndexedCopiesCorrectly() - { - // Arrange - var originalPath = Path.Combine(_tempTestDir, "quarterly_summary.pdf"); - File.WriteAllText(originalPath, "Quarterly data mock"); - - // Act 1: First copy - var success1 = FileOperationHelper.DuplicateFile(originalPath, out var copyPath1, out var error1); - - // Assert 1 - Assert.True(success1); - Assert.Null(error1); - Assert.NotNull(copyPath1); - Assert.True(File.Exists(copyPath1)); - Assert.Equal("quarterly_summary (Copy).pdf", Path.GetFileName(copyPath1)); - - // Act 2: Second copy of original - var success2 = FileOperationHelper.DuplicateFile(originalPath, out var copyPath2, out var error2); - - // Assert 2 - Assert.True(success2); - Assert.Null(error2); - Assert.NotNull(copyPath2); - Assert.True(File.Exists(copyPath2)); - Assert.Equal("quarterly_summary (Copy 2).pdf", Path.GetFileName(copyPath2)); - } - - [Fact] - public void DeleteFile_RemovesFileFromDiskSafely() - { - // Arrange - var filePath = Path.Combine(_tempTestDir, "file_to_delete.pdf"); - File.WriteAllText(filePath, "Mock content"); - Assert.True(File.Exists(filePath)); - - // Act - var success = FileOperationHelper.DeleteFile(filePath, out var error); - - // Assert - Assert.True(success); - Assert.Null(error); - Assert.False(File.Exists(filePath)); - - // Act 2: Deleting already non-existent file succeeds gracefully - var success2 = FileOperationHelper.DeleteFile(filePath, out var error2); - Assert.True(success2); - Assert.Null(error2); - } - - [Fact] - public void HomeViewModel_RenameDialogFlow_WorksEndToEnd() - { - // Arrange - var filePath = Path.Combine(_tempTestDir, "home_rename_sample.pdf"); - File.WriteAllText(filePath, "Home test content"); - - var recentItem = new RecentDocumentItem - { - FilePath = filePath, - Title = "home_rename_sample.pdf", - LastOpened = DateTime.UtcNow - }; - - var mockRecentService = new Mocks.MockRecentDocumentsService(); - mockRecentService.Add(recentItem); - - var vm = new HomeViewModel( - mockRecentService, - new TemplateService(), - new ProjectPersistenceService(), - new PdfEditorApp.Services.Tools.Core.PdfToolRegistry()); - vm.RefreshRecent(); - - string? renamedOld = null; - string? renamedNew = null; - vm.DocumentRenamed += (oldP, newP) => - { - renamedOld = oldP; - renamedNew = newP; - }; - - // Act 1: Prompt Rename - vm.PromptRenameCommand.Execute(filePath); - Assert.True(vm.IsRenameDialogOpen); - Assert.Equal("home_rename_sample", vm.RenameNewName); - Assert.Equal(".pdf", vm.RenameExtension); - Assert.Equal(filePath, vm.RenameTargetFilePath); - - // Act 2: Confirm Rename with new name - vm.RenameNewName = "home_renamed_final"; - vm.ConfirmRenameCommand.Execute(null); - - // Assert - Assert.False(vm.IsRenameDialogOpen); - Assert.NotNull(renamedOld); - Assert.NotNull(renamedNew); - Assert.Equal(filePath, renamedOld); - Assert.EndsWith("home_renamed_final.pdf", renamedNew); - Assert.False(File.Exists(filePath)); - Assert.True(File.Exists(renamedNew)); - - // Check RecentDocuments updated - var updatedItem = vm.RecentDocuments.FirstOrDefault(); - Assert.NotNull(updatedItem); - Assert.Equal(renamedNew, updatedItem.FilePath); - Assert.Equal("home_renamed_final.pdf", updatedItem.Title); - } - - [Fact] - public void HomeViewModel_DeleteDialogFlow_WorksEndToEnd() - { - // Arrange - var filePath = Path.Combine(_tempTestDir, "home_delete_sample.pdf"); - File.WriteAllText(filePath, "Home delete test"); - - var recentItem = new RecentDocumentItem - { - FilePath = filePath, - Title = "home_delete_sample.pdf", - LastOpened = DateTime.UtcNow - }; - - var mockRecentService = new Mocks.MockRecentDocumentsService(); - mockRecentService.Add(recentItem); - - var vm = new HomeViewModel( - mockRecentService, - new TemplateService(), - new ProjectPersistenceService(), - new PdfEditorApp.Services.Tools.Core.PdfToolRegistry()); - vm.RefreshRecent(); - - string? deletedPath = null; - vm.DocumentDeleted += p => deletedPath = p; - - // Act 1: Prompt Delete - vm.PromptDeleteCommand.Execute(filePath); - Assert.True(vm.IsDeleteConfirmDialogOpen); - Assert.Equal(filePath, vm.DeleteTargetFilePath); - Assert.Equal("home_delete_sample.pdf", vm.DeleteTargetFileName); - - // Act 2: Confirm Delete - vm.ConfirmDeleteCommand.Execute(null); - - // Assert - Assert.False(vm.IsDeleteConfirmDialogOpen); - Assert.Equal(filePath, deletedPath); - Assert.False(File.Exists(filePath)); - Assert.Empty(vm.RecentDocuments); - } - - [Fact] - public void HomeViewModel_DuplicateCommand_CreatesDuplicateAndUpdatesRecents() - { - // Arrange - var filePath = Path.Combine(_tempTestDir, "sample_to_dup.pdf"); - File.WriteAllText(filePath, "Duplicate test"); - - var recentItem = new RecentDocumentItem - { - FilePath = filePath, - Title = "sample_to_dup.pdf", - LastOpened = DateTime.UtcNow - }; - - var mockRecentService = new Mocks.MockRecentDocumentsService(); - mockRecentService.Add(recentItem); - - var vm = new HomeViewModel( - mockRecentService, - new TemplateService(), - new ProjectPersistenceService(), - new PdfEditorApp.Services.Tools.Core.PdfToolRegistry()); - vm.RefreshRecent(); - - // Act - vm.DuplicateDocumentCommand.Execute(filePath); - - // Assert - Assert.Equal(2, vm.RecentDocuments.Count); - var copyItem = vm.RecentDocuments.FirstOrDefault(x => x.Title.Contains("(Copy)")); - Assert.NotNull(copyItem); - Assert.True(File.Exists(copyItem.FilePath)); - } -} diff --git a/tests/PdfEditorApp.Tests/DocumentMetadataTests.cs b/tests/PdfEditorApp.Tests/DocumentMetadataTests.cs deleted file mode 100644 index 80e8c48..0000000 --- a/tests/PdfEditorApp.Tests/DocumentMetadataTests.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.ViewModels; -using QuestPDF.Fluent; -using UglyToad.PdfPig; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DocumentMetadataTests -{ - [Fact] - public void PdfDocumentModel_DefaultValues_AreFryPDFAndCodefrydevIn() - { - var model = new PdfDocumentModel(); - - Assert.Equal("FryPDF", model.Creator); - Assert.Equal("codefrydev.in", model.Producer); - - var clone = model.Clone(); - Assert.Equal("FryPDF", clone.Creator); - Assert.Equal("codefrydev.in", clone.Producer); - } - - [Fact] - public async Task PdfExportService_EmbedsFryPDFCreator_AndCodefrydevInProducer() - { - var exportService = new PdfExportService(); - var model = new PdfDocumentModel - { - Title = "Metadata_Verification_Doc.pdf", - Author = "Code Fry Dev Unit Test", - Subject = "PDF Metadata Producer and Creator Verification", - Keywords = "test, metadata, pdf", - Creator = "FryPDF", - Producer = "codefrydev.in" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Width = 595.28, - Height = 841.89 - }; - page.Elements.Add(new PdfTextElement - { - Text = "Testing Producer: codefrydev.in and Creator: FryPDF", - X = 50, - Y = 50, - Width = 400, - Height = 30 - }); - model.Pages.Add(page); - - byte[] pdfBytes = await exportService.ExportToBytesAsync(model); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 0); - - using var pigDoc = UglyToad.PdfPig.PdfDocument.Open(pdfBytes); - Assert.NotNull(pigDoc.Information); - Assert.Equal("FryPDF", pigDoc.Information.Creator); - Assert.Equal("codefrydev.in", pigDoc.Information.Producer); - Assert.Equal("Metadata_Verification_Doc.pdf", pigDoc.Information.Title); - Assert.Equal("Code Fry Dev Unit Test", pigDoc.Information.Author); - } - - [Fact] - public async Task PdfViewerViewModel_MetadataInspection_ShowsCreatorFryPDFAndProducerCodefrydevIn() - { - var exportService = new PdfExportService(); - var model = new PdfDocumentModel - { - Title = "Annual_Report_2026.pdf", - Author = "ACME CORP.", - Subject = "Fiscal Year 2026 Annual Report" - }; - var page = new PdfPageModel { PageNumber = 1, Width = 800, Height = 1131 }; - page.Elements.Add(new PdfTextElement { Text = "Annual Report Overview", X = 50, Y = 50 }); - model.Pages.Add(page); - - byte[] pdfBytes = await exportService.ExportToBytesAsync(model); - - var viewerVm = new PdfViewerViewModel(); - await viewerVm.LoadDocumentBytesAsync(pdfBytes, "Annual_Report_2026.pdf"); - - var creatorItem = viewerVm.MetadataItems.FirstOrDefault(m => m.Label == "Creator Application"); - var producerItem = viewerVm.MetadataItems.FirstOrDefault(m => m.Label == "PDF Producer"); - - Assert.NotNull(creatorItem); - Assert.Equal("FryPDF", creatorItem.Value); - - Assert.NotNull(producerItem); - Assert.Equal("codefrydev.in", producerItem.Value); - } - - [Fact] - public void FryPdfDocument_GeneratesPdf_WithBrandedMetadata() - { - var doc = FryPdfDocument.Create(container => - { - container.Page(page => - { - page.Size(QuestPDF.Helpers.PageSizes.A4); - page.Content().Text("Branded PDF Document"); - }); - }, title: "Branded Document", author: "Code Fry Dev"); - - byte[] pdfBytes = doc.GeneratePdf(); - Assert.NotNull(pdfBytes); - - using var pigDoc = UglyToad.PdfPig.PdfDocument.Open(pdfBytes); - Assert.Equal("FryPDF", pigDoc.Information.Creator); - Assert.Equal("codefrydev.in", pigDoc.Information.Producer); - } - - [Fact] - public async Task PdfPageService_Merge_SetsFryPDFCreatorAndCodefrydevInProducer() - { - var exportService = new PdfExportService(); - var pageService = new PdfPageService(); - - string temp1 = Path.Combine(Path.GetTempPath(), $"merge1_{Guid.NewGuid():N}.pdf"); - string temp2 = Path.Combine(Path.GetTempPath(), $"merge2_{Guid.NewGuid():N}.pdf"); - string tempMerged = Path.Combine(Path.GetTempPath(), $"merged_{Guid.NewGuid():N}.pdf"); - - try - { - var doc1 = new PdfDocumentModel { Title = "Doc 1" }; - doc1.Pages.Add(new PdfPageModel { PageNumber = 1 }); - var doc2 = new PdfDocumentModel { Title = "Doc 2" }; - doc2.Pages.Add(new PdfPageModel { PageNumber = 1 }); - - await exportService.ExportToFileAsync(doc1, temp1); - await exportService.ExportToFileAsync(doc2, temp2); - - var options = new MergeToolOptions - { - InputFiles = new() { temp1, temp2 }, - OutputFilePath = tempMerged - }; - - var result = await pageService.MergePdfAsync(options); - Assert.True(result.Success); - Assert.True(File.Exists(tempMerged)); - - using var pigDoc = UglyToad.PdfPig.PdfDocument.Open(tempMerged); - Assert.Equal("FryPDF", pigDoc.Information.Creator); - Assert.Equal("codefrydev.in", pigDoc.Information.Producer); - } - finally - { - if (File.Exists(temp1)) File.Delete(temp1); - if (File.Exists(temp2)) File.Delete(temp2); - if (File.Exists(tempMerged)) File.Delete(tempMerged); - } - } -} diff --git a/tests/PdfEditorApp.Tests/DynamicToolRegistryTests.cs b/tests/PdfEditorApp.Tests/DynamicToolRegistryTests.cs deleted file mode 100644 index a5f5249..0000000 --- a/tests/PdfEditorApp.Tests/DynamicToolRegistryTests.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels.Tools; -using PdfEditorApp.ViewModels.Tools.Core; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class DynamicToolRegistryTests -{ - private class DummyCustomToolViewModel : PdfToolViewModelBase - { - public DummyCustomToolViewModel(PdfToolDefinition toolDef) : base(null!, toolDef) - { - } - - protected override Task ExecuteCoreAsync(IProgress progress, CancellationToken ct) - { - return Task.FromResult(new ToolExecutionResult { Success = true }); - } - } - - [Fact] - public void PdfToolRegistry_ExposesBuiltInToolsByDefault() - { - var registry = new PdfToolRegistry(); - var allTools = registry.GetAllTools(); - - Assert.NotEmpty(allTools); - Assert.Contains(allTools, t => t.Id == PdfToolId.MergePdf); - Assert.Contains(allTools, t => t.Id == PdfToolId.CompressPdf); - } - - [Fact] - public async Task PdfToolRegistry_MergesPluginToolsDynamically() - { - var context = new FryPluginContext(); - var host = new PluginHost(context); - var registry = new PdfToolRegistry(context); - - var initialCount = registry.GetAllTools().Count; - - // Register a dynamic tool plugin - var dynamicToolPlugin = new DynamicToolTestPlugin(); - host.RegisterPlugin(dynamicToolPlugin); - await host.StartAsync(); - - var toolsAfterMount = registry.GetAllTools(); - Assert.Equal(initialCount + 1, toolsAfterMount.Count); - - var retrievedByStringId = registry.GetTool("vendor.custom.stamp"); - Assert.NotNull(retrievedByStringId); - Assert.Equal("Custom Vector Stamp", retrievedByStringId.Name); - Assert.Equal("RubberStamp", retrievedByStringId.IconKind); - - // Teardown the plugin - await host.StopAsync(); - - var toolsAfterUnmount = registry.GetAllTools(); - Assert.Equal(initialCount, toolsAfterUnmount.Count); - Assert.Null(registry.GetTool("vendor.custom.stamp")); - } - - [Fact] - public async Task PdfToolRegistry_SeamlesslyUpgradesBuiltInToolsWhenPluginsMount() - { - var context = new FryPluginContext(); - var host = new PluginHost(context); - var registry = new PdfToolRegistry(context); - - var initialCount = registry.GetAllTools().Count; - var initialMerge = registry.GetTool(PdfToolId.MergePdf); - Assert.NotNull(initialMerge); - Assert.Equal("MergePdf", initialMerge.StringId); - Assert.Null(initialMerge.ViewModelFactory); - - // Mount the built-in Merge plugin - var mergePlugin = new PdfEditorApp.Plugins.Bundles.MergePdfToolPlugin(); - host.RegisterPlugin(mergePlugin); - await host.StartAsync(); - - // The tool count does NOT increase because it upgrades the built-in tool in place! - var toolsAfterMount = registry.GetAllTools(); - Assert.Equal(initialCount, toolsAfterMount.Count); - - var upgradedMerge = registry.GetTool(PdfToolId.MergePdf); - Assert.NotNull(upgradedMerge); - Assert.Equal("frypdf.tool.merge", upgradedMerge.StringId); - Assert.NotNull(upgradedMerge.ViewModelFactory); - - // Can also retrieve it by its new StringId - var byStringId = registry.GetTool("frypdf.tool.merge"); - Assert.NotNull(byStringId); - Assert.Equal(PdfToolId.MergePdf, byStringId.Id); - - await host.StopAsync(); - } - - [Fact] - public void PdfToolViewModelFactory_ExecutesPluginViewModelFactoryDelegate() - { - var services = new ServiceCollection(); - var sp = services.BuildServiceProvider(); - - var factory = new PdfToolViewModelFactory(null!, new PdfToolRegistry(), sp); - - var toolDef = new PdfToolDefinition - { - Id = PdfToolId.EditPdf, - StringId = "custom.plugin.tool", - Name = "Plugin Tool" - }; - toolDef.ViewModelFactory = _ => new DummyCustomToolViewModel(toolDef); - - var vm = factory.CreateToolViewModel(toolDef); - Assert.IsType(vm); - Assert.Equal("Plugin Tool", vm.Tool.Name); - } - - private class DynamicToolTestPlugin : IFryPlugin - { - public string Id => "plugin.dynamic.tool.test"; - public string Name => "Dynamic Tool Test Plugin"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterTool(new PdfToolDescriptor - { - Id = "vendor.custom.stamp", - Name = "Custom Vector Stamp", - Description = "Apply custom vector stamps.", - Category = "OrganizeAndPage", - IconKind = "RubberStamp", - IconColorHex = "#9333EA", - CreateViewModel = sp => new DummyCustomToolViewModel(new PdfToolDefinition { StringId = "vendor.custom.stamp", Name = "Custom Vector Stamp" }) - }); - - return Task.CompletedTask; - } - } -} diff --git a/tests/PdfEditorApp.Tests/EducationAndCheatSheetTemplateTests.cs b/tests/PdfEditorApp.Tests/EducationAndCheatSheetTemplateTests.cs deleted file mode 100644 index c259ab1..0000000 --- a/tests/PdfEditorApp.Tests/EducationAndCheatSheetTemplateTests.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Services; -using PdfEditorApp.Services.MathEngine; -using PdfEditorApp.ViewModels.ElementViewModels; -using QuestPDF.Fluent; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class EducationAndCheatSheetTemplateTests -{ - private readonly ITemplateService _templateService = new TemplateService(); - private readonly IPdfExportService _exportService = new PdfExportService(); - - [Fact] - public void MathBODMASWorksheetTemplate_CreatesValidDocumentAndGeneratesPdf() - { - var doc = _templateService.CreateMathBODMASWorksheetTemplate(); - Assert.NotNull(doc); - Assert.Equal("Class_6_BODMAS_Worksheet.pdf", doc.Title); - Assert.Equal(3, doc.Pages.Count); - - var allElements = doc.Pages.SelectMany(p => p.Elements).ToList(); - Assert.True(allElements.Count >= 20); - - // Verify key structural components - Assert.Contains(allElements, e => e is PdfTextElement t && t.Text.Contains("BODMAS")); - Assert.Contains(allElements, e => e is PdfTextElement t && t.Text.Contains("Score")); - Assert.Contains(allElements, e => e is PdfMathElement m && m.Formula.Contains("Brackets")); - Assert.Contains(allElements, e => e is PdfTableElement tbl && tbl.Headers.Contains("Q#")); - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 1000); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - - [Fact] - public void BilingualExamPaperTemplate_CreatesValidDocumentAndGeneratesPdf() - { - var doc = _templateService.CreateBilingualExamPaperTemplate(); - Assert.NotNull(doc); - Assert.Equal("Simple_Interest_Bilingual_Exam_Paper.pdf", doc.Title); - Assert.Equal(2, doc.Pages.Count); - - var allElements = doc.Pages.SelectMany(p => p.Elements).ToList(); - Assert.True(allElements.Count >= 25); - - // Verify bilingual English + Hindi elements - Assert.Contains(allElements, e => e is PdfTextElement t && t.Text.Contains("Simple Interest")); - Assert.Contains(allElements, e => e is PdfTextElement t && t.Text.Contains("साधारण ब्याज")); - Assert.Contains(allElements, e => e is PdfMathElement m && m.Formula.Contains("SI =")); - Assert.Contains(allElements, e => e is PdfTableElement tbl && tbl.Headers.Contains("उत्तर")); - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 1000); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - - [Fact] - public void StatesOfMatterDiagramNotesTemplate_CreatesValidDocumentAndGeneratesPdf() - { - var doc = _templateService.CreateStatesOfMatterDiagramNotesTemplate(); - Assert.NotNull(doc); - Assert.Equal("States_of_Matter_Visual_Notes.pdf", doc.Title); - Assert.Equal(2, doc.Pages.Count); - - var allElements = doc.Pages.SelectMany(p => p.Elements).ToList(); - Assert.True(allElements.Count >= 20); - - // Verify SVG vector diagrams are present - var svgElements = allElements.OfType().ToList(); - Assert.True(svgElements.Count >= 2); - Assert.Contains(svgElements, s => s.PresetName == "Triangular Phase Cycle" && !string.IsNullOrEmpty(s.SvgSource)); - Assert.Contains(svgElements, s => s.PresetName == "Particle Arrangement" && !string.IsNullOrEmpty(s.SvgSource)); - - // Verify that SVG vector elements rasterize to high-DPI PNG bytes cleanly - foreach (var svgModel in svgElements) - { - var pngBytes = PdfEditorApp.Services.Tools.Core.PdfPageRenderer.RenderSvgToPngBytes(svgModel.SvgSource, svgModel.Width, svgModel.Height); - Assert.NotNull(pngBytes); - Assert.True(pngBytes.Length > 100); - // Verify PNG header magic bytes - Assert.Equal(0x89, pngBytes[0]); - Assert.Equal((byte)'P', pngBytes[1]); - Assert.Equal((byte)'N', pngBytes[2]); - Assert.Equal((byte)'G', pngBytes[3]); - - // Verify ViewModel loads properly - var svgVm = new SvgElementViewModel(); - svgVm.LoadFromModel(svgModel); - Assert.Equal(svgModel.Width, svgVm.Width); - Assert.Equal(svgModel.Height, svgVm.Height); - Assert.Equal(svgModel.PresetName, svgVm.PresetName); - Assert.False(string.IsNullOrEmpty(svgVm.SvgSource)); - } - - // Verify comparison table and explanatory notes - Assert.Contains(allElements, e => e is PdfTableElement tbl && tbl.Headers.Contains("Solid (ठोस)")); - Assert.Contains(allElements, e => e is PdfTextElement t && t.Text.Contains("Sublimation")); - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 1000); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - - [Fact] - public void QuadrilateralsGuideTemplate_CreatesValidDocumentAndGeneratesPdf() - { - var doc = _templateService.CreateQuadrilateralsGuideTemplate(); - Assert.NotNull(doc); - Assert.Equal("Types_of_Quadrilaterals_Guide.pdf", doc.Title); - Assert.Equal(2, doc.Pages.Count); - - var allElements = doc.Pages.SelectMany(p => p.Elements).ToList(); - Assert.True(allElements.Count >= 20); - - // Verify SVG quadrilateral diagram - var svg = allElements.OfType().FirstOrDefault(s => s.PresetName == "Quadrilaterals Geometry Diagram"); - Assert.NotNull(svg); - Assert.Contains(" e is PdfMathElement m && m.Formula.Contains("360")); - - // Verify quadrilateral property cards across both pages - Assert.Contains(allElements, e => e is PdfTextElement t && t.Text.Contains("Parallelogram")); - Assert.Contains(allElements, e => e is PdfTextElement t && t.Text.Contains("Trapezoid")); - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 1000); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - - [Fact] - public void FactorizationWorksheetTemplate_CreatesValidDocumentAndGeneratesPdf() - { - var doc = _templateService.CreateFactorizationWorksheetTemplate(); - Assert.NotNull(doc); - Assert.Equal("Factorization_150_Questions_Worksheet.pdf", doc.Title); - Assert.Equal(2, doc.Pages.Count); - - var allElements = doc.Pages.SelectMany(p => p.Elements).ToList(); - Assert.True(allElements.Count >= 20); - - // Verify 3-column algebra columns - var textElements = allElements.OfType().ToList(); - Assert.True(textElements.Count >= 6); - - // Verify algebraic identity formulas and solutions table - Assert.Contains(allElements, e => e is PdfMathElement m && m.Formula.Contains("a^2 - b^2")); - Assert.Contains(allElements, e => e is PdfTableElement tbl && tbl.Headers.Contains("Factorized Result")); - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 1000); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - - [Fact] - public void ArduinoCheatSheetTemplate_CreatesValidPosterDocumentAndGeneratesPdf() - { - var doc = _templateService.CreateArduinoCheatSheetTemplate(); - Assert.NotNull(doc); - Assert.Equal("Arduino_Developer_Reference_CheatSheet.pdf", doc.Title); - Assert.Single(doc.Pages); - - var page = doc.Pages[0]; - Assert.Equal(PageFormat.Poster, page.Format); - Assert.Equal(1600, page.Width); - Assert.Equal(1131, page.Height); - Assert.Equal(PageOrientation.Landscape, page.Orientation); - - // Verify multi-column cards, code snippets, pinout table - Assert.Contains(page.Elements, e => e is PdfTextElement t && t.Text.Contains("ARDUINO")); - Assert.Contains(page.Elements, e => e is PdfTableElement tbl && tbl.Headers.Contains("Port")); - Assert.Contains(page.Elements, e => e is PdfTextElement t && t.Text.Contains("setup()")); - Assert.Contains(page.Elements, e => e is PdfTextElement t && t.Text.Contains("attachInterrupt")); - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 1000); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - - [Fact] - public void MathPresetsLibrary_ContainsSchoolArithmeticAndGeometry() - { - var schoolArithmetic = MathPresetsLibrary.GetByCategory(MathCategory.SchoolArithmetic).ToList(); - Assert.NotEmpty(schoolArithmetic); - Assert.Contains(schoolArithmetic, p => p.Name.Contains("BODMAS")); - Assert.Contains(schoolArithmetic, p => p.Name.Contains("Simple Interest")); - Assert.Contains(schoolArithmetic, p => p.Name.Contains("Compound Interest")); - - var geometry = MathPresetsLibrary.GetByCategory(MathCategory.Geometry).ToList(); - Assert.NotEmpty(geometry); - Assert.Contains(geometry, p => p.Name.Contains("Parallelogram")); - Assert.Contains(geometry, p => p.Name.Contains("Rhombus")); - - var algebra = MathPresetsLibrary.GetByCategory(MathCategory.Algebra).ToList(); - Assert.Contains(algebra, p => p.Name.Contains("Square of Binomial")); - Assert.Contains(algebra, p => p.Name.Contains("Sum and Difference of Cubes")); - Assert.Contains(algebra, p => p.Name.Contains("Square Root of Complex Number")); - } - - [Fact] - public void SvgOrnamentLibrary_GeneratesCleanSvgDiagrams() - { - var phaseCycleSvg = SvgOrnamentLibrary.GetTriangularPhaseCycleSvg(); - Assert.NotEmpty(phaseCycleSvg); - Assert.Contains(" 500); - - // Verify PDF signature (%PDF-) - string header = System.Text.Encoding.ASCII.GetString(bytes.Take(5).ToArray()); - Assert.Equal("%PDF-", header); - } - } - finally - { - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } - } -} diff --git a/tests/PdfEditorApp.Tests/FontManagerTests.cs b/tests/PdfEditorApp.Tests/FontManagerTests.cs deleted file mode 100644 index 6ed44d8..0000000 --- a/tests/PdfEditorApp.Tests/FontManagerTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class FontManagerTests -{ - private readonly FontPackageService _fontService = new(); - - [Fact] - public void FontCatalog_ContainsAllMajorWorldwideScriptPackages() - { - var packages = _fontService.GetAllPackages(); - - Assert.NotNull(packages); - Assert.True(packages.Count >= 10, "Catalog must contain at least 10 major language and font packs"); - - // Verify CJK - Assert.Contains(packages, p => p.Id == "zh-hans" && p.Category == FontPackageCategory.EastAsia); - Assert.Contains(packages, p => p.Id == "zh-hant" && p.Category == FontPackageCategory.EastAsia); - Assert.Contains(packages, p => p.Id == "ja" && p.Category == FontPackageCategory.EastAsia); - Assert.Contains(packages, p => p.Id == "ko" && p.Category == FontPackageCategory.EastAsia); - - // Verify Indic, Middle East, SEA, Eurasia - Assert.Contains(packages, p => p.Id == "indic" && p.Category == FontPackageCategory.SouthAsia); - Assert.Contains(packages, p => p.Id == "arabic" && p.Category == FontPackageCategory.MiddleEast); - Assert.Contains(packages, p => p.Id == "hebrew" && p.Category == FontPackageCategory.MiddleEast); - Assert.Contains(packages, p => p.Id == "southeast-asia" && p.Category == FontPackageCategory.SoutheastAsia); - Assert.Contains(packages, p => p.Id == "eurasia" && p.Category == FontPackageCategory.EuropeAndEurasia); - Assert.Contains(packages, p => p.Id == "creative-design" && p.Category == FontPackageCategory.DesignAndTypography); - } - - [Fact] - public void FontCatalog_AllFilesUseGitHubCdnUrlPattern() - { - var packages = _fontService.GetAllPackages(); - Assert.NotEmpty(packages); - - foreach (var pkg in packages) - { - Assert.NotEmpty(pkg.Files); - foreach (var file in pkg.Files) - { - Assert.False(string.IsNullOrWhiteSpace(file.FileName)); - Assert.StartsWith(FontPackageService.FontCdnBaseUrl, file.DownloadUrl); - Assert.EndsWith(file.FileName, file.DownloadUrl); - Assert.True(file.FileSizeBytes > 0, $"File size for {file.FileName} must be positive"); - } - } - } - - [Fact] - public async System.Threading.Tasks.Task DownloadPackageAsync_DownloadsAndInstallsFromGitHubSuccessfully() - { - var packages = _fontService.GetAllPackages(); - var hebrewPack = packages.First(p => p.Id == "hebrew"); - - // Clean any existing files first - await _fontService.DeletePackageAsync(hebrewPack); - - var progressReported = false; - var progress = new Progress(p => { if (p > 0) progressReported = true; }); - - var success = await _fontService.DownloadPackageAsync(hebrewPack, progress); - - Assert.True(success, "Hebrew font package should download successfully from GitHub repository"); - Assert.True(progressReported, "Progress should be reported during download"); - Assert.True(_fontService.IsPackageInstalled(hebrewPack), "Package should be marked as installed"); - - // Cleanup after test - await _fontService.DeletePackageAsync(hebrewPack); - } - - [Fact] - public void FontPackageInfo_FormatBytes_ReturnsHumanReadableSize() - { - Assert.Equal("0 KB", FontPackageInfo.FormatBytes(0)); - Assert.Equal("500.0 KB", FontPackageInfo.FormatBytes(500 * 1024)); - Assert.Equal("15.0 MB", FontPackageInfo.FormatBytes((long)(15.0 * 1024 * 1024))); - } - - [Theory] - [InlineData("你好世界,欢迎使用 FryPDF", "zh-hans")] // Simplified Chinese - [InlineData("こんにちは世界、PDFエディタへようこそ", "ja")] // Japanese (Hiragana/Katakana) - [InlineData("안녕하세요 세계, PDF 편집기", "ko")] // Korean Hangul - [InlineData("भारत का सबसे सुरक्षित PDF संपादक", "indic")] // Hindi Devanagari - [InlineData("مرحبا بالعالم محرر بي دي اف", "arabic")] // Arabic RTL - public void DetectMissingPackageForText_IdentifiesCorrectScriptPackage(string sampleText, string expectedPackageId) - { - // When not installed, should identify the matching package - var detected = _fontService.DetectMissingPackageForText(sampleText); - if (detected != null) - { - Assert.Equal(expectedPackageId, detected.Id); - } - } - - [Fact] - public void UnicodeScriptDetector_ClassifiesVariousScriptsAccurately() - { - // Chinese - Assert.Equal("Noto Sans SC", UnicodeScriptDetector.DetectScriptFontFamily("中华人民共和国商务合同")); - - // Japanese - Assert.Equal("Noto Sans JP", UnicodeScriptDetector.DetectScriptFontFamily("ひらがな と カタカナ")); - - // Korean - Assert.Equal("Noto Sans KR", UnicodeScriptDetector.DetectScriptFontFamily("대한민국 서울특별시")); - - // Devanagari (Hindi) - Assert.Equal("Noto Sans Devanagari", UnicodeScriptDetector.DetectScriptFontFamily("नमस्ते भारतवर्ष")); - - // Arabic - Assert.Equal("Noto Sans Arabic", UnicodeScriptDetector.DetectScriptFontFamily("المملكة العربية السعودية")); - - // Hebrew - Assert.Equal("Noto Sans Hebrew", UnicodeScriptDetector.DetectScriptFontFamily("שלום עולם ומסמכי")); - - // Thai - Assert.Equal("Noto Sans Thai", UnicodeScriptDetector.DetectScriptFontFamily("สวัสดีชาวโลกและเอกสาร")); - - // Greek - Assert.Equal("GFS Neohellenic", UnicodeScriptDetector.DetectScriptFontFamily("Ελληνική Δημοκρατία")); - - // English / Latin default - Assert.Equal("Open Sans", UnicodeScriptDetector.DetectScriptFontFamily("Standard English Business Invoice 2026")); - } - - [Fact] - public void FontManagerViewModel_FilterAndCategorySelection_FiltersCorrectly() - { - var vm = new FontManagerViewModel(_fontService); - - Assert.Equal(vm.TotalPackagesCount, vm.FilteredPackages.Count); - - // Filter by East Asia - vm.SelectCategory("EastAsia"); - Assert.All(vm.FilteredPackages, p => Assert.Equal(FontPackageCategory.EastAsia, p.Category)); - Assert.Equal(4, vm.FilteredPackages.Count); // zh-hans, zh-hant, ja, ko - - // Filter by Search Query - vm.SelectCategory("All"); - vm.SearchQuery = "hindi"; - Assert.Single(vm.FilteredPackages); - Assert.Equal("indic", vm.FilteredPackages[0].Id); - - // Clear filter - vm.SearchQuery = ""; - Assert.Equal(vm.TotalPackagesCount, vm.FilteredPackages.Count); - } - - [Fact] - public void HomeViewModel_NavigatesToFontPackagesSection() - { - var home = new HomeViewModel(); - - home.SelectNavSection("FontPackages"); - - Assert.Equal(HomeNavSection.FontPackages, home.SelectedNavSection); - Assert.True(home.IsFontPackagesSection); - Assert.NotNull(home.FontManager); - } -} diff --git a/tests/PdfEditorApp.Tests/FryPdfViewerTests.cs b/tests/PdfEditorApp.Tests/FryPdfViewerTests.cs deleted file mode 100644 index 74ca2a8..0000000 --- a/tests/PdfEditorApp.Tests/FryPdfViewerTests.cs +++ /dev/null @@ -1,546 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Services; -using PdfEditorApp.Templates; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; -using PdfEditorApp.ViewModels.FryPdfViewer; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class FryPdfViewerTests -{ - private readonly ITemplateService _templateService = new TemplateService(); - - [Fact] - public void FryPdfViewer_LoadFromModel_InitializesPagesAndMetadata() - { - var doc = _templateService.CreateAnnualReportTemplate(); - var viewer = new FryPdfViewerViewModel(); - - viewer.LoadFromModel(doc, "AnnualReport2026.frypdf"); - - Assert.True(viewer.HasPages); - Assert.Equal(doc.Pages.Count, viewer.TotalPagesCount); - Assert.Equal(1, viewer.CurrentPageNumber); - Assert.Equal(0, viewer.CurrentPageIndex); - Assert.NotNull(viewer.CurrentPage); - Assert.Contains("AnnualReport2026", viewer.DocumentTitle); - } - - [Fact] - public void FryPdfViewer_PageNavigation_NextPreviousFirstLast() - { - var doc = _templateService.CreateAnnualReportTemplate(); - var viewer = new FryPdfViewerViewModel(); - viewer.LoadFromModel(doc); - - Assert.True(viewer.CanGoNextPage); - Assert.False(viewer.CanGoPreviousPage); - - // Next page - viewer.NextPageCommand.Execute(null); - Assert.Equal(2, viewer.CurrentPageNumber); - Assert.True(viewer.CanGoPreviousPage); - - // Last page - viewer.LastPageCommand.Execute(null); - Assert.Equal(viewer.TotalPagesCount, viewer.CurrentPageNumber); - Assert.False(viewer.CanGoNextPage); - - // Previous page - viewer.PreviousPageCommand.Execute(null); - Assert.Equal(viewer.TotalPagesCount - 1, viewer.CurrentPageNumber); - - // First page - viewer.FirstPageCommand.Execute(null); - Assert.Equal(1, viewer.CurrentPageNumber); - - // Direct jump - viewer.GoToPageCommand.Execute(2); - Assert.Equal(2, viewer.CurrentPageNumber); - } - - [Fact] - public void FryPdfViewer_ZoomControls_UpdatesZoomLevelCorrectly() - { - var viewer = new FryPdfViewerViewModel(); - - viewer.ZoomLevel = 1.0; - - viewer.ZoomInCommand.Execute(null); - Assert.True(viewer.ZoomLevel > 1.0); - - viewer.ZoomOutCommand.Execute(null); - viewer.ZoomOutCommand.Execute(null); - Assert.True(viewer.ZoomLevel < 1.0); - - viewer.ResetZoomCommand.Execute(null); - Assert.Equal(1.0, viewer.ZoomLevel); - - viewer.FitToWidthCommand.Execute(null); - Assert.Equal(1.25, viewer.ZoomLevel); - - viewer.FitToPageCommand.Execute(null); - Assert.Equal(0.85, viewer.ZoomLevel); - } - - [Fact] - public void InteractiveTable_Filtering_FiltersRowsAccurately() - { - var tableVm = new TableElementViewModel(); - tableVm.Headers.Clear(); - tableVm.Headers.Add(new TableHeaderItem("Service")); - tableVm.Headers.Add(new TableHeaderItem("Cost")); - - tableVm.Rows.Clear(); - tableVm.Rows.Add(new TableRowItem(new[] { "Cloud Architecture", "$6,000" })); - tableVm.Rows.Add(new TableRowItem(new[] { "Avalonia Desktop UI", "$8,400" })); - tableVm.Rows.Add(new TableRowItem(new[] { "QuestPDF Engine", "$4,000" })); - - var interactiveTable = new InteractiveTableViewModel(tableVm); - Assert.Equal(3, interactiveTable.TotalRowCount); - Assert.Equal(3, interactiveTable.FilteredRowCount); - - // Filter by "Avalonia" - interactiveTable.FilterQuery = "Avalonia"; - Assert.Equal(1, interactiveTable.FilteredRowCount); - Assert.Equal("Avalonia Desktop UI", interactiveTable.DisplayedRows[0].GetCell(0)); - - // Clear filter - interactiveTable.ClearFilterCommand.Execute(null); - Assert.Equal(3, interactiveTable.FilteredRowCount); - } - - [Fact] - public void InteractiveTable_Sorting_SortsAlphabeticallyAndNumerically() - { - var tableVm = new TableElementViewModel(); - tableVm.Headers.Clear(); - tableVm.Headers.Add(new TableHeaderItem("Task")); - tableVm.Headers.Add(new TableHeaderItem("Hours")); - - tableVm.Rows.Clear(); - tableVm.Rows.Add(new TableRowItem(new[] { "Zeppelin Design", "10" })); - tableVm.Rows.Add(new TableRowItem(new[] { "Alpha Review", "40" })); - tableVm.Rows.Add(new TableRowItem(new[] { "Beta Optimization", "20" })); - - var interactiveTable = new InteractiveTableViewModel(tableVm); - - // Sort column 0 ascending (Alpha, Beta, Zeppelin) - interactiveTable.SortByColumnCommand.Execute(0); - Assert.Equal("Alpha Review", interactiveTable.DisplayedRows[0].GetCell(0)); - Assert.Equal("Zeppelin Design", interactiveTable.DisplayedRows[2].GetCell(0)); - - // Sort column 0 descending (Zeppelin, Beta, Alpha) - interactiveTable.SortByColumnCommand.Execute(0); - Assert.Equal("Zeppelin Design", interactiveTable.DisplayedRows[0].GetCell(0)); - Assert.Equal("Alpha Review", interactiveTable.DisplayedRows[2].GetCell(0)); - } - - [Fact] - public void InteractiveTable_GenerateCsv_ExportsValidCsv() - { - var tableVm = new TableElementViewModel(); - tableVm.Headers.Clear(); - tableVm.Headers.Add(new TableHeaderItem("Item")); - tableVm.Headers.Add(new TableHeaderItem("Amount")); - - tableVm.Rows.Clear(); - tableVm.Rows.Add(new TableRowItem(new[] { "Consulting", "$100" })); - tableVm.Rows.Add(new TableRowItem(new[] { "Support", "$50" })); - - var interactiveTable = new InteractiveTableViewModel(tableVm); - string csv = interactiveTable.GenerateCsv(); - - Assert.Contains("Item,Amount", csv); - Assert.Contains("Consulting,$100", csv); - Assert.Contains("Support,$50", csv); - } - - [Fact] - public void InteractiveChart_PopulateAndAnimation_CalculatesValuesAndTooltips() - { - var chartVm = new ChartElementViewModel(); - chartVm.Title = "Sales Q1-Q4"; - chartVm.Bars.Clear(); - chartVm.Bars.Add(new ChartBarItem { Category = "Q1", Value = 10, ValueLabel = "$10M", ColorHex = "#0F6CBD" }); - chartVm.Bars.Add(new ChartBarItem { Category = "Q2", Value = 20, ValueLabel = "$20M", ColorHex = "#107C41" }); - chartVm.Bars.Add(new ChartBarItem { Category = "Q3", Value = 30, ValueLabel = "$30M", ColorHex = "#D83B01" }); - - var interactiveChart = new InteractiveChartViewModel(chartVm); - - Assert.Equal(3, interactiveChart.Items.Count); - Assert.Equal(60, interactiveChart.TotalSum); - Assert.Equal(30, interactiveChart.MaxItemValue); - - // Tooltip checks - var q2Item = interactiveChart.Items[1]; - Assert.Equal("Q2: $20M (33.3%)", q2Item.TooltipText); - - // Toggle data table view - Assert.False(interactiveChart.IsShowingDataTable); - interactiveChart.ToggleDataTableCommand.Execute(null); - Assert.True(interactiveChart.IsShowingDataTable); - } - - [Fact] - public void FryPdfViewer_Search_FindsTextAcrossPages() - { - var doc = new PdfDocumentModel { Title = "Searchable Presentation" }; - var page1 = new PdfPageModel { PageNumber = 1 }; - page1.Elements.Add(new PdfTextElement { Text = "First Chapter Introduction", X = 10, Y = 10, Width = 200, Height = 30 }); - var page2 = new PdfPageModel { PageNumber = 2 }; - page2.Elements.Add(new PdfTextElement { Text = "Second Chapter Financials", X = 10, Y = 10, Width = 200, Height = 30 }); - - doc.Pages.Add(page1); - doc.Pages.Add(page2); - - var viewer = new FryPdfViewerViewModel(); - viewer.LoadFromModel(doc); - - viewer.SearchQuery = "Financials"; - Assert.Single(viewer.SearchResults); - Assert.Equal(2, viewer.SearchResults[0].PageNumber); - } - - [Fact] - public void MainViewModel_OpenCurrentInInteractiveReader_TransitionsSeamlessly() - { - var mainVm = new MainViewModel(); - - // Initially on Home - Assert.True(mainVm.IsHomePageVisible); - Assert.False(mainVm.IsFryPdfViewerVisible); - - // Switch to Interactive Reader - mainVm.OpenCurrentInInteractiveReaderCommand.Execute(null); - - Assert.True(mainVm.IsFryPdfViewerVisible); - Assert.False(mainVm.IsEditorVisible); - Assert.False(mainVm.IsHomePageVisible); - Assert.Contains("Interactive Reader", mainVm.WindowTitle); - - // Return to Home - mainVm.NavigateToHomeCommand.Execute(null); - Assert.False(mainVm.IsFryPdfViewerVisible); - Assert.True(mainVm.IsHomePageVisible); - } - - [Fact] - public void PresentationMode_TogglesState() - { - var viewer = new FryPdfViewerViewModel(); - Assert.False(viewer.IsPresentationMode); - - viewer.TogglePresentationModeCommand.Execute(null); - Assert.True(viewer.IsPresentationMode); - - viewer.TogglePresentationModeCommand.Execute(null); - Assert.False(viewer.IsPresentationMode); - } - - [Fact] - public void FryPdfViewer_LoadsAllElements_IntoUnifiedElementsCollection() - { - var doc = _templateService.CreateAnnualReportTemplate(); - var viewer = new FryPdfViewerViewModel(); - viewer.LoadFromModel(doc); - - Assert.NotEmpty(viewer.Pages); - foreach (var page in viewer.Pages) - { - Assert.NotEmpty(page.Elements); - foreach (var el in page.Elements) - { - Assert.True(el.Width > 0, $"Element {el.DisplayName} should have positive width"); - Assert.True(el.Height > 0, $"Element {el.DisplayName} should have positive height"); - } - } - } - - [Fact] - public void InteractiveTable_InheritsFromElementViewModelBase_AndPreservesProperties() - { - var tableVm = new TableElementViewModel - { - X = 50, - Y = 120, - Width = 500, - Height = 300, - ZIndex = 500 - }; - tableVm.Headers.Add(new TableHeaderItem("Metric")); - tableVm.Rows.Add(new TableRowItem(new[] { "Revenue" })); - - var interactiveTable = new InteractiveTableViewModel(tableVm); - Assert.Equal(ElementKind.Table, interactiveTable.Kind); - Assert.Equal(50, interactiveTable.X); - Assert.Equal(120, interactiveTable.Y); - Assert.Equal(500, interactiveTable.Width); - Assert.Equal(300, interactiveTable.Height); - Assert.Equal(500, interactiveTable.ZIndex); - Assert.NotNull(interactiveTable.CopyToClipboardCommand); - } - - [Fact] - public void InteractiveChart_InheritsFromElementViewModelBase_AndPreservesProperties() - { - var chartVm = new ChartElementViewModel - { - X = 60, - Y = 200, - Width = 450, - Height = 250, - ZIndex = 400, - Title = "Quarterly Growth" - }; - chartVm.Bars.Add(new ChartBarItem { Category = "Q1", Value = 15, ValueLabel = "$15k" }); - - var interactiveChart = new InteractiveChartViewModel(chartVm); - Assert.Equal(ElementKind.Chart, interactiveChart.Kind); - Assert.Equal(60, interactiveChart.X); - Assert.Equal(200, interactiveChart.Y); - Assert.Equal(450, interactiveChart.Width); - Assert.Equal(250, interactiveChart.Height); - Assert.Equal(400, interactiveChart.ZIndex); - } - - [Fact] - public void InteractiveExecutiveBriefTemplate_CreatesLandscapeDocumentWithRichElements() - { - var doc = _templateService.CreateInteractiveExecutiveBriefTemplate(); - - Assert.NotNull(doc); - Assert.Equal(3, doc.Pages.Count); - Assert.Contains("QuantumScale", doc.Title); - - foreach (var page in doc.Pages) - { - Assert.Equal(PageOrientation.Landscape, page.Orientation); - Assert.Equal(1131, page.Width); - Assert.Equal(800, page.Height); - Assert.NotEmpty(page.Elements); - } - - // Page 1: Revenue Bar Chart, Donut Pillar Chart & Sticky Note - var page1 = doc.Pages[0]; - var page1Charts = page1.Elements.OfType().ToList(); - Assert.Equal(2, page1Charts.Count); - Assert.Contains(page1Charts, c => c.ChartType == ChartType.BarColumn); - Assert.Contains(page1Charts, c => c.ChartType == ChartType.DonutPie); - Assert.Contains(page1.Elements, e => e is PdfStickyNoteElement); - - // Page 2: Operational Data Table & Regional Capacity Horizontal Bar Chart - var page2 = doc.Pages[1]; - var table = page2.Elements.OfType().FirstOrDefault(); - Assert.NotNull(table); - Assert.Equal(7, table.Headers.Count); - Assert.Equal(7, table.Rows.Count); - var page2Chart = page2.Elements.OfType().FirstOrDefault(); - Assert.NotNull(page2Chart); - Assert.Equal(ChartType.HorizontalBar, page2Chart.ChartType); - - // Page 3: Compliance Checkboxes, Signatures, Redaction, Barcode, Area Chart, QR Code - var page3 = doc.Pages[2]; - var formFields = page3.Elements.OfType().ToList(); - Assert.True(formFields.Count >= 5); - Assert.Contains(formFields, f => f.FieldType == FormFieldType.Checkbox); - Assert.Contains(formFields, f => f.FieldType == FormFieldType.Signature); - Assert.Contains(page3.Elements, e => e is PdfRedactionElement); - Assert.Contains(page3.Elements, e => e is PdfBarcodeElement); - Assert.Contains(page3.Elements, e => e is PdfQrCodeElement); - var page3Chart = page3.Elements.OfType().FirstOrDefault(); - Assert.NotNull(page3Chart); - Assert.Equal(ChartType.Area, page3Chart.ChartType); - } - - [Fact] - public void FryPdfViewer_LoadsLandscapeInteractiveExecutiveDeck_AndOperatesInteractively() - { - var doc = _templateService.CreateInteractiveExecutiveBriefTemplate(); - var viewer = new FryPdfViewerViewModel(); - viewer.LoadFromModel(doc, "QuantumScale_Executive_Brief.frypdf"); - - Assert.Equal(3, viewer.TotalPagesCount); - Assert.NotNull(viewer.CurrentPage); - Assert.Equal(1131, viewer.CurrentPage.Width); - Assert.Equal(800, viewer.CurrentPage.Height); - - // Navigate to Page 2 (with interactive table) - viewer.GoToPageCommand.Execute(2); - Assert.Equal(2, viewer.CurrentPageNumber); - - var tableVm = viewer.CurrentPage.Elements.OfType().FirstOrDefault(); - Assert.NotNull(tableVm); - Assert.Equal(7, tableVm.TotalRowCount); - Assert.Equal(7, tableVm.FilteredRowCount); - - // Real-time table filter - tableVm.FilterQuery = "Agent"; - Assert.Equal(1, tableVm.FilteredRowCount); - Assert.Contains("Autonomous AI Agent", tableVm.DisplayedRows[0].GetCell(0)); - - // Clear filter - tableVm.ClearFilterCommand.Execute(null); - Assert.Equal(7, tableVm.FilteredRowCount); - - // Sort by FY26 ARR descending - tableVm.SortByColumnCommand.Execute(3); - - // CSV export - string csv = tableVm.GenerateCsv(); - Assert.Contains("Business Unit & Segment", csv); - Assert.Contains("Autonomous AI Agent Mesh", csv); - - // Check interactive horizontal bar chart on page 2 - var chartVm = viewer.CurrentPage.Elements.OfType().FirstOrDefault(); - Assert.NotNull(chartVm); - Assert.True(chartVm.IsHorizontalBar); - Assert.Equal(5, chartVm.Items.Count); - Assert.True(chartVm.TotalSum > 100); - } - - [Fact] - public void FryPdfViewer_SlideDeckNavigation_NextAndPreviousButtonsWorkAcrossAllSlides_AndWrapInPresentationMode() - { - var doc = _templateService.CreateInteractiveExecutiveBriefTemplate(); - var viewer = new FryPdfViewerViewModel(); - - // Before document is loaded, commands should not execute - Assert.False(viewer.CanGoNextPage); - Assert.False(viewer.CanGoPreviousPage); - Assert.False(viewer.NextPageCommand.CanExecute(null)); - Assert.False(viewer.PreviousPageCommand.CanExecute(null)); - - // Load document - viewer.LoadFromModel(doc, "QuantumScale_Executive_Brief.frypdf"); - - // Slide 1 of 3: Next is enabled, Previous is disabled - Assert.Equal(1, viewer.CurrentPageNumber); - Assert.True(viewer.CanGoNextPage); - Assert.False(viewer.CanGoPreviousPage); - Assert.True(viewer.NextPageCommand.CanExecute(null)); - Assert.False(viewer.PreviousPageCommand.CanExecute(null)); - - // Advance to Slide 2 - viewer.NextPageCommand.Execute(null); - Assert.Equal(2, viewer.CurrentPageNumber); - Assert.True(viewer.CanGoNextPage); - Assert.True(viewer.CanGoPreviousPage); - Assert.True(viewer.NextPageCommand.CanExecute(null)); - Assert.True(viewer.PreviousPageCommand.CanExecute(null)); - - // Advance to Slide 3 (Slide 3 of 3 - User's Reported Screen) - viewer.NextPageCommand.Execute(null); - Assert.Equal(3, viewer.CurrentPageNumber); - Assert.False(viewer.CanGoNextPage); - Assert.True(viewer.CanGoPreviousPage); - Assert.False(viewer.NextPageCommand.CanExecute(null)); - Assert.True(viewer.PreviousPageCommand.CanExecute(null)); - - // Left button (<) MUST successfully navigate back to Slide 2 - viewer.PreviousPageCommand.Execute(null); - Assert.Equal(2, viewer.CurrentPageNumber); - - // Left button (<) again back to Slide 1 - viewer.PreviousPageCommand.Execute(null); - Assert.Equal(1, viewer.CurrentPageNumber); - - // Test Presentation Mode: Seamless Wrap-Around - viewer.TogglePresentationModeCommand.Execute(null); - Assert.True(viewer.IsPresentationMode); - - // In presentation mode on Slide 1: Previous (<) loops around to Slide 3 - Assert.True(viewer.CanGoPreviousPage); - Assert.True(viewer.PreviousPageCommand.CanExecute(null)); - viewer.PreviousPageCommand.Execute(null); - Assert.Equal(3, viewer.CurrentPageNumber); - - // In presentation mode on Slide 3: Next (>) loops around to Slide 1 - Assert.True(viewer.CanGoNextPage); - Assert.True(viewer.NextPageCommand.CanExecute(null)); - viewer.NextPageCommand.Execute(null); - Assert.Equal(1, viewer.CurrentPageNumber); - } - - [Fact] - public void FryPdfViewer_PresentationMode_SuppressesDistractions_AndCalculatesFitToViewport() - { - var doc = _templateService.CreateInteractiveExecutiveBriefTemplate(); - var viewer = new FryPdfViewerViewModel(); - viewer.LoadFromModel(doc); - - // Standard reading mode state: sidebar visible, padding 36, borders enabled, headers visible - Assert.False(viewer.IsPresentationMode); - Assert.True(viewer.IsSidebarVisibleInView); - Assert.Equal("Transparent", viewer.ViewportBackgroundBrush); - Assert.Equal(new Avalonia.Thickness(36), viewer.ViewportPadding); - Assert.Equal(1.0, viewer.PageBorderThickness); - Assert.True(viewer.ShowHeaderFooterInView); - - // Enter presentation mode: everything suppressed, black background, zero padding, zero borders - viewer.TogglePresentationModeCommand.Execute(null); - Assert.True(viewer.IsPresentationMode); - Assert.False(viewer.IsSidebarVisibleInView); - Assert.Equal("#000000", viewer.ViewportBackgroundBrush); - Assert.Equal(new Avalonia.Thickness(0), viewer.ViewportPadding); - Assert.Equal(0.0, viewer.PageBorderThickness); - Assert.False(viewer.ShowHeaderFooterInView); - - // Test aspect-fit calculation (1131 x 800 slide in 1920 x 1080 display) - viewer.FitToViewport(1920, 1080); - double expectedFit = Math.Round(Math.Min(1920.0 / 1131.0, 1080.0 / 800.0), 3); - Assert.Equal(expectedFit, viewer.ZoomLevel); - - // Exit presentation mode: normal reading mode restored - viewer.TogglePresentationModeCommand.Execute(null); - Assert.False(viewer.IsPresentationMode); - Assert.True(viewer.IsSidebarVisibleInView); - Assert.Equal("Transparent", viewer.ViewportBackgroundBrush); - Assert.Equal(new Avalonia.Thickness(36), viewer.ViewportPadding); - Assert.Equal(1.0, viewer.PageBorderThickness); - Assert.True(viewer.ShowHeaderFooterInView); - } - - [Fact] - public void InteractiveChart_SupportsMultipleChartTypes_AndCenterSummary() - { - var chartVm = new ChartElementViewModel - { - Title = "ARR by Pillar", - ChartType = ChartType.DonutPie - }; - chartVm.Bars.Clear(); - chartVm.Bars.Add(new ChartBarItem { Category = "Mesh", Value = 50, ValueLabel = "$50M", ColorHex = "#4F46E5" }); - chartVm.Bars.Add(new ChartBarItem { Category = "Enclaves", Value = 50, ValueLabel = "$50M", ColorHex = "#0284C7" }); - - var interactiveDonut = new InteractiveChartViewModel(chartVm); - Assert.True(interactiveDonut.IsDonutPie); - Assert.False(interactiveDonut.IsBarColumn); - Assert.False(interactiveDonut.IsHorizontalBar); - Assert.Equal(100, interactiveDonut.TotalSum); - Assert.Equal("$100.0M", interactiveDonut.CenterSummaryValue); - Assert.Equal("Total ARR", interactiveDonut.CenterSummaryLabel); - - // Hover over second item - interactiveDonut.HoveredItem = interactiveDonut.Items[1]; - Assert.Equal("$50M", interactiveDonut.CenterSummaryValue); - Assert.Equal("Enclaves", interactiveDonut.CenterSummaryLabel); - - // Test Horizontal Bar - chartVm.ChartType = ChartType.HorizontalBar; - var interactiveHorizontal = new InteractiveChartViewModel(chartVm); - Assert.True(interactiveHorizontal.IsHorizontalBar); - Assert.False(interactiveHorizontal.IsDonutPie); - Assert.False(interactiveHorizontal.IsBarColumn); - Assert.True(interactiveHorizontal.Items[0].AnimatedWidthPx > 0); - Assert.Equal("50.0%", interactiveHorizontal.Items[0].ProgressPercentageString); - } -} diff --git a/tests/PdfEditorApp.Tests/GestureAndNavigationTests.cs b/tests/PdfEditorApp.Tests/GestureAndNavigationTests.cs deleted file mode 100644 index af5f961..0000000 --- a/tests/PdfEditorApp.Tests/GestureAndNavigationTests.cs +++ /dev/null @@ -1,274 +0,0 @@ -using System; -using Avalonia.Input; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Tests.Mocks; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class GestureAndNavigationTests -{ - [Fact] - public void TrackpadMagnifyGesture_ComputesProportionalScale() - { - double oldZoom = 1.0; - - // Positive delta (pinch out / zoom in by 10%) - double deltaPositive = 0.10; - double zoomIn = Math.Clamp(Math.Round(oldZoom * (1.0 + deltaPositive), 3), 0.1, 5.0); - Assert.Equal(1.10, zoomIn); - - // Negative delta (pinch in / zoom out by 10%) - double deltaNegative = -0.10; - double zoomOut = Math.Clamp(Math.Round(oldZoom * (1.0 + deltaNegative), 3), 0.1, 5.0); - Assert.Equal(0.90, zoomOut); - } - [Fact] - public void PinchZoomMath_PreventsExponentialExplosion_AndClampsProperly() - { - // Simulate pinch scaling logic: initial zoom is 1.0 (100%) - double startZoom = 1.0; - - // User pinches out by 25% (scale = 1.25) - double scale1 = 1.25; - double calculatedZoom1 = Math.Clamp(Math.Round(startZoom * scale1, 3), 0.1, 5.0); - Assert.Equal(1.25, calculatedZoom1); - - // Continuous pinch event 2 in the same gesture (scale = 1.50 relative to start) - double scale2 = 1.50; - double calculatedZoom2 = Math.Clamp(Math.Round(startZoom * scale2, 3), 0.1, 5.0); - Assert.Equal(1.50, calculatedZoom2); - - // Extreme pinch out clamped to max zoom (5.0 / 500%) - double scaleExtreme = 10.0; - double calculatedZoomMax = Math.Clamp(Math.Round(startZoom * scaleExtreme, 3), 0.1, 5.0); - Assert.Equal(5.0, calculatedZoomMax); - - // Extreme pinch in clamped to min zoom (0.1 / 10%) - double scaleMin = 0.01; - double calculatedZoomMin = Math.Clamp(Math.Round(startZoom * scaleMin, 3), 0.1, 5.0); - Assert.Equal(0.1, calculatedZoomMin); - } - - [Fact] - public void TrackpadContinuousZoomDelta_ComputesSmoothProportionalScaling() - { - double oldZoom = 1.0; - - // Small trackpad delta (e.g. 0.05) - double deltaY = 0.05; - double zoomDeltaFactor = Math.Pow(1.002, deltaY * 100); - double newZoom = Math.Clamp(Math.Round(oldZoom * zoomDeltaFactor, 3), 0.1, 5.0); - - // Should be approximately 1.01 (1% smooth increase) - Assert.True(newZoom > 1.0 && newZoom < 1.05); - - // Discrete mouse wheel tick (deltaY = 1.0) - double wheelFactor = 1.15; - double wheelZoom = Math.Clamp(Math.Round(oldZoom * wheelFactor, 3), 0.1, 5.0); - Assert.Equal(1.15, wheelZoom); - } - - [Fact] - public void MainViewModel_ZoomCommands_OperateWithinValidRanges() - { - var mainVm = new MainViewModel(); - mainVm.ZoomLevel = 1.0; - - // Zoom In - mainVm.ZoomIn(); - Assert.True(mainVm.ZoomLevel > 1.0); - - // Reset Zoom - mainVm.ResetZoom(); - Assert.Equal(1.0, mainVm.ZoomLevel); - - // Zoom Out - mainVm.ZoomOut(); - Assert.True(mainVm.ZoomLevel < 1.0); - } - - [Fact] - public void PdfViewerViewModel_ZoomAndJumpPageGestures_ValidateCorrectly() - { - var viewerVm = new PdfViewerViewModel(); - viewerVm.ZoomLevel = 1.0; - - // Zoom In - viewerVm.ZoomIn(); - Assert.Equal(1.25, viewerVm.ZoomLevel); - - // Zoom Out - viewerVm.ZoomOut(); - Assert.Equal(1.0, viewerVm.ZoomLevel); - - // Reset Zoom - viewerVm.ResetZoom(); - Assert.Equal(1.0, viewerVm.ZoomLevel); - - // Fit Presets - viewerVm.FitToWidth(); - Assert.Equal(1.35, viewerVm.ZoomLevel); - - viewerVm.FitToPage(); - Assert.Equal(0.95, viewerVm.ZoomLevel); - - // Percentage Presets - viewerVm.SetZoomPreset("200%"); - Assert.Equal(2.0, viewerVm.ZoomLevel); - - viewerVm.SetZoomPreset("50%"); - Assert.Equal(0.5, viewerVm.ZoomLevel); - } - - [Fact] - public void MainViewModel_WindowTitle_ReflectsActiveViewAndContextCorrectly() - { - CommunityToolkit.Mvvm.Messaging.WeakReferenceMessenger.Default.Reset(); - // 1. Initial State: App opens on Home Dashboard - var mainVm = new MainViewModel( - new PdfExportService(), - new TemplateService(), - new ProjectPersistenceService(), - recentService: new MockRecentDocumentsService()); - mainVm.NavigateToHome(); - Assert.True(mainVm.IsHomePageVisible); - Assert.False(mainVm.IsEditorVisible); - Assert.False(mainVm.IsPdfViewerVisible); - Assert.Equal("FryPDF - Privacy-First PDF Studio", mainVm.WindowTitle); - - // 2. Open a tool in Home page (e.g. Merge PDF) - mainVm.OpenTool(PdfToolId.MergePdf); - Assert.True(mainVm.IsHomePageVisible); - Assert.True(mainVm.Home.IsToolPageActive); - Assert.Contains("Merge", mainVm.WindowTitle); - Assert.EndsWith("FryPDF", mainVm.WindowTitle); - - // 3. Navigate to Licensing section - mainVm.Home.BackToTools(); - mainVm.Home.SelectNavSectionCommand.Execute("Licensing"); - Assert.Equal("Licenses & Third-Party Tools - FryPDF", mainVm.WindowTitle); - - // 4. Open an Editor template document - mainVm.OpenEditorWithTemplate("Invoice"); - Assert.False(mainVm.IsHomePageVisible); - Assert.True(mainVm.IsEditorVisible); - Assert.False(mainVm.IsPdfViewerVisible); - Assert.Contains("FryPDF", mainVm.WindowTitle); - - // 5. Navigate back to Home - mainVm.NavigateToHome(); - Assert.True(mainVm.IsHomePageVisible); - Assert.False(mainVm.IsEditorVisible); - Assert.Equal("FryPDF - Privacy-First PDF Studio", mainVm.WindowTitle); - - // 6. Open in PDF Viewer Mode - mainVm.PdfViewer.DocumentTitle = "Quarterly_Financials.pdf"; - mainVm.OpenInViewer("dummy/Quarterly_Financials.pdf"); - Assert.False(mainVm.IsHomePageVisible); - Assert.True(mainVm.IsPdfViewerVisible); - Assert.Equal("Quarterly_Financials.pdf - FryPDF", mainVm.WindowTitle); - } - - [Fact] - public async System.Threading.Tasks.Task HomeViewModel_NavigationBetweenStandardSections_MaintainsNullDynamicPageViewForInstantVisibility() - { - var navReg = new PdfEditorApp.Services.Navigation.NavigationRegistry(); - var context = new PdfEditorApp.Core.Plugins.FryPluginContext(); - context.RegisterService(navReg); - - var host = new PdfEditorApp.Core.Plugins.PluginHost(context); - var bundle = new PdfEditorApp.Plugins.Bundles.WorkspacePagesBundle(); - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var home = new HomeViewModel( - new RecentDocumentsService(), - new TemplateService(), - new ProjectPersistenceService(), - new PdfEditorApp.Services.Tools.Core.PdfToolRegistry(), - navigationRegistry: navReg); - - // Rapid navigation through all built-in sections must keep DynamicPageView null - // so that pre-mounted, pre-compiled visual trees in HomeView.axaml are used instantaneously - string[] sectionsToTest = new[] - { - "Home", "PdfReader", "NewDocument", "AllTools", "OrganizeAndPage", - "OptimizeAndSecurity", "ConvertFromPdf", "ConvertToPdf", "EditAndForms", - "AiAndAutomation", "Starred", "FontPackages", "TesseractData", - "Trash", "Help", "Licensing", "Settings", "Plugins" - }; - - foreach (var section in sectionsToTest) - { - home.SelectNavSectionCommand.Execute(section); - Assert.Null(home.DynamicPageView); - Assert.False(home.IsToolPageActive); - } - - await host.StopAsync(); - } - - [Fact] - public void HomeViewModel_NavigationCachesContributedPluginView_AndReusesInstance() - { - var navReg = new PdfEditorApp.Services.Navigation.NavigationRegistry(); - int factoryInvocationCount = 0; - navReg.RegisterNavigationItem(new PdfEditorApp.Core.Plugins.Descriptors.NavigationItemDescriptor - { - Id = "CustomAnalyticsExtension", - Title = "Analytics Extension", - Group = "Extensions", - ViewFactory = sp => - { - factoryInvocationCount++; - return new object(); - } - }); - - var home = new HomeViewModel( - new RecentDocumentsService(), - new TemplateService(), - new ProjectPersistenceService(), - new PdfEditorApp.Services.Tools.Core.PdfToolRegistry(), - navigationRegistry: navReg); - - // First navigation invokes factory and caches view - home.SelectNavSectionCommand.Execute("CustomAnalyticsExtension"); - Assert.Equal(1, factoryInvocationCount); - var firstView = home.DynamicPageView; - Assert.NotNull(firstView); - - // Navigate away to Home (standard section) - home.SelectNavSectionCommand.Execute("Home"); - Assert.Null(home.DynamicPageView); - - // Navigate back to extension: MUST reuse cached instance without re-invoking factory - home.SelectNavSectionCommand.Execute("CustomAnalyticsExtension"); - Assert.Equal(1, factoryInvocationCount); - Assert.Same(firstView, home.DynamicPageView); - } - - [Fact] - public void HomeViewModel_NavigationWhileToolActive_AutomaticallyClosesTool() - { - var home = new HomeViewModel( - new RecentDocumentsService(), - new TemplateService(), - new ProjectPersistenceService(), - new PdfEditorApp.Services.Tools.Core.PdfToolRegistry()); - - home.OpenToolPage(PdfToolId.MergePdf); - Assert.True(home.IsToolPageActive); - Assert.NotNull(home.ActiveToolCard); - - // Navigating to Settings must clean up the tool - home.SelectNavSectionCommand.Execute("Settings"); - Assert.False(home.IsToolPageActive); - Assert.Null(home.ActiveToolCard); - Assert.Null(home.ActiveToolViewModel); - Assert.True(home.IsSettingsSection); - } -} diff --git a/tests/PdfEditorApp.Tests/HeadlessCliRunnerTests.cs b/tests/PdfEditorApp.Tests/HeadlessCliRunnerTests.cs deleted file mode 100644 index 3109356..0000000 --- a/tests/PdfEditorApp.Tests/HeadlessCliRunnerTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Threading.Tasks; -using PdfEditorApp.Plugins.Cli; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class HeadlessCliRunnerTests -{ - [Theory] - [InlineData("--help", true)] - [InlineData("-h", true)] - [InlineData("--version", true)] - [InlineData("-v", true)] - [InlineData("--tool", true)] - [InlineData("-t", true)] - [InlineData("--list-tools", true)] - [InlineData("--list-plugins", true)] - [InlineData("--profile", true)] - [InlineData("somefile.pdf", false)] - [InlineData("", false)] - public void IsCliInvocation_DetectsFlagsCorrectly(string flag, bool expected) - { - var args = string.IsNullOrEmpty(flag) ? System.Array.Empty() : new[] { flag }; - var actual = HeadlessCliRunner.IsCliInvocation(args); - Assert.Equal(expected, actual); - } - - [Fact] - public async Task RunCliAsync_HelpFlag_ReturnsZero() - { - var exitCode = await HeadlessCliRunner.RunCliAsync(new[] { "--help" }); - Assert.Equal(0, exitCode); - } - - [Fact] - public async Task RunCliAsync_VersionFlag_ReturnsZero() - { - var exitCode = await HeadlessCliRunner.RunCliAsync(new[] { "--version" }); - Assert.Equal(0, exitCode); - } -} diff --git a/tests/PdfEditorApp.Tests/HelpGuideTests.cs b/tests/PdfEditorApp.Tests/HelpGuideTests.cs deleted file mode 100644 index e808bac..0000000 --- a/tests/PdfEditorApp.Tests/HelpGuideTests.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Linq; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class HelpGuideTests -{ - [Fact] - public void HelpGuideService_InitializesAllCoreCategoriesAndGuides() - { - // Arrange - var service = new HelpGuideService(); - - // Act - var guides = service.GetAllGuides(); - var categories = service.GetAllCategories(); - - // Assert - Assert.NotEmpty(guides); - Assert.True(guides.Count >= 20, $"Expected at least 20 guides, but found {guides.Count}"); - Assert.Contains("All Guides", categories); - Assert.Contains("Getting Started", categories); - Assert.Contains("32 PDF Tools", categories); - Assert.Contains("Live Editor", categories); - Assert.Contains("Automation", categories); - Assert.Contains("Language & Fonts", categories); - Assert.Contains("Shortcuts", categories); - Assert.Contains("FAQ & Support", categories); - - // Verify each guide has non-empty required fields - foreach (var guide in guides) - { - Assert.False(string.IsNullOrWhiteSpace(guide.Id), "Guide Id must not be empty"); - Assert.False(string.IsNullOrWhiteSpace(guide.Title), $"Title for {guide.Id} must not be empty"); - Assert.False(string.IsNullOrWhiteSpace(guide.Category), $"Category for {guide.Id} must not be empty"); - Assert.False(string.IsNullOrWhiteSpace(guide.Summary), $"Summary for {guide.Id} must not be empty"); - Assert.False(string.IsNullOrWhiteSpace(guide.Description), $"Description for {guide.Id} must not be empty"); - Assert.False(string.IsNullOrWhiteSpace(guide.IconKind), $"IconKind for {guide.Id} must not be empty"); - Assert.False(string.IsNullOrWhiteSpace(guide.IconColorHex), $"IconColorHex for {guide.Id} must not be empty"); - Assert.False(string.IsNullOrWhiteSpace(guide.BackgroundAccentHex), $"BackgroundAccentHex for {guide.Id} must not be empty"); - Assert.False(string.IsNullOrWhiteSpace(guide.Badge), $"Badge for {guide.Id} must not be empty"); - Assert.NotEmpty(guide.Steps); - } - } - - [Theory] - [InlineData("Getting Started")] - [InlineData("32 PDF Tools")] - [InlineData("Live Editor")] - [InlineData("Automation")] - [InlineData("Language & Fonts")] - [InlineData("Shortcuts")] - [InlineData("FAQ & Support")] - public void HelpGuideService_GetGuidesByCategory_ReturnsMatchingGuides(string category) - { - // Arrange - var service = new HelpGuideService(); - - // Act - var filtered = service.GetGuidesByCategory(category); - - // Assert - Assert.NotEmpty(filtered); - Assert.All(filtered, g => Assert.Equal(category, g.Category, ignoreCase: true)); - } - - [Fact] - public void HelpGuideService_GetGuideById_FindsSpecificTopics() - { - // Arrange - var service = new HelpGuideService(); - - // Act & Assert - var mergeGuide = service.GetGuideById("tool-merge"); - Assert.NotNull(mergeGuide); - Assert.Equal("Merge PDF Files", mergeGuide.Title); - Assert.Equal(PdfToolId.MergePdf, mergeGuide.RelatedToolId); - - var ocrGuide = service.GetGuideById("tool-ocr-pdf"); - Assert.NotNull(ocrGuide); - Assert.Equal("OCR Text Recognition", ocrGuide.Title); - Assert.Equal(PdfToolId.OcrPdf, ocrGuide.RelatedToolId); - - var mathGuide = service.GetGuideById("editor-math-latex"); - Assert.NotNull(mathGuide); - Assert.Equal("LaTeX Math Equation Studio", mathGuide.Title); - - var richTextGuide = service.GetGuideById("editor-rich-text"); - Assert.NotNull(richTextGuide); - Assert.Equal("Rich Text & Inline Markdown Formatting", richTextGuide.Title); - Assert.Equal("Live Editor", richTextGuide.Category); - Assert.True(richTextGuide.IsFeatured); - Assert.Contains("markdown", richTextGuide.Keywords, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void HelpGuideService_GetGuideByToolId_FindsAssociatedToolGuides() - { - // Arrange - var service = new HelpGuideService(); - - // Act & Assert - var compress = service.GetGuideByToolId(PdfToolId.CompressPdf); - Assert.NotNull(compress); - Assert.Equal("Compress & Optimize PDF", compress.Title); - - var sign = service.GetGuideByToolId(PdfToolId.SignPdf); - Assert.NotNull(sign); - Assert.Equal("Digital Signature Studio", sign.Title); - - var protect = service.GetGuideByToolId(PdfToolId.ProtectPdf); - Assert.NotNull(protect); - Assert.Equal("Protect PDF (AES-256 Encryption)", protect.Title); - } - - [Fact] - public void HelpGuideViewModel_SearchQuery_FiltersGuidesAcrossKeywords() - { - // Arrange - var vm = new HelpGuideViewModel(); - - // Act 1: Search "merge" - vm.SearchQuery = "merge"; - Assert.True(vm.FilteredGuides.Count > 0); - Assert.Contains(vm.FilteredGuides, g => g.Title.Contains("Merge", StringComparison.OrdinalIgnoreCase) || g.Keywords.Contains("merge", StringComparison.OrdinalIgnoreCase)); - - // Act 2: Search "latex" - vm.SearchQuery = "latex"; - Assert.True(vm.FilteredGuides.Count > 0); - Assert.Contains(vm.FilteredGuides, g => g.Id == "editor-math-latex"); - - // Act 3: Search "ocr" - vm.SearchQuery = "ocr"; - Assert.True(vm.FilteredGuides.Count > 0); - Assert.Contains(vm.FilteredGuides, g => g.Id == "tool-ocr-pdf"); - - // Act 4: Non-existent term - vm.SearchQuery = "xyznonexistentquery12345"; - Assert.Empty(vm.FilteredGuides); - Assert.True(vm.HasNoMatchingGuides); - - // Act 5: Clear search - vm.ClearSearchCommand.Execute(null); - Assert.Empty(vm.SearchQuery); - Assert.False(vm.HasNoMatchingGuides); - Assert.Equal(vm.AllGuides.Count, vm.FilteredGuides.Count); - } - - [Fact] - public void HelpGuideViewModel_CategorySelection_UpdatesFilteredList() - { - // Arrange - var vm = new HelpGuideViewModel(); - - // Act - vm.SetCategoryCommand.Execute("Live Editor"); - - // Assert - Assert.Equal("Live Editor", vm.SelectedCategory); - Assert.NotEmpty(vm.FilteredGuides); - Assert.All(vm.FilteredGuides, g => Assert.Equal("Live Editor", g.Category)); - Assert.False(vm.IsDetailViewActive); - Assert.Null(vm.SelectedGuide); - } - - [Fact] - public void HelpGuideViewModel_SelectGuideAndBackToGrid_TogglesDetailView() - { - // Arrange - var vm = new HelpGuideViewModel(); - var targetGuide = vm.AllGuides.First(); - - // Act 1: Select guide - vm.SelectGuideCommand.Execute(targetGuide); - Assert.True(vm.IsDetailViewActive); - Assert.Equal(targetGuide, vm.SelectedGuide); - - // Act 2: Back to grid - vm.BackToGridCommand.Execute(null); - Assert.False(vm.IsDetailViewActive); - Assert.Null(vm.SelectedGuide); - } - - [Fact] - public void HelpGuideViewModel_LaunchTool_FiresToolLaunchRequestedEvent() - { - // Arrange - var vm = new HelpGuideViewModel(); - var mergeGuide = vm.AllGuides.FirstOrDefault(g => g.RelatedToolId == PdfToolId.MergePdf); - Assert.NotNull(mergeGuide); - - PdfToolId? launchedToolId = null; - vm.ToolLaunchRequested += (id) => launchedToolId = id; - - // Act - vm.LaunchToolCommand.Execute(mergeGuide); - - // Assert - Assert.Equal(PdfToolId.MergePdf, launchedToolId); - } - - [Fact] - public void HomeViewModel_HelpSection_NavigationAndIntegration() - { - // Arrange - var home = new HomeViewModel(); - - // Act 1: Select Help section - home.SelectNavSectionCommand.Execute("Help"); - - // Assert 1 - Assert.Equal(HomeNavSection.Help, home.SelectedNavSection); - Assert.True(home.IsHelpSection); - Assert.NotNull(home.HelpGuide); - - // Act 2: Open specific help topic - home.OpenHelpGuideCommand.Execute("tool-compress"); - - // Assert 2 - Assert.Equal(HomeNavSection.Help, home.SelectedNavSection); - Assert.True(home.HelpGuide.IsDetailViewActive); - Assert.NotNull(home.HelpGuide.SelectedGuide); - Assert.Equal("tool-compress", home.HelpGuide.SelectedGuide.Id); - - // Act 3: Open help for specific tool - home.OpenHelpForToolCommand.Execute(PdfToolId.SignPdf); - - // Assert 3 - Assert.Equal(HomeNavSection.Help, home.SelectedNavSection); - Assert.True(home.HelpGuide.IsDetailViewActive); - Assert.NotNull(home.HelpGuide.SelectedGuide); - Assert.Equal(PdfToolId.SignPdf, home.HelpGuide.SelectedGuide.RelatedToolId); - } -} diff --git a/tests/PdfEditorApp.Tests/HomeViewModelTests.cs b/tests/PdfEditorApp.Tests/HomeViewModelTests.cs deleted file mode 100644 index a087813..0000000 --- a/tests/PdfEditorApp.Tests/HomeViewModelTests.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class HomeViewModelTests -{ - private class MockRecentService : IRecentDocumentsService - { - public List Items { get; } = new(); - - public List Load() => new(Items); - public void Add(RecentDocumentItem item) => Items.Insert(0, item); - public void Remove(string filePath) => Items.RemoveAll(x => x.FilePath == filePath); - public void Rename(string oldFilePath, string newFilePath, string newTitle) - { - var match = Items.Find(x => string.Equals(x.FilePath, oldFilePath, StringComparison.OrdinalIgnoreCase)); - if (match != null) - { - match.FilePath = newFilePath; - match.Title = newTitle; - match.LastOpened = DateTime.UtcNow; - } - } - public void Clear() => Items.Clear(); - } - - [Fact] - public void HomeViewModel_InitializesAllTemplatesWithRealisticPreviews() - { - // Arrange & Act - var vm = new HomeViewModel(); - - // Assert - Assert.NotEmpty(vm.AllTemplates); - Assert.True(vm.AllTemplates.Count >= 18, "Should have at least 18 templates initialized including new resumes and research papers"); - - // Verify each non-blank template has a live PagePreview with real elements - foreach (var template in vm.AllTemplates) - { - Assert.NotNull(template.PagePreview); - Assert.NotEmpty(template.Name); - Assert.NotEmpty(template.Category); - - if (!template.IsBlank) - { - Assert.True(template.PagePreview.Elements.Count > 0, $"Template '{template.Name}' should have real page elements in preview"); - } - } - } - - [Fact] - public void HomeViewModel_ToggleGallery_ExpandsAndCollapsesCorrectly() - { - // Arrange - var vm = new HomeViewModel(); - Assert.False(vm.IsTemplateGalleryExpanded); - - // Act & Assert 1: Toggle expands - vm.ToggleTemplateGalleryCommand.Execute(null); - Assert.True(vm.IsTemplateGalleryExpanded); - - // Act & Assert 2: Toggle collapses - vm.ToggleTemplateGalleryCommand.Execute(null); - Assert.False(vm.IsTemplateGalleryExpanded); - - // Act & Assert 3: Direct collapse command - vm.ToggleTemplateGalleryCommand.Execute(null); // expand first - Assert.True(vm.IsTemplateGalleryExpanded); - - vm.CollapseTemplateGalleryCommand.Execute(null); - Assert.False(vm.IsTemplateGalleryExpanded); - } - - [Fact] - public void HomeViewModel_CategoryFilter_FiltersTemplatesAccurately() - { - // Arrange - var vm = new HomeViewModel(); - - // Act 1: Filter Corporate - vm.SetTemplateCategoryCommand.Execute("Corporate"); - Assert.Equal("Corporate", vm.SelectedTemplateCategory); - Assert.All(vm.FilteredTemplates, t => Assert.Equal("Corporate", t.Category, ignoreCase: true)); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "annualreport"); - - // Act 2: Filter Finance - vm.SetTemplateCategoryCommand.Execute("Finance"); - Assert.Equal("Finance", vm.SelectedTemplateCategory); - Assert.All(vm.FilteredTemplates, t => Assert.Equal("Finance", t.Category, ignoreCase: true)); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "invoice"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "financeresearch"); - - // Act 3: Filter Career (All 4 Resume Types) - vm.SetTemplateCategoryCommand.Execute("Career"); - Assert.Equal("Career", vm.SelectedTemplateCategory); - Assert.All(vm.FilteredTemplates, t => Assert.Equal("Career", t.Category, ignoreCase: true)); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "resume"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "resumemodern"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "resumecreative"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "resumeacademic"); - - // Act 4: Filter Academic (All 4 Research Papers) - vm.SetTemplateCategoryCommand.Execute("Academic"); - Assert.Equal("Academic", vm.SelectedTemplateCategory); - Assert.All(vm.FilteredTemplates, t => Assert.Equal("Academic", t.Category, ignoreCase: true)); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "academic"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "mathresearch"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "physicsresearch"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "historyresearch"); - - // Act 5: Filter Certificates - vm.SetTemplateCategoryCommand.Execute("Certificates"); - Assert.All(vm.FilteredTemplates, t => Assert.Equal("Certificates", t.Category, ignoreCase: true)); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "certificate"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "certificatenavygold"); - Assert.Contains(vm.FilteredTemplates, t => t.Id == "diploma"); - - // Act 6: Reset to All - vm.SetTemplateCategoryCommand.Execute("All"); - Assert.Equal(vm.AllTemplates.Count, vm.FilteredTemplates.Count); - } - - [Fact] - public void HomeViewModel_SearchQuery_FiltersByKeywords() - { - // Arrange - var vm = new HomeViewModel(); - - // Act: Search for "quantum" - vm.SearchQuery = "quantum"; - - // Assert - Assert.Contains(vm.FilteredTemplates, t => t.Id == "physicsresearch"); - Assert.DoesNotContain(vm.FilteredTemplates, t => t.Id == "annualreport"); - - // Act 2: Search for "hodge" - vm.SearchQuery = "hodge"; - Assert.Contains(vm.FilteredTemplates, t => t.Id == "mathresearch"); - - // Act 3: Search for "mediterranean" - vm.SearchQuery = "mediterranean"; - Assert.Contains(vm.FilteredTemplates, t => t.Id == "historyresearch"); - - // Clear search - vm.ClearTemplateSearchCommand.Execute(null); - Assert.Equal("", vm.SearchQuery); - Assert.Equal(vm.AllTemplates.Count, vm.FilteredTemplates.Count); - } - - [Fact] - public void HomeViewModel_SearchWithNoMatches_SetsHasNoMatchingTemplates() - { - // Arrange - var vm = new HomeViewModel(); - - // Act - vm.SearchQuery = "NonExistentZebraTemplate12345"; - - // Assert - Assert.Equal(0, vm.MatchingTemplatesCount); - Assert.True(vm.HasNoMatchingTemplates); - } - - [Fact] - public void HomeViewModel_SelectTemplateCommand_FiresOpenTemplateRequestedEvent() - { - // Arrange - var vm = new HomeViewModel(); - string? requestedTemplate = null; - vm.OpenTemplateRequested += t => requestedTemplate = t; - - // Act - vm.SelectTemplateCommand.Execute("resumemodern"); - - // Assert - Assert.Equal("resumemodern", requestedTemplate); - } - - [Fact] - public void HomeViewModel_RecentDocuments_LoadsAndClearsCorrectly() - { - // Arrange - var mockRecent = new MockRecentService(); - mockRecent.Add(new RecentDocumentItem { Title = "Doc1.pdf", FilePath = "/path/Doc1.pdf" }); - mockRecent.Add(new RecentDocumentItem { Title = "Doc2.frypdf", FilePath = "/path/Doc2.frypdf" }); - - var vm = new HomeViewModel(mockRecent, new TemplateService(), new ProjectPersistenceService(), new PdfToolRegistry()); - - // Assert - Assert.True(vm.HasRecentDocuments); - Assert.Equal(2, vm.RecentDocuments.Count); - - // Act: Clear - vm.ClearRecentCommand.Execute(null); - - // Assert - Assert.False(vm.HasRecentDocuments); - Assert.Empty(vm.RecentDocuments); - } - - [Fact] - public void HomeViewModel_OpenToolPage_OpensFullPageWithoutDialog() - { - // Arrange - var toolRegistry = new PdfToolRegistry(); - var operationsService = new PdfDocumentOperationsService(); - var factory = new PdfToolViewModelFactory(operationsService, toolRegistry); - var vm = new HomeViewModel(new MockRecentService(), new TemplateService(), new ProjectPersistenceService(), toolRegistry, null, factory); - - Assert.False(vm.IsToolPageActive); - Assert.Null(vm.ActiveToolViewModel); - - // Act: Open OCR PDF tool - vm.OpenToolPage(PdfToolId.OcrPdf); - - // Assert: Full page is active with initialized Tool ViewModel - Assert.True(vm.IsToolPageActive); - Assert.NotNull(vm.ActiveToolViewModel); - Assert.Equal(PdfToolId.OcrPdf, vm.ActiveToolViewModel.Tool.Id); - - // Act: Go back to tools - vm.BackToToolsCommand.Execute(null); - - // Assert: Full page closed, back to home tools grid - Assert.False(vm.IsToolPageActive); - Assert.Null(vm.ActiveToolViewModel); - } -} diff --git a/tests/PdfEditorApp.Tests/LicensingPageTests.cs b/tests/PdfEditorApp.Tests/LicensingPageTests.cs deleted file mode 100644 index cc99657..0000000 --- a/tests/PdfEditorApp.Tests/LicensingPageTests.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class LicensingPageTests -{ - [Fact] - public void LicensingPage_InitializesAllThirdPartyLibraries() - { - // Arrange & Act - var vm = new HomeViewModel(); - - // Assert - Assert.NotEmpty(vm.AllLicenses); - Assert.Equal(20, vm.AllLicenses.Count); - - var expectedPackages = new[] - { - "Avalonia UI", - "QuestPDF", - "PdfPig & Skia Rendering", - "PdfSharpCore", - "SkiaSharp", - "Tabula Table Extractor", - "DocumentFormat.OpenXml", - "Material.Icons.Avalonia", - "CommunityToolkit.Mvvm", - "QRCoder", - "Microsoft.Extensions.DependencyInjection", - ".NET 10 Runtime & Base Libraries", - "LiveChartsCore & LiveChartsCore.SkiaSharpView", - "Microsoft.Extensions.Logging.Abstractions", - "SIL Open Font License (OFL 1.1) Typeface Collection", - "Roboto & Roboto Mono Font Family", - "Ubuntu Font Family", - "Microsoft.Extensions.AI & OpenAI", - "OllamaSharp", - "Tesseract OCR Engine" - }; - - foreach (var expected in expectedPackages) - { - var item = vm.AllLicenses.FirstOrDefault(l => l.Name == expected); - Assert.NotNull(item); - Assert.False(string.IsNullOrWhiteSpace(item.Version), $"Version for {expected} should not be empty"); - Assert.False(string.IsNullOrWhiteSpace(item.LicenseType), $"LicenseType for {expected} should not be empty"); - Assert.False(string.IsNullOrWhiteSpace(item.Category), $"Category for {expected} should not be empty"); - Assert.False(string.IsNullOrWhiteSpace(item.Purpose), $"Purpose for {expected} should not be empty"); - Assert.False(string.IsNullOrWhiteSpace(item.Maintainer), $"Maintainer for {expected} should not be empty"); - Assert.False(string.IsNullOrWhiteSpace(item.ProjectUrl), $"ProjectUrl for {expected} should not be empty"); - Assert.False(string.IsNullOrWhiteSpace(item.LicenseText), $"LicenseText for {expected} should not be empty"); - Assert.False(string.IsNullOrWhiteSpace(item.IconKind), $"IconKind for {expected} should not be empty"); - } - } - - [Fact] - public void LicensingPage_CategoryFilter_FiltersLibrariesAccurately() - { - // Arrange - var vm = new HomeViewModel(); - - // Act 1: Filter PDF & Document Engines - vm.SetLicenseCategoryCommand.Execute("PDF & Document Engines"); - Assert.Equal("PDF & Document Engines", vm.SelectedLicenseCategory); - Assert.Equal(3, vm.FilteredLicenses.Count); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "QuestPDF"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "PdfPig & Skia Rendering"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "PdfSharpCore"); - - // Act 2: Filter UI & Graphics Frameworks - vm.SetLicenseCategoryCommand.Execute("UI & Graphics Frameworks"); - Assert.Equal(4, vm.FilteredLicenses.Count); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Avalonia UI"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "SkiaSharp"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Material.Icons.Avalonia"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "LiveChartsCore & LiveChartsCore.SkiaSharpView"); - - // Act 3: Filter Office & Data Formats - vm.SetLicenseCategoryCommand.Execute("Office & Data Formats"); - Assert.Equal(3, vm.FilteredLicenses.Count); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Tabula Table Extractor"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "DocumentFormat.OpenXml"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "QRCoder"); - - // Act 4: Filter Architecture & Runtime - vm.SetLicenseCategoryCommand.Execute("Architecture & Runtime"); - Assert.Equal(4, vm.FilteredLicenses.Count); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "CommunityToolkit.Mvvm"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Microsoft.Extensions.DependencyInjection"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == ".NET 10 Runtime & Base Libraries"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Microsoft.Extensions.Logging.Abstractions"); - - // Act 5: Filter AI & Document Intelligence - vm.SetLicenseCategoryCommand.Execute("AI & Document Intelligence"); - Assert.Equal("AI & Document Intelligence", vm.SelectedLicenseCategory); - Assert.Equal(3, vm.FilteredLicenses.Count); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Microsoft.Extensions.AI & OpenAI"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "OllamaSharp"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Tesseract OCR Engine"); - - // Act 6: Filter Typography & Typefaces - vm.SetLicenseCategoryCommand.Execute("Typography & Typefaces"); - Assert.Equal(3, vm.FilteredLicenses.Count); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "SIL Open Font License (OFL 1.1) Typeface Collection"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Roboto & Roboto Mono Font Family"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Ubuntu Font Family"); - - // Act 7: Reset to All - vm.SetLicenseCategoryCommand.Execute("All"); - Assert.Equal(20, vm.FilteredLicenses.Count); - } - - [Fact] - public void LicensingPage_SearchQuery_FiltersLibrariesByKeywords() - { - // Arrange - var vm = new HomeViewModel(); - - // Act 1: Search by name - vm.SearchQuery = "QuestPDF"; - Assert.Single(vm.FilteredLicenses); - Assert.Equal("QuestPDF", vm.FilteredLicenses[0].Name); - - // Act 2: Search by license type - vm.SearchQuery = "Apache"; - Assert.Equal(3, vm.FilteredLicenses.Count); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "PdfPig & Skia Rendering"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Roboto & Roboto Mono Font Family"); - Assert.Contains(vm.FilteredLicenses, l => l.Name == "Tesseract OCR Engine"); - - // Act 3: Search by maintainer - vm.SearchQuery = "Microsoft"; - Assert.True(vm.FilteredLicenses.Count >= 5); - Assert.All(vm.FilteredLicenses, l => Assert.Contains("Microsoft", l.Maintainer + l.Name, StringComparison.OrdinalIgnoreCase)); - - // Act 4: Search font license - vm.SearchQuery = "OFL"; - Assert.Single(vm.FilteredLicenses); - Assert.Equal("SIL Open Font License (OFL 1.1) Typeface Collection", vm.FilteredLicenses[0].Name); - - // Act 5: Clear search - vm.ClearLicenseSearchCommand.Execute(null); - Assert.Equal("", vm.SearchQuery); - Assert.Equal("All", vm.SelectedLicenseCategory); - Assert.Equal(20, vm.FilteredLicenses.Count); - } - - [Fact] - public void LicensingPage_SearchWithNoMatches_SetsHasNoMatchingLicenses() - { - // Arrange - var vm = new HomeViewModel(); - - // Act - vm.SearchQuery = "NonExistentPackageXYZ999"; - - // Assert - Assert.Equal(0, vm.MatchingLicensesCount); - Assert.True(vm.HasNoMatchingLicenses); - } - - [Fact] - public void LicensingPage_ToggleLicenseExpand_TogglesState() - { - // Arrange - var vm = new HomeViewModel(); - var item = vm.AllLicenses.First(); - Assert.False(item.IsExpanded); - - // Act 1: Toggle on - vm.ToggleLicenseExpandCommand.Execute(item); - Assert.True(item.IsExpanded); - - // Act 2: Toggle off - vm.ToggleLicenseExpandCommand.Execute(item); - Assert.False(item.IsExpanded); - } - - [Fact] - public void LicensingPage_Navigation_SelectsLicensingSection() - { - // Arrange - var vm = new HomeViewModel(); - Assert.False(vm.IsLicensingSection); - - // Act - vm.SelectNavSectionCommand.Execute("Licensing"); - - // Assert - Assert.Equal(HomeNavSection.Licensing, vm.SelectedNavSection); - Assert.True(vm.IsLicensingSection); - } - - [Fact] - public void MainViewModel_NavigateToLicensing_SwitchesToHomeLicensingSection() - { - // Arrange - var mainVm = new MainViewModel(); - mainVm.OpenAboutDialogCommand.Execute(null); - Assert.True(mainVm.IsAboutDialogOpen); - - // Act - mainVm.NavigateToLicensingCommand.Execute(null); - - // Assert - Assert.False(mainVm.IsAboutDialogOpen); - Assert.True(mainVm.IsHomePageVisible); - Assert.False(mainVm.IsEditorVisible); - Assert.Equal(HomeNavSection.Licensing, mainVm.Home.SelectedNavSection); - Assert.True(mainVm.Home.IsLicensingSection); - } -} diff --git a/tests/PdfEditorApp.Tests/Material3ExpressiveThemeTests.cs b/tests/PdfEditorApp.Tests/Material3ExpressiveThemeTests.cs deleted file mode 100644 index 86ebbdf..0000000 --- a/tests/PdfEditorApp.Tests/Material3ExpressiveThemeTests.cs +++ /dev/null @@ -1,558 +0,0 @@ -using System; -using System.IO; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class Material3ExpressiveThemeTests -{ - private readonly string _projectRoot; - - public Material3ExpressiveThemeTests() - { - var currentDir = AppContext.BaseDirectory; - string? projectRoot = null; - var dir = new DirectoryInfo(currentDir); - while (dir != null) - { - if (File.Exists(Path.Combine(dir.FullName, "FryPDF.sln")) || Directory.Exists(Path.Combine(dir.FullName, "src", "PdfEditorApp"))) - { - projectRoot = dir.FullName; - break; - } - dir = dir.Parent; - } - - Assert.NotNull(projectRoot); - _projectRoot = projectRoot!; - } - - [Fact] - public void AppAxaml_IncludesMaterial3ExpressiveTokensAndStyles() - { - var appAxamlPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "App.axaml"); - Assert.True(File.Exists(appAxamlPath), "App.axaml must exist"); - var appAxaml = File.ReadAllText(appAxamlPath); - - Assert.Contains("Material3ExpressiveTokens.axaml", appAxaml); - Assert.Contains("Material3ExpressiveStyles.axaml", appAxaml); - } - - [Fact] - public void Material3Tokens_DefinesCompleteM3ShapeScale() - { - var tokensPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Styles", "Material3ExpressiveTokens.axaml"); - Assert.True(File.Exists(tokensPath), "Material3ExpressiveTokens.axaml must exist"); - var tokens = File.ReadAllText(tokensPath); - - // M3 Expressive shape scale tokens - Assert.Contains("CornerRadius x:Key=\"M3ShapeCornerNone\">0", tokens); - Assert.Contains("CornerRadius x:Key=\"M3ShapeCornerExtraSmall\">4", tokens); - Assert.Contains("CornerRadius x:Key=\"M3ShapeCornerSmall\">8", tokens); - Assert.Contains("CornerRadius x:Key=\"M3ShapeCornerMedium\">12", tokens); - Assert.Contains("CornerRadius x:Key=\"M3ShapeCornerLarge\">16", tokens); - Assert.Contains("CornerRadius x:Key=\"M3ShapeCornerExtraLarge\">28", tokens); - Assert.Contains("CornerRadius x:Key=\"M3ShapeCornerFull\">9999", tokens); - } - - [Fact] - public void Material3Tokens_DefinesElevationLevelsAndChubbySliderMetrics() - { - var tokensPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Styles", "Material3ExpressiveTokens.axaml"); - var tokens = File.ReadAllText(tokensPath); - - // M3 Elevation shadows - Assert.Contains("BoxShadows x:Key=\"M3ElevationLevel0\"", tokens); - Assert.Contains("BoxShadows x:Key=\"M3ElevationLevel1\"", tokens); - Assert.Contains("BoxShadows x:Key=\"M3ElevationLevel2\"", tokens); - Assert.Contains("BoxShadows x:Key=\"M3ElevationLevel3\"", tokens); - Assert.Contains("BoxShadows x:Key=\"M3ElevationLevel4\"", tokens); - Assert.Contains("BoxShadows x:Key=\"M3ElevationLevel5\"", tokens); - Assert.Contains("BoxShadows x:Key=\"M3ElevationModalDialog\"", tokens); - - // Chubby tactile slider tokens - Assert.Contains("SliderTrackThemeHeight", tokens); - Assert.Contains("SliderHorizontalThumbWidth", tokens); - Assert.Contains("SliderThumbTactileRadius", tokens); - } - - [Fact] - public void Material3Tokens_DefinesLightAndDarkExpressivePalettesWithBackwardCompatibleAliases() - { - var tokensPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Styles", "Material3ExpressiveTokens.axaml"); - var tokens = File.ReadAllText(tokensPath); - - // M3 Core Color Roles - Assert.Contains("M3PrimaryBrush", tokens); - Assert.Contains("M3OnPrimaryBrush", tokens); - Assert.Contains("M3PrimaryContainerBrush", tokens); - Assert.Contains("M3OnPrimaryContainerBrush", tokens); - Assert.Contains("M3SecondaryBrush", tokens); - Assert.Contains("M3SecondaryContainerBrush", tokens); - Assert.Contains("M3TertiaryBrush", tokens); - Assert.Contains("M3SurfaceBrush", tokens); - Assert.Contains("M3SurfaceContainerBrush", tokens); - Assert.Contains("M3SurfaceContainerHighBrush", tokens); - Assert.Contains("M3OutlineBrush", tokens); - Assert.Contains("M3OutlineVariantBrush", tokens); - - // Backward compatibility brush aliases for existing FluentOffice views - Assert.Contains("WinBgBrush", tokens); - Assert.Contains("WinPanelBrush", tokens); - Assert.Contains("WinBorderBrush", tokens); - Assert.Contains("WinAccentBrush", tokens); - Assert.Contains("WinTextBrush", tokens); - Assert.Contains("WinMutedBrush", tokens); - Assert.Contains("WinHoverBrush", tokens); - Assert.Contains("WinActiveBrush", tokens); - Assert.Contains("WinInputBgBrush", tokens); - } - - [Fact] - public void Material3Styles_DefinesExpressiveComponentClasses() - { - var stylesPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Styles", "Material3ExpressiveStyles.axaml"); - Assert.True(File.Exists(stylesPath), "Material3ExpressiveStyles.axaml must exist"); - var styles = File.ReadAllText(stylesPath); - - // M3 Expressive Buttons - Assert.Contains("Button.m3-filled-btn", styles); - Assert.Contains("Button.m3-tonal-btn", styles); - Assert.Contains("Button.m3-elevated-btn", styles); - Assert.Contains("Button.m3-outlined-btn", styles); - Assert.Contains("Button.m3-text-btn", styles); - Assert.Contains("Button.m3-fab", styles); - Assert.Contains("Button.m3-fab-extended", styles); - Assert.Contains("Button.m3-icon-btn", styles); - - // M3 Segmented Capsules & Chips - Assert.Contains("Border.m3-segmented-container", styles); - Assert.Contains("RadioButton.m3-segment-btn", styles); - Assert.Contains("Button.m3-chip", styles); - Assert.Contains("Button.m3-preset-chip", styles); - Assert.Contains("ToggleButton.m3-filter-chip", styles); - - // M3 Expressive Cards & Containers - Assert.Contains("Border.m3-card-elevated", styles); - Assert.Contains("Border.m3-card-filled", styles); - Assert.Contains("Border.m3-card-outlined", styles); - Assert.Contains("Border.m3-dialog-card", styles); - Assert.Contains("Border.m3-floating-hud", styles); - - // M3 Text Inputs - Assert.Contains("TextBox.m3-outlined", styles); - Assert.Contains("TextBox.m3-search", styles); - } - - [Fact] - public void Material3Styles_DefinesCardActionButtonWrapperStyles_SuppressingPointerOverPresenterBackground() - { - var stylesPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Styles", "Material3ExpressiveStyles.axaml"); - var styles = File.ReadAllText(stylesPath); - - // Verify button wrapper selectors exist - Assert.Contains("Button.card-action-wrapper", styles); - Assert.Contains("Button.pdf-tool-btn", styles); - Assert.Contains("Button.template-card-btn", styles); - Assert.Contains("Button.workflow-banner-btn", styles); - Assert.Contains("Button.card-btn", styles); - - // Verify template pointerover and pressed content presenter backgrounds are suppressed - Assert.Contains(":pointerover /template/ ContentPresenter#PART_ContentPresenter", styles); - Assert.Contains(":pressed /template/ ContentPresenter#PART_ContentPresenter", styles); - - // Verify ClipToBounds is disabled to prevent upward hover translation clipping - Assert.Contains("Property=\"ClipToBounds\" Value=\"False\"", styles); - - // Verify vertical headroom padding is provided on button wrappers to absorb translateY(-2px) - Assert.Contains("Property=\"Padding\" Value=\"0,3,0,1\"", styles); - } - - [Fact] - public void Views_FollowMaterial3ExpressiveCardHierarchy_WithoutStiffHexShadows() - { - var toolsStudioPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "PdfToolsStudioView.axaml"); - var starredPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "StarredToolsPageView.axaml"); - - var toolsStudio = File.ReadAllText(toolsStudioPath); - var starred = File.ReadAllText(starredPath); - - // Assert M3 elevation and corner tokens are used - Assert.Contains("StaticResource M3ElevationLevel1", toolsStudio); - Assert.Contains("StaticResource M3ElevationLevel2", toolsStudio); - Assert.Contains("StaticResource M3ShapeCornerLarge", toolsStudio); - - Assert.Contains("StaticResource M3ElevationLevel1", starred); - Assert.Contains("StaticResource M3ElevationLevel2", starred); - Assert.Contains("StaticResource M3ShapeCornerLarge", starred); - - // Assert old stiff/hardcoded shadows and radii are removed - Assert.DoesNotContain("0 2 6 #08000000", toolsStudio); - Assert.DoesNotContain("0 8 20 #150F6CBD", toolsStudio); - Assert.DoesNotContain("CornerRadius=\"10\"", toolsStudio); - - Assert.DoesNotContain("0 2 6 #08000000", starred); - Assert.DoesNotContain("0 8 20 #150F6CBD", starred); - Assert.DoesNotContain("CornerRadius=\"10\"", starred); - } - - [Fact] - public void InspectorSidebarView_TypographyStudioCard_AdheresToMaterial3Expressive() - { - var inspectorPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "InspectorSidebarView.axaml"); - var inspector = File.ReadAllText(inspectorPath); - - // Verify M3 card container properties on Typography Studio - Assert.Contains("Classes=\"m3-preset-chip\"", inspector); - Assert.Contains("M3SurfaceContainerLowestBrush", inspector); - Assert.Contains("M3PrimaryContainerBrush", inspector); - Assert.Contains("M3OutlineVariantBrush", inspector); - - // Verify presets use proper vector MaterialIcons rather than emojis/unicode - Assert.Contains("Kind=\"Waves\"", inspector); - Assert.Contains("Kind=\"VectorCurve\"", inspector); - Assert.Contains("Kind=\"Arch\"", inspector); - Assert.Contains("Kind=\"TrendingDown\"", inspector); - Assert.Contains("Kind=\"TrendingUp\"", inspector); - Assert.Contains("Kind=\"ArrowUpBoldOutline\"", inspector); - Assert.Contains("Kind=\"ArrowDownBoldOutline\"", inspector); - Assert.Contains("Kind=\"DecagramOutline\"", inspector); - Assert.Contains("Kind=\"CircleHalfFull\"", inspector); - Assert.Contains("Kind=\"BorderColor\"", inspector); - Assert.Contains("Kind=\"LayersOutline\"", inspector); - Assert.Contains("Kind=\"Creation\"", inspector); - Assert.Contains("Kind=\"FormatLetterCase\"", inspector); - - // Verify old typography preset buttons with emojis and math unicode symbols are removed - Assert.DoesNotContain("CommandParameter=\"wave\">= 112px (114px) to ensure group titles are never clipped by scrollbars - Assert.Contains("Height=\"114\"", ribbon); - - // 2. Uses sleek ribbon-scroll-viewer class - Assert.Contains("Classes=\"ribbon-scroll-viewer\"", ribbon); - - // 3. FluentOfficeStyles defines sleek M3 horizontal scrollbar for ribbon - var stylesPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Styles", "FluentOfficeStyles.axaml"); - var styles = File.ReadAllText(stylesPath); - Assert.Contains("ScrollViewer.ribbon-scroll-viewer /template/ ScrollBar:horizontal", styles); - } - - [Fact] - public void InspectorSidebarView_ExpandableSectionHeaders_UseM3SectionToggleWithoutWidthClipping() - { - // 1. Material3ExpressiveStyles defines m3-section-toggle with stretch alignment and no fixed width - var m3StylesPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Styles", "Material3ExpressiveStyles.axaml"); - var m3Styles = File.ReadAllText(m3StylesPath); - Assert.Contains("Style Selector=\"Button.m3-section-toggle\"", m3Styles); - Assert.Contains("Property=\"HorizontalAlignment\" Value=\"Stretch\"", m3Styles); - Assert.Contains("Property=\"HorizontalContentAlignment\" Value=\"Stretch\"", m3Styles); - - // 2. InspectorSidebarView uses m3-section-toggle for expandable headers - var inspectorPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "InspectorSidebarView.axaml"); - var inspector = File.ReadAllText(inspectorPath); - Assert.Contains("Button Classes=\"m3-section-toggle\"", inspector); - Assert.DoesNotContain("Button Classes=\"mini-tool-btn\" HorizontalAlignment=\"Stretch\"", inspector); - } - - [Fact] - public void InspectorSidebarView_LayerOrderAndQuickActions_UsesSegmentedCapsuleAndProportionalButtons() - { - var inspectorPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "InspectorSidebarView.axaml"); - var inspector = File.ReadAllText(inspectorPath); - - // 1. Layer order is contained within an M3 segmented container capsule with 4 segment buttons - Assert.Contains("Border Grid.Column=\"0\" Classes=\"m3-segmented-container\" HorizontalAlignment=\"Stretch\" Padding=\"2\"", inspector); - Assert.Contains("Command=\"{Binding BringToFrontCommand}\"", inspector); - Assert.Contains("Command=\"{Binding BringForwardCommand}\"", inspector); - Assert.Contains("Command=\"{Binding SendBackwardCommand}\"", inspector); - Assert.Contains("Command=\"{Binding SendToBackCommand}\"", inspector); - - // 2. Quick Actions are proportional 32x32 circular buttons avoiding vertical oval distortion - Assert.Contains("Button Classes=\"m3-tonal-btn\" Width=\"32\" Height=\"32\" Padding=\"0\"", inspector); - Assert.Contains("Button Classes=\"danger-btn\" Width=\"32\" Height=\"32\" Padding=\"0\"", inspector); - Assert.Contains("Command=\"{Binding DuplicateSelectedElementCommand}\"", inspector); - Assert.Contains("Command=\"{Binding DeleteSelectedElementCommand}\"", inspector); - - // 3. The old distorted 6-column unsegmented grid row is completely eliminated - Assert.DoesNotContain("\n ", fluentStyles); - - // 3. MainWindow brand button and document title use m3-header-btn - var mainPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "MainWindow.axaml"); - var main = File.ReadAllText(mainPath); - Assert.Contains("Button Classes=\"m3-header-btn\"", main); - Assert.Contains("TextBlock Text=\"FryPDF\"", main); - Assert.Contains("TextBlock Text=\"by CodeFryDev\"", main); - Assert.Contains("TextBlock Text=\"{Binding DocumentTitle}\"", main); - - // 4. PdfViewerView uses m3-header-btn for rename document title - var viewerPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "PdfViewerView.axaml"); - var viewer = File.ReadAllText(viewerPath); - Assert.Contains("Button Classes=\"m3-header-btn\"", viewer); - } - - [Fact] - public void PluginsDialog_AdheresToMaterial3Expressive() - { - var pluginsPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "Dialogs", "PluginsDialog.axaml"); - var xaml = File.ReadAllText(pluginsPath); - - // 1. Dialog Shell uses M3 Extra Large shape, dedicated M3ElevationModalDialog elevation, and M3 Surface brush - Assert.Contains("CornerRadius=\"{StaticResource M3ShapeCornerExtraLarge}\"", xaml); - Assert.Contains("BoxShadow=\"{StaticResource M3ElevationModalDialog}\"", xaml); - Assert.Contains("Background=\"{DynamicResource M3SurfaceBrush}\"", xaml); - - // 2. Elimination of hardcoded dark navy hex colors and raw hex box shadows - Assert.DoesNotContain("Background=\"#0F172A\"", xaml); - Assert.DoesNotContain("Background=\"#1E1B4B\"", xaml); - Assert.DoesNotContain("BorderBrush=\"#1E293B\"", xaml); - Assert.DoesNotContain("BorderBrush=\"#818CF8\"", xaml); - Assert.DoesNotContain("BoxShadow=\"0 28 80 #50000000\"", xaml); - - // 3. Header uses dynamic M3 tokens, tonal badge, and m3-icon-btn close - Assert.Contains("Background=\"{DynamicResource M3SurfaceContainerLowBrush}\"", xaml); - Assert.Contains("Background=\"{DynamicResource M3PrimaryContainerBrush}\"", xaml); - Assert.Contains("Background=\"{DynamicResource M3SecondaryContainerBrush}\"", xaml); - Assert.Contains("Button Grid.Column=\"2\"\n Classes=\"m3-icon-btn\"", xaml); - - // 4. Search bar uses m3-search and Profile selector uses M3 segmented container - Assert.Contains("Classes=\"m3-search\"", xaml); - Assert.Contains("Classes=\"m3-segmented-container\"", xaml); - Assert.Contains("Button Classes=\"m3-segment-btn\"", xaml); - - // 5. Plugin cards use elevated M3 container, tonal icon container, and m3-tonal-btn - Assert.Contains("Background=\"{DynamicResource M3SurfaceContainerLowestBrush}\"", xaml); - Assert.Contains("BoxShadow=\"{StaticResource M3ElevationLevel1}\"", xaml); - Assert.Contains("CornerRadius=\"{StaticResource M3ShapeCornerLarge}\"", xaml); - Assert.Contains("Classes=\"m3-tonal-btn\"", xaml); - - // 6. Footer bar uses tonal background, m3-tonal-btn, and m3-filled-btn - Assert.Contains("Classes=\"m3-filled-btn\"", xaml); - } - - [Fact] - public void AboutDialog_AdheresToMaterial3Expressive() - { - var aboutPath = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "Dialogs", "AboutDialog.axaml"); - var xaml = File.ReadAllText(aboutPath); - - // 1. Dialog Shell uses M3 Extra Large shape, dedicated M3ElevationModalDialog elevation, and M3 Surface brush - Assert.Contains("CornerRadius=\"{StaticResource M3ShapeCornerExtraLarge}\"", xaml); - Assert.Contains("BoxShadow=\"{StaticResource M3ElevationModalDialog}\"", xaml); - Assert.Contains("Background=\"{DynamicResource M3SurfaceBrush}\"", xaml); - - // 2. Elimination of harsh raw box shadows and hardcoded dark navy header - Assert.DoesNotContain("BoxShadow=\"0 28 80 #50000000\"", xaml); - Assert.DoesNotContain("BoxShadow=\"0 4 16 #250D2B45\"", xaml); - Assert.DoesNotContain("Background=\"#0B1222\"", xaml); - Assert.DoesNotContain("BorderBrush=\"#1E293B\"", xaml); - - // 3. Inner 28px clipping shell preserves outer shadow without clipping - Assert.Contains("", xaml); - - // 4. Content cards use M3 ElevationLevel1 and M3 shape tokens - Assert.Contains("BoxShadow=\"{StaticResource M3ElevationLevel1}\"", xaml); - Assert.Contains("BoxShadow=\"{StaticResource M3ElevationLevel2}\"", xaml); - Assert.Contains("CornerRadius=\"{StaticResource M3ShapeCornerMedium}\"", xaml); - Assert.Contains("CornerRadius=\"{StaticResource M3ShapeCornerLarge}\"", xaml); - - // 5. Interactive controls use M3 button classes and pill geometry - Assert.Contains("Classes=\"m3-icon-btn\"", xaml); - Assert.Contains("Classes=\"m3-tonal-btn\"", xaml); - Assert.Contains("Classes=\"m3-filled-btn\"", xaml); - } - - [Fact] - public void ModalDialogCards_UseM3ElevationModalDialog_AndDoNotClipOuterShadow() - { - var dialogFiles = new[] - { - "AboutDialog.axaml", - "PluginsDialog.axaml", - "CommandPaletteDialog.axaml", - "ShortcutsHelpDialog.axaml", - "WorkflowBuilderDialog.axaml", - "PdfToolRunnerDialog.axaml", - "BatchGenerationDialog.axaml", - "DeleteConfirmationDialog.axaml", - "CompareDocumentsDialog.axaml", - "SignatureStudioDialog.axaml", - "PreflightDiagnosticsDialog.axaml", - "CustomStampDialog.axaml", - "DataStudioDialog.axaml", - "MathEquationStudioDialog.axaml" - }; - - foreach (var file in dialogFiles) - { - var path = Path.Combine(_projectRoot, "src", "PdfEditorApp", "Views", "Dialogs", file); - var xaml = File.ReadAllText(path); - - Assert.Contains("BoxShadow=\"{StaticResource M3ElevationModalDialog}\"", xaml); - Assert.Contains("CornerRadius=\"{StaticResource M3ShapeCornerExtraLarge}\"", xaml); - } - } -} - - diff --git a/tests/PdfEditorApp.Tests/MathEngineTests.cs b/tests/PdfEditorApp.Tests/MathEngineTests.cs deleted file mode 100644 index 63611fb..0000000 --- a/tests/PdfEditorApp.Tests/MathEngineTests.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using Xunit; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.MathEngine; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Tests; - -public class MathEngineTests -{ - [Fact] - public void Tokenizer_Should_Tokenize_Basic_Formula() - { - string latex = @"x^2 + \frac{1}{2} = 0"; - var tokens = MathLayoutEngine.Tokenize(latex); - - Assert.NotEmpty(tokens); - Assert.Contains(tokens, t => t.Type == MathLayoutEngine.TokenType.Command && t.Value == "frac"); - Assert.Contains(tokens, t => t.Type == MathLayoutEngine.TokenType.Superscript); - Assert.Contains(tokens, t => t.Type == MathLayoutEngine.TokenType.Operator && t.Value == "+"); - } - - [Fact] - public void Tokenizer_Should_Tokenize_Greek_And_Special_Operators() - { - string latex = @"\int_{-\infty}^{\infty} e^{-\alpha x^2} \, dx = \sqrt{\frac{\pi}{\alpha}}"; - var tokens = MathLayoutEngine.Tokenize(latex); - - Assert.Contains(tokens, t => t.Type == MathLayoutEngine.TokenType.Command && t.Value == "int"); - Assert.Contains(tokens, t => t.Type == MathLayoutEngine.TokenType.Command && t.Value == "alpha"); - Assert.Contains(tokens, t => t.Type == MathLayoutEngine.TokenType.Command && t.Value == "sqrt"); - Assert.Contains(tokens, t => t.Type == MathLayoutEngine.TokenType.Command && t.Value == "pi"); - Assert.Contains(tokens, t => t.Type == MathLayoutEngine.TokenType.Command && t.Value == "infty"); - } - - [Fact] - public void Parser_Should_Parse_Fractions_And_Radicals() - { - string latex = @"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"; - var tokens = MathLayoutEngine.Tokenize(latex); - var parser = new MathLayoutEngine.MathParser(tokens); - var ast = parser.ParseExpression(); - - Assert.NotNull(ast); - var ctx = new MathLayoutEngine.LayoutContext(16, "#000000", MathDisplayStyle.DisplayBlock); - ast.Measure(ctx); - - Assert.True(ast.Width > 0); - Assert.True(ast.Height > 0); - } - - [Fact] - public void Parser_Should_Parse_Large_Operators_With_Limits() - { - string latex = @"\sum_{i=1}^{n} i^2 = \frac{n(n+1)(2n+1)}{6}"; - var tokens = MathLayoutEngine.Tokenize(latex); - var parser = new MathLayoutEngine.MathParser(tokens); - var ast = parser.ParseExpression(); - - Assert.NotNull(ast); - var ctx = new MathLayoutEngine.LayoutContext(16, "#000000", MathDisplayStyle.DisplayBlock); - ast.Measure(ctx); - - Assert.True(ast.Width > 20); - Assert.True(ast.Height > 20); - } - - [Fact] - public void Parser_Should_Parse_Matrices_And_Cases() - { - string matrixLatex = @"\begin{pmatrix} a & b \\ c & d \end{pmatrix}"; - var tokens = MathLayoutEngine.Tokenize(matrixLatex); - var parser = new MathLayoutEngine.MathParser(tokens); - var ast = parser.ParseExpression(); - - Assert.NotNull(ast); - var ctx = new MathLayoutEngine.LayoutContext(16, "#000000", MathDisplayStyle.DisplayBlock); - ast.Measure(ctx); - - Assert.True(ast.Width > 20); - Assert.True(ast.Height > 20); - } - - [Fact] - public void RenderToSvg_Should_Generate_Valid_Svg() - { - string latex = @"\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}"; - var options = new MathRenderOptions - { - FontSize = 16, - TextColorHex = "#0F172A", - ShowEquationNumber = true, - EquationNumber = "(1.1)", - Alignment = TextAlignmentMode.Center - }; - - string svg = MathLayoutEngine.RenderToSvg(latex, options).SvgMarkup; - - Assert.NotNull(svg); - Assert.StartsWith("", svg); - Assert.Contains("viewBox=", svg); - Assert.Contains("(1.1)", svg); - } - - [Fact] - public void PresetsLibrary_All_Presets_Should_Be_Valid_And_Renderable() - { - var presets = MathPresetsLibrary.GetAllPresets(); - Assert.True(presets.Count >= 25, "Expected at least 25 standard mathematical presets"); - - foreach (var preset in presets) - { - Assert.False(string.IsNullOrWhiteSpace(preset.Id), $"Preset ID should not be empty for {preset.Name}"); - Assert.False(string.IsNullOrWhiteSpace(preset.Name), $"Preset Name should not be empty for {preset.Id}"); - Assert.False(string.IsNullOrWhiteSpace(preset.Formula), $"Formula should not be empty for {preset.Id}"); - - // Verify Svg rendering works without throwing - string svg = MathLayoutEngine.RenderToSvg(preset.Formula, new MathRenderOptions { FontSize = 14 }).SvgMarkup; - Assert.NotNull(svg); - Assert.Contains(" Assert.Equal(MathCategory.Calculus, p.Category)); - - var physics = MathPresetsLibrary.GetByCategory(MathCategory.Physics); - Assert.NotEmpty(physics); - - var finance = MathPresetsLibrary.GetByCategory(MathCategory.Finance); - Assert.NotEmpty(finance); - } - - [Fact] - public void PdfMathElement_Polymorphic_Json_Serialization_Should_RoundTrip() - { - var mathEl = new PdfMathElement - { - Id = "math-101", - X = 120, - Y = 240, - Width = 350, - Height = 65, - Formula = @"e^{i\pi} + 1 = 0", - FontSize = 18, - TextColorHex = "#1E293B", - BackgroundColorHex = "#F8FAFC", - BorderColorHex = "#E2E8F0", - BorderThickness = 1.0, - CornerRadius = 6, - ShowBackground = true, - ShowBorder = true, - ShowEquationNumber = true, - EquationNumber = "(Euler)", - Category = MathCategory.Algebra, - PresetName = "Euler's Identity" - }; - - var page = new PdfPageModel - { - PageNumber = 1, - Elements = { mathEl } - }; - - var doc = new PdfDocumentModel - { - Title = "Math Test Doc", - Pages = { page } - }; - - string json = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true }); - Assert.Contains("\"$type\": \"math\"", json); - Assert.Contains("Euler", json); - - var deserializedDoc = JsonSerializer.Deserialize(json); - Assert.NotNull(deserializedDoc); - Assert.Single(deserializedDoc.Pages[0].Elements); - - var loadedEl = deserializedDoc.Pages[0].Elements[0] as PdfMathElement; - Assert.NotNull(loadedEl); - Assert.Equal(ElementKind.Math, loadedEl.Kind); - Assert.Equal(@"e^{i\pi} + 1 = 0", loadedEl.Formula); - Assert.Equal(18, loadedEl.FontSize); - Assert.Equal("(Euler)", loadedEl.EquationNumber); - Assert.Equal(MathCategory.Algebra, loadedEl.Category); - Assert.Equal("Euler's Identity", loadedEl.PresetName); - } - - [Fact] - public void MathElementViewModel_Should_Sync_With_Model_And_RenderSvg() - { - var vm = new MathElementViewModel(); - vm.Formula = @"\frac{d}{dx}\left[ \int_{a}^{x} f(t) \, dt \right] = f(x)"; - vm.FontSize = 14; - vm.ShowEquationNumber = true; - vm.EquationNumber = "(FTC)"; - vm.RenderSvg(); - - Assert.NotNull(vm.SvgSource); - Assert.Contains(" 1000, $"Exported PDF should have non-trivial size (actual: {fileInfo.Length} bytes)"); - - // Check PDF header - byte[] header = new byte[5]; - using (var fs = File.OpenRead(tempFile)) - { - fs.ReadExactly(header, 0, 5); - } - string headerStr = System.Text.Encoding.ASCII.GetString(header); - Assert.Equal("%PDF-", headerStr); - } - finally - { - if (File.Exists(tempFile)) - { - try { File.Delete(tempFile); } catch { } - } - } - } -} diff --git a/tests/PdfEditorApp.Tests/MicrokernelPillarsTests.cs b/tests/PdfEditorApp.Tests/MicrokernelPillarsTests.cs deleted file mode 100644 index b32ec29..0000000 --- a/tests/PdfEditorApp.Tests/MicrokernelPillarsTests.cs +++ /dev/null @@ -1,287 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Services; -using PdfEditorApp.Services.AI; -using PdfEditorApp.Services.Canvas; -using PdfEditorApp.Services.Export; -using PdfEditorApp.Services.Import; -using PdfEditorApp.Services.Inspector; -using PdfEditorApp.Services.Ocr; -using PdfEditorApp.Services.Palette; -using PdfEditorApp.Services.StatusBar; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class MicrokernelPillarsTests -{ - private (PluginHost host, FryPluginContext context, ServiceProvider sp) CreateTestEnvironment() - { - var services = new ServiceCollection(); - - // Registries - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - - services.AddSingleton(); - - var sp = services.BuildServiceProvider(); - var context = new FryPluginContext(sp); - var host = new PluginHost(context); - - return (host, context, sp); - } - - [Fact] - public async Task CanvasElementsBundle_RegistersAllCanvasElements() - { - var (host, context, sp) = CreateTestEnvironment(); - var bundle = new CanvasElementsBundle(); - - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var elements = context.GetRegisteredCanvasElements(); - Assert.NotEmpty(elements); - - var elementIds = elements.Select(e => e.ElementTypeId).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.Contains("frypdf.element.text", elementIds); - Assert.Contains("frypdf.element.shape", elementIds); - Assert.Contains("frypdf.element.image", elementIds); - Assert.Contains("frypdf.element.table", elementIds); - Assert.Contains("frypdf.element.chart", elementIds); - Assert.Contains("frypdf.element.math", elementIds); - Assert.Contains("frypdf.element.formfield", elementIds); - Assert.Contains("frypdf.element.watermark", elementIds); - Assert.Contains("frypdf.element.redaction", elementIds); - Assert.Contains("frypdf.element.stickynote", elementIds); - Assert.Contains("frypdf.element.ink", elementIds); - Assert.Contains("frypdf.element.measurement", elementIds); - } - - [Fact] - public async Task DocumentIoBundle_RegistersImportersAndExporters() - { - var (host, context, sp) = CreateTestEnvironment(); - var bundle = new DocumentIoBundle(); - - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var importers = context.GetRegisteredImporters(); - Assert.NotEmpty(importers); - - var importerExtensions = importers.SelectMany(i => i.SupportedExtensions).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.Contains(".pdf", importerExtensions); - Assert.Contains(".png", importerExtensions); - Assert.Contains(".md", importerExtensions); - - var exporters = context.GetRegisteredExporters(); - Assert.NotEmpty(exporters); - - var exporterIds = exporters.Select(e => e.ExporterId).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.Contains("frypdf.exporter.pdf", exporterIds); - Assert.Contains("frypdf.exporter.markdown", exporterIds); - Assert.Contains("frypdf.exporter.html", exporterIds); - Assert.Contains("frypdf.exporter.text", exporterIds); - Assert.Contains("frypdf.exporter.svg", exporterIds); - } - - [Fact] - public async Task AiProvidersBundle_RegistersAllProviders() - { - var (host, context, sp) = CreateTestEnvironment(); - var bundle = new AiProvidersBundle(); - - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var aiRegistry = sp.GetRequiredService(); - var providers = aiRegistry.GetAllProviders(); - Assert.NotEmpty(providers); - - var providerIds = providers.Select(p => p.ProviderId).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.Contains("ollama", providerIds); - Assert.Contains("groq", providerIds); - Assert.Contains("openai", providerIds); - } - - [Fact] - public async Task OcrEnginesBundle_RegistersAllOcrEngines() - { - var (host, context, sp) = CreateTestEnvironment(); - var bundle = new OcrEnginesBundle(); - - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var ocrRegistry = sp.GetRequiredService(); - var engines = ocrRegistry.GetAllEngines(); - Assert.NotEmpty(engines); - - var engineNames = engines.Select(e => e.EngineName).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.Contains("Apple Vision OCR (macOS)", engineNames); - Assert.Contains("Windows Media OCR", engineNames); - Assert.Contains("Tesseract OCR", engineNames); - } - - [Fact] - public async Task StandardTemplatesBundle_RegistersAll28Templates() - { - var (host, context, sp) = CreateTestEnvironment(); - var bundle = new StandardTemplatesBundle(); - - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var templateService = sp.GetRequiredService(); - var templates = templateService.GetAllTemplates(); - Assert.True(templates.Count >= 28, $"Expected >= 28 templates, got {templates.Count}"); - - var categories = templates.Select(t => t.Category).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); - Assert.Contains("Corporate", categories); - Assert.Contains("Finance", categories); - Assert.Contains("Career", categories); - Assert.Contains("Academic", categories); - Assert.Contains("Certificates", categories); - Assert.Contains("Education", categories); - } - - [Fact] - public async Task StatusBarBundle_RegistersWidgets() - { - var (host, context, sp) = CreateTestEnvironment(); - var bundle = new StatusBarBundle(); - - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var widgets = context.GetRegisteredStatusBarWidgets(); - Assert.NotEmpty(widgets); - - var widgetIds = widgets.Select(w => w.WidgetId).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.Contains("frypdf.status.pagestats", widgetIds); - Assert.Contains("frypdf.status.ready", widgetIds); - Assert.Contains("frypdf.status.memory", widgetIds); - } - - [Fact] - public async Task InspectorBundle_RegistersInspectorSections() - { - var (host, context, sp) = CreateTestEnvironment(); - var bundle = new InspectorBundle(); - - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var sections = context.GetRegisteredInspectorSections(); - Assert.NotEmpty(sections); - - var sectionIds = sections.Select(s => s.SectionId).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.Contains("frypdf.inspector.geometry", sectionIds); - Assert.Contains("frypdf.inspector.appearance", sectionIds); - Assert.Contains("frypdf.inspector.typography", sectionIds); - Assert.Contains("frypdf.inspector.table", sectionIds); - Assert.Contains("frypdf.inspector.chart", sectionIds); - } - - [Fact] - public async Task CommandPaletteBundle_RegistersCommands() - { - var (host, context, sp) = CreateTestEnvironment(); - var bundle = new CommandPaletteBundle(); - - host.RegisterPlugins(bundle.Plugins); - await host.StartAsync(); - - var commands = context.GetRegisteredCommands(); - Assert.NotEmpty(commands); - - var categories = commands.Select(c => c.Category).Distinct().ToList(); - Assert.Contains("File", categories); - Assert.Contains("Edit", categories); - Assert.Contains("Insert", categories); - Assert.Contains("View", categories); - Assert.Contains("Security", categories); - } - - [Fact] - public void DesktopProfile_ContainsAll16Bundles() - { - var profilePath = Path.Combine(AppContext.BaseDirectory, "profiles", "desktop.profile.json"); - if (!File.Exists(profilePath)) - { - // Fallback to source tree path if not copied to bin - profilePath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "profiles", "desktop.profile.json")); - } - - Assert.True(File.Exists(profilePath), $"Profile not found at {profilePath}"); - var profile = ProfileLoader.LoadFromFile(profilePath); - - Assert.Equal("desktop", profile.ProfileName); - Assert.Equal(16, profile.Bundles.Count); - Assert.Contains("FryPdf.Bundle.CanvasElements", profile.Bundles); - Assert.Contains("FryPdf.Bundle.DocumentIo", profile.Bundles); - Assert.Contains("FryPdf.Bundle.AiProviders", profile.Bundles); - Assert.Contains("FryPdf.Bundle.OcrEngines", profile.Bundles); - Assert.Contains("FryPdf.Bundle.Templates", profile.Bundles); - Assert.Contains("FryPdf.Bundle.StatusBar", profile.Bundles); - Assert.Contains("FryPdf.Bundle.Inspector", profile.Bundles); - Assert.Contains("FryPdf.Bundle.CommandPalette", profile.Bundles); - Assert.Contains("FryPdf.Bundle.WorkspacePages", profile.Bundles); - Assert.Contains("FryPdf.Bundle.Dialogs", profile.Bundles); - Assert.Contains("FryPdf.Bundle.EditorSidebars", profile.Bundles); - } - - [Fact] - public void HeadlessProfile_LoadsHeadlessBundles() - { - var profilePath = Path.Combine(AppContext.BaseDirectory, "profiles", "headless.profile.json"); - if (!File.Exists(profilePath)) - { - profilePath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "profiles", "headless.profile.json")); - } - - Assert.True(File.Exists(profilePath), $"Profile not found at {profilePath}"); - var profile = ProfileLoader.LoadFromFile(profilePath); - - Assert.Equal("headless", profile.ProfileName); - Assert.Contains("FryPdf.Bundle.Tools.Organize", profile.Bundles); - Assert.Contains("FryPdf.Bundle.DocumentIo", profile.Bundles); - Assert.DoesNotContain("FryPdf.Bundle.StatusBar", profile.Bundles); - } -} diff --git a/tests/PdfEditorApp.Tests/Mocks/MockRecentDocumentsService.cs b/tests/PdfEditorApp.Tests/Mocks/MockRecentDocumentsService.cs deleted file mode 100644 index ad52080..0000000 --- a/tests/PdfEditorApp.Tests/Mocks/MockRecentDocumentsService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using PdfEditorApp.Models; -using PdfEditorApp.Services; - -namespace PdfEditorApp.Tests.Mocks; - -/// -/// In-memory mock implementation of for isolated unit tests. -/// Prevents unit tests from reading or writing to the user's real application data folder. -/// -public class MockRecentDocumentsService : IRecentDocumentsService -{ - public List Items { get; } = new(); - - public List Load() => new(Items); - - public void Add(RecentDocumentItem item) - { - Items.RemoveAll(x => string.Equals(x.FilePath, item.FilePath, System.StringComparison.OrdinalIgnoreCase)); - Items.Insert(0, item); - } - - public void Remove(string filePath) => - Items.RemoveAll(x => string.Equals(x.FilePath, filePath, System.StringComparison.OrdinalIgnoreCase)); - - public void Rename(string oldFilePath, string newFilePath, string newTitle) - { - var match = Items.Find(x => string.Equals(x.FilePath, oldFilePath, System.StringComparison.OrdinalIgnoreCase)); - if (match != null) - { - match.FilePath = newFilePath; - match.Title = newTitle; - match.LastOpened = System.DateTime.UtcNow; - } - } - - public void Clear() => Items.Clear(); -} diff --git a/tests/PdfEditorApp.Tests/OcrEngineTests.cs b/tests/PdfEditorApp.Tests/OcrEngineTests.cs deleted file mode 100644 index cd47728..0000000 --- a/tests/PdfEditorApp.Tests/OcrEngineTests.cs +++ /dev/null @@ -1,315 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Services.Ocr; -using PdfEditorApp.ViewModels; -using UglyToad.PdfPig; -using Xunit; -using Xunit.Abstractions; - -namespace PdfEditorApp.Tests; - -[Collection("OcrTests")] -public class OcrEngineTests -{ - private readonly ITestOutputHelper _output; - - public OcrEngineTests(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void TesseractModelService_ListsCatalog_AndDetectsLocalFiles() - { - var service = new TesseractModelService(); - Assert.NotEmpty(service.AvailableLanguages); - - var eng = service.AvailableLanguages.FirstOrDefault(l => l.Code == "eng"); - Assert.NotNull(eng); - Assert.Equal("English", eng.DisplayName); - Assert.Contains("eng.traineddata", eng.DownloadUrl); - } - - [Fact] - public void CompositeOcrProvider_InitializesAvailableEngines() - { - var provider = new CompositeOcrProvider(); - Assert.NotNull(provider.ActiveEngine); - Assert.NotEmpty(provider.AvailableEngines); - - if (OperatingSystem.IsMacOS()) - { - Assert.Contains(provider.AvailableEngines, e => e.EngineType == OcrEngineType.OsNative); - Assert.Equal("Apple Vision OCR (macOS)", provider.ActiveEngine.EngineName); - } - } - - [Fact] - public async Task AppleVisionOcr_RecognizesText_WhenOnMacOS() - { - if (!OperatingSystem.IsMacOS()) return; - - string? pdfPath = FindTestPdfPath(); - if (pdfPath == null || !File.Exists(pdfPath)) - { - _output.WriteLine("PDF not found, skipping."); - return; - } - - // Render page 1 to PNG bytes using PdfViewerViewModel - var vm = new PdfViewerViewModel(); - await vm.LoadDocumentAsync(pdfPath); - - byte[]? pngBytes = vm.RenderPageBytesAtScale(1, 1.0f); - Assert.NotNull(pngBytes); - Assert.NotEmpty(pngBytes); - - var engine = new AppleVisionOcrEngine(); - Assert.True(engine.IsAvailable); - - var result = await engine.RecognizeTextAsync(pngBytes, "eng"); - Assert.True(result.Success, $"OCR error: {result.ErrorMessage}"); - Assert.NotEmpty(result.Words); - Assert.NotEmpty(result.Lines); - Assert.False(string.IsNullOrWhiteSpace(result.FullText)); - - _output.WriteLine($"Recognized {result.Words.Count} words and {result.Lines.Count} lines in {result.DurationMs} ms."); - _output.WriteLine($"Sample line: {result.Lines[0].Text}"); - _output.WriteLine($"Sample word bounds: {result.Words[0].Text} -> [{result.Words[0].NormalizedBounds.X:F3}, {result.Words[0].NormalizedBounds.Y:F3}]"); - } - - [Fact] - public async Task PdfViewer_ScannedDocument_AutomaticallyPopulatesWordsViaOcr() - { - if (!OperatingSystem.IsMacOS()) return; - - string? pdfPath = FindTestPdfPath(); - if (pdfPath == null || !File.Exists(pdfPath)) return; - - var vm = new PdfViewerViewModel(); - await vm.LoadDocumentAsync(pdfPath); - - Assert.Single(vm.Pages); - var page = vm.Pages[0]; - - _output.WriteLine($"Page dimensions: {page.WidthPoints}x{page.HeightPoints}, CurrentPdfBytes length: {vm.CurrentPdfBytes?.Length}"); - - var (ocrTxt, ocrW, ocrL) = vm.ExtractOcrPageTextGeometry(1, page.WidthPoints, page.HeightPoints); - _output.WriteLine($"Direct ExtractOcrPageTextGeometry: words={ocrW.Count}, textLength={ocrTxt.Length}"); - - // Ensure geometry runs - vm.EnsurePageGeometry(page); - - // Wait up to 3 seconds for async OCR extraction - for (int i = 0; i < 30 && page.Words.Count == 0; i++) - { - await Task.Delay(100); - } - - _output.WriteLine($"After EnsurePageGeometry: page.Words.Count={page.Words.Count}, IsGeometryLoading={page.IsGeometryLoading}"); - Assert.True(page.Words.Count > 100, $"Expected > 100 words extracted via OCR, but got {page.Words.Count}"); - Assert.NotEmpty(page.TextLines); - Assert.False(string.IsNullOrWhiteSpace(page.ExtractedText)); - - // Verify word bounds are valid in page coordinate space - var firstWord = page.Words[0]; - Assert.True(firstWord.Bounds.Width > 0); - Assert.True(firstWord.Bounds.Height > 0); - Assert.True(firstWord.Bounds.Left >= 0 && firstWord.Bounds.Left <= page.WidthPoints); - Assert.True(firstWord.Bounds.Top >= 0 && firstWord.Bounds.Top <= page.HeightPoints); - - // Verify word selection works (just as PdfTextOverlayControl would do) - page.SelectWord(firstWord); - Assert.True(page.HasSelection); - Assert.Equal(firstWord.Text, page.SelectedText); - Assert.Single(page.SelectionRects); - } - - [Fact] - public async Task PdfOcrService_CreatesSearchablePdf_FromScannedDocument() - { - if (!OperatingSystem.IsMacOS()) return; - - string? pdfPath = FindTestPdfPath(); - if (pdfPath == null || !File.Exists(pdfPath)) return; - - string tempOutPdf = Path.Combine(Path.GetTempPath(), $"Searchable_Test_{Guid.NewGuid():N}.pdf"); - - try - { - var ocrService = new PdfOcrService(); - var options = new Models.OcrToolOptions - { - InputFilePath = pdfPath, - OutputFilePath = tempOutPdf, - GenerateSearchablePdf = true - }; - - var result = await ocrService.OcrPdfAsync(options); - Assert.True(result.Success, $"OCR failed: {result.ErrorMessage}"); - Assert.True(File.Exists(tempOutPdf)); - Assert.True(new FileInfo(tempOutPdf).Length > 1000); - - // Open the resulting PDF with PdfPig and verify it now contains actual vector words! - using var pigDoc = UglyToad.PdfPig.PdfDocument.Open(tempOutPdf); - Assert.Single(pigDoc.GetPage(1).Letters.Take(1)); // Has letters - var words = pigDoc.GetPage(1).GetWords().ToList(); - Assert.True(words.Count > 100, $"Expected searchable PDF to have > 100 words, but got {words.Count}"); - } - finally - { - try { if (File.Exists(tempOutPdf)) File.Delete(tempOutPdf); } catch { } - } - } - - [Fact] - public async Task PdfOcrService_CreatesTextFileAndSearchablePdf_WhenBothRequested() - { - if (!OperatingSystem.IsMacOS()) return; - - string? pdfPath = FindTestPdfPath(); - if (pdfPath == null || !File.Exists(pdfPath)) return; - - string tempOutPdf = Path.Combine(Path.GetTempPath(), $"Searchable_Test_{Guid.NewGuid():N}.pdf"); - string expectedTxt = Path.ChangeExtension(tempOutPdf, ".txt"); - - try - { - var ocrService = new PdfOcrService(); - var options = new Models.OcrToolOptions - { - InputFilePath = pdfPath, - OutputFilePath = tempOutPdf, - GenerateSearchablePdf = true, - GenerateTextFile = true - }; - - var result = await ocrService.OcrPdfAsync(options); - Assert.True(result.Success, $"OCR failed: {result.ErrorMessage}"); - Assert.True(File.Exists(tempOutPdf), "Searchable PDF should exist"); - Assert.True(File.Exists(expectedTxt), "Extracted text file (.txt) should exist"); - Assert.Equal(2, result.OutputFiles.Count); - - string textContent = await File.ReadAllTextAsync(expectedTxt); - Assert.True(textContent.Length > 500, "Text content should contain recognized OCR text"); - Assert.Contains("Arduino", textContent, StringComparison.OrdinalIgnoreCase); - } - finally - { - try { if (File.Exists(tempOutPdf)) File.Delete(tempOutPdf); } catch { } - try { if (File.Exists(expectedTxt)) File.Delete(expectedTxt); } catch { } - } - } - - [Fact] - public async Task OcrPdfToolViewModel_SideBySide_ExtractsPageTextAndUpdatesStats() - { - if (!OperatingSystem.IsMacOS()) return; - - string? pdfPath = FindTestPdfPath(); - if (pdfPath == null || !File.Exists(pdfPath)) return; - - var ops = new PdfEditorApp.Services.PdfDocumentOperationsService(); - var toolDef = new PdfToolRegistry().GetTool(Models.PdfToolId.OcrPdf)!; - - var vm = new OcrPdfToolViewModel(ops, toolDef); - Assert.True(vm.IsSideBySideVisible, "Side-by-side panel should be visible by default"); - Assert.True(vm.GenerateTextFile, "GenerateTextFile should be enabled by default"); - - vm.SelectedFiles.Add(pdfPath); - - // Extract Page 1 text - await vm.ExtractTextForPageAsync(1); - for (int i = 0; i < 30 && (string.IsNullOrWhiteSpace(vm.CurrentPageExtractedText) || vm.IsExtractingPageText); i++) - { - await Task.Delay(100); - } - - _output.WriteLine($"CurrentPageExtractedText length={vm.CurrentPageExtractedText?.Length}: '{vm.CurrentPageExtractedText?.Substring(0, Math.Min(60, vm.CurrentPageExtractedText.Length))}'"); - - Assert.False(string.IsNullOrWhiteSpace(vm.CurrentPageExtractedText), $"CurrentPageExtractedText should be populated, got: '{vm.CurrentPageExtractedText}'"); - Assert.Contains("Arduino", vm.CurrentPageExtractedText, StringComparison.OrdinalIgnoreCase); - Assert.NotEqual("0 words", vm.WordCountText); - Assert.NotEqual("0 chars", vm.CharCountText); - - // Test active text update - vm.ActiveDisplayedText = "Test modified text for side-by-side"; - Assert.Equal("Test modified text for side-by-side", vm.CurrentPageExtractedText); - } - - [Fact] - public async Task PdfLivePreviewViewModel_LoadsTextDocument_SetsTextPropertiesAndHasDocument() - { - string tempTxt = Path.Combine(Path.GetTempPath(), $"Preview_Test_{Guid.NewGuid():N}.txt"); - try - { - await File.WriteAllTextAsync(tempTxt, "Line 1: Arduino Programming\nLine 2: Digital I/O Pins\nLine 3: Serial Communication"); - - var preview = new PdfEditorApp.ViewModels.Shell.PdfLivePreviewViewModel(); - Assert.False(preview.HasDocument); - - await preview.LoadDocumentAsync(tempTxt); - - Assert.True(preview.HasDocument, "HasDocument should be true for text files"); - Assert.True(preview.IsTextDocument, "IsTextDocument should be true for text files"); - Assert.Equal(3, preview.TextDocumentLinesCount); - Assert.Equal(13, preview.TextDocumentWordsCount); - Assert.Contains("Digital I/O Pins", preview.TextDocumentContent); - Assert.Equal("3 lines", preview.PageIndicatorText); - } - finally - { - try { if (File.Exists(tempTxt)) File.Delete(tempTxt); } catch { } - } - } - - [Fact] - public async Task OcrPdfToolViewModel_PreviewTextFile_SwitchesPreviewToTextDocument() - { - var ops = new PdfEditorApp.Services.PdfDocumentOperationsService(); - var toolDef = new PdfToolRegistry().GetTool(Models.PdfToolId.OcrPdf)!; - var vm = new OcrPdfToolViewModel(ops, toolDef); - - string tempTxt = Path.Combine(Path.GetTempPath(), $"Ocr_Preview_Test_{Guid.NewGuid():N}.txt"); - try - { - await File.WriteAllTextAsync(tempTxt, "Sample extracted OCR text from scanned card."); - vm.LastGeneratedTextFilePath = tempTxt; - - await vm.PreviewTextFileAsync(); - - Assert.True(vm.Preview.HasDocument); - Assert.True(vm.Preview.IsTextDocument); - Assert.Contains("Sample extracted OCR text", vm.Preview.TextDocumentContent); - } - finally - { - try { if (File.Exists(tempTxt)) File.Delete(tempTxt); } catch { } - } - } - - private static string? FindTestPdfPath() - { - var dir = new DirectoryInfo(AppContext.BaseDirectory); - while (dir != null) - { - string candidate = Path.Combine(dir.FullName, "Arduino Cheat Sheet_Redacted_Redacted.pdf"); - if (File.Exists(candidate)) return candidate; - dir = dir.Parent; - } - return null; - } -} diff --git a/tests/PdfEditorApp.Tests/OverlayExtensibilityTests.cs b/tests/PdfEditorApp.Tests/OverlayExtensibilityTests.cs deleted file mode 100644 index 1fb5f55..0000000 --- a/tests/PdfEditorApp.Tests/OverlayExtensibilityTests.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Marketplace; -using PdfEditorApp.Plugins.Scratchpad; -using PdfEditorApp.Plugins.Snake; -using PdfEditorApp.Plugins.Telemetry; -using PdfEditorApp.Services.Overlays; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class OverlayExtensibilityTests -{ - [Fact] - public void OverlayDescriptor_SupportsMultipleChromeModes() - { - var standardDesc = new OverlayDescriptor - { - Id = "test.standard", - Title = "Standard Overlay", - ChromeMode = OverlayChromeMode.StandardCard - }; - Assert.False(standardDesc.HasCustomChrome); - - var customDesc = new OverlayDescriptor - { - Id = "test.custom", - Title = "Custom Overlay", - ChromeMode = OverlayChromeMode.CustomChrome - }; - Assert.True(customDesc.HasCustomChrome); - - var pillDesc = new OverlayDescriptor - { - Id = "test.pill", - Title = "Pill Overlay", - ChromeMode = OverlayChromeMode.FloatingPill - }; - Assert.Equal(OverlayChromeMode.FloatingPill, pillDesc.ChromeMode); - } - - [Fact] - public void BringToFront_SetsHighestZIndex_AcrossMultipleActiveOverlays() - { - var desc1 = new OverlayDescriptor { Id = "overlay.1", Title = "Card 1" }; - var desc2 = new OverlayDescriptor { Id = "overlay.2", Title = "Card 2" }; - var desc3 = new OverlayDescriptor { Id = "overlay.3", Title = "Card 3" }; - - var inst1 = new OverlayInstanceViewModel(desc1) { ZIndex = 1 }; - var inst2 = new OverlayInstanceViewModel(desc2) { ZIndex = 2 }; - var inst3 = new OverlayInstanceViewModel(desc3) { ZIndex = 3 }; - - var all = new[] { inst1, inst2, inst3 }; - - // Bring inst1 to front - inst1.BringToFront(all); - Assert.True(inst1.ZIndex > inst2.ZIndex); - Assert.True(inst1.ZIndex > inst3.ZIndex); - Assert.Equal(4, inst1.ZIndex); - - // Bring inst2 to front - inst2.BringToFront(all); - Assert.Equal(5, inst2.ZIndex); - Assert.True(inst2.ZIndex > inst1.ZIndex); - } - - [Fact] - public void FileInstalledPluginStore_PersistsAndLoadsRecords_Cleanly() - { - var tempFile = Path.Combine(Path.GetTempPath(), $"test_installed_{Guid.NewGuid():N}.json"); - try - { - var store = new FileInstalledPluginStore(tempFile); - Assert.Empty(store.GetAll()); - Assert.False(store.IsInstalled("test.plugin")); - - var record = new InstalledPluginRecord - { - PluginId = "test.plugin", - Name = "Test Plugin", - Version = "2.1.0", - IsEnabled = true, - WasOverlayOpen = true - }; - - store.AddOrUpdate(record); - Assert.True(store.IsInstalled("test.plugin")); - Assert.Single(store.GetAll()); - - // Reload from new instance pointing to same file - var store2 = new FileInstalledPluginStore(tempFile); - Assert.True(store2.IsInstalled("test.plugin")); - var loaded = store2.Get("test.plugin"); - Assert.NotNull(loaded); - Assert.Equal("Test Plugin", loaded.Name); - Assert.Equal("2.1.0", loaded.Version); - Assert.True(loaded.WasOverlayOpen); - - // Remove - store2.Remove("test.plugin"); - Assert.False(store2.IsInstalled("test.plugin")); - - // Verify persistence of removal - var store3 = new FileInstalledPluginStore(tempFile); - Assert.False(store3.IsInstalled("test.plugin")); - } - finally - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - } - } - - [Fact] - public async Task CompanionOverlays_CanInstallAndOperateSimultaneously() - { - var tempDir = Path.Combine(AppContext.BaseDirectory, $"frypdf_companion_test_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - try - { - var services = new ServiceCollection(); - App.ConfigureServices(services); - var tempFile = Path.Combine(tempDir, "installed_plugins.json"); - services.AddSingleton(new FileInstalledPluginStore(tempFile)); - services.AddSingleton(sp => - { - var host = sp.GetRequiredService(); - var overlay = sp.GetService(); - var store = sp.GetRequiredService(); - var pluginsDir = Path.Combine(tempDir, "plugins"); - return new PdfEditorApp.Services.Plugins.PluginMarketplaceService(host, overlay, store, null, null, pluginsDir); - }); - var sp = services.BuildServiceProvider(); - - var host = sp.GetRequiredService(); - var overlayReg = sp.GetRequiredService(); - var marketplace = sp.GetRequiredService(); - - await host.StartAsync(); - - // Install Scratchpad - bool installed1 = await marketplace.InstallPluginAsync("frypdf.overlay.scratchpad"); - Assert.True(installed1); - Assert.True(host.IsPluginActive("frypdf.overlay.scratchpad")); - Assert.True(overlayReg.IsOverlayVisible("frypdf.overlay.scratchpad")); - - // Install Telemetry - bool installed2 = await marketplace.InstallPluginAsync("frypdf.overlay.telemetry"); - Assert.True(installed2); - Assert.True(host.IsPluginActive("frypdf.overlay.telemetry")); - Assert.True(overlayReg.IsOverlayVisible("frypdf.overlay.telemetry")); - - // Both are active simultaneously - Assert.Equal(2, overlayReg.ActiveOverlays.Count); - Assert.Contains(overlayReg.ActiveOverlays, o => o.Id == "frypdf.overlay.scratchpad"); - Assert.Contains(overlayReg.ActiveOverlays, o => o.Id == "frypdf.overlay.telemetry"); - - // Verify StandardCard chrome on both - var scratchInst = overlayReg.ActiveOverlays.First(o => o.Id == "frypdf.overlay.scratchpad"); - var telemInst = overlayReg.ActiveOverlays.First(o => o.Id == "frypdf.overlay.telemetry"); - - Assert.True(scratchInst.HasStandardChrome); - Assert.True(telemInst.HasStandardChrome); - - // Test Pin and Minimize commands - scratchInst.TogglePin(); - Assert.True(scratchInst.IsPinned); - - scratchInst.ToggleMinimize(); - Assert.True(scratchInst.IsMinimized); - - await host.StopAsync(); - } - finally - { - try { Directory.Delete(tempDir, recursive: true); } catch { } - } - } - - [Fact] - public void ScratchpadViewModel_TracksCounts_AndAddsTimestamps() - { - var vm = new ScratchpadViewModel - { - NotesText = "Hello World PDF" - }; - - Assert.Equal(3, vm.WordCount); - Assert.Equal(15, vm.CharacterCount); - - vm.AddTimestamp(); - Assert.Contains("Note (", vm.NotesText); - Assert.True(vm.WordCount > 3); - - vm.ClearNotes(); - Assert.Equal(0, vm.WordCount); - Assert.Equal(0, vm.CharacterCount); - } - - [Fact] - public void DocumentTelemetryViewModel_RefreshesMetrics_AndTrimsHeap() - { - var vm = new DocumentTelemetryViewModel(); - Assert.False(string.IsNullOrWhiteSpace(vm.MemoryAllocatedMb)); - Assert.True(vm.GarbageCollections >= 0); - - vm.RunGCTrim(); - Assert.False(string.IsNullOrWhiteSpace(vm.MemoryAllocatedMb)); - } -} diff --git a/tests/PdfEditorApp.Tests/PageOrganizerServiceTests.cs b/tests/PdfEditorApp.Tests/PageOrganizerServiceTests.cs deleted file mode 100644 index b17a374..0000000 --- a/tests/PdfEditorApp.Tests/PageOrganizerServiceTests.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PageOrganizerServiceTests -{ - private static PdfDocumentModel CreateTestDoc(int pageCount) - { - var doc = new PdfDocumentModel - { - Title = "Master_Contract.pdf", - Author = "Legal Department", - Subject = "Multi-party Agreement" - }; - - for (int i = 1; i <= pageCount; i++) - { - var page = new PdfPageModel - { - PageNumber = i, - Width = 800, - Height = 1131, - RotationAngle = 0 - }; - page.Elements.Add(new PdfTextElement - { - Text = $"Clause {i}.0 Content", - X = 50, - Y = 50, - Width = 200, - Height = 30 - }); - doc.Pages.Add(page); - } - - return doc; - } - - [Fact] - public void PageOrganizerService_SplitEveryNPages_SplitsCorrectly() - { - var service = new PageOrganizerService(); - var doc = CreateTestDoc(7); - - // Act: Split every 3 pages - var parts = service.SplitEveryNPages(doc, 3); - - // Assert: 3 parts (3 + 3 + 1) - Assert.Equal(3, parts.Count); - Assert.Equal(3, parts[0].Pages.Count); - Assert.Equal(3, parts[1].Pages.Count); - Assert.Single(parts[2].Pages); - - // Verify page renumbering in each split - Assert.Equal(1, parts[0].Pages[0].PageNumber); - Assert.Equal(2, parts[0].Pages[1].PageNumber); - Assert.Equal(3, parts[0].Pages[2].PageNumber); - - Assert.Equal(1, parts[1].Pages[0].PageNumber); - Assert.Equal(2, parts[1].Pages[1].PageNumber); - Assert.Equal(3, parts[1].Pages[2].PageNumber); - - Assert.Equal(1, parts[2].Pages[0].PageNumber); - - // Verify content preserved - Assert.Equal("Clause 1.0 Content", ((PdfTextElement)parts[0].Pages[0].Elements[0]).Text); - Assert.Equal("Clause 7.0 Content", ((PdfTextElement)parts[2].Pages[0].Elements[0]).Text); - } - - [Fact] - public void PageOrganizerService_SplitByRanges_PartitionsRanges() - { - var service = new PageOrganizerService(); - var doc = CreateTestDoc(6); - - // Act: Split by "1-2, 3-4, 5-6" - var parts = service.SplitByRanges(doc, "1-2, 3-4, 5-6"); - - // Assert - Assert.Equal(3, parts.Count); - Assert.All(parts, p => Assert.Equal(2, p.Pages.Count)); - Assert.Equal("Clause 3.0 Content", ((PdfTextElement)parts[1].Pages[0].Elements[0]).Text); - Assert.Equal("Clause 4.0 Content", ((PdfTextElement)parts[1].Pages[1].Elements[0]).Text); - } - - [Fact] - public void PageOrganizerService_ExtractPages_ExtractsSubset() - { - var service = new PageOrganizerService(); - var doc = CreateTestDoc(5); - - // Act: Extract page indexes 0, 2, 4 (Pages 1, 3, 5) - var extracted = service.ExtractPages(doc, new[] { 0, 2, 4 }); - - // Assert - Assert.Equal(3, extracted.Pages.Count); - Assert.Equal(1, extracted.Pages[0].PageNumber); - Assert.Equal(2, extracted.Pages[1].PageNumber); - Assert.Equal(3, extracted.Pages[2].PageNumber); - Assert.Equal("Clause 1.0 Content", ((PdfTextElement)extracted.Pages[0].Elements[0]).Text); - Assert.Equal("Clause 3.0 Content", ((PdfTextElement)extracted.Pages[1].Elements[0]).Text); - Assert.Equal("Clause 5.0 Content", ((PdfTextElement)extracted.Pages[2].Elements[0]).Text); - } - - [Fact] - public void PageOrganizerService_MergeDocuments_CombinesPagesAndRenumbers() - { - var service = new PageOrganizerService(); - var doc1 = CreateTestDoc(2); - var doc2 = CreateTestDoc(3); - - // Act - var merged = service.MergeDocuments(new[] { doc1, doc2 }, "Merged_Master.pdf"); - - // Assert - Assert.Equal(5, merged.Pages.Count); - for (int i = 0; i < 5; i++) - { - Assert.Equal(i + 1, merged.Pages[i].PageNumber); - } - Assert.Equal("Clause 1.0 Content", ((PdfTextElement)merged.Pages[0].Elements[0]).Text); - Assert.Equal("Clause 1.0 Content", ((PdfTextElement)merged.Pages[2].Elements[0]).Text); // from doc2 - } - - [Fact] - public void PageOrganizerService_BatchRotatePages_RotatesTargetPages() - { - var service = new PageOrganizerService(); - var doc = CreateTestDoc(4); - - // Act: Rotate even pages by 90 - int count = service.BatchRotatePages(doc, PageFilterTarget.EvenPages, 90); - - // Assert - Assert.Equal(2, count); - Assert.Equal(0, doc.Pages[0].RotationAngle); // Page 1 (odd) - Assert.Equal(90, doc.Pages[1].RotationAngle); // Page 2 (even) - Assert.Equal(0, doc.Pages[2].RotationAngle); // Page 3 (odd) - Assert.Equal(90, doc.Pages[3].RotationAngle); // Page 4 (even) - } - - [Fact] - public void PageOrganizerService_ParsePageRanges_ParsesComplexStrings() - { - var service = new PageOrganizerService(); - - var indexes = service.ParsePageRanges("1-3, 5, 7-8", 10); - Assert.Equal(new[] { 0, 1, 2, 4, 6, 7 }, indexes); - - var clamped = service.ParsePageRanges("1-20", 5); - Assert.Equal(new[] { 0, 1, 2, 3, 4 }, clamped); - } - - [Fact] - public void MainViewModel_ReorderPage_MovesAndMaintainsUndo() - { - // Arrange - var vm = new MainViewModel(); - while (vm.Pages.Count < 3) vm.AddPage(); - - var firstPage = vm.Pages[0]; - var secondPage = vm.Pages[1]; - var thirdPage = vm.Pages[2]; - - // Act - Move Page 1 (index 0) to Page 3 (index 2) - vm.ReorderPage(0, 2); - - // Assert - Order is now [secondPage, thirdPage, firstPage] - Assert.Same(secondPage, vm.Pages[0]); - Assert.Same(thirdPage, vm.Pages[1]); - Assert.Same(firstPage, vm.Pages[2]); - Assert.Equal(1, vm.Pages[0].PageNumber); - Assert.Equal(2, vm.Pages[1].PageNumber); - Assert.Equal(3, vm.Pages[2].PageNumber); - - // Act - Undo reorder - vm.UndoRedo.Undo(); - Assert.Same(firstPage, vm.Pages[0]); - Assert.Same(secondPage, vm.Pages[1]); - Assert.Same(thirdPage, vm.Pages[2]); - - // Act - Redo reorder - vm.UndoRedo.Redo(); - Assert.Same(secondPage, vm.Pages[0]); - Assert.Same(thirdPage, vm.Pages[1]); - Assert.Same(firstPage, vm.Pages[2]); - } -} diff --git a/tests/PdfEditorApp.Tests/PdfDeconstructionEngineTests.cs b/tests/PdfEditorApp.Tests/PdfDeconstructionEngineTests.cs deleted file mode 100644 index bcaf6d1..0000000 --- a/tests/PdfEditorApp.Tests/PdfDeconstructionEngineTests.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Microsoft.Extensions.Logging.Abstractions; -using SkiaSharp; -using PdfEditorApp.Core.Deconstruction; -using PdfEditorApp.Core.Deconstruction.Extractors; -using PdfEditorApp.Core.Deconstruction.Utils; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PdfDeconstructionEngineTests -{ - [Fact] - public void PdfImageElement_RawImageData_DoesNotAllocateBase64UntilAccessed() - { - byte[] dummyBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x01, 0x02 }; - var element = new PdfImageElement - { - ImageData = dummyBytes - }; - - // ImageData is set directly - Assert.NotNull(element.ImageData); - Assert.Equal(dummyBytes.Length, element.ImageData.Length); - - // Accessing Base64Data computes lazily - string? base64 = element.Base64Data; - Assert.NotNull(base64); - Assert.Equal(Convert.ToBase64String(dummyBytes), base64); - - // Cloning preserves ImageData - var clone = (PdfImageElement)element.Clone(); - Assert.NotNull(clone.ImageData); - Assert.Equal(dummyBytes, clone.ImageData); - } - - [Fact] - public void ImageElementViewModel_PrioritizesRawImageData_DirectBitmapLoad() - { - // 1. Create a 4x4 PNG in memory - using var bmp = new SKBitmap(4, 4, SKColorType.Rgba8888, SKAlphaType.Opaque); - using var canvas = new SKCanvas(bmp); - canvas.Clear(SKColors.CornflowerBlue); - using var img = SKImage.FromBitmap(bmp); - using var pngData = img.Encode(SKEncodedImageFormat.Png, 100); - byte[] pngBytes = pngData.ToArray(); - - var vm = new ImageElementViewModel(); - vm.ImageData = pngBytes; - - Assert.NotNull(vm.ImageData); - Assert.Equal(pngBytes.Length, vm.ImageData.Length); - - // Model roundtrip preserves raw bytes - var model = (PdfImageElement)vm.ToModel(); - Assert.NotNull(model.ImageData); - Assert.Equal(pngBytes.Length, model.ImageData.Length); - } - - [Fact] - public void ColorContrastHelper_CalculatesRelativeLuminanceAndContrastAccurately() - { - double whiteLum = ColorContrastHelper.GetRelativeLuminance("#FFFFFF"); - double blackLum = ColorContrastHelper.GetRelativeLuminance("#000000"); - - Assert.True(whiteLum > 0.99); - Assert.True(blackLum < 0.01); - - double maxContrast = ColorContrastHelper.GetContrastRatio("#000000", "#FFFFFF"); - Assert.True(maxContrast >= 20.0); // 21:1 ideal - - double lowContrast = ColorContrastHelper.GetContrastRatio("#FEFEFE", "#FFFFFF"); - Assert.True(lowContrast < 1.1); - } - - [Theory] - [InlineData("#FFFFFF", "#FFFFFF", "#0F172A")] // White on white -> flip to dark - [InlineData("#FEFEFE", "#FFFFFF", "#0F172A")] // Near white on white -> flip to dark - [InlineData("#FFFFFF", "#8B0000", "#FFFFFF")] // White on dark red -> keep white (contrast > 4.5) - [InlineData("#FFFFFF", "#0B192C", "#FFFFFF")] // White on dark navy -> keep white - [InlineData("#0F172A", "#0B192C", "#FFFFFF")] // Dark slate on dark navy -> flip to white - [InlineData("#000000", "#FFD700", "#000000")] // Black on gold -> keep black (high contrast) - public void ColorContrastHelper_EnsureLegibleContrast_HandlesArbitraryBackgroundColors( - string textHex, string bgHex, string expectedFinalColor) - { - string result = ColorContrastHelper.EnsureLegibleContrast(textHex, bgHex, minContrastRatio: 3.0); - Assert.Equal(expectedFinalColor, result); - } - - [Fact] - public void CmykColorConverter_ConvertsCmykToCalibratedRgbAccurately() - { - // 1. Pure Cyan (C=255, M=0, Y=0, K=0) -> R=0, G=255, B=255 - CmykColorConverter.ConvertCmykToRgb(255, 0, 0, 0, out byte rC, out byte gC, out byte bC); - Assert.Equal(0, rC); - Assert.Equal(255, gC); - Assert.Equal(255, bC); - - // 2. Pure Magenta (C=0, M=255, Y=0, K=0) -> R=255, G=0, B=255 - CmykColorConverter.ConvertCmykToRgb(0, 255, 0, 0, out byte rM, out byte gM, out byte bM); - Assert.Equal(255, rM); - Assert.Equal(0, gM); - Assert.Equal(255, bM); - - // 3. Pure Yellow (C=0, M=0, Y=255, K=0) -> R=255, G=255, B=0 - CmykColorConverter.ConvertCmykToRgb(0, 0, 255, 0, out byte rY, out byte gY, out byte bY); - Assert.Equal(255, rY); - Assert.Equal(255, gY); - Assert.Equal(0, bY); - - // 4. Pure Black (K=255) -> R=0, G=0, B=0 - CmykColorConverter.ConvertCmykToRgb(0, 0, 0, 255, out byte rK, out byte gK, out byte bK); - Assert.Equal(0, rK); - Assert.Equal(0, gK); - Assert.Equal(0, bK); - } - - [Fact] - public void PdfDeconstructionOptions_CustomThresholds_CanBeCustomizedAndPassed() - { - var options = new PdfDeconstructionOptions - { - PureScannedImageCoverageThreshold = 0.90, - MaxVectorShapesPerPage = 150, - GroupExcessVectorsAsSvg = true, - WatermarkOpacity = 0.20, - HighContrastDarkTextColor = "#1E293B" - }; - - Assert.Equal(0.90, options.PureScannedImageCoverageThreshold); - Assert.Equal(150, options.MaxVectorShapesPerPage); - Assert.True(options.GroupExcessVectorsAsSvg); - Assert.Equal(0.20, options.WatermarkOpacity); - Assert.Equal("#1E293B", options.HighContrastDarkTextColor); - } - - [Fact] - public void PdfTextExtractor_GroupContainersAndText_AssignsSharedGroupIdToContainedText() - { - var shape = new PdfShapeElement - { - X = 50, - Y = 50, - Width = 200, - Height = 100, - FillColorHex = "#F1F5F9" - }; - - var textInside = new PdfTextElement - { - X = 60, - Y = 60, - Width = 100, - Height = 20, - Text = "Inside Card" - }; - - var textOutside = new PdfTextElement - { - X = 350, - Y = 350, - Width = 100, - Height = 20, - Text = "Outside Card" - }; - - var shapes = new List { shape }; - var texts = new List { textInside, textOutside }; - - PdfTextExtractor.GroupContainersAndText(shapes, texts, PdfDeconstructionOptions.Default); - - Assert.NotNull(shape.GroupId); - Assert.Equal(shape.GroupId, textInside.GroupId); - Assert.Null(textOutside.GroupId); - } - - [Theory] - [InlineData(595.28, 841.89, PageFormat.A4)] - [InlineData(612.0, 792.0, PageFormat.Letter)] - [InlineData(612.0, 1008.0, PageFormat.Legal)] - [InlineData(841.89, 1190.55, PageFormat.A3)] - [InlineData(419.53, 595.28, PageFormat.A5)] - public void DeterminePageFormat_AccuratelyIdentifiesStandardPageFormats(double width, double height, PageFormat expected) - { - var result = PdfDeconstructionEngine.DeterminePageFormat(width, height); - Assert.Equal(expected, result); - } - - - [Fact] - public void ColorContrastHelper_TryParseRgb_ParsesHexVariantsCorrectly() - { - Assert.True(ColorContrastHelper.TryParseRgb("#FFF", out byte r1, out byte g1, out byte b1)); - Assert.Equal(255, r1); Assert.Equal(255, g1); Assert.Equal(255, b1); - - Assert.True(ColorContrastHelper.TryParseRgb("#123456", out byte r2, out byte g2, out byte b2)); - Assert.Equal(0x12, r2); Assert.Equal(0x34, g2); Assert.Equal(0x56, b2); - - Assert.True(ColorContrastHelper.TryParseRgb("#FF0F172A", out byte r3, out byte g3, out byte b3)); - Assert.Equal(0x0F, r3); Assert.Equal(0x17, g3); Assert.Equal(0x2A, b3); - - Assert.False(ColorContrastHelper.TryParseRgb("Transparent", out _, out _, out _)); - Assert.False(ColorContrastHelper.TryParseRgb(null, out _, out _, out _)); - } -} diff --git a/tests/PdfEditorApp.Tests/PdfEditorApp.Tests.csproj b/tests/PdfEditorApp.Tests/PdfEditorApp.Tests.csproj deleted file mode 100644 index 083d161..0000000 --- a/tests/PdfEditorApp.Tests/PdfEditorApp.Tests.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net10.0 - enable - enable - true - $(NoWarn);NU1901;NU1902;NU1903;NU1904 - false - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/PdfEditorApp.Tests/PdfEngineTests.cs b/tests/PdfEditorApp.Tests/PdfEngineTests.cs deleted file mode 100644 index dfda68b..0000000 --- a/tests/PdfEditorApp.Tests/PdfEngineTests.cs +++ /dev/null @@ -1,2044 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Templates; -using PdfEditorApp.Templates.Events; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PdfEngineTests -{ - private readonly IPdfExportService _exportService = new PdfExportService(); - private readonly ITemplateService _templateService = new TemplateService(); - private readonly IProjectPersistenceService _persistenceService = new ProjectPersistenceService(); - - [Fact] - public void PdfPigSkiaRenderer_RendersPdfPagesToPngStreamsSuccessfully() - { - var model = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(model); - using var doc = UglyToad.PdfPig.PdfDocument.Open(pdfBytes); - UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.AddSkiaPageFactory(doc); - - // 1. Direct SKPicture (Vector representation) - using var picture = UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.GetPageAsSKPicture(doc, 1); - Assert.NotNull(picture); - Assert.True(picture.CullRect.Width > 0); - Assert.True(picture.CullRect.Height > 0); - - // 2. Direct SKBitmap (Raw in-memory raster) - using var skBitmap = UglyToad.PdfPig.Rendering.Skia.PdfPigExtensions.GetPageAsSKBitmap(doc, 1, 2.5f, SkiaSharp.SKColors.White); - Assert.NotNull(skBitmap); - Assert.True(skBitmap.Width > 0); - Assert.True(skBitmap.Height > 0); - - // 3. Encoded PNG stream verification - using var image = SkiaSharp.SKImage.FromBitmap(skBitmap); - using var data = image.Encode(SkiaSharp.SKEncodedImageFormat.Png, 100); - Assert.NotNull(data); - Assert.True(data.Size > 0); - } - - [Fact] - public void GenerateAnnualReport_ProducesValidPdfBytes() - { - var model = _templateService.CreateAnnualReportTemplate(); - Assert.NotNull(model); - Assert.Equal(3, model.Pages.Count); - - byte[] pdfBytes = _exportService.GeneratePdfBytes(model); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 1000); - - string header = Encoding.ASCII.GetString(pdfBytes, 0, 5); - Assert.Equal("%PDF-", header); - - // Deconstruct the exported Annual Report PDF back into editable model - var importedDoc = PdfEditorApp.Core.Deconstruction.PdfDeconstructionEngine.Deconstruct(pdfBytes, "Annual Report"); - Assert.NotNull(importedDoc); - Assert.Equal(3, importedDoc.Pages.Count); - - for (int pIdx = 0; pIdx < importedDoc.Pages.Count; pIdx++) - { - var origPage = model.Pages[pIdx]; - var impPage = importedDoc.Pages[pIdx]; - Console.WriteLine($"=== PAGE {pIdx + 1} COMPARISON ==="); - Console.WriteLine($"ORIGINAL: {origPage.Elements.Count} elements: {string.Join(", ", origPage.Elements.GroupBy(e => e.GetType().Name).Select(g => $"{g.Key}={g.Count()}"))}"); - Console.WriteLine($"IMPORTED: {impPage.Elements.Count} elements: {string.Join(", ", impPage.Elements.GroupBy(e => e.GetType().Name).Select(g => $"{g.Key}={g.Count()}"))}"); - - // Look for missing high-level constructs or broken items - foreach (var origEl in origPage.Elements) - { - if (origEl is PdfTableElement table) - { - Console.WriteLine($" [ORIGINAL TABLE] X={table.X}, Y={table.Y}, Rows={table.Rows.Count}, Cols={table.Headers.Count}"); - } - else if (origEl is PdfChartElement chart) - { - Console.WriteLine($" [ORIGINAL CHART] X={chart.X}, Y={chart.Y}, Type={chart.ChartType}, Title={chart.Title}"); - } - else if (origEl is PdfQrCodeElement qr) - { - Console.WriteLine($" [ORIGINAL QR] X={qr.X}, Y={qr.Y}, Size={qr.Width}x{qr.Height}"); - } - } - } - } - - [Fact] - public void GenerateInvoice_ProducesValidPdfBytes() - { - var model = _templateService.CreateInvoiceTemplate(); - Assert.NotNull(model); - Assert.Single(model.Pages); - - byte[] pdfBytes = _exportService.GeneratePdfBytes(model); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 500); - - string header = Encoding.ASCII.GetString(pdfBytes, 0, 5); - Assert.Equal("%PDF-", header); - } - - [Fact] - public void GenerateResume_ProducesValidPdfBytes() - { - var model = _templateService.CreateResumeTemplate(); - Assert.NotNull(model); - Assert.Single(model.Pages); - Assert.Equal("John_Doe_Executive_Resume.pdf", model.Title); - - var page = model.Pages[0]; - Assert.NotEmpty(page.Elements); - Assert.True(page.Elements.Count >= 25, "Professional resume should have comprehensive sections and elements"); - - // Verify presence of QR code, avatar badge, dividers, and rich text sections - Assert.Contains(page.Elements, e => e is PdfQrCodeElement); - Assert.Contains(page.Elements, e => e is PdfShapeElement shape && shape.Label == "JD"); - Assert.Contains(page.Elements, e => e is PdfDividerElement); - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.Text.Contains("JOHN DOE")); - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.Text.Contains("EXECUTIVE SUMMARY")); - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.Text.Contains("PROFESSIONAL EXPERIENCE")); - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.Text.Contains("EDUCATION & PROFESSIONAL CREDENTIALS")); - Assert.Contains(page.Elements, e => e is PdfTextElement text && text.Text.Contains("NOTABLE PROJECTS")); - - byte[] pdfBytes = _exportService.GeneratePdfBytes(model); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 2000, "Rich PDF byte stream should be substantial"); - - string header = Encoding.ASCII.GetString(pdfBytes, 0, 5); - Assert.Equal("%PDF-", header); - } - - [Fact] - public void GenerateAcademicPaperAndCertificate_ProducesValidPdfBytes() - { - var paper = _templateService.CreateAcademicPaperTemplate(); - Assert.NotNull(paper); - byte[] paperBytes = _exportService.GeneratePdfBytes(paper); - Assert.NotNull(paperBytes); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(paperBytes, 0, 5)); - - var cert = _templateService.CreateCertificateTemplate(); - Assert.NotNull(cert); - byte[] certBytes = _exportService.GeneratePdfBytes(cert); - Assert.NotNull(certBytes); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(certBytes, 0, 5)); - } - - [Fact] - public void AllRegisteredTemplates_GenerateValidPdfDocumentsAndByteStreams() - { - var templates = _templateService.GetAllTemplates(); - Assert.True(templates.Count >= 18, $"Expected at least 18 templates, found {templates.Count}"); - - foreach (var def in templates) - { - var doc = def.Create(); - Assert.NotNull(doc); - Assert.NotEmpty(doc.Title); - Assert.NotEmpty(doc.Pages); - - var page = doc.Pages[0]; - if (def.Id != "" && def.Id != "blank") // Non-blank template - { - Assert.True(page.Elements.Count >= 3, $"Template '{def.Name}' ({def.Id}) should have at least 3 elements, found {page.Elements.Count}"); - } - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 500, $"Generated PDF for '{def.Name}' should have non-trivial size"); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - } - - [Fact] - public void ResumeTemplates_AllFourTypes_GenerateRichContent() - { - var executive = _templateService.CreateResumeTemplate(); - var modern = _templateService.CreateResumeModernCleanTemplate(); - var creative = _templateService.CreateResumeCreativeMinimalistTemplate(); - var academicCv = _templateService.CreateResumeAcademicCvTemplate(); - - Assert.NotNull(executive); - Assert.NotNull(modern); - Assert.NotNull(creative); - Assert.NotNull(academicCv); - - // Modern Clean tech resume checks - Assert.Contains(modern.Pages[0].Elements, e => e is PdfTextElement t && t.Text.Contains("JOHN DOE")); - Assert.Contains(modern.Pages[0].Elements, e => e is PdfQrCodeElement qr && qr.Content.Contains("github.com/codefrydev")); - - // Creative UI/UX resume checks - Assert.Contains(creative.Pages[0].Elements, e => e is PdfTextElement t && t.Text.Contains("JANE DOE")); - Assert.Contains(creative.Pages[0].Elements, e => e is PdfQrCodeElement qr && qr.Content.Contains("codefrydev.in")); - - // Academic CV checks - Assert.Contains(academicCv.Pages[0].Elements, e => e is PdfTextElement t && t.Text.Contains("JOHN DOE")); - Assert.Contains(academicCv.Pages[0].Elements, e => e is PdfTableElement tbl && tbl.Headers.Contains("Total Amount")); - Assert.Contains(academicCv.Pages[0].Elements, e => e is PdfQrCodeElement qr && qr.Content.Contains("orcid.org")); - } - - [Fact] - public void ResearchPaperTemplates_AllFourTypes_GenerateRichContent() - { - var cs = _templateService.CreateAcademicPaperTemplate(); - var math = _templateService.CreateMathResearchPaperTemplate(); - var physics = _templateService.CreatePhysicsResearchPaperTemplate(); - var history = _templateService.CreateHistoryResearchPaperTemplate(); - var finance = _templateService.CreateFinanceResearchPaperTemplate(); - - Assert.NotNull(cs); - Assert.NotNull(math); - Assert.NotNull(physics); - Assert.NotNull(history); - Assert.NotNull(finance); - - // Mathematics paper checks - Assert.Contains(math.Pages[0].Elements, e => e is PdfTextElement t && t.Text.Contains("Discrete Hodge")); - Assert.Contains(math.Pages[0].Elements, e => e is PdfTableElement tbl && tbl.Headers.Contains("β₁(M)")); - - // Physics paper checks - Assert.Contains(physics.Pages[0].Elements, e => e is PdfTextElement t && t.Text.Contains("Cavity Quantum Electrodynamics")); - Assert.Contains(physics.Pages[0].Elements, e => e is PdfTableElement tbl && tbl.Headers.Contains("T₁ (μs)")); - - // History paper checks - Assert.Contains(history.Pages[0].Elements, e => e is PdfTextElement t && t.Text.Contains("Maritime Trade Networks")); - Assert.Contains(history.Pages[0].Elements, e => e is PdfTableElement tbl && tbl.Headers.Contains("Gold Flow (Ducats)")); - - // Finance paper checks - Assert.Contains(finance.Pages[0].Elements, e => e is PdfTextElement t && t.Text.Contains("Multi-Factor Jump-Diffusion")); - Assert.Contains(finance.Pages[0].Elements, e => e is PdfTableElement tbl && tbl.Headers.Contains("Max DD")); - } - - [Fact] - public async Task ProjectPersistence_RoundTripMatches() - { - var original = _templateService.CreateAnnualReportTemplate(); - string tempPath = Path.Combine(Path.GetTempPath(), $"test_project_{Guid.NewGuid():N}.frypdf"); - - try - { - await _persistenceService.SaveProjectAsync(original, tempPath); - Assert.True(File.Exists(tempPath)); - - var loaded = await _persistenceService.LoadProjectAsync(tempPath); - Assert.NotNull(loaded); - Assert.Equal(original.Title, loaded.Title); - Assert.Equal(original.Pages.Count, loaded.Pages.Count); - Assert.Equal(original.Pages[0].Elements.Count, loaded.Pages[0].Elements.Count); - } - finally - { - if (File.Exists(tempPath)) File.Delete(tempPath); - } - } - - [Fact] - public void UndoRedo_StackOperations_FunctionCorrectly() - { - var undoRedo = new UndoRedoService(); - int val = 0; - - Assert.False(undoRedo.CanUndo); - Assert.False(undoRedo.CanRedo); - - undoRedo.RecordAction("Increment", () => val = 0, () => val = 10); - val = 10; - Assert.True(undoRedo.CanUndo); - Assert.False(undoRedo.CanRedo); - - undoRedo.Undo(); - Assert.Equal(0, val); - Assert.False(undoRedo.CanUndo); - Assert.True(undoRedo.CanRedo); - - undoRedo.Redo(); - Assert.Equal(10, val); - Assert.True(undoRedo.CanUndo); - Assert.False(undoRedo.CanRedo); - } - - [Fact] - public void MainViewModel_FullFeatureSuite_WorksCorrectly() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - Assert.Equal(3, vm.Pages.Count); - Assert.NotNull(vm.CurrentPage); - - // Add page - vm.AddPageCommand.Execute(null); - Assert.Equal(4, vm.Pages.Count); - - // Test Element Additions - int initialCount = vm.CurrentPage.Elements.Count; - vm.AddTextElementCommand.Execute(null); - vm.AddHeadingElementCommand.Execute(null); - vm.AddShapeElementCommand.Execute("Circle"); - vm.AddTableElementCommand.Execute(null); - vm.AddChartElementCommand.Execute(null); - vm.AddWatermarkElementCommand.Execute(null); - vm.AddStampElementCommand.Execute("Approved"); - vm.AddStickyNoteElementCommand.Execute(null); - - Assert.Equal(initialCount + 8, vm.CurrentPage.Elements.Count); - - // Clipboard Copy & Paste & Duplicate - vm.CopyCommand.Execute(null); - vm.PasteCommand.Execute(null); - Assert.Equal(initialCount + 9, vm.CurrentPage.Elements.Count); - - vm.DuplicateCommand.Execute(null); - Assert.Equal(initialCount + 10, vm.CurrentPage.Elements.Count); - - // Reordering & Rotation - int rotation = vm.CurrentPage.RotationAngle; - vm.RotateCurrentPageCommand.Execute(null); - Assert.Equal((rotation + 90) % 360, vm.CurrentPage.RotationAngle); - - vm.RotateCurrentPageCounterClockwiseCommand.Execute(null); - Assert.Equal(rotation, vm.CurrentPage.RotationAngle); - } - - [Fact] - public void GeneratePdf_WithAllAcroFormsAndVisualElements_Succeeds() - { - var doc = new PdfDocumentModel { Title = "Adobe Acrobat Pro Replacement Test Document" }; - var page = new PdfPageModel { PageNumber = 1, Width = 800, Height = 1100, ShowHeaderFooter = true }; - - page.Elements.Add(new PdfFormFieldElement - { - FieldType = FormFieldType.Text, - Label = "Candidate Name:", - Placeholder = "Jane Doe" - }); - - page.Elements.Add(new PdfFormFieldElement - { - FieldType = FormFieldType.Checkbox, - Label = "NDA Signed", - IsChecked = true - }); - - page.Elements.Add(new PdfFormFieldElement - { - FieldType = FormFieldType.Signature, - Label = "Authorized Signatory", - Value = "John Hancock" - }); - - page.Elements.Add(new PdfQrCodeElement - { - Content = "https://github.com/PrashantUnity/PDFCreator", - Label = "VERIFICATION QR" - }); - - page.Elements.Add(new PdfBarcodeElement - { - CodeValue = "DOC-998822", - ShowText = true - }); - - page.Elements.Add(new PdfRedactionElement - { - ExemptionCode = "[REDACTED - (b)(4)]" - }); - - page.Elements.Add(new PdfInkElement - { - IsHighlighter = true, - StrokeThickness = 8 - }); - - page.Elements.Add(new PdfStickyNoteElement - { - Author = "Lead Counsel", - NoteText = "Approved for filing." - }); - - doc.Pages.Add(page); - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 800); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - - [Fact] - public void BatesNumbering_AppliesSequentialCodesAcrossPages() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - Assert.True(vm.Pages.Count >= 3); - - vm.ApplyBatesNumberingCommand.Execute(null); - - Assert.Equal("CONF-BATES-000001", vm.Pages[0].FooterLeft); - Assert.Equal("CONF-BATES-000002", vm.Pages[1].FooterLeft); - Assert.Equal("CONF-BATES-000003", vm.Pages[2].FooterLeft); - } - - [Fact] - public void ExpandedCharts_And_Shapes_Generate_ValidPdf() - { - var doc = new PdfDocumentModel { Title = "Multi-Chart & Multi-Shape Showcase" }; - var page = new PdfPageModel { PageNumber = 1, Width = 800, Height = 1100 }; - - // Test Horizontal Bar, Donut Pie, and Column Charts - page.Elements.Add(new PdfChartElement - { - Title = "Departmental Efficiency", - ChartType = ChartType.HorizontalBar, - Categories = new System.Collections.Generic.List { "Dev", "QA", "Sales" }, - Values = new System.Collections.Generic.List { 8.5, 7.2, 9.1 }, - ValueLabels = new System.Collections.Generic.List { "85%", "72%", "91%" } - }); - - page.Elements.Add(new PdfChartElement - { - Title = "Budget Allocation", - ChartType = ChartType.DonutPie, - Categories = new System.Collections.Generic.List { "Engineering", "Marketing", "Legal" }, - Values = new System.Collections.Generic.List { 50, 30, 20 }, - ValueLabels = new System.Collections.Generic.List { "$5M", "$3M", "$2M" } - }); - - // Test Shapes - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.Star5, - FillColorHex = "#F59E0B" - }); - - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.Heart, - FillColorHex = "#DC2626" - }); - - doc.Pages.Add(page); - - byte[] pdf = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(pdf); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(pdf, 0, 5)); - } - - [Fact] - public void TablePresets_And_BarcodeFormats_ApplyCorrectly() - { - var tableVm = new PdfEditorApp.ViewModels.ElementViewModels.TableElementViewModel(); - tableVm.ApplyPresetStyleCommand.Execute("EnterpriseBlue"); - Assert.Equal("#0F6CBD", tableVm.HeaderBackgroundHex); - Assert.Equal("#F0F7FD", tableVm.AlternateRowBackgroundHex); - - tableVm.ApplyPresetStyleCommand.Execute("DarkModeSlate"); - Assert.Equal("#1E293B", tableVm.HeaderBackgroundHex); - - var barcodeVm = new PdfEditorApp.ViewModels.ElementViewModels.BarcodeElementViewModel(); - barcodeVm.SetFormatCommand.Execute("Ean13"); - Assert.Equal("Ean13", barcodeVm.BarcodeFormat); - - barcodeVm.SetFormatCommand.Execute("Pdf417"); - Assert.Equal("Pdf417", barcodeVm.BarcodeFormat); - } - - [Fact] - public void QrCodePresets_ApplyAccuratePayloads() - { - var qrVm = new PdfEditorApp.ViewModels.ElementViewModels.QrCodeElementViewModel(); - qrVm.ApplyPresetTypeCommand.Execute("Wifi"); - Assert.Contains("WIFI:S:", qrVm.Content); - Assert.Equal("SCAN TO CONNECT WI-FI", qrVm.Label); - - qrVm.ApplyPresetTypeCommand.Execute("VCard"); - Assert.Contains("BEGIN:VCARD", qrVm.Content); - - qrVm.ApplyPresetTypeCommand.Execute("PhoneCall"); - Assert.StartsWith("tel:", qrVm.Content); - - qrVm.ApplyPresetTypeCommand.Execute("Sms"); - Assert.StartsWith("SMSTO:", qrVm.Content); - - qrVm.ApplyPresetTypeCommand.Execute("CryptoAddress"); - Assert.StartsWith("bitcoin:", qrVm.Content); - - qrVm.ApplyPresetTypeCommand.Execute("EventCalendar"); - Assert.Contains("BEGIN:VEVENT", qrVm.Content); - } - - [Fact] - public void QrCodeHelper_GeneratesValidPngAndAvaloniaBitmapWithCustomColorsAndEcc() - { - // 1. Generate PNG bytes with custom colors - byte[] pngBytes = QrCodeHelper.GeneratePngBytes( - "https://github.com/PrashantUnity/PDFCreator", - darkHex: "#0F6CBD", - lightHex: "#EFF6FF", - ecc: QrCodeEccLevel.H, - pixelsPerModule: 12, - drawQuietZones: true); - - Assert.NotNull(pngBytes); - Assert.True(pngBytes.Length > 100); - // Verify PNG magic signature: 0x89, 'P', 'N', 'G' - Assert.Equal(0x89, pngBytes[0]); - Assert.Equal((byte)'P', pngBytes[1]); - Assert.Equal((byte)'N', pngBytes[2]); - Assert.Equal((byte)'G', pngBytes[3]); - - // 2. Generate PNG bytes with custom wifi and error correction - byte[] wifiBytes = QrCodeHelper.GeneratePngBytes( - "WIFI:S:TestNetwork;T:WPA2;P:Secret123;;", - darkHex: "#990000", - lightHex: "#FFFFFF", - ecc: QrCodeEccLevel.Q); - - Assert.NotNull(wifiBytes); - Assert.True(wifiBytes.Length > 100); - Assert.Equal(0x89, wifiBytes[0]); - } - - [Fact] - public void QrCodeElementViewModel_LiveBitmapUpdates_OnPropertyChanged() - { - var vm = new PdfEditorApp.ViewModels.ElementViewModels.QrCodeElementViewModel(); - Assert.NotNull(vm.QrPngBytes); - Assert.True(vm.QrPngBytes.Length > 0); - - vm.Content = "https://codefrydev.in/docs"; - Assert.NotNull(vm.QrPngBytes); - - vm.DarkColorHex = "#16A34A"; - Assert.NotNull(vm.QrPngBytes); - - vm.EccLevel = QrCodeEccLevel.H; - Assert.NotNull(vm.QrPngBytes); - } - - [Fact] - public void Typography_FontFamilyAndSize_SynchronizesWithInspector() - { - var textEl = new PdfEditorApp.ViewModels.ElementViewModels.TextElementViewModel - { - FontFamily = "Georgia", - FontSize = 24, - IsBold = true, - IsItalic = true, - IsUnderline = true - }; - - var page = new PageViewModel(); - page.AddElement(textEl); - - var inspector = new InspectorViewModel(); - inspector.UpdateSelection(textEl, page); - - Assert.Equal("Georgia", inspector.SelectedFontFamily); - Assert.Equal(24, inspector.SelectedFontSize); - - inspector.SelectedFontFamily = "Roboto"; - Assert.Equal("Roboto", textEl.FontFamily); - - inspector.SelectedFontSize = 32; - Assert.Equal(32, textEl.FontSize); - - inspector.ToggleBoldCommand.Execute(null); - Assert.False(textEl.IsBold); - - inspector.ToggleItalicCommand.Execute(null); - Assert.False(textEl.IsItalic); - - inspector.ToggleUnderlineCommand.Execute(null); - Assert.False(textEl.IsUnderline); - } - - [Fact] - public void All21ShapeTypes_ProduceValidVectorPaths() - { - var shapeVm = new PdfEditorApp.ViewModels.ElementViewModels.ShapeElementViewModel - { - Width = 100, - Height = 100, - CornerRadius = 12 - }; - - foreach (ShapeType shapeType in Enum.GetValues()) - { - shapeVm.ShapeType = shapeType; - string path = shapeVm.PathData; - Assert.False(string.IsNullOrWhiteSpace(path), $"PathData for {shapeType} should not be empty"); - Assert.StartsWith("M", path); - } - } - - [Fact] - public void All13ChartTypes_PropertiesAndPersistence_Work() - { - var chartVm = new PdfEditorApp.ViewModels.ElementViewModels.ChartElementViewModel(); - - foreach (ChartType chartType in Enum.GetValues()) - { - chartVm.SetChartTypeCommand.Execute(chartType.ToString()); - Assert.Equal(chartType, chartVm.ChartType); - - var model = chartVm.ToModel() as PdfChartElement; - Assert.NotNull(model); - Assert.Equal(chartType, model.ChartType); - - var roundtripVm = new PdfEditorApp.ViewModels.ElementViewModels.ChartElementViewModel(); - roundtripVm.LoadFromModel(model); - Assert.Equal(chartType, roundtripVm.ChartType); - } - } - [Fact] - public void LiveCharts2_HeadlessSkiaRendering_AllChartTypes_Work() - { - foreach (ChartType chartType in Enum.GetValues()) - { - var chartEl = new PdfChartElement - { - Title = $"Test {chartType}", - ChartType = chartType, - Categories = new List { "Q1", "Q2", "Q3", "Q4" }, - Values = new List { 12, 19, 3, 25 }, - ValueLabels = new List { "$12M", "$19M", "$3M", "$25M" }, - Palette = ChartPalette.CyberNeon, - LegendPosition = ChartLegendPosition.Top, - ShowDataLabels = true, - ShowGridlines = true - }; - - byte[] pngBytes = PdfEditorApp.Core.Analysis.LiveChartsRenderer.RenderChartToPngBytes(chartEl, width: 400, height: 250); - Assert.NotNull(pngBytes); - Assert.NotEmpty(pngBytes); - Assert.True(pngBytes.Length > 100); - } - } - - [Fact] - public void LiveCharts2_MultiSeries_CartesianRendering_Works() - { - var chartEl = new PdfChartElement - { - Title = "2025 vs 2026 Sales Comparison", - ChartType = ChartType.BarColumn, - Categories = new List { "North", "South", "East", "West" }, - Palette = ChartPalette.CorporateBlue, - LegendPosition = ChartLegendPosition.Top, - MultiSeries = new List - { - new() { Name = "2025 Actual", Values = new List { 120, 150, 180, 200 }, ColorHex = "#82BDF0" }, - new() { Name = "2026 Target", Values = new List { 140, 175, 210, 240 }, ColorHex = "#0F6CBD" } - } - }; - - byte[] pngBytes = PdfEditorApp.Core.Analysis.LiveChartsRenderer.RenderChartToPngBytes(chartEl, width: 500, height: 300); - Assert.NotNull(pngBytes); - Assert.NotEmpty(pngBytes); - Assert.True(pngBytes.Length > 200); - } - - [Fact] - public void LiveCharts2_AllPalettes_RenderSuccessfully() - { - foreach (ChartPalette palette in Enum.GetValues()) - { - var colors = PdfEditorApp.Core.Analysis.LiveChartsRenderer.GetPaletteHexColors(palette); - Assert.NotEmpty(colors); - Assert.True(colors.Count >= 4); - - var chartEl = new PdfChartElement - { - Title = $"Palette {palette}", - ChartType = ChartType.SmoothLine, - Palette = palette, - Categories = new List { "Jan", "Feb", "Mar", "Apr" }, - Values = new List { 50, 65, 80, 95 } - }; - - byte[] pngBytes = PdfEditorApp.Core.Analysis.LiveChartsRenderer.RenderChartToPngBytes(chartEl, 300, 200); - Assert.NotEmpty(pngBytes); - } - } - - [Fact] - public void LiveCharts2_ChartElementViewModel_ReactiveUpdates_Work() - { - var vm = new PdfEditorApp.ViewModels.ElementViewModels.ChartElementViewModel(); - Assert.NotNull(vm.CartesianSeries); - Assert.NotEmpty(vm.CartesianSeries); - - // Change palette - vm.SetPaletteCommand.Execute("EmeraldGreen"); - Assert.Equal(ChartPalette.EmeraldGreen, vm.Palette); - - // Change legend position - vm.SetLegendPositionCommand.Execute("Right"); - Assert.Equal(ChartLegendPosition.Right, vm.LegendPosition); - Assert.Equal(LiveChartsCore.Measure.LegendPosition.Right, vm.LiveLegendPosition); - - // Switch to DonutPie - vm.SetChartTypeCommand.Execute("DonutPie"); - Assert.Equal(ChartType.DonutPie, vm.ChartType); - Assert.True(vm.IsDonutPie); - Assert.True(vm.IsPieChart); - Assert.False(vm.IsCartesianChart); - Assert.NotEmpty(vm.PieSeries); - - // Switch to Radar - vm.SetChartTypeCommand.Execute("Radar"); - Assert.Equal(ChartType.Radar, vm.ChartType); - Assert.True(vm.IsRadar); - Assert.True(vm.IsPolarChart); - Assert.NotEmpty(vm.PolarSeries); - - // Add and Remove Data points - int initialBars = vm.Bars.Count; - vm.AddDataPointCommand.Execute(null); - Assert.Equal(initialBars + 1, vm.Bars.Count); - - vm.RemoveDataPointCommand.Execute(null); - Assert.Equal(initialBars, vm.Bars.Count); - } - - [Fact] - public void LiveCharts2_PdfExport_ContainsRenderedChart() - { - var doc = new PdfDocumentModel - { - Pages = new List - { - new() - { - Width = 595, - Height = 842, - Elements = new List - { - new PdfChartElement - { - X = 50, - Y = 100, - Width = 495, - Height = 250, - Title = "Executive Financial Summary", - ChartType = ChartType.SmoothLine, - Palette = ChartPalette.CorporateBlue, - Categories = new List { "2023", "2024", "2025", "2026" }, - Values = new List { 12.5, 18.2, 24.8, 31.0 } - } - } - } - } - }; - - byte[] pdfBytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(pdfBytes); - Assert.NotEmpty(pdfBytes); - Assert.True(pdfBytes.Length > 1000); - } - - [Fact] - public void FullElementDuplicationAndPaste_MaintainsCorrectTypes() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - Assert.NotNull(vm.CurrentPage); - - // Add StickyNote - vm.AddStickyNoteElementCommand.Execute(null); - var sticky = vm.CurrentPage.SelectedElement; - Assert.IsType(sticky); - - // Copy and Paste - vm.CopyCommand.Execute(null); - vm.PasteCommand.Execute(null); - var pasted = vm.CurrentPage.SelectedElement; - Assert.IsType(pasted); - - // Add FormField - vm.AddFormFieldElementCommand.Execute("Signature"); - var formField = vm.CurrentPage.SelectedElement; - Assert.IsType(formField); - - // Duplicate - vm.DuplicateCommand.Execute(null); - var dupFormField = vm.CurrentPage.SelectedElement; - Assert.IsType(dupFormField); - - // Add Ink - vm.AddInkElementCommand.Execute("True"); - var ink = vm.CurrentPage.SelectedElement as PdfEditorApp.ViewModels.ElementViewModels.InkElementViewModel; - Assert.NotNull(ink); - Assert.True(ink.IsHighlighter); - - // Dialog close - vm.OpenNewDocumentDialogCommand.Execute(null); - Assert.True(vm.IsNewDocumentDialogOpen); - vm.CloseNewDocumentDialogCommand.Execute(null); - Assert.False(vm.IsNewDocumentDialogOpen); - } - - [Fact] - public void CommandPalette_IndexingAndFiltering_WorksAccurately() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - Assert.NotEmpty(vm.AllPaletteCommands); - Assert.True(vm.AllPaletteCommands.Count >= 25); - - // Open palette - vm.OpenCommandPaletteCommand.Execute(null); - Assert.True(vm.IsCommandPaletteOpen); - Assert.Equal(vm.AllPaletteCommands.Count, vm.FilteredPaletteCommands.Count); - - // Search for "text" - vm.CommandSearchQuery = "text"; - Assert.NotEmpty(vm.FilteredPaletteCommands); - Assert.All(vm.FilteredPaletteCommands, item => - Assert.True(item.Title.Contains("text", StringComparison.OrdinalIgnoreCase) || - item.Subtitle.Contains("text", StringComparison.OrdinalIgnoreCase) || - item.Category.Contains("text", StringComparison.OrdinalIgnoreCase))); - - // Search for "save" - vm.CommandSearchQuery = "save"; - Assert.NotEmpty(vm.FilteredPaletteCommands); - Assert.Contains(vm.FilteredPaletteCommands, item => item.Title.Contains("Save", StringComparison.OrdinalIgnoreCase)); - - // Navigation - vm.SelectNextPaletteCommand(); - Assert.True(vm.SelectedPaletteIndex >= 0); - vm.SelectPreviousPaletteCommand(); - Assert.Equal(0, vm.SelectedPaletteIndex); - - // Close palette - vm.CloseCommandPalette(); - Assert.False(vm.IsCommandPaletteOpen); - } - - [Fact] - public void PageNavigation_NextPreviousFirstLast_NavigatesProperly() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - Assert.Equal(3, vm.Pages.Count); - Assert.Equal(1, vm.CurrentPageNumber); - - // Next page - vm.NextPageCommand.Execute(null); - Assert.Equal(2, vm.CurrentPageNumber); - - // Next page - vm.NextPageCommand.Execute(null); - Assert.Equal(3, vm.CurrentPageNumber); - - // Next page at boundary (should remain at 3) - vm.NextPageCommand.Execute(null); - Assert.Equal(3, vm.CurrentPageNumber); - - // Previous page - vm.PreviousPageCommand.Execute(null); - Assert.Equal(2, vm.CurrentPageNumber); - - // First page - vm.FirstPageCommand.Execute(null); - Assert.Equal(1, vm.CurrentPageNumber); - - // Last page - vm.LastPageCommand.Execute(null); - Assert.Equal(3, vm.CurrentPageNumber); - } - - [Fact] - public void PageOperations_WithUndoRedo_WorkAccurately() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - int initialPageCount = vm.Pages.Count; - - // Add page - vm.AddPageCommand.Execute(null); - Assert.Equal(initialPageCount + 1, vm.Pages.Count); - - // Undo add page - vm.UndoCommand.Execute(null); - Assert.Equal(initialPageCount, vm.Pages.Count); - - // Redo add page - vm.RedoCommand.Execute(null); - Assert.Equal(initialPageCount + 1, vm.Pages.Count); - - // Duplicate page - vm.DuplicateCurrentPageCommand.Execute(null); - Assert.Equal(initialPageCount + 2, vm.Pages.Count); - - // Undo duplicate - vm.UndoCommand.Execute(null); - Assert.Equal(initialPageCount + 1, vm.Pages.Count); - - // Rotate page with undo - int currentAngle = vm.CurrentPage!.RotationAngle; - vm.RotateCurrentPageCommand.Execute(null); - Assert.Equal((currentAngle + 90) % 360, vm.CurrentPage.RotationAngle); - - vm.UndoCommand.Execute(null); - Assert.Equal(currentAngle, vm.CurrentPage.RotationAngle); - - // Delete page with undo - int beforeDeleteCount = vm.Pages.Count; - vm.DeleteCurrentPageCommand.Execute(null); - Assert.Equal(beforeDeleteCount - 1, vm.Pages.Count); - - vm.UndoCommand.Execute(null); - Assert.Equal(beforeDeleteCount, vm.Pages.Count); - } - - [Fact] - public void ToastNotification_And_ShortcutsDialog_Work() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - - // Show Toast - vm.ShowToast("Document saved successfully", "CheckCircleOutline"); - Assert.Equal("Document saved successfully", vm.ToastMessage); - Assert.Equal("CheckCircleOutline", vm.ToastIcon); - Assert.True(vm.IsToastVisible); - - // Shortcuts Dialog - vm.OpenShortcutsHelpCommand.Execute(null); - Assert.True(vm.IsShortcutsHelpDialogOpen); - - vm.CloseShortcutsHelpCommand.Execute(null); - Assert.False(vm.IsShortcutsHelpDialogOpen); - - // Tool Mode - vm.SetToolMode("Draw"); - Assert.Equal(ToolMode.Draw, vm.ActiveToolMode); - - vm.SetToolMode("Select"); - Assert.Equal(ToolMode.Select, vm.ActiveToolMode); - } - - [Fact] - public void UndoRedo_InspectorTypography_RevertsAndRedoes() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - - var textEl = new TextElementViewModel - { - Text = "Sample Headline", - FontFamily = "Segoe UI", - FontSize = 14, - IsBold = false, - TextColorHex = "#201F1E", - Alignment = TextAlignmentMode.Left - }; - page.AddElement(textEl); - vm.Inspector.UpdateSelection(textEl, page); - - // 1. Text Color change - vm.Inspector.SetTextColorCommand.Execute("#DC2626"); - Assert.Equal("#DC2626", textEl.TextColorHex); - Assert.Equal("Change Text Color", vm.UndoRedo.NextUndoDescription); - - vm.UndoCommand.Execute(null); - Assert.Equal("#201F1E", textEl.TextColorHex); - - vm.RedoCommand.Execute(null); - Assert.Equal("#DC2626", textEl.TextColorHex); - - // 2. Bold toggle - vm.Inspector.ToggleBoldCommand.Execute(null); - Assert.True(textEl.IsBold); - Assert.Equal("Format Bold", vm.UndoRedo.NextUndoDescription); - - vm.UndoCommand.Execute(null); - Assert.False(textEl.IsBold); - - vm.RedoCommand.Execute(null); - Assert.True(textEl.IsBold); - - // 3. Text Alignment - vm.Inspector.SetAlignmentCommand.Execute("Center"); - Assert.Equal(TextAlignmentMode.Center, textEl.Alignment); - - vm.UndoCommand.Execute(null); - Assert.Equal(TextAlignmentMode.Left, textEl.Alignment); - - vm.RedoCommand.Execute(null); - Assert.Equal(TextAlignmentMode.Center, textEl.Alignment); - } - - [Fact] - public void UndoRedo_InspectorShapeFormatting_RevertsAndRedoes() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - - var shapeEl = new ShapeElementViewModel - { - ShapeType = ShapeType.Rectangle, - FillColorHex = "#F0F7FD", - StrokeColorHex = "#0F6CBD" - }; - page.AddElement(shapeEl); - vm.Inspector.UpdateSelection(shapeEl, page); - - // Fill Color - vm.Inspector.SetShapeFillColorCommand.Execute("#16A34A"); - Assert.Equal("#16A34A", shapeEl.FillColorHex); - - vm.UndoCommand.Execute(null); - Assert.Equal("#F0F7FD", shapeEl.FillColorHex); - - vm.RedoCommand.Execute(null); - Assert.Equal("#16A34A", shapeEl.FillColorHex); - - // Stroke Color - vm.Inspector.SetShapeStrokeColorCommand.Execute("#EA580C"); - Assert.Equal("#EA580C", shapeEl.StrokeColorHex); - - vm.UndoCommand.Execute(null); - Assert.Equal("#0F6CBD", shapeEl.StrokeColorHex); - - vm.RedoCommand.Execute(null); - Assert.Equal("#EA580C", shapeEl.StrokeColorHex); - - // Shape Type - vm.Inspector.SetShapeTypeCommand.Execute("Circle"); - Assert.Equal(ShapeType.Circle, shapeEl.ShapeType); - - vm.UndoCommand.Execute(null); - Assert.Equal(ShapeType.Rectangle, shapeEl.ShapeType); - - vm.RedoCommand.Execute(null); - Assert.Equal(ShapeType.Circle, shapeEl.ShapeType); - } - - [Fact] - public void UndoRedo_InspectorAlignment_And_Layering_RevertsAndRedoes() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - - page.Elements.Clear(); - var bottomEl = new ShapeElementViewModel { X = 10, Y = 10 }; - var shapeEl = new ShapeElementViewModel - { - X = 200, - Y = 300, - Width = 100, - Height = 100 - }; - var topEl = new ShapeElementViewModel { X = 50, Y = 50 }; - page.AddElement(bottomEl); - page.AddElement(shapeEl); - page.AddElement(topEl); - vm.Inspector.UpdateSelection(shapeEl, page); - - // Align Left - vm.Inspector.AlignLeftCommand.Execute(null); - Assert.Equal(60, shapeEl.X); - - vm.UndoCommand.Execute(null); - Assert.Equal(200, shapeEl.X); - - vm.RedoCommand.Execute(null); - Assert.Equal(60, shapeEl.X); - - // Align Center - vm.Inspector.AlignCenterCommand.Execute(null); - double expectedCenter = (page.Width - shapeEl.Width) / 2; - Assert.Equal(expectedCenter, shapeEl.X); - - vm.UndoCommand.Execute(null); - Assert.Equal(60, shapeEl.X); - - // Layering Z-Order - int oldZ = shapeEl.ZIndex; - vm.Inspector.BringToFrontCommand.Execute(null); - Assert.True(shapeEl.ZIndex > oldZ); - Assert.Equal(page.Elements.Count - 1, page.Elements.IndexOf(shapeEl)); - - vm.UndoCommand.Execute(null); - Assert.Equal(oldZ, shapeEl.ZIndex); - Assert.Equal(1, page.Elements.IndexOf(shapeEl)); - - // Send to Back - vm.Inspector.SendToBackCommand.Execute(null); - Assert.Equal(1, shapeEl.ZIndex); - Assert.Equal(0, page.Elements.IndexOf(shapeEl)); - - vm.UndoCommand.Execute(null); - Assert.Equal(oldZ, shapeEl.ZIndex); - Assert.Equal(1, page.Elements.IndexOf(shapeEl)); - } - - [Fact] - public void UndoRedo_ElementDeleteAndDuplicate_WorksAccurately() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - int initialCount = page.Elements.Count; - - var textEl = new TextElementViewModel { Text = "Target Element" }; - page.AddElement(textEl); - Assert.Equal(initialCount + 1, page.Elements.Count); - - vm.Inspector.UpdateSelection(textEl, page); - - // Duplicate - vm.Inspector.DuplicateSelectedElementCommand.Execute(null); - Assert.Equal(initialCount + 2, page.Elements.Count); - - vm.UndoCommand.Execute(null); - Assert.Equal(initialCount + 1, page.Elements.Count); - - vm.RedoCommand.Execute(null); - Assert.Equal(initialCount + 2, page.Elements.Count); - - // Delete - vm.Inspector.DeleteSelectedElementCommand.Execute(null); - Assert.Equal(initialCount + 1, page.Elements.Count); - - vm.UndoCommand.Execute(null); - Assert.Equal(initialCount + 2, page.Elements.Count); - } - - [Fact] - public void SignatureService_GeneratesValidSignaturesAndInitials() - { - var sigService = new SignatureService(); - - var sig = sigService.CreateCursiveSignatureElement("Alexander Hamilton", SignatureStyle.CursiveElegance, 100, 200); - Assert.NotNull(sig); - Assert.Equal("Alexander Hamilton", sig.Text); - Assert.Equal("Georgia", sig.FontFamily); - Assert.True(sig.IsItalic); - - var dateStamp = sigService.CreateDateStampElement(100, 200); - Assert.NotNull(dateStamp); - Assert.Contains(DateTime.Now.Year.ToString(), dateStamp.Text); - - var initials = sigService.CreateInitialsElement("AH", 100, 200); - Assert.NotNull(initials); - Assert.Equal("AH", initials.Label); - Assert.Equal(ShapeType.Circle, initials.ShapeType); - - var checkmark = sigService.CreateMarkupBadge("✓", "#16A34A", 100, 200); - Assert.NotNull(checkmark); - Assert.Equal("✓", checkmark.Label); - } - - [Fact] - public void DocumentAuditService_EvaluatesHealthAndChecks() - { - var auditService = new DocumentAuditService(); - var doc = _templateService.CreateAnnualReportTemplate(); - - var report = auditService.RunAudit(doc); - Assert.NotNull(report); - Assert.True(report.HealthScore >= 70); - Assert.NotEmpty(report.Grade); - Assert.Equal(3, report.TotalPages); - Assert.True(report.TotalWordCount > 0); - Assert.NotEmpty(report.Issues); - } - - [Fact] - public void SecuritySettings_PasswordEncryptionAndPermissions_ExportPasses() - { - var doc = _templateService.CreateInvoiceTemplate(); - doc.SecuritySettings.IsPasswordProtected = true; - doc.SecuritySettings.OpenPassword = "SecurePassword123!"; - doc.SecuritySettings.AllowPrinting = true; - doc.SecuritySettings.AllowContentCopying = false; - doc.SecuritySettings.ScrubMetadataOnExport = true; - - byte[] pdfBytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 500); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(pdfBytes, 0, 5)); - } - - [Fact] - public void DocumentSanitization_ClearsMetadataAndComments() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - page.AddElement(new StickyNoteElementViewModel { NoteText = "Internal confidential review comment" }); - - vm.SanitizeDocument(); - Assert.Equal("Anonymous", vm.DocumentAuthor); - Assert.Equal("", vm.DocumentSubject); - Assert.True(vm.SecuritySettings.ScrubMetadataOnExport); - Assert.True(vm.SecuritySettings.RemoveCommentsOnExport); - Assert.DoesNotContain(page.Elements, e => e is StickyNoteElementViewModel); - } - - [Fact] - public void Typography_StrikethroughAndTextCase_TransformationsWork() - { - var textVm = new TextElementViewModel { Text = "hello world acrobat suite" }; - - textVm.TransformUppercase(); - Assert.Equal("HELLO WORLD ACROBAT SUITE", textVm.Text); - - textVm.TransformTitleCase(); - Assert.Equal("Hello World Acrobat Suite", textVm.Text); - - textVm.TransformLowercase(); - Assert.Equal("hello world acrobat suite", textVm.Text); - - textVm.ToggleBulletList(); - Assert.StartsWith("• ", textVm.Text); - - textVm.ToggleNumberedList(); - Assert.StartsWith("1. ", textVm.Text); - - textVm.IsStrikethrough = true; - var model = (PdfTextElement)textVm.ToModel(); - Assert.True(model.IsStrikethrough); - } - - [Fact] - public void AcroForms_ValidationAndOptions_WorkCorrectly() - { - var formVm = new FormFieldElementViewModel - { - FieldType = FormFieldType.Dropdown, - FieldName = "StateProvince", - ValidationType = FormValidationType.CustomRegex, - CustomValidationRegex = @"^[A-Z]{2}$", - IsReadOnly = false, - IsRequired = true, - DefaultValue = "CA", - Tooltip = "Select 2-letter state code" - }; - formVm.Options.Clear(); - formVm.AddOption("NY"); - formVm.AddOption("TX"); - - Assert.Equal(2, formVm.Options.Count); - - var model = (PdfFormFieldElement)formVm.ToModel(); - Assert.Equal(FormFieldType.Dropdown, model.FieldType); - Assert.Equal("StateProvince", model.FieldName); - Assert.Equal(FormValidationType.CustomRegex, model.ValidationType); - Assert.Equal("CA", model.DefaultValue); - Assert.Equal(2, model.Options.Count); - } - - [Fact] - public void SmartDistribution_DistributesElementsEvenly() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - page.Elements.Clear(); - - var el1 = new ShapeElementViewModel { X = 0, Y = 100, Width = 50, Height = 50 }; - var el2 = new ShapeElementViewModel { X = 100, Y = 100, Width = 50, Height = 50 }; - var el3 = new ShapeElementViewModel { X = 300, Y = 100, Width = 50, Height = 50 }; - - page.AddElement(el1); - page.AddElement(el2); - page.AddElement(el3); - - vm.Inspector.UpdateSelection(el1, page); - vm.Inspector.DistributeHorizontally(); - - // Total span: 0 to 350 (width 350). Elements total width: 150. Remaining space: 200 / 2 = 100 gap. - // Expected X: el1 = 0, el2 = 150, el3 = 300. - Assert.Equal(0, el1.X); - Assert.Equal(150, el2.X); - Assert.Equal(300, el3.X); - } - - [Fact] - public void Inspector_TypographyAndTextDecorations_ReflectImmediately() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - var textEl = new TextElementViewModel { Text = "Sample Heading", FontSize = 16 }; - page.AddElement(textEl); - - vm.Inspector.UpdateSelection(textEl, page); - Assert.NotNull(vm.Inspector.TextElement); - - // Toggle Strikethrough and Underline - vm.Inspector.ToggleStrikethroughCommand.Execute(null); - Assert.True(textEl.IsStrikethrough); - Assert.NotNull(textEl.TextDecorations); - - vm.Inspector.ToggleUnderlineCommand.Execute(null); - Assert.True(textEl.IsUnderline); - Assert.NotNull(textEl.TextDecorations); - - // Change text color - vm.Inspector.SetTextColorCommand.Execute("#0F6CBD"); - Assert.Equal("#0F6CBD", textEl.TextColorHex); - - // Set alignment - vm.Inspector.SetAlignmentCommand.Execute("Center"); - Assert.Equal(TextAlignmentMode.Center, textEl.Alignment); - - // Switch Font Family - vm.Inspector.SelectedFontFamily = "Georgia"; - Assert.Equal("Georgia", textEl.FontFamily); - - vm.Inspector.SelectedFontFamily = "Roboto"; - Assert.Equal("Roboto", textEl.FontFamily); - Assert.NotNull(textEl.AvaloniaFontFamily); - - vm.Inspector.SelectedFontFamily = "Playfair Display"; - Assert.Equal("Playfair Display", textEl.FontFamily); - Assert.NotNull(textEl.AvaloniaFontFamily); - - vm.Inspector.SelectedFontFamily = "Dancing Script"; - Assert.Equal("Dancing Script", textEl.FontFamily); - Assert.NotNull(textEl.AvaloniaFontFamily); - - vm.Inspector.SelectedFontFamily = "Courier New"; - Assert.Equal("Courier New", textEl.FontFamily); - - vm.Inspector.SelectedFontFamily = "Impact"; - Assert.Equal("Impact", textEl.FontFamily); - - // Computed line height - textEl.LineHeight = 1.5; - Assert.Equal(16 * 1.5, textEl.ComputedLineHeight); - } - - [Fact] - public void Inspector_ShapeStylesAndLabels_ReflectImmediately() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - var shapeEl = new ShapeElementViewModel { Width = 120, Height = 60 }; - page.AddElement(shapeEl); - - vm.Inspector.UpdateSelection(shapeEl, page); - Assert.NotNull(vm.Inspector.ShapeElement); - - // Change shape type - vm.Inspector.SetShapeTypeCommand.Execute("Star4Badge"); - Assert.Equal(ShapeType.Star4Badge, shapeEl.ShapeType); - Assert.NotEmpty(shapeEl.PathData); - - // Set colors - vm.Inspector.SetShapeFillColorCommand.Execute("#16A34A"); - Assert.Equal("#16A34A", shapeEl.FillColorHex); - - vm.Inspector.SetShapeStrokeColorCommand.Execute("#DC2626"); - Assert.Equal("#DC2626", shapeEl.StrokeColorHex); - - // Set label - shapeEl.Label = "CERTIFIED"; - vm.Inspector.SetShapeLabelColorCommand.Execute("#FFFFFF"); - Assert.Equal("#FFFFFF", shapeEl.LabelColorHex); - } - - [Fact] - public void Inspector_AllElementColorsAndProperties_ReflectProperly() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - - // Divider - var divEl = new DividerElementViewModel(); - page.AddElement(divEl); - vm.Inspector.UpdateSelection(divEl, page); - vm.Inspector.SetDividerColorCommand.Execute("#EA580C"); - Assert.Equal("#EA580C", divEl.ColorHex); - - // Ink - var inkEl = new InkElementViewModel(); - page.AddElement(inkEl); - vm.Inspector.UpdateSelection(inkEl, page); - vm.Inspector.SetInkColorCommand.Execute("#7C3AED"); - Assert.Equal("#7C3AED", inkEl.StrokeColorHex); - - // Sticky note - var noteEl = new StickyNoteElementViewModel(); - page.AddElement(noteEl); - vm.Inspector.UpdateSelection(noteEl, page); - vm.Inspector.SetStickyNoteColorCommand.Execute("#DBEAFE"); - Assert.Equal("#DBEAFE", noteEl.ColorHex); - - // Redaction - var redEl = new RedactionElementViewModel(); - page.AddElement(redEl); - vm.Inspector.UpdateSelection(redEl, page); - vm.Inspector.SetRedactionFillColorCommand.Execute("#0F172A"); - Assert.Equal("#0F172A", redEl.FillColorHex); - - // Form field - var formEl = new FormFieldElementViewModel(); - page.AddElement(formEl); - vm.Inspector.UpdateSelection(formEl, page); - vm.Inspector.SetFormFieldTypeCommand.Execute("DatePicker"); - Assert.Equal(FormFieldType.DatePicker, formEl.FieldType); - vm.Inspector.SetFormFieldBorderColorCommand.Execute("#16A34A"); - Assert.Equal("#16A34A", formEl.BorderColorHex); - } - - [Fact] - public void DocumentCompareService_DetectsDifferences_BetweenRevisions() - { - var compareService = new DocumentCompareService(); - - var original = new PdfDocumentModel - { - Title = "Contract v1.0", - Pages = new System.Collections.Generic.List - { - new PdfPageModel - { - PageNumber = 1, - Elements = new System.Collections.Generic.List - { - new PdfTextElement { Id = "el-1", Text = "Initial Clause A", X = 50, Y = 50 }, - new PdfTextElement { Id = "el-2", Text = "Unchanged Clause B", X = 50, Y = 100 } - } - } - } - }; - - var modified = new PdfDocumentModel - { - Title = "Contract v2.0", - Pages = new System.Collections.Generic.List - { - new PdfPageModel - { - PageNumber = 1, - Elements = new System.Collections.Generic.List - { - new PdfTextElement { Id = "el-1", Text = "Amended Clause A (Updated)", X = 50, Y = 50 }, - new PdfTextElement { Id = "el-2", Text = "Unchanged Clause B", X = 50, Y = 100 }, - new PdfTextElement { Id = "el-3", Text = "Newly Added Clause C", X = 50, Y = 150 } - } - } - } - }; - - var report = compareService.CompareDocuments(original, modified); - - Assert.NotNull(report); - Assert.Equal(1, report.AdditionsCount); // el-3 added - Assert.Equal(1, report.ModificationsCount); // el-1 modified - Assert.Equal(0, report.DeletionsCount); - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.ElementAdded && d.Description.Contains("Newly Added Clause C")); - Assert.Contains(report.Differences, d => d.DiffType == CompareDiffType.TextModified && d.OldValue == "Initial Clause A"); - } - - [Fact] - public void BatesNumbering_CustomPrefixAndPosition_AppliesAndRemovesCorrectly() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - Assert.True(vm.Pages.Count >= 3); - - vm.BatesPrefix = "LEGAL-BATES-"; - vm.BatesStartingNumber = 101; - vm.BatesNumberOfDigits = 5; - vm.BatesPosition = BatesPosition.TopRight; - - vm.ApplyBatesNumberingCommand.Execute(null); - - Assert.Equal("LEGAL-BATES-00101", vm.Pages[0].HeaderRight); - Assert.Equal("LEGAL-BATES-00102", vm.Pages[1].HeaderRight); - Assert.Equal("LEGAL-BATES-00103", vm.Pages[2].HeaderRight); - - // Remove Bates numbering - vm.RemoveBatesNumberingCommand.Execute(null); - Assert.True(string.IsNullOrEmpty(vm.Pages[0].HeaderRight)); - Assert.True(string.IsNullOrEmpty(vm.Pages[1].HeaderRight)); - } - - [Fact] - public void OrganizePages_BatchRotate_RotatesPagesCorrectly() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - int initialPage1Angle = vm.Pages[0].RotationAngle; - int initialPage2Angle = vm.Pages[1].RotationAngle; - - vm.BatchRotatePagesCommand.Execute("all"); - - Assert.Equal((initialPage1Angle + 90) % 360, vm.Pages[0].RotationAngle); - Assert.Equal((initialPage2Angle + 90) % 360, vm.Pages[1].RotationAngle); - } - - [Fact] - public void AcroForms_RecalculateFormFields_CalculatesFormulas() - { - var page = new PageViewModel(); - - var f1 = new FormFieldElementViewModel { FieldName = "Subtotal", Value = "150.00" }; - var f2 = new FormFieldElementViewModel { FieldName = "Tax", Value = "15.00" }; - var f3 = new FormFieldElementViewModel - { - FieldName = "Total", - CalculationFormula = CalculationFormula.Sum, - CalculationSourceFields = "Subtotal, Tax" - }; - var fAvg = new FormFieldElementViewModel - { - FieldName = "AverageItem", - CalculationFormula = CalculationFormula.Average, - CalculationSourceFields = "Subtotal, Tax" - }; - - page.AddElement(f1); - page.AddElement(f2); - page.AddElement(f3); - page.AddElement(fAvg); - - page.RecalculateFormFields(); - - Assert.Equal("165.00", f3.Value); - Assert.Equal("82.50", fAvg.Value); - } - - [Fact] - public void MeasurementElement_CalculatesDistance_AndFormatsUnits() - { - var meas = new PdfMeasurementElement - { - StartX = 0, - StartY = 0, - EndX = 72, - EndY = 0, // 72 pts = 1.00 inch - Unit = RulerUnit.Inches - }; - - Assert.Equal(72.0, meas.CalculateDistance(), 2); - Assert.Equal("1.00 in", meas.GetFormattedDistance()); - - meas.Unit = RulerUnit.Millimeters; - Assert.Equal("25.4 mm", meas.GetFormattedDistance()); - - meas.Unit = RulerUnit.Points; - Assert.Equal("72.0 pt", meas.GetFormattedDistance()); - } - - [Fact] - public void FindAndReplace_FindsAndReplacesText_AcrossElements() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - var page = vm.CurrentPage!; - - var txt1 = new TextElementViewModel { Text = "Acme Global Corporation Quarterly Financial Summary" }; - var txt2 = new TextElementViewModel { Text = "Prepared exclusively for Acme Global Leadership" }; - page.AddElement(txt1); - page.AddElement(txt2); - - vm.FindQuery = "Acme Global"; - vm.ReplaceQuery = "Apex International"; - vm.FindMatchCase = true; - - vm.FindNextCommand.Execute(null); - Assert.Equal(2, vm.FindMatchesCount); - - vm.ReplaceAllCommand.Execute(null); - - Assert.Contains("Apex International Corporation", txt1.Text); - Assert.Contains("Apex International Leadership", txt2.Text); - } - - [Fact] - public void AboutDialog_OpenClose_AndSupportCommands_Work() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - - // Verify initial state - Assert.False(vm.IsAboutDialogOpen); - - // Open About Dialog - vm.OpenAboutDialogCommand.Execute(null); - Assert.True(vm.IsAboutDialogOpen); - - // Close About Dialog - vm.CloseAboutDialogCommand.Execute(null); - Assert.False(vm.IsAboutDialogOpen); - - // Direct method call - vm.OpenAboutDialog(); - Assert.True(vm.IsAboutDialogOpen); - vm.CloseAboutDialog(); - Assert.False(vm.IsAboutDialogOpen); - - // Verify Command Palette has About & Support commands - vm.OpenCommandPaletteCommand.Execute(null); - vm.FilterPaletteCommands("About"); - Assert.True(vm.FilteredPaletteCommands.Count > 0); - - vm.FilterPaletteCommands("codefrydev@gmail.com"); - Assert.True(vm.FilteredPaletteCommands.Count > 0); - - vm.FilterPaletteCommands("codefrydev.in"); - Assert.True(vm.FilteredPaletteCommands.Count > 0); - - vm.FilterPaletteCommands("Microsoft Store"); - Assert.True(vm.FilteredPaletteCommands.Count > 0); - - vm.FilterPaletteCommands("GitHub"); - Assert.True(vm.FilteredPaletteCommands.Count > 0); - - Assert.NotNull(vm.OpenMicrosoftStoreCommand); - Assert.NotNull(vm.OpenGitHubCommand); - } - - [Fact] - public void TemplateRegistry_AllDefinitions_ProduceValidDocumentsAndExport() - { - var templateService = new TemplateService(); - var allTemplates = templateService.GetAllTemplates(); - - Assert.NotEmpty(allTemplates); - Assert.True(allTemplates.Count >= 6, "Expected at least 6 default templates registered"); - - foreach (var t in allTemplates) - { - Assert.False(string.IsNullOrWhiteSpace(t.Id)); - Assert.False(string.IsNullOrWhiteSpace(t.Name)); - Assert.False(string.IsNullOrWhiteSpace(t.Description)); - Assert.False(string.IsNullOrWhiteSpace(t.IconKind)); - Assert.False(string.IsNullOrWhiteSpace(t.AccentColorHex)); - - var doc = t.Create(); - Assert.NotNull(doc); - Assert.NotEmpty(doc.Pages); - - byte[] bytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(bytes); - Assert.True(bytes.Length > 200); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(bytes, 0, 5)); - } - } - - [Fact] - public void CommunityTemplate_CustomRegistration_Works() - { - var templateService = new TemplateService(); - var customTemplate = new CustomCommunitySampleTemplate(); - - templateService.RegisterTemplate(customTemplate); - - var retrieved = templateService.CreateTemplate("custom-flyer"); - Assert.NotNull(retrieved); - Assert.Equal("Community Event Flyer", retrieved.Title); - Assert.Single(retrieved.Pages); - - var vm = new MainViewModel(_exportService, templateService, _persistenceService); - vm.CreateNewFromTemplate("custom-flyer"); - Assert.Equal("Community Event Flyer", vm.DocumentTitle); - } - - private class CustomCommunitySampleTemplate : PdfEditorApp.Templates.ITemplateDefinition - { - public string Id => "custom-flyer"; - public string Name => "Community Flyer"; - public string Description => "Sample community event flyer template"; - public string Category => "Events"; - public string IconKind => "BullhornOutline"; - public string AccentColorHex => "#E11D48"; - - public PdfDocumentModel Create() - { - var doc = new PdfDocumentModel { Title = "Community Event Flyer" }; - var page = new PdfPageModel { PageNumber = 1, Width = 800, Height = 1131 }; - page.Elements.Add(new PdfTextElement { Text = "Community Open Day 2026", FontSize = 28, IsBold = true }); - doc.Pages.Add(page); - return doc; - } - } - - [Fact] - public void TemplateGallery_FilteringAndSearch_WorksProperly() - { - var vm = new MainViewModel(_exportService, _templateService, _persistenceService); - - // Open Dialog resets query and category - vm.OpenNewDocumentDialog(); - Assert.True(vm.IsNewDocumentDialogOpen); - Assert.Equal("All", vm.SelectedTemplateCategory); - Assert.Equal("", vm.TemplateSearchQuery); - - // All visible initially - Assert.True(vm.IsAnnualReportTemplateVisible); - Assert.True(vm.IsInvoiceTemplateVisible); - Assert.True(vm.IsResumeTemplateVisible); - Assert.True(vm.IsAcademicPaperTemplateVisible); - Assert.True(vm.IsCertificateTemplateVisible); - Assert.True(vm.IsBlankTemplateVisible); - Assert.False(vm.HasNoMatchingTemplates); - - // Category filter: Career - vm.SetTemplateCategory("Career"); - Assert.True(vm.IsResumeTemplateVisible); - Assert.False(vm.IsAnnualReportTemplateVisible); - Assert.False(vm.IsInvoiceTemplateVisible); - - // Category filter: Finance - vm.SetTemplateCategory("Finance"); - Assert.True(vm.IsInvoiceTemplateVisible); - Assert.False(vm.IsResumeTemplateVisible); - - // Search query filter: "report" - vm.SetTemplateCategory("All"); - vm.TemplateSearchQuery = "report"; - Assert.True(vm.IsAnnualReportTemplateVisible); - Assert.False(vm.IsInvoiceTemplateVisible); - Assert.False(vm.IsResumeTemplateVisible); - - // Clear search - vm.ClearTemplateSearch(); - Assert.Equal("", vm.TemplateSearchQuery); - Assert.True(vm.IsResumeTemplateVisible); - - // Non-existent search query shows empty state - vm.TemplateSearchQuery = "xyznonexistenttemplate"; - Assert.True(vm.HasNoMatchingTemplates); - - // Close dialog - vm.CloseNewDocumentDialog(); - Assert.False(vm.IsNewDocumentDialogOpen); - } - - [Fact] - public void CertificateOfAchievement_RedAndGold_HasFullVectorAccentsAndMatchesReferenceImage() - { - var model = _templateService.CreateCertificateTemplate(); - Assert.NotNull(model); - Assert.Single(model.Pages); - Assert.Equal("Certificate_of_Achievement.pdf", model.Title); - - var page = model.Pages[0]; - Assert.Equal(PageOrientation.Landscape, page.Orientation); - Assert.NotEmpty(page.Elements); - - // Verify corner polygonal shapes exist - var shapes = page.Elements.OfType().ToList(); - Assert.True(shapes.Count >= 7, "Must contain all corner polygon wedges, accents, and medal badge"); - - // Verify medal ribbon badge - var medal = shapes.FirstOrDefault(s => s.ShapeType == ShapeType.MedalRibbonBadge); - Assert.NotNull(medal); - Assert.Equal("#F59E0B", medal.FillColorHex); - Assert.Equal("#990000", medal.SecondaryFillColorHex); - - // Verify typography elements - var texts = page.Elements.OfType().ToList(); - Assert.Contains(texts, t => t.Text == "CERTIFICATE" && t.TextColorHex == "#990000"); - Assert.Contains(texts, t => t.Text.Contains("OUTSTANDING ACHIEVEMENT")); - Assert.Contains(texts, t => t.Text.Contains("THIS CERTIFICATE IS PROUDLY PRESENTED TO")); - Assert.Contains(texts, t => t.Text == "John Doe" && t.FontFamily == "Great Vibes"); - Assert.Contains(texts, t => t.Text.Contains("Mathematics & Computational Science Olympiad")); - Assert.Contains(texts, t => t.Text == "Jane Doe" && t.FontFamily == "Great Vibes"); - Assert.Contains(texts, t => t.Text.Contains("Dr. Jane Doe, Ph.D.")); - Assert.Contains(texts, t => t.Text.Contains("President")); - Assert.Contains(texts, t => t.Text.Contains("Date")); - - // Verify PDF byte export - byte[] pdfBytes = _exportService.GeneratePdfBytes(model); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 2000); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(pdfBytes, 0, 5)); - } - - [Fact] - public void CertificateNavyGoldAndDiploma_ProduceValidPdfBytes() - { - var navyCert = _templateService.CreateCertificateNavyGoldTemplate(); - Assert.NotNull(navyCert); - byte[] navyBytes = _exportService.GeneratePdfBytes(navyCert); - Assert.NotNull(navyBytes); - Assert.True(navyBytes.Length > 1500); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(navyBytes, 0, 5)); - - var diploma = _templateService.CreateDiplomaAcademicTemplate(); - Assert.NotNull(diploma); - byte[] diplomaBytes = _exportService.GeneratePdfBytes(diploma); - Assert.NotNull(diplomaBytes); - Assert.True(diplomaBytes.Length > 1500); - Assert.Equal("%PDF-", Encoding.ASCII.GetString(diplomaBytes, 0, 5)); - } - - [Fact] - public void SvgShapeHelper_AllShapeTypes_GenerateValidSvgPaths() - { - foreach (ShapeType type in Enum.GetValues()) - { - string path = SvgShapeHelper.GetVectorPath(type, 200, 150, 8, "M 0,0 L 50,50 Z"); - Assert.False(string.IsNullOrWhiteSpace(path), $"Path for {type} should not be empty"); - Assert.StartsWith("M", path); - - var element = new PdfShapeElement - { - ShapeType = type, - Width = 200, - Height = 150, - FillColorHex = "#990000", - StrokeColorHex = "#F59E0B", - StrokeThickness = 2.0, - CustomPathData = type == ShapeType.CustomSvgPath ? "M 0,0 L 100,0 L 50,100 Z" : null - }; - - string svg = SvgShapeHelper.GenerateSvgMarkup(element); - Assert.Contains("", svg); - } - } - - [Fact] - public async Task ShapeElement_CustomPathDataAndColors_PersistCorrectly() - { - var model = new PdfDocumentModel { Title = "ShapePersistenceTest.pdf" }; - var page = new PdfPageModel { Width = 800, Height = 600 }; - page.Elements.Add(new PdfShapeElement - { - ShapeType = ShapeType.CustomSvgPath, - CustomPathData = "M 0,0 L 220,0 L 160,380 Z", - FillColorHex = "#990000", - StrokeColorHex = "#F59E0B", - StrokeThickness = 2.5, - SecondaryFillColorHex = "#D97706", - SecondaryStrokeColorHex = "#B45309", - X = 50, - Y = 50, - Width = 220, - Height = 380 - }); - model.Pages.Add(page); - - string tempPath = Path.Combine(Path.GetTempPath(), $"shape_test_{Guid.NewGuid():N}.frypdf"); - try - { - await _persistenceService.SaveProjectAsync(model, tempPath); - var loaded = await _persistenceService.LoadProjectAsync(tempPath); - - Assert.NotNull(loaded); - Assert.Single(loaded.Pages); - var loadedShape = Assert.IsType(loaded.Pages[0].Elements[0]); - Assert.Equal(ShapeType.CustomSvgPath, loadedShape.ShapeType); - Assert.Equal("M 0,0 L 220,0 L 160,380 Z", loadedShape.CustomPathData); - Assert.Equal("#990000", loadedShape.FillColorHex); - Assert.Equal("#F59E0B", loadedShape.StrokeColorHex); - Assert.Equal("#D97706", loadedShape.SecondaryFillColorHex); - Assert.Equal("#B45309", loadedShape.SecondaryStrokeColorHex); - } - finally - { - if (File.Exists(tempPath)) File.Delete(tempPath); - } - } - - [Fact] - public void MainViewModel_CertificateQuickInsertionCommands_FunctionProperly() - { - var vm = new MainViewModel(); - - Assert.NotNull(vm.CurrentPage); - int initialCount = vm.CurrentPage.Elements.Count; - - vm.AddMedalBadgeElement(); - Assert.Equal(initialCount + 1, vm.CurrentPage.Elements.Count); - Assert.IsType(vm.CurrentPage.Elements.Last()); - - vm.AddLaurelSealElement(); - Assert.Equal(initialCount + 2, vm.CurrentPage.Elements.Count); - - vm.AddRibbonBannerElement(); - Assert.Equal(initialCount + 3, vm.CurrentPage.Elements.Count); - - vm.AddCornerAccentElement("TopLeft"); - Assert.Equal(initialCount + 4, vm.CurrentPage.Elements.Count); - - vm.AddCornerAccentElement("BottomRight"); - Assert.Equal(initialCount + 5, vm.CurrentPage.Elements.Count); - - vm.AddSignatureBlock(); - Assert.True(vm.CurrentPage.Elements.Count >= initialCount + 9); - - vm.AddDateBlock(); - Assert.True(vm.CurrentPage.Elements.Count >= initialCount + 12); - } - - [Fact] - public void WeddingInvitationTraditionalTemplate_MatchesReferenceAndGeneratesValidPdfBytes() - { - var doc = WeddingInvitationTraditionalTemplate.GenerateDocument(); - Assert.NotNull(doc); - Assert.Single(doc.Pages); - - var page = doc.Pages[0]; - Assert.Equal(600, page.Width); - Assert.Equal(900, page.Height); - - // Assert presence of key ceremonial elements - Assert.Contains(page.Elements, e => e is PdfSvgElement svg && svg.PresetName == "MarigoldToran"); - Assert.Contains(page.Elements, e => e is PdfSvgElement svg && svg.PresetName == "GaneshaCrest"); - Assert.Contains(page.Elements, e => e is PdfSvgElement svg && svg.PresetName == "DottedFloralDivider"); - Assert.Contains(page.Elements, e => e is PdfSvgElement svg && svg.PresetName == "TraditionalDeepam"); - Assert.Contains(page.Elements, e => e is PdfSvgElement svg && svg.PresetName == "PlantainTrees"); - Assert.Contains(page.Elements, e => e is PdfTextElement txt && txt.Text.Contains("Shree Ganeshay Namah")); - Assert.Contains(page.Elements, e => e is PdfTextElement txt && txt.Text == "MUHURTHAM"); - Assert.Contains(page.Elements, e => e is PdfTextElement txt && txt.Text == "RECEPTION"); - - // Export to PDF - byte[] pdfBytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 2000); - - string header = System.Text.Encoding.ASCII.GetString(pdfBytes.Take(5).ToArray()); - Assert.Equal("%PDF-", header); - } - - [Fact] - public void WeddingInvitationRoyalFloralTemplate_GeneratesValidPdfBytes() - { - var doc = WeddingInvitationRoyalFloralTemplate.GenerateDocument(); - Assert.NotNull(doc); - Assert.Single(doc.Pages); - - var page = doc.Pages[0]; - Assert.Contains(page.Elements, e => e is PdfSvgElement svg && svg.PresetName == "BotanicalWreath"); - Assert.Contains(page.Elements, e => e is PdfQrCodeElement qr && qr.Label.Contains("RSVP")); - - byte[] pdfBytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 2000); - Assert.Equal("%PDF-", System.Text.Encoding.ASCII.GetString(pdfBytes.Take(5).ToArray())); - } - - [Fact] - public void GalaInvitationTemplate_GeneratesValidPdfBytes() - { - var doc = GalaInvitationTemplate.GenerateDocument(); - Assert.NotNull(doc); - Assert.Single(doc.Pages); - - var page = doc.Pages[0]; - Assert.Contains(page.Elements, e => e is PdfSvgElement svg && svg.PresetName == "ArtDecoFrame"); - Assert.Contains(page.Elements, e => e is PdfQrCodeElement qr && qr.Label.Contains("VIP")); - - byte[] pdfBytes = _exportService.GeneratePdfBytes(doc); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 2000); - Assert.Equal("%PDF-", System.Text.Encoding.ASCII.GetString(pdfBytes.Take(5).ToArray())); - } - - [Fact] - public void SvgOrnamentLibrary_AllPresets_GenerateValidSvgMarkup() - { - Assert.True(SvgOrnamentLibrary.Presets.Count >= 10); - - foreach (var (name, svg) in SvgOrnamentLibrary.Presets) - { - Assert.False(string.IsNullOrWhiteSpace(svg), $"Preset {name} is empty"); - Assert.StartsWith("", svg.TrimEnd()); - Assert.Contains("viewBox", svg); - - // Test tinting - string tinted = SvgOrnamentLibrary.GetSvg(name, "#FF5500"); - Assert.NotNull(tinted); - } - } - - [Fact] - public async Task SvgElement_PersistenceAndViewModelRoundTrip_Succeeds() - { - var original = new PdfSvgElement - { - X = 50, - Y = 100, - Width = 180, - Height = 180, - PresetName = "GaneshaCrest", - SvgSource = SvgOrnamentLibrary.GetGaneshaCrestSvg(), - TintColorHex = "#B45309", - KeepAspectRatio = true - }; - - var vm = new SvgElementViewModel(); - vm.LoadFromModel(original); - - Assert.Equal(50, vm.X); - Assert.Equal(100, vm.Y); - Assert.Equal(180, vm.Width); - Assert.Equal("GaneshaCrest", vm.PresetName); - Assert.Equal("#B45309", vm.TintColorHex); - Assert.False(string.IsNullOrEmpty(vm.PathGeometryData)); - - var exportedModel = Assert.IsType(vm.ToModel()); - Assert.Equal(original.PresetName, exportedModel.PresetName); - Assert.Equal(original.TintColorHex, exportedModel.TintColorHex); - - var doc = new PdfDocumentModel(); - var page = new PdfPageModel(); - page.Elements.Add(exportedModel); - doc.Pages.Add(page); - - string tempPath = Path.Combine(Path.GetTempPath(), $"svg_test_{Guid.NewGuid():N}.frypdf"); - try - { - await _persistenceService.SaveProjectAsync(doc, tempPath); - var loaded = await _persistenceService.LoadProjectAsync(tempPath); - Assert.NotNull(loaded); - var loadedSvg = Assert.IsType(loaded.Pages[0].Elements[0]); - Assert.Equal("GaneshaCrest", loadedSvg.PresetName); - Assert.Equal("#B45309", loadedSvg.TintColorHex); - } - finally - { - if (File.Exists(tempPath)) File.Delete(tempPath); - } - } - - [Fact] - public void MainViewModel_SvgQuickInsertionCommands_FunctionProperly() - { - var vm = new MainViewModel(); - Assert.NotNull(vm.CurrentPage); - int initialCount = vm.CurrentPage.Elements.Count; - - vm.AddSvgElement("GaneshaCrest"); - Assert.Equal(initialCount + 1, vm.CurrentPage.Elements.Count); - var svgEl = Assert.IsType(vm.CurrentPage.Elements.Last()); - Assert.Equal("GaneshaCrest", svgEl.PresetName); - - vm.AddOrnamentElement("MarigoldToran"); - Assert.Equal(initialCount + 2, vm.CurrentPage.Elements.Count); - - vm.AddOrnamentElement("BotanicalWreath"); - Assert.Equal(initialCount + 3, vm.CurrentPage.Elements.Count); - } -} - - - diff --git a/tests/PdfEditorApp.Tests/PdfImportAndViewerTests.cs b/tests/PdfEditorApp.Tests/PdfImportAndViewerTests.cs deleted file mode 100644 index a1735f0..0000000 --- a/tests/PdfEditorApp.Tests/PdfImportAndViewerTests.cs +++ /dev/null @@ -1,1219 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Templates; -using PdfEditorApp.ViewModels; -using UglyToad.PdfPig.Rendering.Skia; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PdfImportAndViewerTests -{ - private readonly IPdfExportService _exportService = new PdfExportService(); - private readonly ITemplateService _templateService = new TemplateService(); - private readonly IPdfImportService _importService = new PdfImportService(); - private readonly IProjectPersistenceService _persistenceService = new ProjectPersistenceService(); - - [Fact] - public async Task PdfImportService_ImportsBinaryPdfBytes_AccuratelyExtractsPagesAndElements() - { - // 1. Create a rich 3-page annual report PDF - var sourceModel = _templateService.CreateAnnualReportTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 0); - - // 2. Import into editable PdfDocumentModel - var importedModel = await _importService.ImportPdfBytesAsync(pdfBytes, "Annual_Report.pdf"); - - Assert.NotNull(importedModel); - Assert.False(string.IsNullOrWhiteSpace(importedModel.Title)); - Assert.Equal(3, importedModel.Pages.Count); - - // Verify Page 1 - var page1 = importedModel.Pages[0]; - Assert.True(page1.Width > 0); - Assert.True(page1.Height > 0); - Assert.NotEmpty(page1.Elements); - - // Verify editable text elements were extracted with crisp metrics - var textElements = page1.Elements.OfType().ToList(); - Assert.NotEmpty(textElements); - Assert.Contains(textElements, t => !string.IsNullOrWhiteSpace(t.Text)); - - // Verify no duplicate background canvas image was injected on digital vector PDF - var bgCanvas = page1.Elements.OfType().FirstOrDefault(e => e.AltText.Contains("Background Canvas")); - Assert.Null(bgCanvas); - } - - [Fact] - public async Task ProjectPersistenceService_LoadsRealPdfFile_WithoutJsonDeserializationErrors() - { - // 1. Create temporary PDF file - string tempPdfPath = Path.Combine(Path.GetTempPath(), $"FryPdf_Test_{Guid.NewGuid():N}.pdf"); - try - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - await File.WriteAllBytesAsync(tempPdfPath, pdfBytes); - - // 2. Load through ProjectPersistenceService (which previously threw '%' is an invalid start of a value error) - var loadedModel = await _persistenceService.LoadProjectAsync(tempPdfPath); - - Assert.NotNull(loadedModel); - Assert.Single(loadedModel.Pages); - Assert.NotEmpty(loadedModel.Pages[0].Elements); - } - finally - { - if (File.Exists(tempPdfPath)) File.Delete(tempPdfPath); - } - } - - [Fact] - public async Task ProjectPersistenceService_SavesAndLoadsJsonProject_RoundtripsAccurately() - { - string tempProjPath = Path.Combine(Path.GetTempPath(), $"FryPdf_Project_{Guid.NewGuid():N}.frypdf"); - try - { - var original = _templateService.CreateResumeTemplate(); - await _persistenceService.SaveProjectAsync(original, tempProjPath); - - var loaded = await _persistenceService.LoadProjectAsync(tempProjPath); - Assert.NotNull(loaded); - Assert.Equal(original.Title, loaded.Title); - Assert.Equal(original.Pages.Count, loaded.Pages.Count); - Assert.Equal(original.Pages[0].Elements.Count, loaded.Pages[0].Elements.Count); - } - finally - { - if (File.Exists(tempProjPath)) File.Delete(tempProjPath); - } - } - - [Fact] - public async Task PdfViewerViewModel_LoadsPdfBytes_RendersPagesAndExtractsMetadata() - { - var sourceModel = _templateService.CreateCertificateTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Certificate_Of_Excellence.pdf"); - - Assert.True(viewer.HasDocument); - Assert.Equal("Certificate_Of_Excellence.pdf", viewer.DocumentTitle); - Assert.Equal(1, viewer.TotalPagesCount); - Assert.Equal(1, viewer.CurrentPageNumber); - Assert.NotEmpty(viewer.Pages); - Assert.NotEmpty(viewer.MetadataItems); - - // Verify page details - var firstPage = viewer.Pages[0]; - Assert.Equal(1, firstPage.PageNumber); - Assert.True(firstPage.WidthPoints > 0); - Assert.True(firstPage.HeightPoints > 0); - } - - [Fact] - public async Task PdfViewerViewModel_Search_FindsMatchesAcrossPages() - { - var sourceModel = _templateService.CreateAnnualReportTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Annual_Report.pdf"); - - // Execute text search for a common business word in the annual report - viewer.SearchQuery = "Revenue"; - - // If "Revenue" is present in the document text, verify search results are generated - if (viewer.SearchResults.Count > 0) - { - Assert.True(viewer.MatchCount > 0); - Assert.True(viewer.CurrentMatchIndex >= 1); - - // Test Next & Previous Match navigation - int initialIndex = viewer.CurrentMatchIndex; - viewer.NextMatch(); - Assert.True(viewer.CurrentMatchIndex >= 1); - viewer.PreviousMatch(); - Assert.True(viewer.CurrentMatchIndex >= 1); - } - } - - [Fact] - public async Task PdfViewerViewModel_ZoomControls_UpdatesZoomLevelAccurately() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - - // Initial Zoom: 1.0 (100%) - Assert.Equal(1.0, viewer.ZoomLevel); - Assert.Equal("100%", viewer.ZoomPercentageText); - - // Zoom In - viewer.ZoomIn(); - Assert.True(viewer.ZoomLevel > 1.0); - - // Zoom Out - viewer.ZoomOut(); - viewer.ZoomOut(); - Assert.True(viewer.ZoomLevel < 1.0); - - // Reset Zoom - viewer.ResetZoom(); - Assert.Equal(1.0, viewer.ZoomLevel); - Assert.Equal("100%", viewer.ZoomPercentageText); - - // Fit Width & Fit Page - viewer.FitToWidth(); - Assert.Equal(1.35, viewer.ZoomLevel); - - viewer.FitToPage(); - Assert.Equal(0.95, viewer.ZoomLevel); - } - - [Fact] - public async Task PdfViewerViewModel_PageRotation_RotatesClockwiseAndCounterClockwise() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - Assert.NotNull(viewer.SelectedPage); - - int initialRotation = viewer.SelectedPage.RotationAngle; - - // Rotate CW - viewer.RotateClockwise(); - Assert.Equal((initialRotation + 90) % 360, viewer.SelectedPage.RotationAngle); - - // Rotate CCW - viewer.RotateCounterClockwise(); - Assert.Equal(initialRotation, viewer.SelectedPage.RotationAngle); - } - - [Fact] - public async Task PdfViewerViewModel_Annotations_AddsAndDeletesAnnotationsSuccessfully() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - - Assert.Empty(viewer.Annotations); - Assert.False(viewer.HasAnnotations); - - // Add Highlight - viewer.AddHighlightAnnotation(); - Assert.Single(viewer.Annotations); - Assert.True(viewer.HasAnnotations); - Assert.Equal("Highlight", viewer.Annotations[0].Type); - - // Add Stamp - viewer.AddStamp("APPROVED"); - Assert.Equal(2, viewer.Annotations.Count); - Assert.Equal("Stamp", viewer.Annotations[1].Type); - Assert.Contains("APPROVED", viewer.Annotations[1].Content); - - // Add Sticky Note - viewer.NewNoteText = "Needs sign-off from finance."; - viewer.ConfirmAddNote(); - Assert.Equal(3, viewer.Annotations.Count); - Assert.Equal("StickyNote", viewer.Annotations[2].Type); - - // Delete Annotation - var note = viewer.Annotations[2]; - viewer.DeleteAnnotation(note); - Assert.Equal(2, viewer.Annotations.Count); - } - - [Fact] - public async Task PdfViewerViewModel_ExtractsWordsAndLinesGeometry_AccuratelyOnPageLoad() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - - var page1 = viewer.SelectedPage; - Assert.NotNull(page1); - Assert.NotEmpty(page1.Words); - Assert.NotEmpty(page1.TextLines); - - // Verify words geometry - foreach (var word in page1.Words) - { - Assert.False(string.IsNullOrWhiteSpace(word.Text)); - Assert.True(word.Bounds.Width > 0); - Assert.True(word.Bounds.Height > 0); - Assert.True(word.Bounds.Left >= 0); - Assert.True(word.Bounds.Top >= 0); - } - - // Verify lines geometry - foreach (var line in page1.TextLines) - { - Assert.False(string.IsNullOrWhiteSpace(line.Text)); - Assert.NotEmpty(line.Words); - Assert.True(line.Bounds.Width > 0); - Assert.True(line.Bounds.Height > 0); - } - } - - [Fact] - public async Task PdfViewerViewModel_WordAndLineSelection_SetsAccurateSelectionAndBounds() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - - var page1 = viewer.SelectedPage!; - Assert.NotEmpty(page1.Words); - var targetWord = page1.Words[0]; - - // 1. Select Word - page1.SelectWord(targetWord); - Assert.True(page1.HasSelection); - Assert.Equal(targetWord.Text, page1.SelectedText); - Assert.Single(page1.SelectionRects); - Assert.Equal(targetWord.Bounds, page1.SelectionRects[0]); - - // 2. Select Line - var targetLine = page1.TextLines[0]; - page1.SelectLine(targetLine); - Assert.True(page1.HasSelection); - Assert.Equal(targetLine.Text, page1.SelectedText); - Assert.Single(page1.SelectionRects); - - // 3. Clear Selection - page1.ClearSelection(); - Assert.False(page1.HasSelection); - Assert.Empty(page1.SelectedText); - Assert.Empty(page1.SelectionRects); - } - - [Fact] - public async Task PdfViewerViewModel_DragRangeSelection_SelectsMultiLineText() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - - var page1 = viewer.SelectedPage!; - Assert.True(page1.TextLines.Count >= 2); - - var line1 = page1.TextLines[0]; - var line2 = page1.TextLines[1]; - - // Drag from first line middle to second line middle - var startPoint = new Avalonia.Point(line1.Bounds.Left + 5, line1.Bounds.Center.Y); - var endPoint = new Avalonia.Point(line2.Bounds.Right - 5, line2.Bounds.Center.Y); - - page1.SetSelectionRange(startPoint, endPoint); - - Assert.True(page1.HasSelection); - Assert.NotEmpty(page1.SelectedText); - Assert.True(page1.SelectionRects.Count >= 2); - } - - [Fact] - public async Task PdfViewerViewModel_SelectAll_SelectsAllPageText() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - - viewer.SelectAllPageText(); - - Assert.True(viewer.HasTextSelection); - Assert.Equal(viewer.SelectedPage!.ExtractedText, viewer.ActiveSelectedText); - Assert.NotEmpty(viewer.SelectedPage.SelectionRects); - - viewer.ClearSelection(); - Assert.False(viewer.HasTextSelection); - Assert.Empty(viewer.ActiveSelectedText); - } - - [Fact] - public async Task PdfViewerViewModel_HighlightSelectedText_CreatesAnnotationWithExactRectangles() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - - var page1 = viewer.SelectedPage!; - var firstWord = page1.Words[0]; - page1.SelectWord(firstWord); - viewer.ActiveSelectedText = page1.SelectedText; - viewer.ActiveSelectedPageNumber = page1.PageNumber; - viewer.HasTextSelection = true; - - viewer.HighlightSelectedText("#A7F3D0"); - - Assert.True(viewer.HasAnnotations); - var hlAnn = viewer.Annotations.FirstOrDefault(a => a.Type == "Highlight"); - Assert.NotNull(hlAnn); - Assert.Equal("#A7F3D0", hlAnn.ColorHex); - Assert.Equal(firstWord.Text, hlAnn.Content); - Assert.NotEmpty(hlAnn.HighlightRects); - Assert.Equal(firstWord.Bounds, hlAnn.HighlightRects[0]); - - // Selection should be cleared after highlighting - Assert.False(page1.HasSelection); - Assert.False(viewer.HasTextSelection); - } - - [Fact] - public async Task PdfViewerViewModel_SearchSelectedText_SetsQueryAndNavigatesMatches() - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentBytesAsync(pdfBytes, "Invoice.pdf"); - - var page1 = viewer.SelectedPage!; - var firstWord = page1.Words[0]; - page1.SelectWord(firstWord); - viewer.ActiveSelectedText = firstWord.Text; - - viewer.SearchSelectedText(); - - Assert.True(viewer.IsSearchBarVisible); - Assert.Equal(firstWord.Text, viewer.SearchQuery); - Assert.True(viewer.HasSearchResults); - Assert.True(viewer.TotalMatchesCount >= 1); - } - - [Fact] - public async Task PdfViewerViewModel_EditInStudioBridge_TriggersEventWithFilePath() - { - string tempPdfPath = Path.Combine(Path.GetTempPath(), $"FryPdf_Studio_Bridge_{Guid.NewGuid():N}.pdf"); - try - { - var sourceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - await File.WriteAllBytesAsync(tempPdfPath, pdfBytes); - - var viewer = new PdfViewerViewModel(); - await viewer.LoadDocumentAsync(tempPdfPath); - - string? requestedPath = null; - WeakReferenceMessenger.Default.Register(this, (r, m) => requestedPath = m.FilePath); - try - { - viewer.EditInStudio(); - Assert.NotNull(requestedPath); - Assert.Equal(tempPdfPath, requestedPath); - } - finally - { - WeakReferenceMessenger.Default.Unregister(this); - } - } - finally - { - if (File.Exists(tempPdfPath)) File.Delete(tempPdfPath); - } - } - - [Fact] - public void ImageElementViewModel_Base64Data_LoadsAndRendersBitmapSuccessfully() - { - // 1. Generate a valid 1x1 PNG image as base64 - byte[] validPngBytes = new byte[] - { - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, - 0x0D, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x60, 0x60, 0x60, 0x00, - 0x00, 0x00, 0x04, 0x00, 0x01, 0x5D, 0x36, 0xBD, 0x7E, 0x00, 0x00, 0x00, - 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 - }; - string base64 = Convert.ToBase64String(validPngBytes); - - var model = new PdfImageElement - { - X = 50, - Y = 50, - Width = 100, - Height = 100, - Base64Data = base64, - AltText = "Test Base64 Image" - }; - - var vm = new PdfEditorApp.ViewModels.ElementViewModels.ImageElementViewModel(); - vm.LoadFromModel(model); - - Assert.Equal(base64, vm.Base64Data); - Assert.Equal("Test Base64 Image", vm.DisplayName); - - var roundTripped = (PdfImageElement)vm.ToModel(); - Assert.Equal(base64, roundTripped.Base64Data); - Assert.Equal("Test Base64 Image", roundTripped.AltText); - } - - [Fact] - public async Task PdfImportService_ImportsPdf_PageBackgroundAndElementsHaveCleanRendering() - { - var sourceModel = _templateService.CreateCertificateTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(sourceModel); - - var imported = await _importService.ImportPdfBytesAsync(pdfBytes, "Certificate.pdf"); - Assert.NotNull(imported); - Assert.Single(imported.Pages); - - var page = imported.Pages[0]; - Assert.NotEmpty(page.Elements); - - // Verify that digital vector PDF does NOT add duplicate raster background underlay - var bgCanvas = page.Elements.OfType().FirstOrDefault(e => e.AltText.Contains("Background Canvas")); - Assert.Null(bgCanvas); - - // Verify text elements are extracted - var textElements = page.Elements.OfType().ToList(); - Assert.NotEmpty(textElements); - - var pageVm = new PageViewModel(); - pageVm.LoadFromModel(page); - Assert.NotEmpty(pageVm.Elements); - } - - [Fact] - public async Task PdfImportService_MultiColumnInvoice_ExtractsSeparateTextBlocks() - { - var invoiceModel = _templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(invoiceModel); - - var imported = await _importService.ImportPdfBytesAsync(pdfBytes, "Invoice_MultiColumn.pdf"); - Assert.NotNull(imported); - Assert.Single(imported.Pages); - - var textElements = imported.Pages[0].Elements.OfType().ToList(); - Assert.NotEmpty(textElements); - Assert.True(textElements.Count >= 2); - - // Verify that individual text blocks have non-zero dimensions and non-empty text - foreach (var txt in textElements) - { - Assert.False(string.IsNullOrWhiteSpace(txt.Text)); - Assert.True(txt.Width > 0); - Assert.True(txt.Height > 0); - } - } - - [Fact] - public async Task DeconstructSampleFiles_Investigate() - { - string baseDir = AppContext.BaseDirectory; - string rootDir = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..")); - string sample1Path = Path.Combine(rootDir, "sample1.pdf"); - string sample2Path = Path.Combine(rootDir, "sample2.pdf"); - string mathChapterPath = Path.Combine(rootDir, "Class_6_Math_Chapter_1_Pattern_In_Mathematics.pdf"); - - if (!File.Exists(sample1Path)) - { - return; - } - - var doc1 = await _importService.ImportPdfAsync(sample1Path); - - // Assertions for Sample 1 (Government ID Card): - Assert.True(doc1.Pages.Count >= 1); - var page1 = doc1.Pages[0]; - Assert.True(page1.Elements.Count > 0); - - // Check vertical rotated text elements extracted cleanly - var rotatedElements = page1.Elements.OfType().Where(t => t.Rotation == 270.0 || t.Rotation == 90.0).ToList(); - Assert.NotEmpty(rotatedElements); - Assert.Contains(rotatedElements, r => r.Text.Length > 5); - - // Assert vertical divider elements extracted (solid border lines along center fold) - var verticalDividers = page1.Elements.OfType().Where(d => d.IsVertical).ToList(); - Assert.NotEmpty(verticalDividers); - Assert.Contains(verticalDividers, d => d.Height > 400); - - // Check horizontal text blocks have valid bounds and content - var horizontalTexts = page1.Elements.OfType().Where(t => t.Rotation == 0).ToList(); - Assert.True(horizontalTexts.Count >= 10, $"Expected multiple horizontal text blocks. Found {horizontalTexts.Count}."); - Assert.All(horizontalTexts, h => - { - Assert.False(string.IsNullOrWhiteSpace(h.Text)); - Assert.True(h.Width > 0); - Assert.True(h.Height > 0); - }); - - // Assert formatted 12-digit ID token is cleanly extracted and positioned above bottom banner (Y < 715) - var bottomIdNumber = horizontalTexts.FirstOrDefault(t => - System.Text.RegularExpressions.Regex.IsMatch(t.Text, @"\b\d{4}\s\d{4}\s\d{4}\b") && t.Y > 680); - Assert.NotNull(bottomIdNumber); - Assert.True(bottomIdNumber.Y < 715.0, $"Expected bottom ID number Y < 715, actual={bottomIdNumber.Y}"); - - // Check that text elements with multi-character content are extracted - Assert.Contains(horizontalTexts, h => h.Text.Length >= 4); - - // Assertions for Sample 2 (Arduino Cheat Sheet): - if (File.Exists(sample2Path)) - { - var doc2 = await _importService.ImportPdfAsync(sample2Path); - Assert.True(doc2.Pages.Count >= 1); - var s2Page1 = doc2.Pages[0]; - - // Verify title and headers extracted - var titleText = s2Page1.Elements.OfType().FirstOrDefault(t => t.Text.Contains("Arduino", StringComparison.OrdinalIgnoreCase) || t.Text.Contains("Cheat Sheet", StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(titleText); - - // Verify shapes / container cards extracted - var shapes2 = s2Page1.Elements.OfType().ToList(); - Assert.True(shapes2.Count >= 4, $"Expected multiple container cards and shapes in Arduino cheat sheet. Found {shapes2.Count}."); - - // Verify section headings (Structure & Flow, Operators, Built-in Functions, Libraries) - var headings = s2Page1.Elements.OfType().Where(t => - t.Text.Contains("Structure & Flow") || - t.Text.Contains("Operators") || - t.Text.Contains("Built-in Functions") || - t.Text.Contains("Libraries")).ToList(); - Assert.NotEmpty(headings); - } - - // Assertions for Textbook Chapter: - if (File.Exists(mathChapterPath)) - { - var doc3 = await _importService.ImportPdfAsync(mathChapterPath); - Assert.True(doc3.Pages.Count >= 1); - var s3Page1 = doc3.Pages[0]; - - // Verify vector shapes and dividers extracted - var shapes3 = s3Page1.Elements.OfType().ToList(); - var dividers3 = s3Page1.Elements.OfType().ToList(); - Assert.True(shapes3.Count > 0 || dividers3.Count > 0); - - // Verify watermark image is locked with low ZIndex - var watermarks = s3Page1.Elements.OfType().Where(img => img.IsLocked).ToList(); - Assert.NotEmpty(watermarks); - Assert.True(watermarks[0].Opacity <= 0.35); - Assert.Equal(0, watermarks[0].ZIndex); - - // Verify header text extracted with Poppins / display font and high contrast - var mathTitle = s3Page1.Elements.OfType().FirstOrDefault(t => t.Text.Contains("MATHEMATICS", StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(mathTitle); - Assert.Equal("Poppins", mathTitle.FontFamily); - - // Verify section heading extracted - var sectionHeading = s3Page1.Elements.OfType().FirstOrDefault(t => t.Text.Contains("1.1 What is Mathematics", StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(sectionHeading); - } - } - - [Fact] - public void BooleanToStretchConverter_ConvertsCorrectly() - { - var converter = PdfEditorApp.Converters.BooleanToStretchConverter.Instance; - var uniform = converter.Convert(true, typeof(Avalonia.Media.Stretch), null, System.Globalization.CultureInfo.InvariantCulture); - var fill = converter.Convert(false, typeof(Avalonia.Media.Stretch), null, System.Globalization.CultureInfo.InvariantCulture); - - Assert.Equal(Avalonia.Media.Stretch.Uniform, uniform); - Assert.Equal(Avalonia.Media.Stretch.Fill, fill); - } - - [Fact] - public void RotationToTransformConverter_ConvertsAnglesToRotateTransformsAccurately() - { - var converter = PdfEditorApp.Converters.RotationToTransformConverter.Instance; - - // 0 degrees / zero values return null (no transform overhead) - Assert.Null(converter.Convert(0.0, typeof(Avalonia.Media.ITransform), null, System.Globalization.CultureInfo.InvariantCulture)); - Assert.Null(converter.Convert(0, typeof(Avalonia.Media.ITransform), null, System.Globalization.CultureInfo.InvariantCulture)); - Assert.Null(converter.Convert(null, typeof(Avalonia.Media.ITransform), null, System.Globalization.CultureInfo.InvariantCulture)); - - // 270 degrees (vertical marginalia text) - var t270 = converter.Convert(270.0, typeof(Avalonia.Media.ITransform), null, System.Globalization.CultureInfo.InvariantCulture); - Assert.NotNull(t270); - var rt270 = Assert.IsType(t270); - Assert.Equal(270.0, rt270.Angle); - - // 90 degrees - var t90 = converter.Convert(90.0, typeof(Avalonia.Media.ITransform), null, System.Globalization.CultureInfo.InvariantCulture); - Assert.NotNull(t90); - var rt90 = Assert.IsType(t90); - Assert.Equal(90.0, rt90.Angle); - - // ConvertBack roundtrips - Assert.Equal(270.0, (double)(converter.ConvertBack(rt270, typeof(double), null, System.Globalization.CultureInfo.InvariantCulture) ?? 0.0)); - } - - [Fact] - public async Task PageViewModel_LoadsDeconstructedDocument_AllowsEditingAndExportRoundtrip() - { - var templateService = new TemplateService(); - var exportService = new PdfExportService(); - - var originalDoc = templateService.CreateAnnualReportTemplate(); - byte[] pdfBytes = await exportService.ExportToBytesAsync(originalDoc); - - var doc = await _importService.ImportPdfBytesAsync(pdfBytes, "Annual_Report.pdf"); - Assert.NotEmpty(doc.Pages); - - var pageVm = new PdfEditorApp.ViewModels.PageViewModel(); - pageVm.LoadFromModel(doc.Pages[0]); - - Assert.NotEmpty(pageVm.Elements); - - // Find title text element - var titleVm = pageVm.Elements.OfType().FirstOrDefault(t => t.Text.Contains("ANNUAL CORPORATE REPORT", StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(titleVm); - - // Edit text - titleVm.Text = "ADVANCED ANNUAL REPORT"; - titleVm.FontSize = 28.0; - titleVm.TextColorHex = "#2563EB"; - - // Verify ToModel preserves modifications - var roundtripModel = pageVm.ToModel(); - var roundtripTitle = roundtripModel.Elements.OfType().FirstOrDefault(t => t.Text == "ADVANCED ANNUAL REPORT"); - Assert.NotNull(roundtripTitle); - Assert.Equal(28.0, roundtripTitle.FontSize); - Assert.Equal("#2563EB", roundtripTitle.TextColorHex); - } - - [Fact] - public async Task AnnualReport_ExportAndImport_PreservesAllElementsLosslessly() - { - var templateService = new TemplateService(); - var exportService = new PdfExportService(); - - var originalDoc = templateService.CreateAnnualReportTemplate(); - Assert.Equal(3, originalDoc.Pages.Count); - - byte[] pdfBytes = await exportService.ExportToBytesAsync(originalDoc); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 0); - - var importedDoc = await _importService.ImportPdfBytesAsync(pdfBytes, "Annual_Report.pdf"); - Assert.NotNull(importedDoc); - Assert.Equal(3, importedDoc.Pages.Count); - - for (int pIdx = 0; pIdx < originalDoc.Pages.Count; pIdx++) - { - var origPage = originalDoc.Pages[pIdx]; - var impPage = importedDoc.Pages[pIdx]; - - Assert.Equal(origPage.Elements.Count, impPage.Elements.Count); - - for (int eIdx = 0; eIdx < origPage.Elements.Count; eIdx++) - { - var origEl = origPage.Elements[eIdx]; - var impEl = impPage.Elements[eIdx]; - - Assert.Equal(origEl.GetType(), impEl.GetType()); - Assert.Equal(origEl.X, impEl.X); - Assert.Equal(origEl.Y, impEl.Y); - Assert.Equal(origEl.Width, impEl.Width); - Assert.Equal(origEl.Height, impEl.Height); - - if (origEl is PdfTableElement origTable && impEl is PdfTableElement impTable) - { - Assert.Equal(origTable.Headers.Count, impTable.Headers.Count); - Assert.Equal(origTable.Rows.Count, impTable.Rows.Count); - } - else if (origEl is PdfChartElement origChart && impEl is PdfChartElement impChart) - { - Assert.Equal(origChart.ChartType, impChart.ChartType); - Assert.Equal(origChart.Title, impChart.Title); - } - else if (origEl is PdfTextElement origText && impEl is PdfTextElement impText) - { - Assert.Equal(origText.Text, impText.Text); - Assert.Equal(origText.FontSize, impText.FontSize); - Assert.Equal(origText.TextColorHex, impText.TextColorHex); - } - } - } - } - - [Fact] - public async Task Invoice_ExportAndImport_PreservesTablesAndFormulasLosslessly() - { - var templateService = new TemplateService(); - var exportService = new PdfExportService(); - - var originalDoc = templateService.CreateInvoiceTemplate(); - byte[] pdfBytes = await exportService.ExportToBytesAsync(originalDoc); - - var importedDoc = await _importService.ImportPdfBytesAsync(pdfBytes, "Invoice.pdf"); - Assert.NotNull(importedDoc); - Assert.Single(importedDoc.Pages); - - var origTable = originalDoc.Pages[0].Elements.OfType().FirstOrDefault(); - var impTable = importedDoc.Pages[0].Elements.OfType().FirstOrDefault(); - - Assert.NotNull(origTable); - Assert.NotNull(impTable); - Assert.Equal(origTable.Headers.Count, impTable.Headers.Count); - Assert.Equal(origTable.Rows.Count, impTable.Rows.Count); - } - - [Fact] - public async Task GenerateVisualComparison_SideBySide_SavesArtifacts() - { - string? envArtifactDir = Environment.GetEnvironmentVariable("FRYPDF_ARTIFACT_DIR"); - string artifactDir = !string.IsNullOrWhiteSpace(envArtifactDir) - ? envArtifactDir - : Path.Combine(AppContext.BaseDirectory, "VisualArtifacts"); - Directory.CreateDirectory(artifactDir); - - string baseDir = AppContext.BaseDirectory; - string rootDir = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..")); - string sampleDir = Path.Combine(rootDir, "sample"); - if (Directory.Exists(sampleDir)) - { - var pdfFiles = Directory.GetFiles(sampleDir, "*.pdf").OrderBy(f => f).ToList(); - foreach (var pdfFile in pdfFiles) - { - string fileName = Path.GetFileNameWithoutExtension(pdfFile); - string safeName = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars())).Replace(" ", "_").Replace("(", "").Replace(")", ""); - await GenerateComparisonForPdf(pdfFile, Path.Combine(artifactDir, $"{safeName}_side_by_side.png"), fileName, 1); - } - } - } - - [Fact] - public async Task ImportPdfBytesAsync_GcdOfferLetter_CorrectlyExtractsTableAndPreservesLBracketBorders() - { - string baseDir = AppContext.BaseDirectory; - string rootDir = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..")); - string offerLetterPath = Path.Combine(rootDir, "sample", "GCD Offer Letter - Aayush Bhardwaj (Technology).pdf"); - if (!File.Exists(offerLetterPath)) return; - - byte[] pdfBytes = await File.ReadAllBytesAsync(offerLetterPath); - var docModel = await _importService.ImportPdfBytesAsync(pdfBytes, "GCD Offer Letter"); - - Assert.Equal(2, docModel.Pages.Count); - - var page1 = docModel.Pages[0]; - // 1. Verify compensation table text elements are extracted cleanly - var textElements = page1.Elements.OfType().ToList(); - Assert.Contains(textElements, t => t.Text.Contains("Components", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(textElements, t => t.Text.Contains("Basic", StringComparison.OrdinalIgnoreCase)); - Assert.Contains(textElements, t => t.Text.Contains("Total Gross", StringComparison.OrdinalIgnoreCase)); - - // 2. Verify non-rectangular polygon border is extracted as CustomSvgPath shape, not flattened to a solid rectangle - var shapes = page1.Elements.OfType().ToList(); - var customSvgShapes = shapes.Where(s => s.ShapeType == ShapeType.CustomSvgPath).ToList(); - Assert.NotEmpty(customSvgShapes); - Assert.All(customSvgShapes, s => Assert.False(string.IsNullOrWhiteSpace(s.CustomPathData))); - - // 3. Verify no giant solid rectangle covering the page area (> 50,000 sq pt) - Assert.DoesNotContain(shapes, s => s.ShapeType == ShapeType.Rectangle && s.Width * s.Height > 50000); - } - - private async Task GenerateComparisonForPdf(string pdfPath, string outputPath, string title, int pageIndex = 1) - { - byte[] pdfBytes = await File.ReadAllBytesAsync(pdfPath); - var docModel = await _importService.ImportPdfBytesAsync(pdfBytes, Path.GetFileName(pdfPath)); - if (docModel.Pages.Count < pageIndex) return; - - float scale = 1.5f; - - // 1. Render Original PDF via PdfPig + Skia - using var rawDoc = UglyToad.PdfPig.PdfDocument.Open(pdfBytes); - PdfPigExtensions.AddSkiaPageFactory(rawDoc); - using var origStream = PdfPigExtensions.GetPageAsPng(rawDoc, pageIndex, scale, 100); - using var origSkData = SkiaSharp.SKData.CreateCopy(origStream.ToArray()); - using var origBitmap = SkiaSharp.SKBitmap.Decode(origSkData); - - // 2. Render Deconstructed FryPDF PageModel via Skia - var deconstructedPage = docModel.Pages[pageIndex - 1]; - using var reconBitmap = RenderPageModelToSkiaBitmap(deconstructedPage, scale); - - // 3. Compose Side-by-Side Image - int bannerH = 70; - int pw = origBitmap.Width; - int ph = origBitmap.Height; - int rw = reconBitmap.Width; - int rh = reconBitmap.Height; - int contentW = pw + rw + 20; // 20px gutter - int contentH = Math.Max(ph, rh); - - int totalW = contentW + 40; // 20px padding left/right - int totalH = bannerH + contentH + 40; // 20px padding bottom - - using var comparisonBitmap = new SkiaSharp.SKBitmap(totalW, totalH); - using var canvas = new SkiaSharp.SKCanvas(comparisonBitmap); - - // Dark Background - canvas.Clear(new SkiaSharp.SKColor(15, 23, 42)); // #0F172A - - // Banner Header - using var titlePaint = new SkiaSharp.SKPaint - { - Color = SkiaSharp.SKColors.White, - IsAntialias = true - }; - using var titleTypeface = SkiaSharp.SKTypeface.FromFamilyName("Segoe UI", SkiaSharp.SKFontStyleWeight.Bold, SkiaSharp.SKFontStyleWidth.Normal, SkiaSharp.SKFontStyleSlant.Upright); - using var titleFont = new SkiaSharp.SKFont(titleTypeface, 20); - canvas.DrawText($"Side-by-Side Visual Verification: {title}", 24, 34, titleFont, titlePaint); - - using var subFont = new SkiaSharp.SKFont(titleTypeface, 12); - using var subPaint = new SkiaSharp.SKPaint { Color = new SkiaSharp.SKColor(148, 163, 184), IsAntialias = true }; - canvas.DrawText($"Elements: {deconstructedPage.Elements.Count} ({deconstructedPage.Elements.OfType().Count()} images, {deconstructedPage.Elements.OfType().Count()} text blocks, {deconstructedPage.Elements.OfType().Count()} shapes, {deconstructedPage.Elements.OfType().Count()} svgs, {deconstructedPage.Elements.OfType().Count()} tables) | Canvas Size: {deconstructedPage.Width:F0}x{deconstructedPage.Height:F0} pt", 24, 54, subFont, subPaint); - - // Labels - float leftX = 20; - float rightX = 20 + pw + 20; - float topY = bannerH; - - using var colLabelFont = new SkiaSharp.SKFont(titleTypeface, 13); - using var leftLabelPaint = new SkiaSharp.SKPaint { Color = new SkiaSharp.SKColor(56, 189, 248), IsAntialias = true }; - using var rightLabelPaint = new SkiaSharp.SKPaint { Color = new SkiaSharp.SKColor(74, 222, 128), IsAntialias = true }; - - // Draw Images - canvas.DrawBitmap(origBitmap, leftX, topY); - canvas.DrawBitmap(reconBitmap, rightX, topY); - - // Borders - using var borderPaint = new SkiaSharp.SKPaint - { - Color = new SkiaSharp.SKColor(51, 65, 85), - Style = SkiaSharp.SKPaintStyle.Stroke, - StrokeWidth = 2 - }; - canvas.DrawRect(leftX, topY, pw, ph, borderPaint); - canvas.DrawRect(rightX, topY, rw, rh, borderPaint); - - // Save comparison image - using var outImage = SkiaSharp.SKImage.FromBitmap(comparisonBitmap); - using var outData = outImage.Encode(SkiaSharp.SKEncodedImageFormat.Png, 95); - using var outStream = File.OpenWrite(outputPath); - outData.SaveTo(outStream); - - Console.WriteLine($"[Visual Verification] Saved side-by-side comparison to {outputPath} ({totalW}x{totalH} px)"); - } - - private static SkiaSharp.SKBitmap RenderPageModelToSkiaBitmap(PdfPageModel page, float scale) - { - int w = (int)Math.Max(100, page.Width * scale); - int h = (int)Math.Max(100, page.Height * scale); - var bitmap = new SkiaSharp.SKBitmap(w, h); - using var canvas = new SkiaSharp.SKCanvas(bitmap); - - var bgPaint = new SkiaSharp.SKPaint - { - Color = SkiaSharp.SKColor.TryParse(page.BackgroundColorHex, out var bgC) ? bgC : SkiaSharp.SKColors.White, - Style = SkiaSharp.SKPaintStyle.Fill - }; - canvas.DrawRect(0, 0, w, h, bgPaint); - - var sorted = page.Elements.OrderBy(e => e.ZIndex).ThenBy(e => e.Y).ThenBy(e => e.X).ToList(); - foreach (var el in sorted) - { - canvas.Save(); - - float ex = (float)el.X * scale; - float ey = (float)el.Y * scale; - float ew = (float)el.Width * scale; - float eh = (float)el.Height * scale; - - if (el.Rotation != 0) - { - canvas.RotateDegrees((float)el.Rotation, ex + (ew / 2f), ey + (eh / 2f)); - } - - byte alpha = (byte)Math.Clamp((int)(el.Opacity * 255), 0, 255); - - if (el is PdfImageElement img && !string.IsNullOrEmpty(img.Base64Data)) - { - try - { - byte[] imgBytes = Convert.FromBase64String(img.Base64Data); - using var skData = SkiaSharp.SKData.CreateCopy(imgBytes); - using var skImg = SkiaSharp.SKImage.FromEncodedData(skData); - if (skImg != null) - { - using var imgPaint = new SkiaSharp.SKPaint { Color = new SkiaSharp.SKColor(255, 255, 255, alpha) }; - var destRect = SkiaSharp.SKRect.Create(ex, ey, ew, eh); - canvas.DrawImage(skImg, destRect, imgPaint); - } - } - catch { } - } - else if (el is PdfSvgElement svg && !string.IsNullOrWhiteSpace(svg.SvgSource)) - { - try - { - byte[]? pngBytes = PdfEditorApp.Services.Tools.Core.PdfPageRenderer.RenderSvgToPngBytes(svg.SvgSource, ew, eh); - if (pngBytes != null && pngBytes.Length > 0) - { - using var skData = SkiaSharp.SKData.CreateCopy(pngBytes); - using var skImg = SkiaSharp.SKImage.FromEncodedData(skData); - if (skImg != null) - { - using var imgPaint = new SkiaSharp.SKPaint { Color = new SkiaSharp.SKColor(255, 255, 255, alpha) }; - var destRect = SkiaSharp.SKRect.Create(ex, ey, ew, eh); - canvas.DrawImage(skImg, destRect, imgPaint); - } - } - } - catch { } - } - else if (el is PdfShapeElement shp) - { - if (shp.ShapeType == ShapeType.CustomSvgPath && !string.IsNullOrWhiteSpace(shp.CustomPathData)) - { - using var skPath = SkiaSharp.SKPath.ParseSvgPathData(shp.CustomPathData); - if (skPath != null) - { - canvas.Save(); - canvas.Translate(ex, ey); - if (scale != 1.0f) - { - canvas.Scale(scale, scale); - } - - if (shp.FillColorHex != "Transparent" && SkiaSharp.SKColor.TryParse(shp.FillColorHex, out var fillC)) - { - using var fPaint = new SkiaSharp.SKPaint - { - Color = fillC.WithAlpha(alpha), - Style = SkiaSharp.SKPaintStyle.Fill, - IsAntialias = true - }; - canvas.DrawPath(skPath, fPaint); - } - - if (shp.StrokeColorHex != "Transparent" && shp.StrokeThickness > 0 && SkiaSharp.SKColor.TryParse(shp.StrokeColorHex, out var strokeC)) - { - using var sPaint = new SkiaSharp.SKPaint - { - Color = strokeC.WithAlpha(alpha), - Style = SkiaSharp.SKPaintStyle.Stroke, - StrokeWidth = (float)shp.StrokeThickness, - IsAntialias = true - }; - canvas.DrawPath(skPath, sPaint); - } - - canvas.Restore(); - } - } - else - { - if (shp.FillColorHex != "Transparent" && SkiaSharp.SKColor.TryParse(shp.FillColorHex, out var fillC)) - { - using var fPaint = new SkiaSharp.SKPaint - { - Color = fillC.WithAlpha(alpha), - Style = SkiaSharp.SKPaintStyle.Fill, - IsAntialias = true - }; - if (shp.CornerRadius > 0) - canvas.DrawRoundRect(ex, ey, ew, eh, (float)shp.CornerRadius * scale, (float)shp.CornerRadius * scale, fPaint); - else - canvas.DrawRect(ex, ey, ew, eh, fPaint); - } - - if (shp.StrokeColorHex != "Transparent" && shp.StrokeThickness > 0 && SkiaSharp.SKColor.TryParse(shp.StrokeColorHex, out var strokeC)) - { - using var sPaint = new SkiaSharp.SKPaint - { - Color = strokeC.WithAlpha(alpha), - Style = SkiaSharp.SKPaintStyle.Stroke, - StrokeWidth = (float)shp.StrokeThickness * scale, - IsAntialias = true - }; - if (shp.CornerRadius > 0) - canvas.DrawRoundRect(ex, ey, ew, eh, (float)shp.CornerRadius * scale, (float)shp.CornerRadius * scale, sPaint); - else - canvas.DrawRect(ex, ey, ew, eh, sPaint); - } - } - } - else if (el is PdfDividerElement div) - { - if (SkiaSharp.SKColor.TryParse(div.ColorHex, out var divC)) - { - using var dPaint = new SkiaSharp.SKPaint - { - Color = divC.WithAlpha(alpha), - Style = SkiaSharp.SKPaintStyle.Fill, - IsAntialias = true - }; - if (div.IsVertical) - { - canvas.DrawRect(ex, ey, Math.Max(1f, (float)div.Thickness * scale), eh, dPaint); - } - else - { - canvas.DrawRect(ex, ey, ew, Math.Max(1f, (float)div.Thickness * scale), dPaint); - } - } - } - else if (el is PdfTableElement tbl) - { - using var borderPaint = new SkiaSharp.SKPaint - { - Color = SkiaSharp.SKColor.TryParse(tbl.BorderColorHex, out var bc) ? bc : SkiaSharp.SKColors.Black, - Style = SkiaSharp.SKPaintStyle.Stroke, - StrokeWidth = 1f * scale - }; - canvas.DrawRect(ex, ey, ew, eh, borderPaint); - - float totalRows = 1 + tbl.Rows.Count; - float rowH = eh / Math.Max(1, totalRows); - float colW = ew / Math.Max(1, tbl.Headers.Count); - - // Header background - using var hBgPaint = new SkiaSharp.SKPaint - { - Color = SkiaSharp.SKColor.TryParse(tbl.HeaderBackgroundHex, out var hbc) ? hbc : new SkiaSharp.SKColor(218, 233, 248), - Style = SkiaSharp.SKPaintStyle.Fill - }; - canvas.DrawRect(ex, ey, ew, rowH, hBgPaint); - - var hTextColor = SkiaSharp.SKColor.TryParse(tbl.HeaderTextHex, out var htc) ? htc : SkiaSharp.SKColors.Black; - using var hTextPaint = new SkiaSharp.SKPaint { Color = hTextColor, IsAntialias = true }; - using var cellTextPaint = new SkiaSharp.SKPaint { Color = SkiaSharp.SKColors.Black, IsAntialias = true }; - using var hFont = new SkiaSharp.SKFont(SkiaSharp.SKTypeface.FromFamilyName("Arial", SkiaSharp.SKFontStyleWeight.Bold, SkiaSharp.SKFontStyleWidth.Normal, SkiaSharp.SKFontStyleSlant.Upright), 9.5f * scale); - using var cellFont = new SkiaSharp.SKFont(SkiaSharp.SKTypeface.FromFamilyName("Arial", SkiaSharp.SKFontStyleWeight.Normal, SkiaSharp.SKFontStyleWidth.Normal, SkiaSharp.SKFontStyleSlant.Upright), 9.0f * scale); - - for (int c = 0; c < tbl.Headers.Count; c++) - { - canvas.DrawText(tbl.Headers[c], ex + (c * colW) + 5, ey + rowH - 4, hFont, hTextPaint); - canvas.DrawLine(ex + (c * colW), ey, ex + (c * colW), ey + eh, borderPaint); - } - - for (int r = 0; r < tbl.Rows.Count; r++) - { - float rY = ey + ((r + 1) * rowH); - canvas.DrawLine(ex, rY, ex + ew, rY, borderPaint); - - var row = tbl.Rows[r]; - for (int c = 0; c < Math.Min(tbl.Headers.Count, row.Count); c++) - { - canvas.DrawText(row[c], ex + (c * colW) + 5, rY + rowH - 4, cellFont, cellTextPaint); - } - } - } - else if (el is PdfTextElement txt && !string.IsNullOrEmpty(txt.Text)) - { - var txtColor = SkiaSharp.SKColor.TryParse(txt.TextColorHex, out var tc) ? tc : SkiaSharp.SKColors.Black; - using var tPaint = new SkiaSharp.SKPaint - { - Color = txtColor.WithAlpha(alpha), - IsAntialias = true - }; - - var weight = txt.IsBold ? SkiaSharp.SKFontStyleWeight.Bold : SkiaSharp.SKFontStyleWeight.Normal; - var slant = txt.IsItalic ? SkiaSharp.SKFontStyleSlant.Italic : SkiaSharp.SKFontStyleSlant.Upright; - using var typeface = MatchSkiaTypeface(txt.FontFamily, txt.Text, weight, slant); - using var font = new SkiaSharp.SKFont(typeface, (float)txt.FontSize * scale); - - double multiplier = txt.LineHeight > 0.1 ? txt.LineHeight : 1.35; - float linePitch = (float)(txt.FontSize * multiplier) * scale; - var lines = txt.Text.Split('\n'); - float curY = ey + ((float)txt.FontSize * scale * 0.90f); // baseline approximation - - for (int li = 0; li < lines.Length; li++) - { - string lineText = lines[li]; - if (lineText.Contains('➢')) - { - using var symTypeface = SkiaSharp.SKTypeface.FromFamilyName("Apple Symbols", weight, SkiaSharp.SKFontStyleWidth.Normal, slant) - ?? SkiaSharp.SKTypeface.FromFamilyName("Arial Unicode MS", weight, SkiaSharp.SKFontStyleWidth.Normal, slant) - ?? typeface; - using var symFont = new SkiaSharp.SKFont(symTypeface, (float)txt.FontSize * scale); - canvas.DrawText(lineText, ex, curY, symFont, tPaint); - } - else - { - canvas.DrawText(lineText, ex, curY, font, tPaint); - } - curY += linePitch; - } - } - - canvas.Restore(); - } - - return bitmap; - } - - private static SkiaSharp.SKTypeface MatchSkiaTypeface(string fontFamily, string text, SkiaSharp.SKFontStyleWeight weight, SkiaSharp.SKFontStyleSlant slant) - { - if (PdfEditorApp.Core.Analysis.UnicodeScriptDetector.ContainsDevanagari(text)) - { - string[] devanagariFamilies = { "Noto Sans Devanagari", "Kohinoor Devanagari", "Devanagari Sangam MN", "Nirmala UI", "Mangal", "Arial Unicode MS" }; - foreach (var fam in devanagariFamilies) - { - var dtf = SkiaSharp.SKTypeface.FromFamilyName(fam, weight, SkiaSharp.SKFontStyleWidth.Normal, slant); - if (dtf != null && !dtf.FamilyName.Equals("Arial", StringComparison.OrdinalIgnoreCase) && !dtf.FamilyName.Equals("Helvetica", StringComparison.OrdinalIgnoreCase)) - return dtf; - } - } - else if (PdfEditorApp.Core.Analysis.UnicodeScriptDetector.ContainsCjk(text)) - { - string[] cjkFamilies = { "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "SimSun" }; - foreach (var fam in cjkFamilies) - { - var ctf = SkiaSharp.SKTypeface.FromFamilyName(fam, weight, SkiaSharp.SKFontStyleWidth.Normal, slant); - if (ctf != null && !ctf.FamilyName.Equals("Arial", StringComparison.OrdinalIgnoreCase)) - return ctf; - } - } - - return SkiaSharp.SKTypeface.FromFamilyName(fontFamily, weight, SkiaSharp.SKFontStyleWidth.Normal, slant) - ?? SkiaSharp.SKTypeface.Default; - } - - [Fact] - public void PdfViewerViewModel_RequestPagesVisible_RendersVisiblePagesAndDisposesFarPages() - { - var vm = new PdfViewerViewModel(); - for (int i = 1; i <= 30; i++) - { - vm.Pages.Add(new PdfViewerPageItem - { - PageNumber = i, - WidthPoints = 500, - HeightPoints = 700 - }); - } - - // Simulate user scrolling down to Page 18 - vm.RequestPagesVisible(18, 18); - - var page18 = vm.Pages[17]; - Assert.Equal(18, page18.PageNumber); - - // Changing CurrentPageNumber moves selection cleanly - vm.CurrentPageNumber = 18; - Assert.True(page18.IsSelected); - Assert.False(vm.Pages[0].IsSelected); - Assert.Equal(18, vm.CurrentPageNumber); - Assert.Equal(18, vm.SelectedPage?.PageNumber); - } -} - diff --git a/tests/PdfEditorApp.Tests/PdfLayoutAnalyzerTests.cs b/tests/PdfEditorApp.Tests/PdfLayoutAnalyzerTests.cs deleted file mode 100644 index f59ca5d..0000000 --- a/tests/PdfEditorApp.Tests/PdfLayoutAnalyzerTests.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Deconstruction; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Core.Utils; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Templates; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PdfLayoutAnalyzerTests -{ - private readonly IPdfExportService _exportService = new PdfExportService(); - private readonly ITemplateService _templateService = new TemplateService(); - private readonly IPdfImportService _importService = new PdfImportService(); - - [Fact] - public void NormalizeFontFamily_MapsCommonPdfFontNamesToCleanFamilies() - { - // Latin - Sans-Serif - Assert.Equal("Arial", PdfLayoutAnalyzer.NormalizeFontFamily("ABCDEE+Arial-BoldMT")); - Assert.Equal("Roboto", PdfLayoutAnalyzer.NormalizeFontFamily("Roboto-Medium")); - Assert.Equal("Inter", PdfLayoutAnalyzer.NormalizeFontFamily("Inter-Regular")); - Assert.Equal("Open Sans", PdfLayoutAnalyzer.NormalizeFontFamily("OpenSans-Regular")); - Assert.Equal("Open Sans", PdfLayoutAnalyzer.NormalizeFontFamily("SegoeUI-Semibold")); // Segoe → Open Sans cross-platform - Assert.Equal("Lato", PdfLayoutAnalyzer.NormalizeFontFamily("Lato-Bold")); - Assert.Equal("Poppins", PdfLayoutAnalyzer.NormalizeFontFamily("Poppins-SemiBold")); - Assert.Equal("Ubuntu", PdfLayoutAnalyzer.NormalizeFontFamily("Ubuntu-Regular")); - - // Latin - Serif - Assert.Equal("PT Serif", PdfLayoutAnalyzer.NormalizeFontFamily("TimesNewRomanPS-Italic")); // Times → PT Serif - Assert.Equal("PT Serif", PdfLayoutAnalyzer.NormalizeFontFamily("Georgia-Bold")); - Assert.Equal("Crimson Text", PdfLayoutAnalyzer.NormalizeFontFamily("Garamond-Regular")); - Assert.Equal("Merriweather", PdfLayoutAnalyzer.NormalizeFontFamily("Merriweather-Light")); - Assert.Equal("Playfair Display", PdfLayoutAnalyzer.NormalizeFontFamily("PlayfairDisplay-Bold")); - - // Monospace - Assert.Equal("Fira Code", PdfLayoutAnalyzer.NormalizeFontFamily("CourierNewPSMT")); // Courier → Fira Code - - // Indian Scripts - Assert.Equal("Noto Sans Devanagari", PdfLayoutAnalyzer.NormalizeFontFamily("NirmalaUI-Bold")); - Assert.Equal("Noto Sans Devanagari", PdfLayoutAnalyzer.NormalizeFontFamily("Mangal")); - Assert.Equal("Noto Sans Tamil", PdfLayoutAnalyzer.NormalizeFontFamily("Latha-Regular")); - Assert.Equal("Noto Sans Telugu", PdfLayoutAnalyzer.NormalizeFontFamily("Gautami")); - Assert.Equal("Noto Sans Arabic", PdfLayoutAnalyzer.NormalizeFontFamily("Arabic-Regular")); - Assert.Equal("Noto Sans Bengali", PdfLayoutAnalyzer.NormalizeFontFamily("Vrinda-Bengali")); - - // Null/empty fallback → Open Sans (clean default) - Assert.Equal("Open Sans", PdfLayoutAnalyzer.NormalizeFontFamily(null)); - Assert.Equal("Open Sans", PdfLayoutAnalyzer.NormalizeFontFamily("")); - Assert.Equal("Open Sans", PdfLayoutAnalyzer.NormalizeFontFamily("SomeObscureUnknownFont123")); - } - - [Fact] - public async Task PdfDeconstructionEngine_ImportsAnnualReport_ClustersParagraphsAndRetainsCleanCanvas() - { - // 1. Generate multi-page annual report - var reportDoc = _templateService.CreateAnnualReportTemplate(); - byte[] pdfBytes = _exportService.GeneratePdfBytes(reportDoc); - - // 2. Deconstruct using PdfDeconstructionEngine - var deconstructed = PdfDeconstructionEngine.Deconstruct(pdfBytes, "Annual_Report.pdf"); - - Assert.NotNull(deconstructed); - Assert.Equal(3, deconstructed.Pages.Count); - - var page1 = deconstructed.Pages[0]; - Assert.Equal(PageFormat.A4, page1.Format); - Assert.Equal("#FFFFFF", page1.BackgroundColorHex); - - // Verify no raster ghost background image was created - var bgImage = page1.Elements.OfType().FirstOrDefault(img => img.AltText.Contains("Background Canvas")); - Assert.Null(bgImage); - - // Verify text elements are extracted and have clean bounding boxes - var textElements = page1.Elements.OfType().ToList(); - Assert.NotEmpty(textElements); - - foreach (var txt in textElements) - { - Assert.False(string.IsNullOrWhiteSpace(txt.Text)); - Assert.True(txt.Width > 0); - Assert.True(txt.Height > 0); - Assert.False(string.IsNullOrWhiteSpace(txt.FontFamily)); - Assert.True(txt.FontSize >= 6); - } - } - - [Fact] - public async Task PdfDeconstructionEngine_EditImportedText_SavesAndExportsCleanlyWithoutUnderlyingArtifacts() - { - // 1. Create invoice PDF - var originalInvoice = _templateService.CreateInvoiceTemplate(); - byte[] originalBytes = _exportService.GeneratePdfBytes(originalInvoice); - - // 2. Import into model - var imported = await _importService.ImportPdfBytesAsync(originalBytes, "Invoice_To_Edit.pdf"); - Assert.NotNull(imported); - var page = imported.Pages[0]; - - // 3. Count elements before edit - int elementCountBefore = page.Elements.Count; - Assert.True(elementCountBefore > 0, "Imported page must have elements before editing"); - - // 4. Edit first text element (simulates user editing in Studio) - var textElement = page.Elements.OfType().First(); - textElement.Text = "FryPDF Super Invoice"; - textElement.Width = 300; - textElement.Height = 30; - - // 5. Export modified document — must produce valid PDF bytes - byte[] modifiedBytes = _exportService.GeneratePdfBytes(imported); - Assert.NotNull(modifiedBytes); - Assert.True(modifiedBytes.Length > 1000, "Exported PDF must have substantial content"); - - // 6. Verify re-import succeeds (no crash, correct page count) - var reimported = await _importService.ImportPdfBytesAsync(modifiedBytes, "Modified_Invoice.pdf"); - Assert.NotNull(reimported); - Assert.Single(reimported.Pages); - Assert.NotEmpty(reimported.Pages[0].Elements); - } - - [Fact] - public void PdfDocumentSanitizer_SanitizePdfBytes_EnsuresEofTermination() - { - byte[] rawBytes = System.Text.Encoding.ASCII.GetBytes("%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\n"); - byte[] sanitized = PdfDocumentSanitizer.SanitizePdfBytes(rawBytes); - - string sanitizedText = System.Text.Encoding.ASCII.GetString(sanitized); - Assert.Contains("%%EOF", sanitizedText); - } - - [Fact] - public void UnicodeScriptDetector_DetectsRtlAndDevanagari() - { - string arabic = "مرحبا بكم في عالم الابتكار"; - string hebrew = "שלום עולם"; - string hindi = "नमस्ते भारत"; - string english = "Welcome to FryPDF Studio"; - - Assert.True(UnicodeScriptDetector.IsRtlText(arabic)); - Assert.True(UnicodeScriptDetector.IsRtlText(hebrew)); - Assert.False(UnicodeScriptDetector.IsRtlText(hindi)); - Assert.False(UnicodeScriptDetector.IsRtlText(english)); - - Assert.True(UnicodeScriptDetector.ContainsDevanagari(hindi)); - Assert.False(UnicodeScriptDetector.ContainsDevanagari(english)); - } - - [Fact] - public void PageViewModel_GroupAndUngroup_ManagesGroupIds() - { - var page = new PageViewModel(); - var t1 = new TextElementViewModel { Text = "Header", X = 10, Y = 10, Width = 100, Height = 30 }; - var t2 = new TextElementViewModel { Text = "Body", X = 10, Y = 50, Width = 100, Height = 50 }; - var s1 = new ShapeElementViewModel { X = 5, Y = 5, Width = 120, Height = 100 }; - - page.AddElement(t1); - page.AddElement(t2); - page.AddElement(s1); - - page.SelectElements(new ElementViewModelBase[] { t1, t2, s1 }); - Assert.Equal(3, page.SelectedElements.Count); - - page.GroupSelected(); - Assert.NotNull(t1.GroupId); - Assert.Equal(t1.GroupId, t2.GroupId); - Assert.Equal(t1.GroupId, s1.GroupId); - - page.UngroupSelected(); - Assert.Null(t1.GroupId); - Assert.Null(t2.GroupId); - Assert.Null(s1.GroupId); - } - - [Fact] - public void PageViewModel_SmartAlignmentTools_AlignsElementsAccurately() - { - var page = new PageViewModel(); - var e1 = new TextElementViewModel { X = 10, Y = 20, Width = 100, Height = 30 }; - var e2 = new TextElementViewModel { X = 50, Y = 80, Width = 80, Height = 40 }; - var e3 = new TextElementViewModel { X = 150, Y = 150, Width = 120, Height = 50 }; - - page.AddElement(e1); - page.AddElement(e2); - page.AddElement(e3); - page.SelectElements(new[] { e1, e2, e3 }); - - // Align Left: All X should be minX = 10 - page.AlignSelectedLeft(); - Assert.Equal(10, e1.X); - Assert.Equal(10, e2.X); - Assert.Equal(10, e3.X); - - // Align Top: All Y should be minY = 20 - page.AlignSelectedTop(); - Assert.Equal(20, e1.Y); - Assert.Equal(20, e2.Y); - Assert.Equal(20, e3.Y); - - // Set distinct X positions and test Distribute Horizontally - e1.X = 10; - e2.X = 80; - e3.X = 250; - page.DistributeSelectedHorizontally(); - Assert.Equal(10, e1.X); - Assert.True(e2.X > e1.X + e1.Width); - Assert.True(e3.X > e2.X + e2.Width); - } - - /// - /// Regression test: Verify that Rotate270 marginalia text from vertical margins - /// (sample1.pdf) is extracted with Rotation=270 and that CanvasX/CanvasY are placed - /// so that the rotation pivot falls at the PDF text center (correct geometric placement). - /// - [Fact] - public async Task GovernmentId_Rotate270MarginaliaText_HasCorrectRotationAndPlacement() - { - string baseDir = AppContext.BaseDirectory; - string rootDir = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..")); - string sample1Path = Path.Combine(rootDir, "sample1.pdf"); - if (!File.Exists(sample1Path)) return; // Skip if not present in CI - - var doc = await _importService.ImportPdfAsync(sample1Path); - Assert.NotNull(doc); - Assert.NotEmpty(doc.Pages); - - var page1 = doc.Pages[0]; - var textElements = page1.Elements.OfType().ToList(); - - // 1. Verify at least some rotated text elements exist (document has left-margin marginalia) - var rotated270 = textElements.Where(t => t.Rotation == 270.0).ToList(); - Assert.NotEmpty(rotated270); - - // 2. Verify at least one element contains marginalia text - var marginaliaEl = rotated270.FirstOrDefault(t => t.Text.Length > 5); - Assert.NotNull(marginaliaEl); - - // 3. Verify the element has positive Width and Height (unrotated dimensions) - Assert.True(marginaliaEl.Width > 0, "Rotated text element must have positive Width"); - Assert.True(marginaliaEl.Height > 0, "Rotated text element must have positive Height"); - - // 4. Verify Width (text run length) >> Height (font thickness): for a long string like - // marginalia text, Width should be significantly larger than Height. - Assert.True(marginaliaEl.Width > marginaliaEl.Height * 2, - $"For rotated marginalia, Width ({marginaliaEl.Width:F1}) should be much larger than Height ({marginaliaEl.Height:F1})"); - - // 5. The canvas X position should be near the left margin of the page. - // After 270° rotation, the visual left edge = CanvasX + Width/2 - Height/2. - // The pivot (center of element) must be near the original PDF text X center (~59-62pt). - double pivotX = marginaliaEl.X + (marginaliaEl.Width / 2.0); - Assert.True(pivotX < 150, $"Left-margin rotated text pivot X ({pivotX:F1}) should be near left margin (<150pt)"); - - // 6. Verify CanvasY is within page bounds - Assert.True(marginaliaEl.Y >= 0, "CanvasY must be non-negative"); - Assert.True(marginaliaEl.Y < page1.Height, $"CanvasY ({marginaliaEl.Y:F1}) must be within page height ({page1.Height:F1}pt)"); - } -} diff --git a/tests/PdfEditorApp.Tests/PdfReaderTests.cs b/tests/PdfEditorApp.Tests/PdfReaderTests.cs deleted file mode 100644 index 3f2d533..0000000 --- a/tests/PdfEditorApp.Tests/PdfReaderTests.cs +++ /dev/null @@ -1,695 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Templates; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PdfReaderTests -{ - private readonly IPdfExportService _exportService = new PdfExportService(); - private readonly ITemplateService _templateService = new TemplateService(); - - private byte[] CreateSamplePdfBytes(int pageCount = 3) - { - var model = _templateService.CreateAnnualReportTemplate(); - return _exportService.GeneratePdfBytes(model); - } - - [Fact] - public async Task PdfViewer_LoadsDocument_PopulatesMetadataAndSpreads() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - Assert.True(vm.HasDocument); - Assert.False(vm.IsLoading); - Assert.Equal("SampleReport.pdf", vm.DocumentTitle); - Assert.Equal(3, vm.TotalPagesCount); - Assert.Equal(1, vm.CurrentPageNumber); - Assert.NotNull(vm.SelectedPage); - Assert.Equal(3, vm.Pages.Count); - Assert.NotEmpty(vm.MetadataItems); - Assert.NotEmpty(vm.PageSpreads); - Assert.Equal("Page 1 (Cover)", vm.PageSpreads[0].SpreadLabel); - } - - [Fact] - public async Task PdfViewer_PageNavigationAndDirectJump_UpdatesStateCorrectly() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - // Next page - vm.NextPageCommand.Execute(null); - Assert.Equal(2, vm.CurrentPageNumber); - Assert.Equal("2", vm.JumpPageText); - Assert.Equal(2, vm.SelectedPage?.PageNumber); - - // Previous page - vm.PreviousPageCommand.Execute(null); - Assert.Equal(1, vm.CurrentPageNumber); - - // Last page - vm.LastPageCommand.Execute(null); - Assert.Equal(3, vm.CurrentPageNumber); - - // First page - vm.FirstPageCommand.Execute(null); - Assert.Equal(1, vm.CurrentPageNumber); - - // Direct Page Jump - vm.JumpPageText = "3"; - vm.CommitJumpPageCommand.Execute(null); - Assert.Equal(3, vm.CurrentPageNumber); - - // Invalid Page Jump resets to current - vm.JumpPageText = "99"; - vm.CommitJumpPageCommand.Execute(null); - Assert.Equal(3, vm.CurrentPageNumber); - Assert.Equal("3", vm.JumpPageText); - } - - [Fact] - public async Task PdfViewer_LayoutModes_TogglesContinuousSingleAndSpread() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - Assert.Equal(PdfViewLayoutMode.ContinuousScroll, vm.SelectedLayoutMode); - Assert.True(vm.IsContinuousScroll); - Assert.False(vm.IsSinglePageMode); - Assert.False(vm.IsTwoPageSpreadMode); - - // Switch to Single Page - vm.SetLayoutModeCommand.Execute("SinglePage"); - Assert.Equal(PdfViewLayoutMode.SinglePage, vm.SelectedLayoutMode); - Assert.False(vm.IsContinuousScroll); - Assert.True(vm.IsSinglePageMode); - Assert.False(vm.IsTwoPageSpreadMode); - - // Switch to Two-Page Spread - vm.SetLayoutModeCommand.Execute("TwoPageSpread"); - Assert.Equal(PdfViewLayoutMode.TwoPageSpread, vm.SelectedLayoutMode); - Assert.False(vm.IsContinuousScroll); - Assert.False(vm.IsSinglePageMode); - Assert.True(vm.IsTwoPageSpreadMode); - Assert.Equal(2, vm.PageSpreads.Count); // Cover (p1) + Spread (p2-p3) - } - - [Fact] - public async Task PdfViewer_ReadingThemes_SwitchesThemesAndUpdatesColors() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - // Default theme - Assert.Equal(PdfReaderTheme.Default, vm.ReadingTheme); - Assert.True(vm.IsThemeDefault); - Assert.Equal("#FFFFFF", vm.ThemePaperBackgroundHex); - - // Sepia - vm.SetReadingThemeCommand.Execute("Sepia"); - Assert.Equal(PdfReaderTheme.Sepia, vm.ReadingTheme); - Assert.True(vm.IsThemeSepia); - Assert.Equal("#FBF0D9", vm.ThemePaperBackgroundHex); - Assert.Equal("#433422", vm.ThemeTextColorHex); - - // Dark Night Mode - vm.SetReadingThemeCommand.Execute("Dark"); - Assert.Equal(PdfReaderTheme.Dark, vm.ReadingTheme); - Assert.True(vm.IsThemeDark); - Assert.Equal("#1E293B", vm.ThemePaperBackgroundHex); - Assert.Equal("#F1F5F9", vm.ThemeTextColorHex); - - // High Contrast - vm.SetReadingThemeCommand.Execute("HighContrast"); - Assert.Equal(PdfReaderTheme.HighContrast, vm.ReadingTheme); - Assert.True(vm.IsThemeHighContrast); - Assert.Equal("#000000", vm.ThemePaperBackgroundHex); - Assert.Equal("#FFFF00", vm.ThemeTextColorHex); - } - - [Fact] - public async Task PdfViewer_ZoomOperations_ClampsWithinSafeBounds() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - Assert.Equal(1.0, vm.ZoomLevel); - Assert.Equal("100%", vm.ZoomPercentageText); - - vm.ZoomInCommand.Execute(null); - Assert.Equal(1.25, vm.ZoomLevel); - Assert.Equal("125%", vm.ZoomPercentageText); - - vm.FitToWidthCommand.Execute(null); - Assert.Equal(1.35, vm.ZoomLevel); - - vm.FitToPageCommand.Execute(null); - Assert.Equal(0.95, vm.ZoomLevel); - - vm.SetZoomPresetCommand.Execute("200%"); - Assert.Equal(2.0, vm.ZoomLevel); - - vm.ResetZoomCommand.Execute(null); - Assert.Equal(1.0, vm.ZoomLevel); - } - - [Fact] - public async Task PdfViewer_ReviewAnnotations_AddsHighlightsStickyNotesAndStamps() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - // 1. Add Highlight - vm.AddHighlightAnnotationCommand.Execute("#A7F3D0"); - Assert.Single(vm.Annotations); - Assert.Equal("Highlight", vm.Annotations[0].Type); - Assert.Equal("#A7F3D0", vm.Annotations[0].ColorHex); - Assert.Single(vm.SelectedPage!.PageAnnotations); - - // 2. Add Sticky Note - vm.NewNoteText = "Check Q3 audit numbers."; - vm.ConfirmAddNoteCommand.Execute(null); - Assert.Equal(2, vm.Annotations.Count); - Assert.Equal("StickyNote", vm.Annotations[1].Type); - Assert.Equal("Check Q3 audit numbers.", vm.Annotations[1].Content); - - // 3. Add Stamp - vm.AddStampCommand.Execute("APPROVED"); - Assert.Equal(3, vm.Annotations.Count); - Assert.Equal("Stamp", vm.Annotations[2].Type); - Assert.Contains("APPROVED", vm.Annotations[2].Content); - - // 4. Delete Annotation - var toDelete = vm.Annotations[0]; - vm.DeleteAnnotationCommand.Execute(toDelete); - Assert.Equal(2, vm.Annotations.Count); - Assert.DoesNotContain(toDelete, vm.Annotations); - } - - [Fact] - public async Task PdfViewer_VisualRotation_RotatesPageAndAllPages() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - Assert.Equal(0, vm.SelectedPage?.RotationAngle); - - vm.RotateClockwiseCommand.Execute(null); - Assert.Equal(90, vm.SelectedPage?.RotationAngle); - - vm.RotateCounterClockwiseCommand.Execute(null); - Assert.Equal(0, vm.SelectedPage?.RotationAngle); - - vm.RotateAllPagesClockwiseCommand.Execute(null); - Assert.All(vm.Pages, p => Assert.Equal(90, p.RotationAngle)); - } - - [Fact] - public void HomeViewModel_NavigatesToPdfReaderSection() - { - var home = new HomeViewModel(); - Assert.True(home.IsHomeSection); - Assert.False(home.IsPdfReaderSection); - - home.SelectNavSectionCommand.Execute("PdfReader"); - Assert.Equal(HomeNavSection.PdfReader, home.SelectedNavSection); - Assert.True(home.IsPdfReaderSection); - Assert.False(home.IsHomeSection); - } - - [Fact] - public async Task PdfViewer_RenderPageAtScale_RendersCustomResolutionBitmap() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - // Render Page 1 bytes at standard scale 2.75 - var bytes1 = vm.RenderPageBytesAtScale(1, 2.75f); - Assert.NotNull(bytes1); - Assert.True(bytes1.Length > 0); - - // Render Page 1 bytes at high-DPI zoom scale 4.0 - var bytesHigh = vm.RenderPageBytesAtScale(1, 4.0f); - Assert.NotNull(bytesHigh); - Assert.True(bytesHigh.Length > bytes1.Length); - } - - [Fact] - public async Task PdfViewer_PageSelectionAndThumbnailClick_TriggersScrollToPageAndUpdatesIsSelected() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(3); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - int requestedPage = 0; - vm.ScrollToPageRequested += (pageNum) => - { - requestedPage = pageNum; - }; - - // Click Page 2 Thumbnail - vm.SelectPage(vm.Pages[1]); - - Assert.Equal(2, vm.CurrentPageNumber); - Assert.Equal(2, vm.SelectedPage?.PageNumber); - Assert.Equal(2, requestedPage); - Assert.False(vm.Pages[0].IsSelected); - Assert.True(vm.Pages[1].IsSelected); - Assert.False(vm.Pages[2].IsSelected); - - // Click Page 3 Thumbnail - vm.SelectPage(vm.Pages[2]); - - Assert.Equal(3, vm.CurrentPageNumber); - Assert.Equal(3, vm.SelectedPage?.PageNumber); - Assert.Equal(3, requestedPage); - Assert.False(vm.Pages[0].IsSelected); - Assert.False(vm.Pages[1].IsSelected); - Assert.True(vm.Pages[2].IsSelected); - - // Click Page 1 Thumbnail - vm.SelectPage(vm.Pages[0]); - - Assert.Equal(1, vm.CurrentPageNumber); - Assert.Equal(1, vm.SelectedPage?.PageNumber); - Assert.Equal(1, requestedPage); - Assert.True(vm.Pages[0].IsSelected); - Assert.False(vm.Pages[1].IsSelected); - Assert.False(vm.Pages[2].IsSelected); - } - - [Fact] - public async Task PdfViewer_JumpToBookmark_ScrollsToTargetPage() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(3); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - int requestedPage = 0; - vm.ScrollToPageRequested += (pageNum) => - { - requestedPage = pageNum; - }; - - var bookmark = new PdfViewerBookmarkItem { Title = "Chapter 2", PageNumber = 2 }; - vm.JumpToBookmarkCommand.Execute(bookmark); - - Assert.Equal(2, vm.CurrentPageNumber); - Assert.Equal(2, vm.SelectedPage?.PageNumber); - Assert.Equal(2, requestedPage); - Assert.True(vm.Pages[1].IsSelected); - } - - [Fact] - public async Task PdfViewer_JumpToAnnotation_ScrollsToTargetPage() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(3); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - int requestedPage = 0; - vm.ScrollToPageRequested += (pageNum) => - { - requestedPage = pageNum; - }; - - var annotation = new PdfViewerAnnotationItem { Type = "StickyNote", PageNumber = 3, Content = "Important remark", ColorHex = "#0284C7" }; - vm.JumpToAnnotationCommand.Execute(annotation); - - Assert.Equal(3, vm.CurrentPageNumber); - Assert.Equal(3, vm.SelectedPage?.PageNumber); - Assert.Equal(3, requestedPage); - Assert.True(vm.Pages[2].IsSelected); - } - - [Fact] - public async Task PdfViewer_JumpToSearchMatch_ScrollsToTargetPage() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(3); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - int requestedPage = 0; - vm.ScrollToPageRequested += (pageNum) => - { - requestedPage = pageNum; - }; - - var match = new PdfViewerSearchMatch { PageNumber = 2, Snippet = "Search match snippet", MatchIndex = 0 }; - vm.JumpToMatchCommand.Execute(match); - - Assert.Equal(2, vm.CurrentPageNumber); - Assert.Equal(2, vm.SelectedPage?.PageNumber); - Assert.Equal(2, requestedPage); - Assert.True(vm.Pages[1].IsSelected); - } - - [Fact] - public async Task PdfViewer_TwoPageSpreadNavigation_NavigatesInTwoPageSteps() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(3); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - vm.SetLayoutModeCommand.Execute("TwoPageSpread"); - Assert.True(vm.IsTwoPageSpreadMode); - Assert.Equal(1, vm.CurrentPageNumber); - - int requestedPage = 0; - vm.ScrollToPageRequested += (pageNum) => - { - requestedPage = pageNum; - }; - - // Next page in 2-page spread - vm.NextPageCommand.Execute(null); - Assert.Equal(3, vm.CurrentPageNumber); - Assert.Equal(3, requestedPage); - - // Previous page in 2-page spread - vm.PreviousPageCommand.Execute(null); - Assert.Equal(1, vm.CurrentPageNumber); - Assert.Equal(1, requestedPage); - } - - [Fact] - public async Task PdfViewer_FitToWidth_WithViewportProvider_DynamicallyScalesToAvailableWidth() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(1); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - Assert.True(vm.HasDocument); - var page = vm.Pages[0]; - Assert.True(page.WidthPoints > 0); - - // Simulate viewport of 1200 width with 64px padding (32 on each side) - vm.ViewportSizeProvider = () => (1200.0, 800.0, 64.0, 64.0); - - vm.FitToWidthCommand.Execute(null); - - // Expected available width = 1200 - 64 - 8 = 1128 - // Expected zoom = 1128 / page.WidthPoints clamped and rounded to 2 decimals - double expectedZoom = Math.Clamp(Math.Round(1128.0 / page.WidthPoints, 2), 0.25, 5.0); - if (expectedZoom * page.WidthPoints > 1128.0) - { - expectedZoom = Math.Max(0.25, Math.Round(expectedZoom - 0.01, 2)); - } - - Assert.Equal(expectedZoom, vm.ZoomLevel); - Assert.True(vm.IsFitToWidthActive); - Assert.False(vm.IsFitToPageActive); - Assert.Equal(PdfViewerZoomMode.FitWidth, vm.ZoomMode); - - // Verify page width at zoom fits comfortably within available width without horizontal overflow - double renderedWidth = page.WidthPoints * vm.ZoomLevel; - Assert.True(renderedWidth <= (1200.0 - 64.0)); - } - - [Fact] - public async Task PdfViewer_FitToPage_WithViewportProvider_DynamicallyScalesToFitBothDimensions() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(1); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - var page = vm.Pages[0]; - - // Simulate viewport of 800x600 with 64px horizontal and vertical padding - vm.ViewportSizeProvider = () => (800.0, 600.0, 64.0, 64.0); - - vm.FitToPageCommand.Execute(null); - - Assert.True(vm.IsFitToPageActive); - Assert.False(vm.IsFitToWidthActive); - Assert.Equal(PdfViewerZoomMode.FitPage, vm.ZoomMode); - - // Verify both dimensions fit within viewport minus padding - double renderedWidth = page.WidthPoints * vm.ZoomLevel; - double renderedHeight = page.HeightPoints * vm.ZoomLevel; - Assert.True(renderedWidth <= (800.0 - 64.0)); - Assert.True(renderedHeight <= (600.0 - 64.0)); - } - - [Fact] - public async Task PdfViewer_TwoPageSpread_FitToWidth_AccountsForSpreadWidth() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(3); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - vm.SetLayoutModeCommand.Execute("TwoPageSpread"); - Assert.True(vm.IsTwoPageSpreadMode); - - // Navigate to spread with two pages (page 2 & 3) - vm.NextPageCommand.Execute(null); - Assert.NotNull(vm.SelectedSpread); - - vm.ViewportSizeProvider = () => (1600.0, 900.0, 64.0, 64.0); - vm.FitToWidthCommand.Execute(null); - - Assert.True(vm.IsFitToWidthActive); - Assert.True(vm.ZoomLevel > 0); - - if (vm.SelectedSpread.LeftPage != null && vm.SelectedSpread.RightPage != null) - { - double combinedPageWidth = (vm.SelectedSpread.LeftPage.WidthPoints + vm.SelectedSpread.RightPage.WidthPoints) * vm.ZoomLevel; - // Combined width + 16px gap must fit inside 1600 - 64 - Assert.True(combinedPageWidth + 16.0 <= (1600.0 - 64.0)); - } - } - - [Fact] - public async Task PdfViewer_ManualZoom_ExitsFitModes() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(1); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - - vm.ViewportSizeProvider = () => (1200.0, 800.0, 64.0, 64.0); - vm.FitToWidthCommand.Execute(null); - Assert.True(vm.IsFitToWidthActive); - - // Manual zoom in - vm.ZoomInCommand.Execute(null); - Assert.False(vm.IsFitToWidthActive); - Assert.False(vm.IsFitToPageActive); - Assert.Equal(PdfViewerZoomMode.Custom, vm.ZoomMode); - - // Fit to page - vm.FitToPageCommand.Execute(null); - Assert.True(vm.IsFitToPageActive); - - // Manual reset zoom - vm.ResetZoomCommand.Execute(null); - Assert.False(vm.IsFitToPageActive); - Assert.Equal(PdfViewerZoomMode.Custom, vm.ZoomMode); - } - - [Fact] - public async Task PdfViewer_DedicatedLayoutCommands_SwitchModesAndAutoFit() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(3); - await vm.LoadDocumentBytesAsync(pdfBytes, "SampleReport.pdf"); - vm.ViewportSizeProvider = () => (1000.0, 700.0, 64.0, 64.0); - - // Initially Continuous Scroll - Assert.True(vm.IsContinuousScroll); - Assert.False(vm.IsSinglePageMode); - Assert.False(vm.IsTwoPageSpreadMode); - - // Switch to Single Page - vm.SetSinglePageLayoutCommand.Execute(null); - Assert.False(vm.IsContinuousScroll); - Assert.True(vm.IsSinglePageMode); - Assert.False(vm.IsTwoPageSpreadMode); - Assert.Equal(PdfViewLayoutMode.SinglePage, vm.SelectedLayoutMode); - Assert.NotNull(vm.SelectedPage); - Assert.True(vm.SelectedPage.WidthPoints > 0); - Assert.True(vm.SelectedPage.HeightPoints > 0); - Assert.True(vm.ZoomLevel > 0); - Assert.True(vm.IsFitToPageActive); - - // Switch to Two Page Spread - vm.SetTwoPageSpreadLayoutCommand.Execute(null); - Assert.False(vm.IsContinuousScroll); - Assert.False(vm.IsSinglePageMode); - Assert.True(vm.IsTwoPageSpreadMode); - Assert.Equal(PdfViewLayoutMode.TwoPageSpread, vm.SelectedLayoutMode); - Assert.NotNull(vm.SelectedSpread); - Assert.True(vm.ZoomLevel > 0); - Assert.True(vm.SelectedSpread.LeftPage != null || vm.SelectedSpread.RightPage != null); - Assert.True(vm.IsFitToPageActive); - - // Switch back to Continuous Scroll - vm.SetContinuousScrollLayoutCommand.Execute(null); - Assert.True(vm.IsContinuousScroll); - Assert.False(vm.IsSinglePageMode); - Assert.False(vm.IsTwoPageSpreadMode); - Assert.Equal(PdfViewLayoutMode.ContinuousScroll, vm.SelectedLayoutMode); - } - - [Fact] - public void PdfViewer_ReadingThemeCommands_SwitchThemesAndColors() - { - var vm = new PdfViewerViewModel(); - - // Default state - Assert.True(vm.IsThemeDefault); - Assert.False(vm.IsThemeSepia); - Assert.False(vm.IsThemeDark); - Assert.False(vm.IsThemeHighContrast); - Assert.Equal("#F1F5F9", vm.ThemeBackgroundHex); - Assert.Equal("#FFFFFF", vm.ThemePaperBackgroundHex); - - // Switch to Sepia - vm.SetSepiaReadingThemeCommand.Execute(null); - Assert.False(vm.IsThemeDefault); - Assert.True(vm.IsThemeSepia); - Assert.False(vm.IsThemeDark); - Assert.False(vm.IsThemeHighContrast); - Assert.Equal(PdfReaderTheme.Sepia, vm.ReadingTheme); - Assert.Equal("#EDE3C9", vm.ThemeBackgroundHex); - Assert.Equal("#FBF0D9", vm.ThemePaperBackgroundHex); - Assert.Equal("#433422", vm.ThemeTextColorHex); - Assert.Equal("#E6D5B8", vm.ThemeBorderColorHex); - - // Switch to Dark Night - vm.SetDarkReadingThemeCommand.Execute(null); - Assert.False(vm.IsThemeDefault); - Assert.False(vm.IsThemeSepia); - Assert.True(vm.IsThemeDark); - Assert.False(vm.IsThemeHighContrast); - Assert.Equal(PdfReaderTheme.Dark, vm.ReadingTheme); - Assert.Equal("#0F172A", vm.ThemeBackgroundHex); - Assert.Equal("#1E293B", vm.ThemePaperBackgroundHex); - Assert.Equal("#F1F5F9", vm.ThemeTextColorHex); - Assert.Equal("#334155", vm.ThemeBorderColorHex); - - // Switch to High Contrast - vm.SetHighContrastReadingThemeCommand.Execute(null); - Assert.False(vm.IsThemeDefault); - Assert.False(vm.IsThemeSepia); - Assert.False(vm.IsThemeDark); - Assert.True(vm.IsThemeHighContrast); - Assert.Equal(PdfReaderTheme.HighContrast, vm.ReadingTheme); - Assert.Equal("#000000", vm.ThemeBackgroundHex); - Assert.Equal("#000000", vm.ThemePaperBackgroundHex); - Assert.Equal("#FFFF00", vm.ThemeTextColorHex); - - // Switch back to Daylight - vm.SetDefaultReadingThemeCommand.Execute(null); - Assert.True(vm.IsThemeDefault); - Assert.False(vm.IsThemeSepia); - Assert.False(vm.IsThemeDark); - Assert.False(vm.IsThemeHighContrast); - Assert.Equal(PdfReaderTheme.Default, vm.ReadingTheme); - Assert.Equal("#F1F5F9", vm.ThemeBackgroundHex); - Assert.Equal("#FFFFFF", vm.ThemePaperBackgroundHex); - } - - [Fact] - public void PdfViewer_ApplyThemeToSkBitmap_TransformsPixels() - { - using var source = new SkiaSharp.SKBitmap(10, 10); - using (var canvas = new SkiaSharp.SKCanvas(source)) - { - canvas.Clear(SkiaSharp.SKColors.White); - } - - // Apply Dark mode - using var darkBmp = PdfViewerViewModel.ApplyThemeToSkBitmap(source, PdfReaderTheme.Dark); - Assert.NotNull(darkBmp); - Assert.Equal(10, darkBmp.Width); - Assert.Equal(10, darkBmp.Height); - - // Pure white should be darkened to dark slate - var pixel = darkBmp.GetPixel(5, 5); - Assert.True(pixel.Red < 100); - Assert.True(pixel.Green < 100); - Assert.True(pixel.Blue < 100); - - // Apply Sepia - using var sepiaBmp = PdfViewerViewModel.ApplyThemeToSkBitmap(source, PdfReaderTheme.Sepia); - Assert.NotNull(sepiaBmp); - var sepiaPixel = sepiaBmp.GetPixel(5, 5); - Assert.True(sepiaPixel.Red > sepiaPixel.Blue); // warm tint has more red than blue - } - - [Fact] - public void PdfViewer_RotateCommands_UpdateRotationAngleAndDynamicFit() - { - var vm = new PdfViewerViewModel(); - var page1 = new PdfViewerPageItem { PageNumber = 1, WidthPoints = 595, HeightPoints = 842, RotationAngle = 0 }; - var page2 = new PdfViewerPageItem { PageNumber = 2, WidthPoints = 595, HeightPoints = 842, RotationAngle = 0 }; - vm.Pages.Add(page1); - vm.Pages.Add(page2); - vm.SelectedPage = page1; - - // Initial state - Assert.Equal(0, page1.RotationAngle); - - // Rotate clockwise: 0 -> 90 -> 180 -> 270 -> 0 - vm.RotateClockwiseCommand.Execute(null); - Assert.Equal(90, page1.RotationAngle); - - vm.RotateClockwiseCommand.Execute(null); - Assert.Equal(180, page1.RotationAngle); - - vm.RotateClockwiseCommand.Execute(null); - Assert.Equal(270, page1.RotationAngle); - - vm.RotateClockwiseCommand.Execute(null); - Assert.Equal(0, page1.RotationAngle); - - // Rotate counter-clockwise: 0 -> 270 -> 180 -> 90 -> 0 - vm.RotateCounterClockwiseCommand.Execute(null); - Assert.Equal(270, page1.RotationAngle); - - vm.RotateCounterClockwiseCommand.Execute(null); - Assert.Equal(180, page1.RotationAngle); - - vm.RotateCounterClockwiseCommand.Execute(null); - Assert.Equal(90, page1.RotationAngle); - - vm.RotateCounterClockwiseCommand.Execute(null); - Assert.Equal(0, page1.RotationAngle); - - // Rotate all pages clockwise - vm.RotateAllPagesClockwiseCommand.Execute(null); - Assert.Equal(90, page1.RotationAngle); - Assert.Equal(90, page2.RotationAngle); - - // Dynamic fit to width with 90 degree rotation should use HeightPoints (842) as target width instead of WidthPoints (595) - vm.FitToWidthDynamic(viewportWidth: 1000, horizontalPadding: 64); - double expectedZoom90 = Math.Clamp(Math.Round((1000 - 64 - 8) / 842.0, 2), 0.25, 5.0); - Assert.Equal(expectedZoom90, vm.ZoomLevel); - - // Rotate back to 0 - page1.RotationAngle = 0; - vm.FitToWidthDynamic(viewportWidth: 1000, horizontalPadding: 64); - double zoomAt0 = vm.ZoomLevel; - Assert.True(zoomAt0 > expectedZoom90); // Portrait fit zoom (1.55) is larger than landscape fit zoom (1.10) - } -} - - diff --git a/tests/PdfEditorApp.Tests/PdfToolViewModelsTests.cs b/tests/PdfEditorApp.Tests/PdfToolViewModelsTests.cs deleted file mode 100644 index a7dbfa8..0000000 --- a/tests/PdfEditorApp.Tests/PdfToolViewModelsTests.cs +++ /dev/null @@ -1,405 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PdfToolViewModelsTests -{ - private readonly IPdfDocumentOperationsService _operationsService; - private readonly IPdfToolRegistry _toolRegistry; - private readonly IPdfToolViewModelFactory _factory; - - public PdfToolViewModelsTests() - { - _toolRegistry = new PdfToolRegistry(); - var pageService = new PdfPageService(); - var optService = new PdfOptimizationService(); - var secService = new PdfSecurityService(); - var convService = new PdfConversionService(); - var ocrService = new PdfOcrService(); - var formService = new PdfFormService(); - var aiService = new AiDocumentService(); - var transService = new DocumentTranslationService(); - var workflowEngine = new PdfWorkflowEngine(pageService, optService, secService, convService, ocrService); - - _operationsService = new PdfDocumentOperationsService( - _toolRegistry, pageService, optService, secService, convService, ocrService, formService, aiService, transService, workflowEngine); - - _factory = new PdfToolViewModelFactory(_operationsService, _toolRegistry); - } - - [Fact] - public void Factory_CreatesDedicatedViewModelsForAllTools() - { - var allTools = _toolRegistry.GetAllTools(); - Assert.NotEmpty(allTools); - - foreach (var def in allTools) - { - if (def.Id == PdfToolId.WorkflowBuilder) continue; - - var vm = _factory.Create(def.Id); - Assert.NotNull(vm); - Assert.Equal(def.Id, vm.Tool.Id); - Assert.Equal(def.Name, vm.Tool.Name); - Assert.False(vm.IsRunning); - Assert.False(vm.IsComplete); - Assert.False(vm.HasError); - } - } - - [Fact] - public void Factory_InstantiatesExactDerivedTypeForSpecificTools() - { - Assert.IsType(_factory.Create(PdfToolId.MergePdf)); - Assert.IsType(_factory.Create(PdfToolId.SplitPdf)); - Assert.IsType(_factory.Create(PdfToolId.CompressPdf)); - Assert.IsType(_factory.Create(PdfToolId.Watermark)); - Assert.IsType(_factory.Create(PdfToolId.RotatePdf)); - Assert.IsType(_factory.Create(PdfToolId.ProtectPdf)); - Assert.IsType(_factory.Create(PdfToolId.UnlockPdf)); - Assert.IsType(_factory.Create(PdfToolId.ComparePdf)); - Assert.IsType(_factory.Create(PdfToolId.HtmlToPdf)); - Assert.IsType(_factory.Create(PdfToolId.SignPdf)); - Assert.IsType(_factory.Create(PdfToolId.OcrPdf)); - Assert.IsType(_factory.Create(PdfToolId.AiSummarizer)); - Assert.IsType(_factory.Create(PdfToolId.TranslatePdf)); - Assert.IsType(_factory.Create(PdfToolId.PdfToMarkdown)); - Assert.IsType(_factory.Create(PdfToolId.PageNumbers)); - Assert.IsType(_factory.Create(PdfToolId.ScanToPdf)); - Assert.IsType(_factory.Create(PdfToolId.CropPdf)); - Assert.IsType(_factory.Create(PdfToolId.PdfForms)); - Assert.IsType(_factory.Create(PdfToolId.RedactPdf)); - Assert.IsType(_factory.Create(PdfToolId.OrganizePdf)); - Assert.IsType(_factory.Create(PdfToolId.PdfToPdfA)); - Assert.IsType(_factory.Create(PdfToolId.RepairPdf)); - Assert.IsType(_factory.Create(PdfToolId.PdfToJpg)); - Assert.IsType(_factory.Create(PdfToolId.JpgToPdf)); - Assert.IsType(_factory.Create(PdfToolId.PdfToWord)); - Assert.IsType(_factory.Create(PdfToolId.WordToPdf)); - Assert.IsType(_factory.Create(PdfToolId.PdfToExcel)); - Assert.IsType(_factory.Create(PdfToolId.ExcelToPdf)); - Assert.IsType(_factory.Create(PdfToolId.PdfToPowerPoint)); - Assert.IsType(_factory.Create(PdfToolId.PowerPointToPdf)); - Assert.IsType(_factory.Create(PdfToolId.EditPdf)); - } - - [Fact] - public void MergePdfToolViewModel_ReordersFilesCorrectly() - { - var vm = (MergePdfToolViewModel)_factory.Create(PdfToolId.MergePdf); - vm.SelectedFiles.Add("file1.pdf"); - vm.SelectedFiles.Add("file2.pdf"); - vm.SelectedFiles.Add("file3.pdf"); - - Assert.Equal(3, vm.SelectedFiles.Count); - Assert.Equal("file1.pdf", vm.SelectedFiles[0]); - - // Move item down - vm.MoveFileDownCommand.Execute("file1.pdf"); - Assert.Equal("file2.pdf", vm.SelectedFiles[0]); - Assert.Equal("file1.pdf", vm.SelectedFiles[1]); - - // Move item up - vm.MoveFileUpCommand.Execute("file3.pdf"); - Assert.Equal("file3.pdf", vm.SelectedFiles[1]); - Assert.Equal("file1.pdf", vm.SelectedFiles[2]); - - // Remove item - vm.RemoveFileCommand.Execute("file3.pdf"); - Assert.Equal(2, vm.SelectedFiles.Count); - Assert.DoesNotContain("file3.pdf", vm.SelectedFiles); - - // Clear - vm.ClearFilesCommand.Execute(null); - Assert.Empty(vm.SelectedFiles); - } - - [Fact] - public void SplitPdfToolViewModel_ConfiguresSplitModes() - { - var vm = (SplitPdfToolViewModel)_factory.Create(PdfToolId.SplitPdf); - Assert.Equal(SplitExtractMode.SplitEveryNPages, vm.SplitMode); - - vm.SplitPagesInterval = 3; - Assert.Equal(3, vm.SplitPagesInterval); - - vm.SplitOddEven = true; - vm.ExtractOddPages = true; - Assert.True(vm.SplitOddEven); - Assert.True(vm.ExtractOddPages); - } - - [Fact] - public void CompressPdfToolViewModel_UpdatesDpiBasedOnPreset() - { - var vm = (CompressPdfToolViewModel)_factory.Create(PdfToolId.CompressPdf); - Assert.Equal(PdfCompressionLevel.Balanced, vm.CompressionLevel); - Assert.Equal(150, vm.ImageQualityDpi); - - vm.CompressionLevel = PdfCompressionLevel.MaximumCompression; - vm.ImageQualityDpi = 72; - Assert.Equal(PdfCompressionLevel.MaximumCompression, vm.CompressionLevel); - Assert.Equal(72, vm.ImageQualityDpi); - } - - [Fact] - public void WatermarkToolViewModel_MaintainsFormattingAndPosition() - { - var vm = (WatermarkToolViewModel)_factory.Create(PdfToolId.Watermark); - vm.WatermarkText = "CONFIDENTIAL DRAFT"; - vm.Opacity = 0.45; - vm.RotationAngle = 30; - vm.ColorHex = "#DC2626"; - vm.Position = WatermarkPosition.TopLeft; - - Assert.Equal("CONFIDENTIAL DRAFT", vm.WatermarkText); - Assert.Equal(0.45, vm.Opacity); - Assert.Equal(30, vm.RotationAngle); - Assert.Equal("#DC2626", vm.ColorHex); - Assert.Equal(WatermarkPosition.TopLeft, vm.Position); - } - - [Fact] - public void RotatePdfToolViewModel_CyclesRotationDegrees() - { - var vm = (RotatePdfToolViewModel)_factory.Create(PdfToolId.RotatePdf); - Assert.Equal(90, vm.RotationDegrees); - - vm.RotationDegrees = 180; - Assert.Equal(180, vm.RotationDegrees); - - vm.RotationDegrees = 270; - Assert.Equal(270, vm.RotationDegrees); - } - - [Fact] - public void ProtectPdfToolViewModel_ConfiguresPasswordsAndPermissions() - { - var vm = (ProtectPdfToolViewModel)_factory.Create(PdfToolId.ProtectPdf); - vm.UserPassword = "UserPass123"; - vm.OwnerPassword = "OwnerPass456"; - vm.AllowPrinting = false; - vm.AllowCopying = false; - - Assert.Equal("UserPass123", vm.UserPassword); - Assert.Equal("OwnerPass456", vm.OwnerPassword); - Assert.False(vm.AllowPrinting); - Assert.False(vm.AllowCopying); - } - - [Fact] - public void SignPdfToolViewModel_ConfiguresSignerDetails() - { - var vm = (SignPdfToolViewModel)_factory.Create(PdfToolId.SignPdf); - vm.SignerName = "John Doe"; - vm.Reason = "Approved Contract"; - vm.Location = "San Francisco, CA"; - vm.Style = SignatureStyle.CursiveElegance; - - Assert.Equal("John Doe", vm.SignerName); - Assert.Equal("Approved Contract", vm.Reason); - Assert.Equal("San Francisco, CA", vm.Location); - Assert.Equal(SignatureStyle.CursiveElegance, vm.Style); - } - - [Fact] - public void HomeViewModel_OpensToolPageWithDedicatedViewModel() - { - var vm = new HomeViewModel( - new MockRecentService(), - new TemplateService(), - new ProjectPersistenceService(), - _toolRegistry, - null, - _factory); - - Assert.Null(vm.ActiveToolViewModel); - Assert.False(vm.IsToolPageActive); - - // Act: Open Watermark tool - vm.OpenToolPageCommand.Execute(PdfToolId.Watermark); - - // Assert - Assert.True(vm.IsToolPageActive); - Assert.NotNull(vm.ActiveToolViewModel); - Assert.IsType(vm.ActiveToolViewModel); - Assert.Equal(PdfToolId.Watermark, vm.ActiveToolViewModel.Tool.Id); - - // Act: Go back - vm.BackToToolsCommand.Execute(null); - - // Assert - Assert.False(vm.IsToolPageActive); - Assert.Null(vm.ActiveToolViewModel); - } - - [Fact] - public void HomeViewModel_ToolStarringSynchronizesBidirectionally() - { - var vm = new HomeViewModel( - new MockRecentService(), - new TemplateService(), - new ProjectPersistenceService(), - _toolRegistry, - null, - _factory); - - // Open Rotate tool - vm.OpenToolPageCommand.Execute(PdfToolId.RotatePdf); - Assert.NotNull(vm.ActiveToolViewModel); - Assert.False(vm.ActiveToolViewModel.IsToolStarred); - - // Toggle star via ViewModel - vm.ActiveToolViewModel.ToggleStarCommand.Execute(null); - Assert.True(vm.ActiveToolViewModel.IsToolStarred); - Assert.NotNull(vm.ActiveToolCard); - Assert.True(vm.ActiveToolCard.IsStarred); - } - - [Fact] - public void PdfToolViewModelBase_SynchronizesPreviewItemsAndReordering() - { - var vm = (MergePdfToolViewModel)_factory.Create(PdfToolId.MergePdf); - vm.SelectedFiles.Add("doc1.pdf"); - vm.SelectedFiles.Add("doc2.pdf"); - vm.SyncPreviewItems(); - - Assert.Equal(2, vm.SelectedFilePreviewItems.Count); - Assert.Equal("doc1.pdf", vm.SelectedFilePreviewItems[0].FileName); - Assert.Equal("#1", vm.SelectedFilePreviewItems[0].OrderIndexText); - Assert.Equal("doc2.pdf", vm.SelectedFilePreviewItems[1].FileName); - Assert.Equal("#2", vm.SelectedFilePreviewItems[1].OrderIndexText); - - // Reorder - vm.MoveFileDownCommand.Execute("doc1.pdf"); - Assert.Equal("doc2.pdf", vm.SelectedFilePreviewItems[0].FileName); - Assert.Equal("#1", vm.SelectedFilePreviewItems[0].OrderIndexText); - Assert.Equal("doc1.pdf", vm.SelectedFilePreviewItems[1].FileName); - Assert.Equal("#2", vm.SelectedFilePreviewItems[1].OrderIndexText); - } - - [Fact] - public void PdfFileHelper_SanitizesTrailingJunkBytesAfterEof() - { - string rawPdf = "%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< >>\nstartxref\n0\n%%EOF\n\n\0\0\0"; - byte[] bytes = System.Text.Encoding.ASCII.GetBytes(rawPdf); - byte[] sanitized = PdfFileHelper.SanitizePdfBytes(bytes); - - string sanitizedText = System.Text.Encoding.ASCII.GetString(sanitized); - Assert.EndsWith("%%EOF\r\n", sanitizedText); - Assert.DoesNotContain("trailing web server junk", sanitizedText); - } - - [Fact] - public void RedactionMarkItem_RaisesPropertyChangedOnDisplayProperties() - { - var item = new RedactionMarkItem - { - Region = new RedactionRegion { PageIndex = 0, X = 10, Y = 20, Width = 100, Height = 50 }, - Label = "Test" - }; - - var changedProps = new System.Collections.Generic.List(); - item.PropertyChanged += (s, e) => { if (e.PropertyName != null) changedProps.Add(e.PropertyName); }; - - item.DisplayX = 15; - item.DisplayY = 25; - item.DisplayWidth = 150; - item.DisplayHeight = 75; - - Assert.Contains(nameof(RedactionMarkItem.DisplayX), changedProps); - Assert.Contains(nameof(RedactionMarkItem.DisplayY), changedProps); - Assert.Contains(nameof(RedactionMarkItem.DisplayWidth), changedProps); - Assert.Contains(nameof(RedactionMarkItem.DisplayHeight), changedProps); - } - - [Fact] - public void RedactPdfToolViewModel_AddManualMark_CreatesBoxEvenWithoutText() - { - var vm = (RedactPdfToolViewModel)_factory.Create(PdfToolId.RedactPdf); - vm.Preview.Pages.Add(new PdfToolPreviewPage { PageNumber = 1, WidthPoints = 612, HeightPoints = 792 }); - vm.Preview.SelectedPage = vm.Preview.Pages[0]; - vm.Preview.CurrentPageNumber = 1; - vm.Preview.ZoomLevel = 1.0; - - // Drag a box where no text exists (raw rectangle mode) - vm.AddManualMark(new Avalonia.Rect(50, 100, 200, 80), forceDrawBox: true); - - Assert.NotEmpty(vm.Marks); - Assert.Single(vm.Marks); - var mark = vm.Marks[0]; - Assert.Equal(50, mark.Region.X); - Assert.Equal(100, mark.Region.Y); - Assert.Equal(200, mark.Region.Width); - Assert.Equal(80, mark.Region.Height); - Assert.Equal("Manual selection", mark.Label); - - Assert.NotEmpty(vm.CurrentPageMarks); - Assert.Equal(50, vm.CurrentPageMarks[0].DisplayX); - Assert.Equal(100, vm.CurrentPageMarks[0].DisplayY); - Assert.Equal(200, vm.CurrentPageMarks[0].DisplayWidth); - Assert.Equal(80, vm.CurrentPageMarks[0].DisplayHeight); - } - - [Fact] - public void RedactPdfToolViewModel_AddManualMark_DefaultDragFallsBackToBoxWhenNoText() - { - var vm = (RedactPdfToolViewModel)_factory.Create(PdfToolId.RedactPdf); - vm.Preview.Pages.Add(new PdfToolPreviewPage { PageNumber = 1, WidthPoints = 612, HeightPoints = 792 }); - vm.Preview.SelectedPage = vm.Preview.Pages[0]; - vm.Preview.CurrentPageNumber = 1; - vm.Preview.ZoomLevel = 1.0; - - // User drags a box without holding Option/Alt (forceDrawBox: false) over a signature or image - vm.AddManualMark(new Avalonia.Rect(40, 80, 160, 50), forceDrawBox: false); - - Assert.NotEmpty(vm.Marks); - Assert.Single(vm.Marks); - var mark = vm.Marks[0]; - Assert.Equal(40, mark.Region.X); - Assert.Equal(80, mark.Region.Y); - Assert.Equal(160, mark.Region.Width); - Assert.Equal(50, mark.Region.Height); - Assert.Equal("Manual selection", mark.Label); - } - - private class MockRecentService : IRecentDocumentsService - { - public List Items { get; } = new(); - public List Load() => new(Items); - public void Add(RecentDocumentItem item) => Items.Insert(0, item); - public void Remove(string filePath) => Items.RemoveAll(x => x.FilePath == filePath); - public void Rename(string oldFilePath, string newFilePath, string newTitle) - { - var match = Items.Find(x => string.Equals(x.FilePath, oldFilePath, StringComparison.OrdinalIgnoreCase)); - if (match != null) - { - match.FilePath = newFilePath; - match.Title = newTitle; - match.LastOpened = DateTime.UtcNow; - } - } - public void Clear() => Items.Clear(); - } -} diff --git a/tests/PdfEditorApp.Tests/PdfToolsServiceTests.cs b/tests/PdfEditorApp.Tests/PdfToolsServiceTests.cs deleted file mode 100644 index c8e5236..0000000 --- a/tests/PdfEditorApp.Tests/PdfToolsServiceTests.cs +++ /dev/null @@ -1,955 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools; -using Xunit; - -namespace PdfEditorApp.Tests; - -/// -/// Comprehensive tests for all PDF Tool services: page operations, optimization, -/// security, conversion, OCR, forms, AI, translation, workflow engine, and tool registry. -/// -public class PdfToolsServiceTests -{ - // ────────────────────────────────────────────────────────────────────────── - // Helper: create a minimal valid PDF binary in memory using PdfSharpCore - // ────────────────────────────────────────────────────────────────────────── - - private static string CreateTempPdf(int pages = 2, string? prefix = null) - { - var path = Path.Combine(Path.GetTempPath(), $"{prefix ?? "test"}_{Guid.NewGuid():N}.pdf"); - using var doc = new PdfSharpCore.Pdf.PdfDocument(); - doc.Info.Title = "Test Document"; - for (int i = 0; i < pages; i++) - { - var page = doc.AddPage(); - page.Width = PdfSharpCore.Drawing.XUnit.FromPoint(595); - page.Height = PdfSharpCore.Drawing.XUnit.FromPoint(842); - using var gfx = PdfSharpCore.Drawing.XGraphics.FromPdfPage(page); - var font = new PdfSharpCore.Drawing.XFont("Arial", 12); - gfx.DrawString($"Test Page {i + 1}", font, PdfSharpCore.Drawing.XBrushes.Black, - new PdfSharpCore.Drawing.XRect(50, 50, 495, 30), PdfSharpCore.Drawing.XStringFormats.TopLeft); - } - doc.Save(path); - return path; - } - - private static string CreateImageHeavyTempPdf(int pages = 2) - { - var path = Path.Combine(Path.GetTempPath(), $"img_heavy_{Guid.NewGuid():N}.pdf"); - using var doc = new PdfSharpCore.Pdf.PdfDocument(); - for (int i = 0; i < pages; i++) - { - var page = doc.AddPage(); - page.Width = PdfSharpCore.Drawing.XUnit.FromPoint(595); - page.Height = PdfSharpCore.Drawing.XUnit.FromPoint(842); - using var gfx = PdfSharpCore.Drawing.XGraphics.FromPdfPage(page); - - using var bmp = new SkiaSharp.SKBitmap(600, 600); - using var canvas = new SkiaSharp.SKCanvas(bmp); - canvas.Clear(SkiaSharp.SKColors.LightBlue); - using var paint = new SkiaSharp.SKPaint { Color = SkiaSharp.SKColors.DarkRed, StrokeWidth = 6 }; - for (int line = 0; line < 50; line++) - { - canvas.DrawLine(0, line * 12, 600, 600 - line * 12, paint); - } - using var img = SkiaSharp.SKImage.FromBitmap(bmp); - using var data = img.Encode(SkiaSharp.SKEncodedImageFormat.Png, 100); - using var ximg = PdfSharpCore.Drawing.XImage.FromStream(() => new MemoryStream(data.ToArray())); - gfx.DrawImage(ximg, 50, 50, 495, 495); - } - doc.Save(path); - return path; - } - - private static void CleanupFiles(params string[] paths) - { - foreach (var p in paths) - if (p != null && File.Exists(p)) try { File.Delete(p); } catch { } - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfPageService Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task PdfPageService_Merge_CombinesMultipleFiles() - { - var file1 = CreateTempPdf(2, "merge1"); - var file2 = CreateTempPdf(3, "merge2"); - var outPath = Path.Combine(Path.GetTempPath(), $"merged_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfPageService(); - var opts = new MergeToolOptions - { - InputFiles = new List { file1, file2 }, - OutputFilePath = outPath - }; - var result = await svc.MergePdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - // 2 + 3 = 5 pages - using var merged = UglyToad.PdfPig.PdfDocument.Open(outPath); - Assert.Equal(5, merged.NumberOfPages); - } - finally { CleanupFiles(file1, file2, outPath); } - } - - [Fact] - public async Task PdfPageService_Merge_FailsOnMissingFile() - { - var svc = new PdfPageService(); - var result = await svc.MergePdfAsync(new MergeToolOptions - { - InputFiles = new List { "/nonexistent/file.pdf" }, - OutputFilePath = "/tmp/out.pdf" - }); - Assert.False(result.Success); - Assert.NotEmpty(result.ErrorMessage ?? ""); - } - - [Fact] - public void PdfFileHelper_IsFileLocked_DetectsLockedFiles() - { - string tempPath = Path.Combine(Path.GetTempPath(), $"lock_test_{Guid.NewGuid():N}.pdf"); - File.WriteAllText(tempPath, "Sample PDF"); - try - { - Assert.False(PdfFileHelper.IsFileLocked(tempPath)); - - // Exclusively open/lock the file - using (var stream = new FileStream(tempPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) - { - Assert.True(PdfFileHelper.IsFileLocked(tempPath)); - } - - // Once disposed, lock is released - Assert.False(PdfFileHelper.IsFileLocked(tempPath)); - } - finally - { - if (File.Exists(tempPath)) File.Delete(tempPath); - } - } - - [Fact] - public void PdfFileHelper_ResolveSafeOutputPath_AvoidsLockedFilesAndAvoidPaths() - { - string tempDir = Path.Combine(Path.GetTempPath(), $"safe_path_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - string lockedDefault = Path.Combine(tempDir, "Merged_Document.pdf"); - File.WriteAllText(lockedDefault, "existing document"); - - try - { - // When default is locked, it resolves to Merged_Document_1.pdf - using (var stream = new FileStream(lockedDefault, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) - { - string resolved = PdfFileHelper.ResolveSafeOutputPath(null, tempDir, "Merged_Document", ".pdf"); - Assert.Equal(Path.Combine(tempDir, "Merged_Document_1.pdf"), resolved); - } - - // Avoid list (e.g. input file matching candidate) - string resolvedAvoid = PdfFileHelper.ResolveSafeOutputPath(null, tempDir, "Merged_Document", ".pdf", [lockedDefault]); - Assert.Equal(Path.Combine(tempDir, "Merged_Document_1.pdf"), resolvedAvoid); - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public async Task PdfPageService_Merge_AutoResolvesWhenDefaultIsLocked() - { - string tempDir = Path.Combine(Path.GetTempPath(), $"merge_lock_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - string file1 = Path.Combine(tempDir, "Input1.pdf"); - string file2 = Path.Combine(tempDir, "Input2.pdf"); - File.Copy(CreateTempPdf(1, "src1"), file1, true); - File.Copy(CreateTempPdf(1, "src2"), file2, true); - - // Lock the default output path - string lockedDefault = Path.Combine(tempDir, "Merged_Document.pdf"); - File.WriteAllText(lockedDefault, "Locked existing file"); - - try - { - using (var stream = new FileStream(lockedDefault, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) - { - var svc = new PdfPageService(); - var opts = new MergeToolOptions - { - InputFiles = new List { file1, file2 } - // OutputFilePath left empty to trigger auto-resolution - }; - - var result = await svc.MergePdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.NotNull(result.OutputFilePath); - Assert.True(File.Exists(result.OutputFilePath)); - Assert.NotEqual(lockedDefault, result.OutputFilePath); - Assert.Contains("Merged_Document_1.pdf", result.OutputFilePath); - } - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public async Task PdfPageService_Split_ProducesOnePdfPerPage() - { - var inputPath = CreateTempPdf(4, "split"); - var outDir = Path.Combine(Path.GetTempPath(), $"split_{Guid.NewGuid():N}"); - try - { - var svc = new PdfPageService(); - var opts = new SplitToolOptions - { - InputFilePath = inputPath, - OutputDirectory = outDir, - Mode = SplitExtractMode.SplitEveryNPages, - PagesPerSplit = 1 - }; - var result = await svc.SplitPdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.NotNull(result.OutputFiles); - Assert.Equal(4, result.OutputFiles!.Count); - foreach (var f in result.OutputFiles) Assert.True(File.Exists(f)); - } - finally - { - CleanupFiles(inputPath); - if (Directory.Exists(outDir)) Directory.Delete(outDir, true); - } - } - - [Fact] - public async Task PdfPageService_Split_ByRanges_ProducesCorrectCount() - { - var inputPath = CreateTempPdf(6, "splitrange"); - var outDir = Path.Combine(Path.GetTempPath(), $"splitrange_{Guid.NewGuid():N}"); - try - { - var svc = new PdfPageService(); - var opts = new SplitToolOptions - { - InputFilePath = inputPath, - OutputDirectory = outDir, - Mode = SplitExtractMode.SplitByPageRanges, - RangeExpression = "1-2, 3-4, 5-6" - }; - var result = await svc.SplitPdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.Equal(3, result.OutputFiles!.Count); - } - finally - { - CleanupFiles(inputPath); - if (Directory.Exists(outDir)) Directory.Delete(outDir, true); - } - } - - [Fact] - public async Task PdfPageService_Rotate_ProducesOutputFile() - { - var inputPath = CreateTempPdf(2, "rotate"); - var outPath = Path.Combine(Path.GetTempPath(), $"rotated_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfPageService(); - var opts = new RotateToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - RotationDegrees = 90, - TargetFilter = PageFilterTarget.All - }; - var result = await svc.RotatePdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - Assert.True(new FileInfo(outPath).Length > 0); - } - finally { CleanupFiles(inputPath, outPath); } - } - - [Fact] - public async Task PdfPageService_AddPageNumbers_ProducesValidPdf() - { - var inputPath = CreateTempPdf(3, "pagenums"); - var outPath = Path.Combine(Path.GetTempPath(), $"pagenums_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfPageService(); - var opts = new PageNumberToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - Position = PageNumberPosition.BottomCenter, - Template = "Page {n} of {total}", - FontSize = 10 - }; - var result = await svc.AddPageNumbersAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(inputPath, outPath); } - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfOptimizationService Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task PdfOptimizationService_Compress_ProducesOutputFile() - { - var inputPath = CreateTempPdf(3, "compress"); - var outPath = Path.Combine(Path.GetTempPath(), $"compressed_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfOptimizationService(); - var opts = new CompressToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - Level = PdfCompressionLevel.Balanced - }; - var result = await svc.CompressPdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - Assert.True(result.OutputSizeBytes <= result.OriginalSizeBytes); - } - finally { CleanupFiles(inputPath, outPath); } - } - - [Fact] - public async Task PdfOptimizationService_Compress_ImageHeavyPdf_SignificantlyReducesFileSize() - { - var inputPath = CreateImageHeavyTempPdf(2); - var outPath = Path.Combine(Path.GetTempPath(), $"compressed_img_{Guid.NewGuid():N}.pdf"); - try - { - long origSize = new FileInfo(inputPath).Length; - Assert.True(origSize > 10000, "Test image PDF should have substantial size."); - - var svc = new PdfOptimizationService(); - var opts = new CompressToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - Level = PdfCompressionLevel.MaximumCompression, - ImageQualityDpi = 96, - RemoveMetadata = true - }; - var result = await svc.CompressPdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - Assert.True(result.OutputSizeBytes < result.OriginalSizeBytes, $"Expected compressed size ({result.OutputSizeBytes}) to be strictly less than original size ({result.OriginalSizeBytes})."); - } - finally { CleanupFiles(inputPath, outPath); } - } - - [Fact] - public async Task PdfOptimizationService_Compress_NeverIncreasesFileSizeOnAlreadyCompactPdf() - { - var inputPath = CreateTempPdf(2, "compact"); - var outPath = Path.Combine(Path.GetTempPath(), $"compressed_compact_{Guid.NewGuid():N}.pdf"); - try - { - long origSize = new FileInfo(inputPath).Length; - - var svc = new PdfOptimizationService(); - var opts = new CompressToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - Level = PdfCompressionLevel.Balanced - }; - var result = await svc.CompressPdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - Assert.True(result.OutputSizeBytes <= origSize, $"Output size ({result.OutputSizeBytes}) should NEVER exceed original size ({origSize})."); - } - finally { CleanupFiles(inputPath, outPath); } - } - - [Fact] - public async Task PdfOptimizationService_Compress_FailsOnMissingFile() - { - var svc = new PdfOptimizationService(); - var result = await svc.CompressPdfAsync(new CompressToolOptions - { - InputFilePath = "/nonexistent/file.pdf", - OutputFilePath = "/tmp/out.pdf" - }); - Assert.False(result.Success); - } - - [Fact] - public async Task PdfOptimizationService_Repair_ProducesOutputFile() - { - var inputPath = CreateTempPdf(2, "repair"); - var outPath = Path.Combine(Path.GetTempPath(), $"repaired_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfOptimizationService(); - var opts = new RepairToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath - }; - var result = await svc.RepairPdfAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(inputPath, outPath); } - } - - [Fact] - public async Task PdfOptimizationService_ConvertToPdfA_ProducesOutputFile() - { - var inputPath = CreateTempPdf(1, "pdfa"); - var outPath = Path.Combine(Path.GetTempPath(), $"pdfa_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfOptimizationService(); - var opts = new PdfAToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - Standard = PdfAStandard.PdfA1b - }; - var result = await svc.ConvertToPdfAAsync(opts); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(inputPath, outPath); } - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfSecurityService Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task PdfSecurityService_ProtectAndUnlock_RoundTrip() - { - var inputPath = CreateTempPdf(2, "protect"); - var protectedPath = Path.Combine(Path.GetTempPath(), $"protected_{Guid.NewGuid():N}.pdf"); - var unlockedPath = Path.Combine(Path.GetTempPath(), $"unlocked_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfSecurityService(); - - // Protect - var protectResult = await svc.ProtectPdfAsync(new SecurityToolOptions - { - InputFilePath = inputPath, - OutputFilePath = protectedPath, - UserPassword = "user123", - OwnerPassword = "owner456", - AllowPrinting = true, - AllowCopying = false - }); - Assert.True(protectResult.Success, protectResult.ErrorMessage); - Assert.True(File.Exists(protectedPath)); - - // Unlock - var unlockResult = await svc.UnlockPdfAsync(new UnlockToolOptions - { - InputFilePath = protectedPath, - OutputFilePath = unlockedPath, - Password = "owner456" - }); - Assert.True(unlockResult.Success, unlockResult.ErrorMessage); - Assert.True(File.Exists(unlockedPath)); - } - finally { CleanupFiles(inputPath, protectedPath, unlockedPath); } - } - - [Fact] - public async Task PdfSecurityService_Protect_FailsOnMissingFile() - { - var svc = new PdfSecurityService(); - var result = await svc.ProtectPdfAsync(new SecurityToolOptions - { - InputFilePath = "/nonexistent/file.pdf", - OutputFilePath = "/tmp/protected.pdf", - UserPassword = "pass" - }); - Assert.False(result.Success); - } - - [Fact] - public async Task PdfSecurityService_AddWatermark_ProducesOutputFile() - { - var inputPath = CreateTempPdf(2, "watermark"); - var outPath = Path.Combine(Path.GetTempPath(), $"watermarked_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfSecurityService(); - var result = await svc.AddWatermarkAsync(new WatermarkToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - Text = "CONFIDENTIAL", - Opacity = 0.3, - FontSize = 48, - RotationAngle = -45 - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(inputPath, outPath); } - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfConversionService Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task PdfConversionService_PdfToWord_ProducesDocxFile() - { - var inputPath = CreateTempPdf(2, "toword"); - var outPath = Path.Combine(Path.GetTempPath(), $"output_{Guid.NewGuid():N}.docx"); - try - { - var svc = new PdfConversionService(); - var result = await svc.ConvertPdfToWordAsync(new WordConversionOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - Assert.True(new FileInfo(outPath).Length > 0); - } - finally { CleanupFiles(inputPath, outPath); } - } - - [Fact] - public async Task PdfConversionService_PdfToExcel_ProducesXlsxFile() - { - var inputPath = CreateTempPdf(1, "toexcel"); - var outPath = Path.Combine(Path.GetTempPath(), $"output_{Guid.NewGuid():N}.xlsx"); - try - { - var svc = new PdfConversionService(); - var result = await svc.ConvertPdfToExcelAsync(new ExcelConversionOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(inputPath, outPath); } - } - - [Fact] - public async Task PdfConversionService_PdfToImages_ProducesImageFiles() - { - var inputPath = CreateTempPdf(2, "toimages"); - var outDir = Path.Combine(Path.GetTempPath(), $"images_{Guid.NewGuid():N}"); - try - { - var svc = new PdfConversionService(); - var result = await svc.ConvertPdfToImagesAsync(new ImageConversionOptions - { - InputFilePath = inputPath, - OutputDirectory = outDir, - OutputFormat = "png", - Dpi = 72 - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.NotNull(result.OutputFiles); - Assert.Equal(2, result.OutputFiles!.Count); - foreach (var f in result.OutputFiles) Assert.True(File.Exists(f)); - } - finally - { - CleanupFiles(inputPath); - if (Directory.Exists(outDir)) Directory.Delete(outDir, true); - } - } - - [Fact] - public async Task PdfConversionService_ImagesToPdf_ProducesValidPdf() - { - // Create a temp PNG - string imgPath = Path.Combine(Path.GetTempPath(), $"img_{Guid.NewGuid():N}.png"); - using (var surface = SkiaSharp.SKSurface.Create(new SkiaSharp.SKImageInfo(400, 600))) - { - surface.Canvas.Clear(SkiaSharp.SKColors.LightBlue); - using var img = surface.Snapshot(); - using var data = img.Encode(SkiaSharp.SKEncodedImageFormat.Png, 90); - using var fs = File.OpenWrite(imgPath); - data.SaveTo(fs); - } - var outPath = Path.Combine(Path.GetTempPath(), $"fromimages_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfConversionService(); - var result = await svc.ConvertImagesToPdfAsync(new ImagesToPdfOptions - { - ImageFiles = new List { imgPath }, - OutputFilePath = outPath - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(imgPath, outPath); } - } - - [Fact] - public async Task PdfConversionService_HtmlToPdf_ProducesValidPdf() - { - var outPath = Path.Combine(Path.GetTempPath(), $"fromhtml_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfConversionService(); - var result = await svc.ConvertHtmlToPdfAsync(new HtmlToPdfOptions - { - HtmlContentOrUrl = "

    Test

    Hello PDF world

    ", - IsUrl = false, - OutputFilePath = outPath - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(outPath); } - } - - [Fact] - public async Task PdfConversionService_PdfToMarkdown_ProducesMarkdownFile() - { - var inputPath = CreateTempPdf(2, "tomd"); - var outPath = Path.Combine(Path.GetTempPath(), $"output_{Guid.NewGuid():N}.md"); - try - { - var svc = new PdfConversionService(); - var result = await svc.ConvertPdfToMarkdownAsync(new MarkdownConversionOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - IncludeMetadataHeader = true - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - var content = await File.ReadAllTextAsync(outPath); - Assert.Contains("---", content); // YAML frontmatter - } - finally { CleanupFiles(inputPath, outPath); } - } - - [Fact] - public async Task PdfConversionService_PdfToWord_SupportsCancellation() - { - var inputPath = CreateTempPdf(2, "cancelword"); - var outPath = Path.Combine(Path.GetTempPath(), $"cancelword_{Guid.NewGuid():N}.docx"); - try - { - var svc = new PdfConversionService(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); // cancel immediately - await Assert.ThrowsAnyAsync(() => - svc.ConvertPdfToWordAsync(new WordConversionOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath - }, ct: cts.Token)); - } - finally { CleanupFiles(inputPath, outPath); } - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfOcrService Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task PdfOcrService_OcrPdf_ProducesOutputFile() - { - var inputPath = CreateTempPdf(2, "ocr"); - var outPath = Path.Combine(Path.GetTempPath(), $"ocr_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfOcrService(); - var result = await svc.OcrPdfAsync(new OcrToolOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(inputPath, outPath); } - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfFormService Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task PdfFormService_ExtractFields_ReturnsResult() - { - var inputPath = CreateTempPdf(1, "forms"); - try - { - var svc = new PdfFormService(); - var fields = await svc.ExtractFormFieldsAsync(inputPath); - // A simple text PDF has no AcroForm fields - should succeed with empty dictionary - Assert.NotNull(fields); - } - finally { CleanupFiles(inputPath); } - } - - // ────────────────────────────────────────────────────────────────────────── - // AiDocumentService Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task AiDocumentService_Summarize_ProducesNonEmptySummary() - { - var inputPath = CreateTempPdf(3, "ai"); - try - { - var svc = new AiDocumentService(); - var result = await svc.SummarizePdfAsync(new AiSummaryOptions - { - InputFilePath = inputPath, - MaxBulletPoints = 3 - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.NotEmpty(result.Message ?? ""); - } - finally { CleanupFiles(inputPath); } - } - - [Fact] - public async Task AiDocumentService_Summarize_FailsOnMissingFile() - { - var svc = new AiDocumentService(); - var result = await svc.SummarizePdfAsync(new AiSummaryOptions - { - InputFilePath = "/nonexistent/ghost.pdf" - }); - Assert.False(result.Success); - } - - // ────────────────────────────────────────────────────────────────────────── - // DocumentTranslationService Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task DocumentTranslationService_Translate_ProducesOutputFile() - { - var inputPath = CreateTempPdf(2, "translate"); - var outPath = Path.Combine(Path.GetTempPath(), $"translated_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new DocumentTranslationService(); - var result = await svc.TranslatePdfAsync(new TranslationOptions - { - InputFilePath = inputPath, - OutputFilePath = outPath, - TargetLanguage = "Spanish" - }); - Assert.True(result.Success, result.ErrorMessage); - Assert.True(File.Exists(outPath)); - } - finally { CleanupFiles(inputPath, outPath); } - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfWorkflowEngine Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task PdfWorkflowEngine_ExecuteWorkflow_RunsAllSteps() - { - var inputPath = CreateTempPdf(3, "workflow"); - var outDir = Path.Combine(Path.GetTempPath(), $"workflow_{Guid.NewGuid():N}"); - Directory.CreateDirectory(outDir); - try - { - var engine = new PdfWorkflowEngine(); - - var workflow = new WorkflowDefinition - { - Name = "Test Workflow", - OutputDirectory = outDir, - Steps = new List - { - new() { ToolId = PdfToolId.CompressPdf, IsEnabled = true }, - new() { ToolId = PdfToolId.RotatePdf, IsEnabled = true, ParametersJson = "{\"RotationDegrees\":90}" } - } - }; - - var result = await engine.ExecuteWorkflowAsync(workflow, new List { inputPath }); - Assert.True(result.Success, result.ErrorMessage); - Assert.NotNull(result.OutputFiles); - Assert.True(result.OutputFiles!.Count > 0); - } - finally - { - CleanupFiles(inputPath); - if (Directory.Exists(outDir)) Directory.Delete(outDir, true); - } - } - - [Fact] - public void PdfWorkflowEngine_SaveAndLoad_WorkflowDefinition() - { - var engine = new PdfWorkflowEngine(); - - var workflow = new WorkflowDefinition - { - Name = "Save/Load Test", - Description = "Tests serialization roundtrip", - Steps = new List - { - new() { ToolId = PdfToolId.MergePdf }, - new() { ToolId = PdfToolId.CompressPdf } - } - }; - - string json = engine.SerializeWorkflow(workflow); - Assert.NotEmpty(json); - - var loaded = engine.DeserializeWorkflow(json); - Assert.NotNull(loaded); - Assert.Equal("Save/Load Test", loaded!.Name); - Assert.Equal(2, loaded.Steps.Count); - Assert.Equal(PdfToolId.MergePdf, loaded.Steps[0].ToolId); - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfToolRegistry Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public void PdfToolRegistry_GetAllTools_Returns32Tools() - { - var registry = new PdfToolRegistry(); - var tools = registry.GetAllTools(); - Assert.NotEmpty(tools); - Assert.True(tools.Count >= 32, $"Expected 32+ tool definitions, got {tools.Count}"); - } - - [Fact] - public void PdfToolRegistry_GetTool_ReturnsCorrectDefinition() - { - var registry = new PdfToolRegistry(); - var merge = registry.GetTool(PdfToolId.MergePdf); - Assert.NotNull(merge); - Assert.Equal(PdfToolId.MergePdf, merge!.Id); - Assert.NotEmpty(merge.Name); - Assert.NotEmpty(merge.Description); - } - - [Fact] - public void PdfToolRegistry_GetByCategory_ReturnsSubset() - { - var registry = new PdfToolRegistry(); - var organizeTools = registry.GetToolsByCategory(PdfToolCategory.OrganizeAndPage); - Assert.NotEmpty(organizeTools); - Assert.All(organizeTools, t => Assert.Equal(PdfToolCategory.OrganizeAndPage, t.Category)); - } - - [Fact] - public void PdfToolRegistry_AllTools_HaveUniqueIds() - { - var registry = new PdfToolRegistry(); - var tools = registry.GetAllTools(); - var ids = tools.Select(t => t.Id).ToList(); - Assert.Equal(ids.Count, ids.Distinct().Count()); - } - - [Fact] - public void PdfToolRegistry_AllTools_HaveValidMetadata() - { - var registry = new PdfToolRegistry(); - foreach (var tool in registry.GetAllTools()) - { - Assert.NotEmpty(tool.Name); - Assert.NotEmpty(tool.Description); - Assert.NotEmpty(tool.IconColorHex); - } - } - - // ────────────────────────────────────────────────────────────────────────── - // PdfDocumentOperationsService Integration Tests - // ────────────────────────────────────────────────────────────────────────── - - [Fact] - public async Task PdfDocumentOperationsService_Merge_ReturnsSuccess() - { - var file1 = CreateTempPdf(2, "opsmerge1"); - var file2 = CreateTempPdf(2, "opsmerge2"); - var outPath = Path.Combine(Path.GetTempPath(), $"opsmerge_{Guid.NewGuid():N}.pdf"); - try - { - var svc = new PdfDocumentOperationsService(); - var result = await svc.ExecuteToolAsync(PdfToolId.MergePdf, - new MergeToolOptions - { - InputFiles = new List { file1, file2 }, - OutputFilePath = outPath - }); - Assert.True(result.Success, result.ErrorMessage); - } - finally { CleanupFiles(file1, file2, outPath); } - } - - [Fact] - public async Task PdfDocumentOperationsService_ProgressReporting_Fires() - { - var inputPath = CreateTempPdf(3, "progress"); - var outDir = Path.Combine(Path.GetTempPath(), $"progressdir_{Guid.NewGuid():N}"); - try - { - var svc = new PdfDocumentOperationsService(); - var progressValues = new List(); - var progress = new Progress(v => progressValues.Add(v)); - - await svc.ExecuteToolAsync(PdfToolId.SplitPdf, - new SplitToolOptions - { - InputFilePath = inputPath, - OutputDirectory = outDir, - Mode = SplitExtractMode.SplitEveryNPages, - PagesPerSplit = 1 - }, - progress: progress); - - // Give progress callbacks a moment to fire - await Task.Delay(50); - Assert.True(progressValues.Count > 0, "Progress should have reported at least once"); - } - finally - { - CleanupFiles(inputPath); - if (Directory.Exists(outDir)) Directory.Delete(outDir, true); - } - } -} diff --git a/tests/PdfEditorApp.Tests/PipelineTests.cs b/tests/PdfEditorApp.Tests/PipelineTests.cs deleted file mode 100644 index 9792c8f..0000000 --- a/tests/PdfEditorApp.Tests/PipelineTests.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins.Pipelines; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PipelineTests -{ - private class TestPipelineContext - { - public List Trace { get; } = new(); - public int Value { get; set; } - } - - [Fact] - public async Task Waterfall_ExecutesMiddlewareInWrappingOrderWithTerminal() - { - var manager = new PipelineManager(); - var context = new TestPipelineContext(); - - manager.RegisterWaterfall("test:export", async (ctx, next) => - { - ctx.Trace.Add("M1-Before"); - ctx.Value += 10; - await next(); - ctx.Trace.Add("M1-After"); - }); - - manager.RegisterWaterfall("test:export", async (ctx, next) => - { - ctx.Trace.Add("M2-Before"); - ctx.Value *= 2; - await next(); - ctx.Trace.Add("M2-After"); - }); - - await manager.ExecuteWaterfallAsync("test:export", context, terminal: () => - { - context.Trace.Add("Terminal"); - context.Value += 5; - return Task.CompletedTask; - }); - - // Pipeline execution order: M1-Before -> M2-Before -> Terminal -> M2-After -> M1-After - Assert.Equal(new[] { "M1-Before", "M2-Before", "Terminal", "M2-After", "M1-After" }, context.Trace); - // Value: (0 + 10) * 2 + 5 = 25 - Assert.Equal(25, context.Value); - } - - [Fact] - public async Task Bail_HaltsOnFirstNonNullResult() - { - var manager = new PipelineManager(); - var executionLog = new List(); - - manager.RegisterBail("convert:file", async input => - { - executionLog.Add("Handler1"); - if (input.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)) - return await Task.FromResult("DocxConverted"); - return null; - }); - - manager.RegisterBail("convert:file", async input => - { - executionLog.Add("Handler2"); - if (input.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase)) - return await Task.FromResult("ExcelConverted"); - return null; - }); - - manager.RegisterBail("convert:file", async input => - { - executionLog.Add("Handler3"); - return await Task.FromResult("FallbackConverted"); - }); - - // Test 1: Handled by Handler2 - var resultXlsx = await manager.ExecuteBailAsync("convert:file", "sheet.xlsx"); - Assert.Equal("ExcelConverted", resultXlsx); - Assert.Equal(new[] { "Handler1", "Handler2" }, executionLog); - - executionLog.Clear(); - - // Test 2: Handled by Handler1 - var resultDocx = await manager.ExecuteBailAsync("convert:file", "doc.docx"); - Assert.Equal("DocxConverted", resultDocx); - Assert.Equal(new[] { "Handler1" }, executionLog); - } - - [Fact] - public async Task Parallel_ExecutesAllHandlersConcurrently() - { - var manager = new PipelineManager(); - var results = new List(); - var lockObj = new object(); - - manager.RegisterParallel("event:notify", async val => - { - await Task.Delay(10); - lock (lockObj) results.Add(val * 1); - }); - - manager.RegisterParallel("event:notify", async val => - { - await Task.Delay(5); - lock (lockObj) results.Add(val * 2); - }); - - await manager.ExecuteParallelAsync("event:notify", 5); - - Assert.Equal(2, results.Count); - Assert.Contains(5, results); - Assert.Contains(10, results); - } - - [Fact] - public async Task Serial_ExecutesHandlersInDeterministicOrder() - { - var manager = new PipelineManager(); - var results = new List(); - - manager.RegisterSerial("validate:doc", async name => - { - await Task.Delay(15); - results.Add($"Step1-{name}"); - }); - - manager.RegisterSerial("validate:doc", async name => - { - await Task.Delay(5); - results.Add($"Step2-{name}"); - }); - - await manager.ExecuteSerialAsync("validate:doc", "Sample"); - - Assert.Equal(new[] { "Step1-Sample", "Step2-Sample" }, results); - } - - [Fact] - public async Task Pipeline_DisposingRegistration_RemovesFromExecution() - { - var manager = new PipelineManager(); - var count = 0; - - var reg = manager.RegisterSerial("counter", _ => - { - count++; - return Task.CompletedTask; - }); - - await manager.ExecuteSerialAsync("counter", 0); - Assert.Equal(1, count); - - // Dispose the registration - reg.Dispose(); - - await manager.ExecuteSerialAsync("counter", 0); - Assert.Equal(1, count); // Not called again - } -} diff --git a/tests/PdfEditorApp.Tests/PluginAssemblyLoaderTests.cs b/tests/PdfEditorApp.Tests/PluginAssemblyLoaderTests.cs deleted file mode 100644 index 5a6ca62..0000000 --- a/tests/PdfEditorApp.Tests/PluginAssemblyLoaderTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.IO; -using PdfEditorApp.Plugins.Loader; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PluginAssemblyLoaderTests -{ - [Fact] - public void LoadPluginAssembly_ThrowsWhenFileNotFound() - { - var nonExistentPath = Path.Combine(Path.GetTempPath(), $"missing_{Guid.NewGuid():N}.dll"); - Assert.Throws(() => PluginAssemblyLoader.LoadPluginAssembly(nonExistentPath)); - } - - [Fact] - public void DiscoverAndLoadDirectory_HandlesNonExistentDirectoryGracefully() - { - var nonExistentDir = Path.Combine(Path.GetTempPath(), $"dir_{Guid.NewGuid():N}"); - var results = PluginAssemblyLoader.DiscoverAndLoadDirectory(nonExistentDir); - Assert.Empty(results); - } - - [Fact] - public void DiscoverAndLoadDirectory_HandlesEmptyDirectory() - { - var tempDir = Path.Combine(Path.GetTempPath(), $"plugins_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - try - { - var results = PluginAssemblyLoader.DiscoverAndLoadDirectory(tempDir); - Assert.Empty(results); - } - finally - { - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, recursive: true); - } - } - } -} diff --git a/tests/PdfEditorApp.Tests/PluginEcosystemTests.cs b/tests/PdfEditorApp.Tests/PluginEcosystemTests.cs deleted file mode 100644 index 721f8d5..0000000 --- a/tests/PdfEditorApp.Tests/PluginEcosystemTests.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Converters; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Manifests; -using PdfEditorApp.Core.Plugins.Settings; -using PdfEditorApp.Plugins.Loader; -using PdfEditorApp.Services.Inspector; -using PdfEditorApp.Services.Ribbon; -using PdfEditorApp.Services.Sidebar; -using PdfEditorApp.Services.StatusBar; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PluginEcosystemTests -{ - [Fact] - public void DynamicRibbon_Tabs_Groups_And_Actions_Are_Reactive() - { - var registry = new RibbonRegistry(); - bool changedFired = false; - registry.RegistryChanged += () => changedFired = true; - - // Register tab - using var tabReg = registry.RegisterTab(new RibbonTabDescriptor - { - Id = "CustomAiTab", - Title = "AI Studio", - Order = 10 - }); - - Assert.True(changedFired); - Assert.Single(registry.GetAllTabs()); - Assert.Equal("CustomAiTab", registry.GetAllTabs()[0].Id); - - // Register group - using var groupReg = registry.RegisterGroup(new RibbonGroupDescriptor - { - Id = "AiGenGroup", - TabId = "CustomAiTab", - Title = "Generation", - Order = 1 - }); - - // Register action - bool actionExecuted = false; - registry.RegisterAction(new RibbonActionDescriptor - { - Id = "action.generate.summary", - TabId = "CustomAiTab", - GroupId = "AiGenGroup", - Label = "Summarize", - IconKind = "Brain", - Action = _ => actionExecuted = true - }); - - var groups = registry.GetGroupsForTab("CustomAiTab"); - Assert.Single(groups); - Assert.Equal("Generation", groups[0].Title); - - var actions = registry.GetActionsForGroup("CustomAiTab", "AiGenGroup"); - Assert.Single(actions); - Assert.Equal("Summarize", actions[0].Label); - - // Execute action - actions[0].Action!(null!); - Assert.True(actionExecuted); - - // Unregister action - Assert.True(registry.UnregisterAction("action.generate.summary")); - Assert.Empty(registry.GetActionsForGroup("CustomAiTab", "AiGenGroup")); - } - - [Fact] - public void DynamicSidebar_Registration_And_Selection_Works() - { - var registry = new SidebarRegistry(); - bool changed = false; - registry.RegistryChanged += () => changed = true; - - var sidebarDesc = new SidebarTabDescriptor - { - Id = "sidebar.ai.copilot", - Title = "AI Copilot", - IconKind = "Robot", - Tooltip = "Interactive PDF Assistant", - ViewFactory = sp => "CopilotViewInstance" - }; - - using var unreg = registry.RegisterTab(sidebarDesc); - - Assert.True(changed); - Assert.Single(registry.GetAllTabs()); - Assert.Equal("sidebar.ai.copilot", registry.GetTab("sidebar.ai.copilot")?.Id); - - var view = sidebarDesc.ViewFactory(null!); - Assert.Equal("CopilotViewInstance", view); - } - - [Fact] - public void DynamicInspector_Reacts_To_RegistryChanged() - { - var registry = new InspectorRegistry(); - var inspectorVm = new InspectorViewModel(inspectorRegistry: registry); - - Assert.Empty(inspectorVm.DynamicSections); - - // Register section - var secDesc = new InspectorSectionDescriptor - { - SectionId = "section.crypto.verify", - Title = "Signature Verification", - AppliesTo = target => target is string s && s == "Signature", - Factory = (sp, el) => $"VerifiedPanelFor_{el}" - }; - - using var unreg = registry.RegisterSection(secDesc); - - // Target matching - inspectorVm.RefreshDynamicSections("Signature"); - Assert.Contains(inspectorVm.DynamicSections, s => s is DynamicInspectorSectionViewModel d && (string?)d.Content == "VerifiedPanelFor_Signature"); - - // Target not matching - inspectorVm.RefreshDynamicSections("Text"); - Assert.DoesNotContain(inspectorVm.DynamicSections, s => s is DynamicInspectorSectionViewModel d && (string?)d.Content == "VerifiedPanelFor_Signature"); - - // Target null - inspectorVm.RefreshDynamicSections(null); - Assert.Empty(inspectorVm.DynamicSections); - } - - [Fact] - public void StatusBarRegistry_ZeroBaseline_And_WidgetViewModel_Rendering() - { - var registry = new StatusBarRegistry(seedDefaults: false); - Assert.Empty(registry.GetAllWidgets()); - - var desc = new StatusBarWidgetDescriptor - { - WidgetId = "test.widget.zoom", - Alignment = StatusBarAlignment.Right, - Order = 10, - ToolTip = "Current Zoom Level", - Factory = sp => new StatusBarWidgetViewModel - { - WidgetId = "test.widget.zoom", - Label = "100%", - IconKind = "Magnify", - ToolTip = "Current Zoom Level", - IsActive = true - } - }; - - using var unreg = registry.RegisterWidget(desc); - var widgets = registry.GetWidgets(StatusBarAlignment.Right); - Assert.Single(widgets); - - var vm = widgets[0].Factory(null!) as StatusBarWidgetViewModel; - Assert.NotNull(vm); - Assert.Equal("100%", vm.Label); - Assert.Equal("Magnify", vm.IconKind); - Assert.Equal("Current Zoom Level", vm.ToolTip); - Assert.True(vm.IsActive); - } - - [Fact] - public void PluginSettingsStore_Persists_And_Retrieves_Typed_Values() - { - var tempFile = Path.Combine(Path.GetTempPath(), $"plugin_settings_{Guid.NewGuid():N}.json"); - try - { - var store = new FilePluginSettingsStore(tempFile); - - store.SetSetting("frypdf.ai.groq", "ApiKey", "gsk_test_12345"); - store.SetSetting("frypdf.ai.groq", "MaxTokens", 4096); - store.SetSetting("frypdf.ai.groq", "StreamingEnabled", true); - store.Save(); - - // Reload from file - var store2 = new FilePluginSettingsStore(tempFile); - Assert.Equal("gsk_test_12345", store2.GetSetting("frypdf.ai.groq", "ApiKey", "")); - Assert.Equal(4096, store2.GetSetting("frypdf.ai.groq", "MaxTokens", 0)); - Assert.True(store2.GetSetting("frypdf.ai.groq", "StreamingEnabled", false)); - Assert.Equal("defaultVal", store2.GetSetting("frypdf.ai.groq", "NonExistent", "defaultVal")); - } - finally - { - if (File.Exists(tempFile)) File.Delete(tempFile); - } - } - - [Fact] - public void FryPluginPackageLoader_Creates_And_Unpacks_Package() - { - var tempDir = Path.Combine(Path.GetTempPath(), $"fry_test_pkg_{Guid.NewGuid():N}"); - var outputPkg = Path.Combine(Path.GetTempPath(), $"test_plugin_{Guid.NewGuid():N}.fryplugin"); - var extractDir = Path.Combine(Path.GetTempPath(), $"fry_extract_{Guid.NewGuid():N}"); - - try - { - Directory.CreateDirectory(tempDir); - - var manifest = new PluginManifest - { - Id = "com.sample.ocr", - Name = "Sample OCR Plugin", - Version = "1.2.0", - EntryPoint = "SampleOcr.dll", - Description = "High accuracy OCR package", - SettingsSchema = new Dictionary - { - ["Language"] = new() { Type = "select", Label = "Language", DefaultValue = "eng", Options = new() { "eng", "deu", "fra" } } - } - }; - - var manifestJson = JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(Path.Combine(tempDir, "plugin.json"), manifestJson); - - // Copy a valid current assembly DLL to act as entry point - var dummyDllPath = typeof(PluginEcosystemTests).Assembly.Location; - File.Copy(dummyDllPath, Path.Combine(tempDir, "SampleOcr.dll")); - - // Pack - FryPluginPackageLoader.CreatePackage(tempDir, outputPkg); - Assert.True(File.Exists(outputPkg)); - - // Unpack and load - var result = FryPluginPackageLoader.UnpackAndLoad(outputPkg, extractDir); - Assert.NotNull(result); - Assert.Equal("com.sample.ocr", result.Manifest.Id); - Assert.Equal("Sample OCR Plugin", result.Manifest.Name); - Assert.True(Directory.Exists(result.InstallDirectory)); - - result.Dispose(); - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); - if (Directory.Exists(extractDir)) Directory.Delete(extractDir, true); - if (File.Exists(outputPkg)) File.Delete(outputPkg); - } - } - - [Fact] - public void ValuesEqualMultiConverter_Evaluates_Correctly() - { - var converter = ValuesEqualMultiConverter.Instance; - - // Equal strings (case-insensitive) - Assert.True((bool)converter.Convert(new object?[] { "CustomTab", "customtab" }, typeof(bool), null, null!)!); - - // Different strings - Assert.False((bool)converter.Convert(new object?[] { "CustomTab", "OtherTab" }, typeof(bool), null, null!)!); - - // Null handling - Assert.True((bool)converter.Convert(new object?[] { null, null }, typeof(bool), null, null!)!); - Assert.False((bool)converter.Convert(new object?[] { "Tab", null }, typeof(bool), null, null!)!); - Assert.False((bool)converter.Convert(new object?[] { null, "Tab" }, typeof(bool), null, null!)!); - - // Less than 2 values - Assert.False((bool)converter.Convert(new object?[] { "Tab" }, typeof(bool), null, null!)!); - } - - [Fact] - public void PluginAssemblyLoader_DiscoverAndLoadDirectory_DiscoversPackagesAndSubdirectories() - { - var tempPluginsDir = Path.Combine(Path.GetTempPath(), $"fry_plugins_discovery_{Guid.NewGuid():N}"); - try - { - Directory.CreateDirectory(tempPluginsDir); - - // 1. Create a subdirectory with a dummy plugin manifest & DLL - var subDir = Path.Combine(tempPluginsDir, "sample_subplugin"); - Directory.CreateDirectory(subDir); - var manifest = new PluginManifest - { - Id = "com.sample.subplugin", - Name = "Sample Subplugin", - Version = "1.0.0", - EntryPoint = "PluginAssembly.dll" - }; - File.WriteAllText(Path.Combine(subDir, "plugin.json"), JsonSerializer.Serialize(manifest)); - File.Copy(typeof(PluginEcosystemTests).Assembly.Location, Path.Combine(subDir, "PluginAssembly.dll")); - - // 2. Discover - var packages = PluginAssemblyLoader.DiscoverAndLoadDirectory(tempPluginsDir); - Assert.NotEmpty(packages); - - foreach (var pkg in packages) - { - pkg.Dispose(); - } - } - finally - { - if (Directory.Exists(tempPluginsDir)) Directory.Delete(tempPluginsDir, true); - } - } -} diff --git a/tests/PdfEditorApp.Tests/PluginKernelTests.cs b/tests/PdfEditorApp.Tests/PluginKernelTests.cs deleted file mode 100644 index 92264de..0000000 --- a/tests/PdfEditorApp.Tests/PluginKernelTests.cs +++ /dev/null @@ -1,361 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PluginKernelTests -{ - private interface ITestServiceA { string GetValue(); } - private interface ITestServiceB { int GetCount(); } - - private class TestServiceA : ITestServiceA - { - public string GetValue() => "A"; - } - - private class TestServiceB : ITestServiceB - { - public int GetCount() => 42; - } - - private class PluginA : IFryPlugin - { - public string Id => "plugin.a"; - public string Name => "Plugin A"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterService(new TestServiceA()); - ctx.RegisterTool(new PdfToolDescriptor - { - Id = "tool.test.a", - Name = "Tool A", - Description = "Description A", - Category = "Test", - IconKind = "TestIcon" - }); - return Task.CompletedTask; - } - } - - private class PluginB : IFryPlugin - { - public string Id => "plugin.b"; - public string Name => "Plugin B"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => new[] { typeof(ITestServiceA) }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - var svc = ctx.GetService(); - ctx.RegisterService(new TestServiceB()); - return Task.CompletedTask; - } - } - - private class CyclicPlugin1 : IFryPlugin - { - public string Id => "cyclic.1"; - public string Name => "Cyclic 1"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => new[] { typeof(ITestServiceB) }; - public IReadOnlyList ProvidedServices => new[] { typeof(ITestServiceA) }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterService(new TestServiceA()); - return Task.CompletedTask; - } - } - - private class CyclicPlugin2 : IFryPlugin - { - public string Id => "cyclic.2"; - public string Name => "Cyclic 2"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => new[] { typeof(ITestServiceA) }; - public IReadOnlyList ProvidedServices => new[] { typeof(ITestServiceB) }; - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterService(new TestServiceB()); - return Task.CompletedTask; - } - } - - [Fact] - public async Task PluginHost_TopologicalSort_ActivatesDependenciesBeforeConsumers() - { - var host = new PluginHost(); - // Register in reverse order: B (consumer) before A (provider) - host.RegisterPlugin(new PluginB()); - host.RegisterPlugin(new PluginA()); - - await host.StartAsync(); - - Assert.Equal(2, host.LoadedPlugins.Count); - Assert.Equal("plugin.a", host.LoadedPlugins[0].Id); - Assert.Equal("plugin.b", host.LoadedPlugins[1].Id); - - Assert.True(host.Context.HasService()); - Assert.True(host.Context.HasService()); - - var tool = host.Context.GetTool("tool.test.a"); - Assert.NotNull(tool); - Assert.Equal("Tool A", tool.Name); - } - - [Fact] - public async Task PluginHost_MissingDependency_ThrowsPluginMissingDependencyException() - { - var host = new PluginHost(); - // Register only PluginB, which requires ITestServiceA (not provided) - host.RegisterPlugin(new PluginB()); - - var ex = await Assert.ThrowsAsync(() => host.StartAsync()); - Assert.Equal("plugin.b", ex.PluginId); - Assert.Equal(typeof(ITestServiceA), ex.MissingServiceType); - } - - [Fact] - public async Task PluginHost_CyclicDependency_ThrowsPluginCircularDependencyException() - { - var host = new PluginHost(); - host.RegisterPlugin(new CyclicPlugin1()); - host.RegisterPlugin(new CyclicPlugin2()); - - await Assert.ThrowsAsync(() => host.StartAsync()); - } - - [Fact] - public async Task PluginHost_StopAsync_UnwindsEffectsAndRemovesServicesAndTools() - { - var host = new PluginHost(); - var disposedOrder = new List(); - - var pluginWithDisposal = new CustomDisposablePlugin(disposedOrder); - host.RegisterPlugin(new PluginA()); - host.RegisterPlugin(pluginWithDisposal); - - await host.StartAsync(); - Assert.True(host.Context.HasService()); - Assert.NotNull(host.Context.GetTool("tool.test.a")); - - await host.StopAsync(); - - Assert.Empty(host.LoadedPlugins); - Assert.False(host.Context.HasService()); - Assert.Null(host.Context.GetTool("tool.test.a")); - - // Verify LIFO effect disposal occurred - Assert.Contains("custom.disposed", disposedOrder); - } - - [Fact] - public async Task PluginHost_DisablePluginAsync_CascadesToDependentConsumers() - { - var host = new PluginHost(); - host.RegisterPlugin(new PluginA()); // provides ITestServiceA - host.RegisterPlugin(new PluginB()); // requires ITestServiceA, provides ITestServiceB - - await host.StartAsync(); - Assert.Equal(PluginState.Active, host.GetPluginState("plugin.a")); - Assert.Equal(PluginState.Active, host.GetPluginState("plugin.b")); - - // Disabling PluginA must cascade and suspend PluginB as well! - await host.DisablePluginAsync("plugin.a"); - - Assert.Equal(PluginState.Suspended, host.GetPluginState("plugin.a")); - Assert.Equal(PluginState.Suspended, host.GetPluginState("plugin.b")); - Assert.False(host.Context.HasService()); - Assert.False(host.Context.HasService()); - Assert.Null(host.Context.GetTool("tool.test.a")); - } - - [Fact] - public async Task PluginHost_EnablePluginAsync_ReactivatesAndAutoMountsSuspendedConsumers() - { - var host = new PluginHost(); - host.RegisterPlugin(new PluginA()); - host.RegisterPlugin(new PluginB()); - - await host.StartAsync(); - await host.DisablePluginAsync("plugin.a"); - - Assert.Equal(PluginState.Suspended, host.GetPluginState("plugin.a")); - Assert.Equal(PluginState.Suspended, host.GetPluginState("plugin.b")); - - // Re-enabling PluginA must automatically auto-mount PluginB because its dependency is now met! - await host.EnablePluginAsync("plugin.a"); - - Assert.Equal(PluginState.Active, host.GetPluginState("plugin.a")); - Assert.Equal(PluginState.Active, host.GetPluginState("plugin.b")); - Assert.True(host.Context.HasService()); - Assert.True(host.Context.HasService()); - Assert.NotNull(host.Context.GetTool("tool.test.a")); - } - - [Fact] - public async Task PluginHost_ReloadPluginAsync_CyclesStateAndRebuildsEffects() - { - var host = new PluginHost(); - var effectsLog = new List(); - - var testPlugin = new ActionPlugin("plugin.reload.test", - apply: ctx => - { - effectsLog.Add("applied"); - ctx.RegisterEffect(() => effectsLog.Add("disposed")); - return Task.CompletedTask; - }); - - host.RegisterPlugin(testPlugin); - await host.StartAsync(); - - Assert.Equal(new[] { "applied" }, effectsLog); - - await host.ReloadPluginAsync("plugin.reload.test"); - - Assert.Equal(new[] { "applied", "disposed", "applied" }, effectsLog); - Assert.Equal(PluginState.Active, host.GetPluginState("plugin.reload.test")); - } - - [Fact] - public void PdfToolRegistry_ZeroBaseline_StartsEmptyAndReactsToPluginContext() - { - var ctx = new FryPluginContext(); - var registry = new PdfEditorApp.Services.Tools.Core.PdfToolRegistry(ctx, seedDefaults: false); - - // Initially empty - Assert.Empty(registry.GetAllTools()); - - bool changedFired = false; - registry.RegistryChanged += () => changedFired = true; - - // Register a tool dynamically in context - using (ctx.RegisterTool(new PdfToolDescriptor - { - Id = "frypdf.tool.custom", - Name = "Custom Tool", - Description = "Custom dynamic tool", - Category = "General", - IconKind = "Wrench", - CreateViewModel = sp => null! - })) - { - Assert.True(changedFired); - Assert.Single(registry.GetAllTools()); - Assert.Equal("Custom Tool", registry.GetAllTools()[0].Name); - } - - // After disposing the registration, the tool is cleanly gone! - Assert.Empty(registry.GetAllTools()); - } - - [Fact] - public void CanvasElementRegistry_ZeroBaseline_StartsEmptyAndPopulatesViaPlugins() - { - var registry = new PdfEditorApp.Services.Canvas.CanvasElementRegistry(null, seedBuiltIns: false); - - Assert.Empty(registry.GetAllDescriptors()); - - var desc = new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.custom", - DisplayName = "Custom Element", - ModelType = typeof(CustomTestElementModel), - ViewModelType = typeof(PdfEditorApp.ViewModels.ElementViewModels.TextElementViewModel), - IconKind = "Star", - Factory = (sp, m) => new PdfEditorApp.ViewModels.ElementViewModels.TextElementViewModel() - }; - - registry.RegisterElement(desc); - Assert.Single(registry.GetAllDescriptors()); - Assert.NotNull(registry.GetDescriptor("frypdf.element.custom")); - - registry.UnregisterElement("frypdf.element.custom"); - Assert.Empty(registry.GetAllDescriptors()); - } - - [Fact] - public void DynamicElementJsonResolver_SerializesAndDeserializesCustomPluginElement() - { - var descriptors = new List - { - new CanvasElementDescriptor - { - ElementTypeId = "frypdf.element.custom_test", - DisplayName = "Custom Test", - ModelType = typeof(CustomTestElementModel), - ViewModelType = typeof(PdfEditorApp.ViewModels.ElementViewModels.TextElementViewModel), - IconKind = "Star" - } - }; - - var options = PdfEditorApp.Core.Models.Elements.DynamicElementJsonResolver.CreateOptions(() => descriptors); - - PdfEditorApp.Core.Models.Elements.PdfElementBase element = new CustomTestElementModel - { - CustomProperty = "CordisParity" - }; - - string json = System.Text.Json.JsonSerializer.Serialize(element, options); - Assert.Contains("\"$type\":\"frypdf.element.custom_test\"", json); - Assert.Contains("\"CustomProperty\":\"CordisParity\"", json); - - var deserialized = System.Text.Json.JsonSerializer.Deserialize(json, options); - Assert.NotNull(deserialized); - Assert.IsType(deserialized); - Assert.Equal("CordisParity", ((CustomTestElementModel)deserialized).CustomProperty); - } - - private class ActionPlugin : IFryPlugin - { - private readonly Func _apply; - public ActionPlugin(string id, Func apply) - { - Id = id; - _apply = apply; - } - - public string Id { get; } - public string Name => Id; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) => _apply(ctx); - } - - private class CustomTestElementModel : PdfEditorApp.Core.Models.Elements.PdfElementBase - { - public override PdfEditorApp.Core.Models.ElementKind Kind => PdfEditorApp.Core.Models.ElementKind.Text; - public string CustomProperty { get; set; } = string.Empty; - } - - private class CustomDisposablePlugin : IFryPlugin - { - private readonly List _disposedLog; - - public CustomDisposablePlugin(List disposedLog) - { - _disposedLog = disposedLog; - } - - public string Id => "plugin.custom.disposable"; - public string Name => "Custom Disposable"; - public Version Version => new(1, 0, 0); - public IReadOnlyList RequiredServices => Array.Empty(); - - public Task ApplyAsync(IFryPluginContext ctx, CancellationToken ct = default) - { - ctx.RegisterEffect(() => _disposedLog.Add("custom.disposed")); - return Task.CompletedTask; - } - } -} diff --git a/tests/PdfEditorApp.Tests/PluginsManagerPageTests.cs b/tests/PdfEditorApp.Tests/PluginsManagerPageTests.cs deleted file mode 100644 index 016f6f1..0000000 --- a/tests/PdfEditorApp.Tests/PluginsManagerPageTests.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Marketplace; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Services.Plugins; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PluginsManagerPageTests -{ - private ServiceProvider CreateTestServices(string? testDir = null) - { - var services = new ServiceCollection(); - App.ConfigureServices(services); - if (testDir != null) - { - var storePath = Path.Combine(testDir, "installed_plugins.json"); - services.AddSingleton(new FileInstalledPluginStore(storePath)); - services.AddSingleton(sp => - { - var host = sp.GetRequiredService(); - var overlay = sp.GetService(); - var store = sp.GetRequiredService(); - var pluginsDir = Path.Combine(testDir, "plugins"); - return new PluginMarketplaceService(host, overlay, store, null, null, pluginsDir); - }); - } - return services.BuildServiceProvider(); - } - - [Fact] - public async Task PluginsManager_LoadsAllInstalledPluginsAndCategorizesCorrectly() - { - var sp = CreateTestServices(); - var host = sp.GetRequiredService(); - - // Mount domain bundles - var bundles = new IFryPluginBundle[] - { - new ToolsOrganizeBundle(), - new ToolsSecurityBundle(), - new ToolsConversionBundle(), - new ToolsIntelligenceBundle(), - new DataStudioBundle() - }; - - var profile = new PluginProfile - { - ProfileName = "desktop", - Bundles = bundles.Select(b => b.Id).ToList() - }; - ProfileLoader.ApplyProfile(profile, host, bundles); - await host.StartAsync(); - - var toolRegistry = sp.GetRequiredService(); - var marketplace = sp.GetRequiredService(); - - var vm = new PluginsManagerViewModel(host, marketplace, toolRegistry); - await vm.LoadAllDataAsync(); - - Assert.Equal(33, vm.InstalledCount); - Assert.Equal(33, vm.ActiveInstalledCount); - Assert.NotEmpty(vm.FilteredInstalledPlugins); - - // Verify Categories - var categories = vm.FilteredInstalledPlugins.Select(p => p.Category).Distinct().ToList(); - Assert.Contains("Tools & Productivity", categories); - - // Verify specific plugins are present - Assert.Contains(vm.FilteredInstalledPlugins, p => p.Id == "frypdf.tool.merge"); - Assert.Contains(vm.FilteredInstalledPlugins, p => p.Id == "frypdf.tool.compress"); - - await host.StopAsync(); - } - - [Fact] - public async Task PluginsManager_SearchAndFilter_WorksAcrossBothInstalledAndMarketplace() - { - var sp = CreateTestServices(); - var host = sp.GetRequiredService(); - var bundles = new IFryPluginBundle[] { new ToolsOrganizeBundle(), new ToolsSecurityBundle() }; - var profile = new PluginProfile { ProfileName = "desktop", Bundles = bundles.Select(b => b.Id).ToList() }; - ProfileLoader.ApplyProfile(profile, host, bundles); - await host.StartAsync(); - - var marketplace = sp.GetRequiredService(); - var vm = new PluginsManagerViewModel(host, marketplace); - await vm.LoadAllDataAsync(); - - // 1. Search Installed - vm.SearchQuery = "merge"; - Assert.Single(vm.FilteredInstalledPlugins); - Assert.Equal("frypdf.tool.merge", vm.FilteredInstalledPlugins[0].Id); - - // 2. Clear Search - vm.SearchQuery = ""; - Assert.Equal(13, vm.FilteredInstalledPlugins.Count); - - // 3. Switch to Marketplace Tab & Search - vm.SelectedTab = PluginsManagerTab.Marketplace; - Assert.NotEmpty(vm.FilteredMarketplacePlugins); - - vm.SearchQuery = "Snake"; - Assert.Single(vm.FilteredMarketplacePlugins); - Assert.Equal("frypdf.overlay.snake", vm.FilteredMarketplacePlugins[0].Id); - - vm.SearchQuery = "Arcade"; - Assert.Equal(2, vm.FilteredMarketplacePlugins.Count); - Assert.Contains(vm.FilteredMarketplacePlugins, m => m.Id == "frypdf.overlay.snake"); - Assert.Contains(vm.FilteredMarketplacePlugins, m => m.Id == "com.frypdf.plugin.tictactoe"); - - // 4. Filter by Category - vm.SearchQuery = ""; - vm.SelectedCategory = "UI & Extensions"; - Assert.Contains(vm.FilteredMarketplacePlugins, m => m.Id == "frypdf.overlay.snake"); - - await host.StopAsync(); - } - - [Fact] - public async Task PluginsManager_Selection_PopulatesDetailViewModelAndContributions() - { - var sp = CreateTestServices(); - var host = sp.GetRequiredService(); - var bundles = new IFryPluginBundle[] { new ToolsConversionBundle() }; - var profile = new PluginProfile { ProfileName = "desktop", Bundles = bundles.Select(b => b.Id).ToList() }; - ProfileLoader.ApplyProfile(profile, host, bundles); - await host.StartAsync(); - - var marketplace = sp.GetRequiredService(); - var vm = new PluginsManagerViewModel(host, marketplace); - await vm.LoadAllDataAsync(); - - Assert.NotEmpty(vm.FilteredInstalledPlugins); - var target = vm.FilteredInstalledPlugins.First(p => p.Id == "frypdf.tool.pdftoword"); - - vm.SelectedInstalledPlugin = target; - Assert.NotNull(vm.SelectedDetail); - Assert.Equal("frypdf.tool.pdftoword", vm.SelectedDetail.Id); - Assert.Equal(target.Name, vm.SelectedDetail.Name); - Assert.True(vm.SelectedDetail.IsInstalled); - Assert.True(vm.SelectedDetail.IsActive); - Assert.NotEmpty(vm.SelectedDetail.ContributedFeatures); - Assert.NotEmpty(vm.SelectedDetail.Highlights); - Assert.NotEmpty(vm.SelectedDetail.Dependencies); - - // Test Sub-Tab navigation - vm.SelectedDetail.SelectTab("Contributions"); - Assert.Equal(ExtensionDetailTab.Contributions, vm.SelectedDetail.SelectedTab); - - vm.SelectedDetail.SelectTab("Runtime"); - Assert.Equal(ExtensionDetailTab.Runtime, vm.SelectedDetail.SelectedTab); - Assert.Contains("Active", vm.SelectedDetail.RuntimeStatus); - - await host.StopAsync(); - } - - [Fact] - public async Task PluginsManager_Marketplace_InstallsAndMountsRealPluginIntoHost() - { - var tempDir = Path.Combine(AppContext.BaseDirectory, $"frypdf_pm_test_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - try - { - var sp = CreateTestServices(tempDir); - var host = sp.GetRequiredService(); - var marketplace = sp.GetRequiredService(); - - var vm = new PluginsManagerViewModel(host, marketplace); - await vm.LoadAllDataAsync(); - - vm.SelectedTab = PluginsManagerTab.Marketplace; - var snakeItem = vm.FilteredMarketplacePlugins.First(m => m.Id == "frypdf.overlay.snake"); - vm.SelectedMarketplacePlugin = snakeItem; - - Assert.NotNull(vm.SelectedDetail); - Assert.Equal("frypdf.overlay.snake", vm.SelectedDetail.Id); - Assert.Equal("FryPDF Team", vm.SelectedDetail.Publisher); - Assert.True(vm.SelectedDetail.IsOfficial); - Assert.True(vm.SelectedDetail.IsVerified); - - // Verify initially not active in host - Assert.False(host.IsPluginActive("frypdf.overlay.snake")); - - // Perform 1-click install: mounts real SnakeGamePlugin into PluginHost and activates it! - await vm.InstallMarketplacePluginCommand.ExecuteAsync("frypdf.overlay.snake"); - - Assert.True(marketplace.IsPluginInstalled("frypdf.overlay.snake")); - Assert.True(host.IsPluginActive("frypdf.overlay.snake")); - - // Clean up: uninstalls and disables from host - await marketplace.UninstallPluginAsync("frypdf.overlay.snake"); - Assert.False(marketplace.IsPluginInstalled("frypdf.overlay.snake")); - Assert.False(host.IsPluginActive("frypdf.overlay.snake")); - } - finally - { - try { Directory.Delete(tempDir, recursive: true); } catch { } - } - } - - [Fact] - public async Task WorkspacePagesBundle_RegistersPluginsPageDescriptorCorrectly() - { - var sp = CreateTestServices(); - var navRegistry = sp.GetRequiredService(); - - var bundle = new WorkspacePagesBundle(); - Assert.Contains(bundle.Plugins, p => p.Id == "frypdf.page.plugins"); - - var ctx = sp.GetRequiredService(); - var plugin = bundle.Plugins.First(p => p.Id == "frypdf.page.plugins"); - await plugin.ApplyAsync(ctx); - - var item = navRegistry.GetItem("Plugins"); - Assert.NotNull(item); - Assert.Equal("Plugins & Extensions", item.Title); - Assert.Equal("Preferences", item.Group); - Assert.Equal("PuzzleOutline", item.IconKind); - Assert.NotNull(item.ViewFactory); - } - - [Fact] - public void HomeViewModel_NavigatesToPluginsSectionCorrectly() - { - var sp = CreateTestServices(); - var home = sp.GetRequiredService(); - - Assert.NotNull(home.PluginsManager); - - home.SelectNavSectionCommand.Execute("Plugins"); - - Assert.True(home.IsPluginsSection); - Assert.Equal(PdfEditorApp.Core.Models.HomeNavSection.Plugins, home.SelectedNavSection); - } -} diff --git a/tests/PdfEditorApp.Tests/PluginsStudioUiTests.cs b/tests/PdfEditorApp.Tests/PluginsStudioUiTests.cs deleted file mode 100644 index 276ea65..0000000 --- a/tests/PdfEditorApp.Tests/PluginsStudioUiTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class PluginsStudioUiTests -{ - [Fact] - public async Task PluginsDialog_PopulatesAndFiltersPluginsCorrectly() - { - // 1. Setup DI container with plugin host and bundles - var services = new ServiceCollection(); - App.ConfigureServices(services); - var sp = services.BuildServiceProvider(); - - var host = sp.GetRequiredService(); - - // Mount all 5 domain bundles - var bundles = new IFryPluginBundle[] - { - new ToolsOrganizeBundle(), - new ToolsSecurityBundle(), - new ToolsConversionBundle(), - new ToolsIntelligenceBundle(), - new DataStudioBundle() - }; - - var profile = new PluginProfile - { - ProfileName = "desktop", - Bundles = bundles.Select(b => b.Id).ToList() - }; - ProfileLoader.ApplyProfile(profile, host, bundles); - await host.StartAsync(); - - // 2. Resolve MainViewModel - var mainVm = sp.GetRequiredService(); - mainVm.PluginHost = host; - - // 3. Open Plugins Dialog - mainVm.OpenPluginsDialogCommand.Execute(null); - - Assert.True(mainVm.IsPluginsDialogOpen); - Assert.NotEmpty(mainVm.FilteredPluginsList); - Assert.Equal(33, mainVm.FilteredPluginsList.Count); - - // Verify specific plugins - Assert.Contains(mainVm.FilteredPluginsList, p => p.Id == "frypdf.tool.merge"); - Assert.Contains(mainVm.FilteredPluginsList, p => p.Id == "frypdf.tool.compress"); - Assert.Contains(mainVm.FilteredPluginsList, p => p.Id == "frypdf.tool.aisummarizer"); - - // 4. Test Search Filtering - mainVm.PluginSearchQuery = "merge"; - Assert.Equal(2, mainVm.FilteredPluginsList.Count); // Merge PDF and Batch Mail Merge - Assert.Contains(mainVm.FilteredPluginsList, p => p.Id == "frypdf.tool.merge"); - Assert.Contains(mainVm.FilteredPluginsList, p => p.Id == "frypdf.tool.batchgeneration"); - - mainVm.PluginSearchQuery = "compress"; - Assert.Single(mainVm.FilteredPluginsList); - Assert.Equal("frypdf.tool.compress", mainVm.FilteredPluginsList[0].Id); - - // 5. Clear Search - mainVm.PluginSearchQuery = ""; - Assert.Equal(33, mainVm.FilteredPluginsList.Count); - - // 6. Close Dialog - mainVm.ClosePluginsDialogCommand.Execute(null); - Assert.False(mainVm.IsPluginsDialogOpen); - - await host.StopAsync(); - } - - [Fact] - public async Task PluginsDialog_WithAll16Bundles_DisplaysRichCategorizedPlugins() - { - var services = new ServiceCollection(); - App.ConfigureServices(services); - var sp = services.BuildServiceProvider(); - - var host = sp.GetRequiredService(); - - var bundles = new IFryPluginBundle[] - { - new ToolsOrganizeBundle(), - new ToolsSecurityBundle(), - new ToolsConversionBundle(), - new ToolsIntelligenceBundle(), - new DataStudioBundle(), - new CanvasElementsBundle(), - new DocumentIoBundle(), - new AiProvidersBundle(), - new OcrEnginesBundle(), - new StandardTemplatesBundle(), - new StatusBarBundle(), - new InspectorBundle(), - new CommandPaletteBundle(), - new WorkspacePagesBundle(), - new DialogsBundle(), - new EditorSidebarsBundle() - }; - - var profile = new PluginProfile - { - ProfileName = "desktop", - Bundles = bundles.Select(b => b.Id).ToList() - }; - ProfileLoader.ApplyProfile(profile, host, bundles); - await host.StartAsync(); - - var mainVm = sp.GetRequiredService(); - mainVm.PluginHost = host; - - mainVm.OpenPluginsDialogCommand.Execute(null); - - Assert.True(mainVm.IsPluginsDialogOpen); - Assert.True(mainVm.FilteredPluginsList.Count >= 70, $"Expected >= 70 plugins but found {mainVm.FilteredPluginsList.Count}"); - - // Verify categories exist across diverse subsystems - var categories = mainVm.FilteredPluginsList.Select(p => p.Category).Distinct().ToList(); - Assert.Contains("Workspace Navigation", categories); - Assert.Contains("Modal Studios & Dialogs", categories); - Assert.Contains("Editor Sidebars", categories); - Assert.Contains("Canvas Elements", categories); - Assert.Contains("Document I/O", categories); - Assert.Contains("AI Providers", categories); - Assert.Contains("OCR Engines", categories); - Assert.Contains("Document Templates", categories); - Assert.Contains("Status Bar Widgets", categories); - Assert.Contains("Property Inspector", categories); - Assert.Contains("Command Palette", categories); - - // Test searching for specific subsystems - mainVm.PluginSearchQuery = "ai"; - Assert.NotEmpty(mainVm.FilteredPluginsList); - Assert.Contains(mainVm.FilteredPluginsList, p => p.Id.Contains("ai")); - - mainVm.PluginSearchQuery = "sidebar"; - Assert.NotEmpty(mainVm.FilteredPluginsList); - Assert.Contains(mainVm.FilteredPluginsList, p => p.Category == "Editor Sidebars"); - - mainVm.PluginSearchQuery = "dialog"; - Assert.NotEmpty(mainVm.FilteredPluginsList); - Assert.Contains(mainVm.FilteredPluginsList, p => p.Category == "Modal Studios & Dialogs"); - - mainVm.ClosePluginsDialogCommand.Execute(null); - Assert.False(mainVm.IsPluginsDialogOpen); - - await host.StopAsync(); - } -} diff --git a/tests/PdfEditorApp.Tests/ProfileAndBundleTests.cs b/tests/PdfEditorApp.Tests/ProfileAndBundleTests.cs deleted file mode 100644 index 249ac8e..0000000 --- a/tests/PdfEditorApp.Tests/ProfileAndBundleTests.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Models; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class ProfileAndBundleTests -{ - private class DummyOperationsService : IPdfDocumentOperationsService - { - public IPdfToolRegistry ToolRegistry => null!; - public IPdfPageService PageService => null!; - public IPdfOptimizationService OptimizationService => null!; - public IPdfSecurityService SecurityService => null!; - public IPdfConversionService ConversionService => null!; - public IPdfOcrService OcrService => null!; - public IPdfFormService FormService => null!; - public IAiDocumentService AiService => null!; - public IDocumentTranslationService TranslationService => null!; - public IPdfWorkflowEngine WorkflowEngine => null!; - - public Task ExecuteToolAsync(PdfToolId toolId, object options, IProgress? progress = null, CancellationToken ct = default) - { - return Task.FromResult(new ToolExecutionResult { Success = true }); - } - } - - [Fact] - public void ProfileLoader_ParseValidJson_Succeeds() - { - var json = """ - { - "name": "Custom Profile", - "description": "Test custom profile configuration", - "version": "1.0.0", - "bundles": [ - "FryPdf.Bundle.Tools.Organize", - "FryPdf.Bundle.Tools.Security" - ], - "enabledPlugins": [ - "frypdf.tool.merge", - "frypdf.tool.split" - ], - "disabledPlugins": [ - "frypdf.tool.compress" - ], - "settings": { - "maxMemoryMb": "1024", - "enableTelemetry": "false" - } - } - """; - - var profile = ProfileLoader.LoadProfileFromJson(json); - - Assert.NotNull(profile); - Assert.Equal("Custom Profile", profile.Name); - Assert.Equal("1.0.0", profile.Version); - Assert.Contains("FryPdf.Bundle.Tools.Organize", profile.Bundles); - Assert.Contains("FryPdf.Bundle.Tools.Security", profile.Bundles); - Assert.Contains("frypdf.tool.merge", profile.EnabledPlugins); - Assert.Contains("frypdf.tool.compress", profile.DisabledPlugins); - Assert.True(profile.IsPluginEnabled("frypdf.tool.merge")); - Assert.False(profile.IsPluginEnabled("frypdf.tool.compress")); - Assert.True(profile.IsBundleEnabled("FryPdf.Bundle.Tools.Organize")); - } - - [Fact] - public void ProfileLoader_LoadFromFile_ReadsCorrectly() - { - var tempFile = Path.Combine(Path.GetTempPath(), $"frypdf_profile_{Guid.NewGuid():N}.json"); - try - { - File.WriteAllText(tempFile, """ - { - "name": "Temp Test Profile", - "version": "2.0.0" - } - """); - - var profile = ProfileLoader.LoadProfileFromFile(tempFile); - Assert.NotNull(profile); - Assert.Equal("Temp Test Profile", profile.Name); - Assert.Equal("2.0.0", profile.Version); - } - finally - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - } - } - - [Fact] - public async Task Bundles_InstantiateAndRegisterCorrectPlugins() - { - var organizeBundle = new ToolsOrganizeBundle(); - Assert.Equal("FryPdf.Bundle.Tools.Organize", organizeBundle.Id); - Assert.Equal(6, organizeBundle.Plugins.Count); - Assert.Contains(organizeBundle.Plugins, p => p.Id == "frypdf.tool.merge"); - Assert.Contains(organizeBundle.Plugins, p => p.Id == "frypdf.tool.split"); - Assert.Contains(organizeBundle.Plugins, p => p.Id == "frypdf.tool.rotate"); - Assert.Contains(organizeBundle.Plugins, p => p.Id == "frypdf.tool.organize"); - Assert.Contains(organizeBundle.Plugins, p => p.Id == "frypdf.tool.crop"); - Assert.Contains(organizeBundle.Plugins, p => p.Id == "frypdf.tool.pagenumbers"); - - var securityBundle = new ToolsSecurityBundle(); - Assert.Equal("FryPdf.Bundle.Tools.Security", securityBundle.Id); - Assert.Equal(7, securityBundle.Plugins.Count); - Assert.Contains(securityBundle.Plugins, p => p.Id == "frypdf.tool.compress"); - Assert.Contains(securityBundle.Plugins, p => p.Id == "frypdf.tool.protect"); - Assert.Contains(securityBundle.Plugins, p => p.Id == "frypdf.tool.unlock"); - Assert.Contains(securityBundle.Plugins, p => p.Id == "frypdf.tool.sign"); - - var conversionBundle = new ToolsConversionBundle(); - Assert.Equal("FryPdf.Bundle.Tools.Conversion", conversionBundle.Id); - Assert.Equal(12, conversionBundle.Plugins.Count); - Assert.Contains(conversionBundle.Plugins, p => p.Id == "frypdf.tool.pdftoword"); - Assert.Contains(conversionBundle.Plugins, p => p.Id == "frypdf.tool.wordtopdf"); - - var intelligenceBundle = new ToolsIntelligenceBundle(); - Assert.Equal("FryPdf.Bundle.Tools.Intelligence", intelligenceBundle.Id); - Assert.Equal(6, intelligenceBundle.Plugins.Count); - Assert.Contains(intelligenceBundle.Plugins, p => p.Id == "frypdf.tool.aisummarizer"); - Assert.Contains(intelligenceBundle.Plugins, p => p.Id == "frypdf.tool.ocr"); - - var dataStudioBundle = new DataStudioBundle(); - Assert.Equal("FryPdf.Bundle.DataStudio", dataStudioBundle.Id); - Assert.Equal(2, dataStudioBundle.Plugins.Count); - - // Verify mounting into PluginHost with dummy operations service - var ctx = new FryPluginContext(); - ctx.RegisterService(new DummyOperationsService()); - var host = new PluginHost(ctx); - - foreach (var plugin in organizeBundle.Plugins) - { - host.RegisterPlugin(plugin); - } - - await host.StartAsync(); - Assert.Equal(6, host.LoadedPlugins.Count); - - await host.StopAsync(); - Assert.Empty(host.LoadedPlugins); - } - - [Fact] - public async Task ProfileLoader_ApplyToHostAsync_RespectsFilters() - { - var profile = new PluginProfile - { - Name = "Filtered Profile", - EnabledPlugins = new() { "frypdf.tool.merge", "frypdf.tool.compress" }, - DisabledPlugins = new() { "frypdf.tool.split" } - }; - - var allAvailablePlugins = new IFryPlugin[] - { - new ToolsOrganizeBundle().Plugins[0], // merge - new ToolsOrganizeBundle().Plugins[1], // split - new ToolsSecurityBundle().Plugins[0], // compress - new ToolsSecurityBundle().Plugins[1] // repair - }; - - var ctx = new FryPluginContext(); - ctx.RegisterService(new DummyOperationsService()); - var host = new PluginHost(ctx); - - await ProfileLoader.ApplyToHostAsync(host, profile, allAvailablePlugins); - - // Only merge and compress should be active - Assert.Equal(2, host.LoadedPlugins.Count); - Assert.Contains(host.LoadedPlugins, p => p.Id == "frypdf.tool.merge"); - Assert.Contains(host.LoadedPlugins, p => p.Id == "frypdf.tool.compress"); - Assert.DoesNotContain(host.LoadedPlugins, p => p.Id == "frypdf.tool.split"); - Assert.DoesNotContain(host.LoadedPlugins, p => p.Id == "frypdf.tool.repair"); - - await host.StopAsync(); - } -} diff --git a/tests/PdfEditorApp.Tests/RemotePluginMarketplaceTests.cs b/tests/PdfEditorApp.Tests/RemotePluginMarketplaceTests.cs deleted file mode 100644 index 97a56c1..0000000 --- a/tests/PdfEditorApp.Tests/RemotePluginMarketplaceTests.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Marketplace; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Overlays; -using PdfEditorApp.Services.Plugins; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class RemotePluginMarketplaceTests -{ - private static IServiceProvider CreateTestServices(string? testDir = null, HttpClient? httpClient = null, string? registryBaseUrl = null) - { - var services = new ServiceCollection(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => - { - var storePath = testDir != null - ? Path.Combine(testDir, "installed_plugins.json") - : Path.Combine(AppContext.BaseDirectory, $"installed_plugins_test_{Guid.NewGuid():N}.json"); - return new FileInstalledPluginStore(storePath); - }); - - services.AddSingleton(sp => - { - var host = sp.GetRequiredService(); - var overlay = sp.GetRequiredService(); - var store = sp.GetRequiredService(); - var pluginsDir = testDir != null ? Path.Combine(testDir, "plugins") : null; - return new PluginMarketplaceService(host, overlay, store, httpClient, registryBaseUrl, pluginsDir); - }); - - return services.BuildServiceProvider(); - } - - [Fact] - public async Task FetchRemoteCatalogAsync_ParsesOfficialGitHubCatalog_WhenOnline() - { - var sp = CreateTestServices(); - var marketplace = sp.GetRequiredService(); - - var remoteItems = await marketplace.FetchRemoteCatalogAsync(); - - // If machine is online, catalog from codefrydev/PDFCreator-resources is fetched - if (remoteItems.Count > 0) - { - var ticTacToe = remoteItems.FirstOrDefault(i => i.Id == "com.frypdf.plugin.tictactoe"); - Assert.NotNull(ticTacToe); - Assert.Equal("Tic-Tac-Toe", ticTacToe.Name); - Assert.Equal("Code Fry Dev", ticTacToe.Publisher); - Assert.Equal("UI & Extensions", ticTacToe.Category); - Assert.StartsWith("https://raw.githubusercontent.com/codefrydev/PDFCreator-resources/", ticTacToe.DownloadUrl); - Assert.Contains("tictactoe", ticTacToe.Tags); - - // Verify merging with curated extensions - var fullCatalog = await marketplace.GetCatalogAsync(); - Assert.Contains(fullCatalog, i => i.Id == "frypdf.overlay.snake"); - Assert.Contains(fullCatalog, i => i.Id == "com.frypdf.plugin.tictactoe"); - } - } - - [Fact] - public async Task FetchRemoteCatalogAsync_HandlesOfflineGracefully_WithoutThrowing() - { - // Simulate completely invalid / offline registry - using var offlineClient = new HttpClient { Timeout = TimeSpan.FromMilliseconds(500) }; - var sp = CreateTestServices(httpClient: offlineClient, registryBaseUrl: "http://invalid-unreachable-domain-987654.xyz"); - var marketplace = sp.GetRequiredService(); - - // Must not throw any unhandled exception - var remote = await marketplace.FetchRemoteCatalogAsync(); - Assert.NotNull(remote); - - // Curated local extensions must remain accessible - var catalog = await marketplace.GetCatalogAsync(); - Assert.NotEmpty(catalog); - Assert.Contains(catalog, i => i.Id == "frypdf.overlay.snake"); - } - - [Fact] - public async Task InstallPluginAsync_SupportsRemotePluginPackage_WithMockHttp() - { - var tempDir = Path.Combine(AppContext.BaseDirectory, $"frypdf_remote_test_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - - try - { - // Use pre-packaged TicTacToe.fryplugin from examples - var tictactoePkg = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../docs/examples/TicTacToePlugin/bin/Release/net10.0/TicTacToe.fryplugin")); - byte[] pkgBytes; - if (File.Exists(tictactoePkg)) - { - pkgBytes = await File.ReadAllBytesAsync(tictactoePkg); - } - else - { - // Fallback: build staging directory - var stagingDir = Path.Combine(tempDir, "staging"); - Directory.CreateDirectory(stagingDir); - File.WriteAllText(Path.Combine(stagingDir, "plugin.json"), @"{""id"":""com.frypdf.plugin.tictactoe"",""name"":""Tic-Tac-Toe"",""version"":""1.0.0"",""entryPoint"":""TicTacToePlugin.dll""}"); - var tttDll = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../docs/examples/TicTacToePlugin/bin/Release/net10.0/TicTacToePlugin.dll")); - if (File.Exists(tttDll)) - { - File.Copy(tttDll, Path.Combine(stagingDir, "TicTacToePlugin.dll"), true); - } - var mockPkgPath = Path.Combine(tempDir, "TicTacToe.fryplugin"); - System.IO.Compression.ZipFile.CreateFromDirectory(stagingDir, mockPkgPath); - pkgBytes = await File.ReadAllBytesAsync(mockPkgPath); - } - - // Mock HttpMessageHandler returning mock catalog and real package - var mockHandler = new MockHttpMessageHandler(pkgBytes); - using var httpClient = new HttpClient(mockHandler) { Timeout = TimeSpan.FromSeconds(5) }; - - var sp = CreateTestServices(testDir: tempDir, httpClient: httpClient, registryBaseUrl: "https://mock.frypdf.dev/plugins"); - var marketplace = sp.GetRequiredService(); - var host = sp.GetRequiredService(); - - // Fetch catalog - var remoteItems = await marketplace.FetchRemoteCatalogAsync(forceRefresh: true); - Assert.Contains(remoteItems, i => i.Id == "com.frypdf.plugin.tictactoe"); - var tttItem = remoteItems.First(i => i.Id == "com.frypdf.plugin.tictactoe"); - Assert.Equal("com.frypdf.plugin.tictactoe", tttItem.Id); - - // Install TicTacToe plugin - string lastStatus = ""; - bool installed = await marketplace.InstallPluginAsync("com.frypdf.plugin.tictactoe", statusCallback: s => lastStatus = s); - Assert.True(installed, $"Install failed with status: {lastStatus}"); - Assert.True(marketplace.IsPluginInstalled("com.frypdf.plugin.tictactoe")); - - // Uninstall - bool uninstalled = await marketplace.UninstallPluginAsync("com.frypdf.plugin.tictactoe"); - Assert.True(uninstalled); - Assert.False(marketplace.IsPluginInstalled("com.frypdf.plugin.tictactoe")); - } - finally - { - try { Directory.Delete(tempDir, recursive: true); } catch { } - } - } - - [Fact] - public async Task InstallPluginAsync_DownloadsRealPackageFromGitHubCdn_AndMountsSuccessfully() - { - var tempDir = Path.Combine(AppContext.BaseDirectory, $"frypdf_live_cdn_test_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - - try - { - var sp = CreateTestServices(testDir: tempDir); - var marketplace = sp.GetRequiredService(); - var host = sp.GetRequiredService(); - var overlayReg = sp.GetRequiredService(); - - // Fetch remote catalog from live GitHub CDN - var catalog = await marketplace.FetchRemoteCatalogAsync(); - var tttItem = catalog.FirstOrDefault(i => i.Id == "com.frypdf.plugin.tictactoe"); - if (tttItem != null) - { - // Install from real CDN - bool installed = await marketplace.InstallPluginAsync("com.frypdf.plugin.tictactoe"); - Assert.True(installed); - Assert.True(marketplace.IsPluginInstalled("com.frypdf.plugin.tictactoe")); - Assert.True(host.IsPluginActive("com.frypdf.plugin.tictactoe")); - - // Verify overlay opened - Assert.True(overlayReg.IsOverlayVisible("com.frypdf.plugin.tictactoe")); - - // Clean uninstall - bool uninstalled = await marketplace.UninstallPluginAsync("com.frypdf.plugin.tictactoe"); - Assert.True(uninstalled); - Assert.False(marketplace.IsPluginInstalled("com.frypdf.plugin.tictactoe")); - Assert.False(host.IsPluginActive("com.frypdf.plugin.tictactoe")); - } - } - finally - { - try { Directory.Delete(tempDir, recursive: true); } catch { } - } - } - - [Fact] - public async Task PluginsManagerViewModel_Search_RanksTicTacToeFirst_WhenSearchingTic() - { - var sp = CreateTestServices(); - var marketplace = sp.GetRequiredService(); - var host = sp.GetRequiredService(); - - var vm = new PdfEditorApp.ViewModels.PluginsManagerViewModel(host, marketplace); - await vm.LoadAllDataAsync(); - - vm.SelectedTab = PdfEditorApp.ViewModels.PluginsManagerTab.Marketplace; - vm.SearchQuery = "tic"; - - Assert.NotEmpty(vm.FilteredMarketplacePlugins); - // Tic-Tac-Toe should be ranked at index 0 because its name starts with "tic" - Assert.Equal("com.frypdf.plugin.tictactoe", vm.FilteredMarketplacePlugins[0].Id); - Assert.Equal("Tic-Tac-Toe", vm.FilteredMarketplacePlugins[0].Name); - } - - private class MockHttpMessageHandler : HttpMessageHandler - { - private readonly byte[] _pkgBytes; - - public MockHttpMessageHandler(byte[] pkgBytes) - { - _pkgBytes = pkgBytes; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var uri = request.RequestUri?.ToString() ?? string.Empty; - - if (uri.EndsWith("catalog.json", StringComparison.OrdinalIgnoreCase)) - { - var catalogJson = @"[ - { - ""id"": ""com.frypdf.plugin.tictactoe"", - ""name"": ""Tic-Tac-Toe"", - ""publisher"": ""Code Fry Dev"", - ""version"": ""1.0.0"", - ""category"": ""UI & Extensions"", - ""description"": ""Interactive floating Tic-Tac-Toe mini-game with local 2-Player mode and intelligent AI opponent."", - ""downloadUrl"": ""https://mock.frypdf.dev/plugins/TicTacToe.fryplugin"", - ""formattedSize"": ""51 KB"" - } -]"; - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(catalogJson, System.Text.Encoding.UTF8, "application/json") - }); - } - - if (uri.EndsWith(".fryplugin", StringComparison.OrdinalIgnoreCase)) - { - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(_pkgBytes) - }); - } - - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); - } - } -} diff --git a/tests/PdfEditorApp.Tests/RichTextEngineTests.cs b/tests/PdfEditorApp.Tests/RichTextEngineTests.cs deleted file mode 100644 index 4d503b8..0000000 --- a/tests/PdfEditorApp.Tests/RichTextEngineTests.cs +++ /dev/null @@ -1,459 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Xunit; -using PdfEditorApp.Core.Analysis; -using PdfEditorApp.Core.Data; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Typography; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Tests; - -public class RichTextEngineTests -{ - [Fact] - public void PdfTextSpan_Clone_CreatesDeepCopy() - { - var original = new PdfTextSpan - { - Text = "Sample Run", - FontFamily = "Roboto", - FontSize = 18.5, - IsBold = true, - IsItalic = true, - IsUnderline = true, - IsStrikethrough = false, - TextColorHex = "#0F6CBD", - HighlightColorHex = "#FFEB3B", - Script = TextScriptMode.Superscript, - LinkUrl = "https://example.com" - }; - - var clone = original.Clone(); - - Assert.NotSame(original, clone); - Assert.Equal(original.Text, clone.Text); - Assert.Equal(original.FontFamily, clone.FontFamily); - Assert.Equal(original.FontSize, clone.FontSize); - Assert.Equal(original.IsBold, clone.IsBold); - Assert.Equal(original.IsItalic, clone.IsItalic); - Assert.Equal(original.IsUnderline, clone.IsUnderline); - Assert.Equal(original.IsStrikethrough, clone.IsStrikethrough); - Assert.Equal(original.TextColorHex, clone.TextColorHex); - Assert.Equal(original.HighlightColorHex, clone.HighlightColorHex); - Assert.Equal(original.Script, clone.Script); - Assert.Equal(original.LinkUrl, clone.LinkUrl); - } - - [Fact] - public void PdfTextElement_WithSpans_ClonesDeeplyAndSynchronizes() - { - var element = new PdfTextElement - { - Text = "Fallback Plain", - FontFamily = "Arial", - FontSize = 14, - Spans = new List - { - new() { Text = "Hello ", IsBold = true }, - new() { Text = "World", TextColorHex = "#DC2626", Script = TextScriptMode.Superscript } - } - }; - - element.SynchronizePlainTextFromSpans(); - Assert.Equal("Hello World", element.Text); - - var clone = (PdfTextElement)element.Clone(); - Assert.NotSame(element, clone); - Assert.NotNull(clone.Spans); - Assert.Equal(2, clone.Spans.Count); - Assert.NotSame(element.Spans[0], clone.Spans[0]); - Assert.Equal("Hello ", clone.Spans[0].Text); - Assert.True(clone.Spans[0].IsBold); - Assert.Equal("World", clone.Spans[1].Text); - Assert.Equal("#DC2626", clone.Spans[1].TextColorHex); - Assert.Equal(TextScriptMode.Superscript, clone.Spans[1].Script); - } - - [Fact] - public void PdfTextElement_BackwardCompatibility_NullSpansWorksAsPlainText() - { - var element = new PdfTextElement - { - Text = "Legacy plain text", - FontFamily = "Segoe UI", - FontSize = 12, - Spans = null - }; - - Assert.Equal("Legacy plain text", element.GetEffectivePlainText()); - element.SynchronizePlainTextFromSpans(); - Assert.Equal("Legacy plain text", element.Text); - - var clone = (PdfTextElement)element.Clone(); - Assert.Null(clone.Spans); - Assert.Equal("Legacy plain text", clone.Text); - } - - [Fact] - public void RichTextHelper_ParsesMarkdownTokensAccurately() - { - string input = "Start **Bold Text** then *Italic Text* and ~~Strikethrough~~ plus Underline end."; - var spans = RichTextHelper.ParseMarkdownToSpans(input); - - Assert.NotNull(spans); - Assert.True(spans.Count >= 5); - - Assert.Equal("Start ", spans[0].Text); - Assert.Equal("Bold Text", spans[1].Text); - Assert.True(spans[1].IsBold); - - Assert.Equal(" then ", spans[2].Text); - Assert.Equal("Italic Text", spans[3].Text); - Assert.True(spans[3].IsItalic); - - var strikeSpan = spans.FirstOrDefault(s => s.Text == "Strikethrough"); - Assert.NotNull(strikeSpan); - Assert.True(strikeSpan.IsStrikethrough); - - var underlineSpan = spans.FirstOrDefault(s => s.Text == "Underline"); - Assert.NotNull(underlineSpan); - Assert.True(underlineSpan.IsUnderline); - } - - [Fact] - public void RichTextHelper_ParsesSubscriptAndSuperscript() - { - string input = "Formula: H~2~O and Exponent: x^2^"; - var spans = RichTextHelper.ParseMarkdownToSpans(input); - - Assert.NotNull(spans); - var subSpan = spans.FirstOrDefault(s => s.Text == "2" && s.Script == TextScriptMode.Subscript); - Assert.NotNull(subSpan); - - var superSpan = spans.FirstOrDefault(s => s.Text == "2" && s.Script == TextScriptMode.Superscript); - Assert.NotNull(superSpan); - } - - [Fact] - public void RichTextHelper_ParsesColorTagsAndHyperlinks() - { - string input = "Here is Blue Text and [Click Here](https://example.com)"; - var spans = RichTextHelper.ParseMarkdownToSpans(input); - - Assert.NotNull(spans); - var blueSpan = spans.FirstOrDefault(s => s.Text == "Blue Text"); - Assert.NotNull(blueSpan); - Assert.Equal("#0F6CBD", blueSpan.TextColorHex); - - var linkSpan = spans.FirstOrDefault(s => s.Text == "Click Here"); - Assert.NotNull(linkSpan); - Assert.Equal("https://example.com", linkSpan.LinkUrl); - Assert.True(linkSpan.IsUnderline); - } - - [Fact] - public void RichTextHelper_SpansToMarkdown_ReconstructsMarkupLosslessly() - { - var spans = new List - { - new() { Text = "Normal " }, - new() { Text = "Bold", IsBold = true }, - new() { Text = " and " }, - new() { Text = "Italic", IsItalic = true }, - new() { Text = " and " }, - new() { Text = "Super", Script = TextScriptMode.Superscript } - }; - - string markdown = RichTextHelper.SpansToMarkdown(spans); - Assert.Contains("**Bold**", markdown); - Assert.Contains("*Italic*", markdown); - Assert.Contains("^Super^", markdown); - } - - [Fact] - public void TextElementViewModel_SetMarkdownText_UpdatesSpansAndPlainText() - { - var vm = new TextElementViewModel(); - vm.SetMarkdownText("Total is **$100.00** PAID"); - - Assert.NotNull(vm.Spans); - Assert.True(vm.Spans.Count >= 3); - Assert.Equal("Total is $100.00 PAID", vm.Text); - - var model = (PdfTextElement)vm.ToModel(); - Assert.NotNull(model.Spans); - Assert.Equal(vm.Spans.Count, model.Spans.Count); - - var reloadedVm = new TextElementViewModel(model); - Assert.NotNull(reloadedVm.Spans); - Assert.Equal(vm.Spans.Count, reloadedVm.Spans.Count); - Assert.Equal("Total is $100.00 PAID", reloadedVm.Text); - } - - [Fact] - public void DataMergeEngine_HydratesPlaceholdersWithinSpans() - { - var doc = new PdfDocumentModel(); - var page = new PdfPageModel(); - doc.Pages.Add(page); - - var textElement = new PdfTextElement - { - X = 50, - Y = 50, - Width = 300, - Height = 40, - Spans = new List - { - new() { Text = "Dear " }, - new() { Text = "{{CustomerName}}", IsBold = true, TextColorHex = "#0F6CBD" }, - new() { Text = ", your balance is " }, - new() { Text = "{{Amount}}", IsBold = true } - } - }; - textElement.SynchronizePlainTextFromSpans(); - page.Elements.Add(textElement); - - var engine = new DataMergeEngine(); - var record = new Dictionary - { - { "CustomerName", "Jane Doe" }, - { "Amount", "$450.00" } - }; - - var hydratedDoc = engine.HydrateDocument(doc, record); - var hydratedText = hydratedDoc.Pages[0].Elements.OfType().First(); - - Assert.NotNull(hydratedText.Spans); - Assert.Equal(4, hydratedText.Spans.Count); - Assert.Equal("Jane Doe", hydratedText.Spans[1].Text); - Assert.True(hydratedText.Spans[1].IsBold); - Assert.Equal("#0F6CBD", hydratedText.Spans[1].TextColorHex); - Assert.Equal("$450.00", hydratedText.Spans[3].Text); - Assert.Equal("Dear Jane Doe, your balance is $450.00", hydratedText.Text); - } - - [Fact] - public void PdfExportService_ExportsMultiSpanTextElement_GeneratesValidPdfBytes() - { - var doc = new PdfDocumentModel(); - var page = new PdfPageModel { Width = 595, Height = 842 }; - doc.Pages.Add(page); - - var textElement = new PdfTextElement - { - X = 50, - Y = 50, - Width = 400, - Height = 60, - FontFamily = "Arial", - FontSize = 14, - Spans = new List - { - new() { Text = "Invoice Number: " }, - new() { Text = "INV-2026-001", IsBold = true, TextColorHex = "#0F6CBD" }, - new() { Text = " (Status: " }, - new() { Text = "APPROVED", IsBold = true, TextColorHex = "#16A34A" }, - new() { Text = ", Formula: H" }, - new() { Text = "2", Script = TextScriptMode.Subscript }, - new() { Text = "O)" } - } - }; - textElement.SynchronizePlainTextFromSpans(); - page.Elements.Add(textElement); - - var exporter = new PdfExportService(); - byte[] pdfBytes = exporter.GeneratePdfBytes(doc); - - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 500); - - // Verify standard PDF header magic bytes %PDF- - string header = System.Text.Encoding.ASCII.GetString(pdfBytes.Take(5).ToArray()); - Assert.Equal("%PDF-", header); - } - - [Fact] - public void TextLayoutEngine_GenerateSvgMarkup_EmitsTspanElementsForSpans() - { - var element = new PdfTextElement - { - X = 10, - Y = 10, - Width = 250, - Height = 50, - FontFamily = "Segoe UI", - FontSize = 14, - Spans = new List - { - new() { Text = "Prefix ", IsBold = false }, - new() { Text = "BoldRed", IsBold = true, TextColorHex = "#DC2626" }, - new() { Text = "2", Script = TextScriptMode.Superscript } - } - }; - element.SynchronizePlainTextFromSpans(); - - string svg = TextLayoutEngine.GenerateSvgMarkup(element); - Assert.NotNull(svg); - Assert.Contains("().ToList(); - Assert.NotEmpty(textElements); - - // Verify that multi-span elements exist with rich formatting - var multiSpanElements = textElements.Where(t => t.Spans != null && t.Spans.Count > 1).ToList(); - Assert.True(multiSpanElements.Count >= 5); - - // Verify subscripts and superscripts in the specimen - bool hasSubscript = multiSpanElements.Any(t => t.Spans!.Any(s => s.Script == TextScriptMode.Subscript)); - bool hasSuperscript = multiSpanElements.Any(t => t.Spans!.Any(s => s.Script == TextScriptMode.Superscript)); - bool hasColors = multiSpanElements.Any(t => t.Spans!.Any(s => !string.IsNullOrEmpty(s.TextColorHex))); - bool hasLinks = multiSpanElements.Any(t => t.Spans!.Any(s => !string.IsNullOrEmpty(s.LinkUrl))); - - Assert.True(hasSubscript); - Assert.True(hasSuperscript); - Assert.True(hasColors); - Assert.True(hasLinks); - - // Verify QuestPDF export - var exporter = new PdfExportService(); - byte[] pdfBytes = exporter.GeneratePdfBytes(doc); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 1000); - } - - [Fact] - public void TemplateService_CreatesOcrAnalysisReportTemplate_ValidModelAndSpans() - { - var templateService = new TemplateService(); - var doc = templateService.CreateOcrAnalysisReportTemplate(); - - Assert.NotNull(doc); - Assert.Single(doc.Pages); - var page = doc.Pages[0]; - - // Verify table, QR code, and text elements - var tables = page.Elements.OfType().ToList(); - var qrCodes = page.Elements.OfType().ToList(); - var textElements = page.Elements.OfType().ToList(); - - Assert.Single(tables); - Assert.Single(qrCodes); - Assert.NotEmpty(textElements); - - // Verify multi-span OCR annotations - var multiSpanElements = textElements.Where(t => t.Spans != null && t.Spans.Count > 1).ToList(); - Assert.True(multiSpanElements.Count >= 4); - - // Verify QuestPDF export - var exporter = new PdfExportService(); - byte[] pdfBytes = exporter.GeneratePdfBytes(doc); - Assert.NotNull(pdfBytes); - Assert.True(pdfBytes.Length > 1000); - } - - [Fact] - public void ToggleInlineFormatting_Bold_WrapsSelectedText() - { - string fullText = "normative period of rec\nmacro environment,\nd top-line revenue"; - int start = 24; - int length = 18; // "macro environment," - - var result = RichTextHelper.ToggleInlineFormatting(fullText, start, length, InlineFormatType.Bold); - - Assert.Equal("normative period of rec\n**macro environment,**\nd top-line revenue", result.NewText); - Assert.Equal(24, result.NewSelectionStart); - Assert.Equal(22, result.NewSelectionLength); - } - - [Fact] - public void ToggleInlineFormatting_Bold_TogglesOffWhenAlreadyWrapped() - { - string fullText = "normative period of rec\n**macro environment,**\nd top-line revenue"; - int start = 24; - int length = 22; // "**macro environment,**" - - var result = RichTextHelper.ToggleInlineFormatting(fullText, start, length, InlineFormatType.Bold); - - Assert.Equal("normative period of rec\nmacro environment,\nd top-line revenue", result.NewText); - Assert.Equal(24, result.NewSelectionStart); - Assert.Equal(18, result.NewSelectionLength); - } - - [Fact] - public void ToggleInlineFormatting_ItalicAndUnderline_WrapsAndUnwraps() - { - string text = "Hello World"; - // Italic on "World" - var italicResult = RichTextHelper.ToggleInlineFormatting(text, 6, 5, InlineFormatType.Italic); - Assert.Equal("Hello *World*", italicResult.NewText); - - // Unwrap Italic - var unitalicResult = RichTextHelper.ToggleInlineFormatting(italicResult.NewText, italicResult.NewSelectionStart, italicResult.NewSelectionLength, InlineFormatType.Italic); - Assert.Equal("Hello World", unitalicResult.NewText); - - // Underline on "World" - var underlineResult = RichTextHelper.ToggleInlineFormatting(text, 6, 5, InlineFormatType.Underline); - Assert.Equal("Hello World", underlineResult.NewText); - - // Unwrap Underline - var unwrapUnderline = RichTextHelper.ToggleInlineFormatting(underlineResult.NewText, underlineResult.NewSelectionStart, underlineResult.NewSelectionLength, InlineFormatType.Underline); - Assert.Equal("Hello World", unwrapUnderline.NewText); - } - - [Fact] - public void ToggleInlineFormatting_Color_WrapsAndUpdatesHex() - { - string text = "Hello World"; - var colorResult = RichTextHelper.ToggleInlineFormatting(text, 6, 5, InlineFormatType.Color, "#0F6CBD"); - Assert.Equal("Hello World", colorResult.NewText); - - // Change color to #DC2626 - var changeColor = RichTextHelper.ToggleInlineFormatting(colorResult.NewText, colorResult.NewSelectionStart, colorResult.NewSelectionLength, InlineFormatType.Color, "#DC2626"); - Assert.Equal("Hello World", changeColor.NewText); - } - - [Fact] - public void ToggleInlineFormatting_HandlesLeadingAndTrailingWhitespace() - { - string text = "Hello World Today"; - // Selected " World " with whitespace around it - var result = RichTextHelper.ToggleInlineFormatting(text, 5, 7, InlineFormatType.Bold); - Assert.Equal("Hello **World** Today", result.NewText); - } - - [Fact] - public void RichTextHelper_ParsesAndSerializesSizeAndFontTags() - { - string input = "Start Big Text and Modern Font end."; - var spans = RichTextHelper.ParseMarkdownToSpans(input); - - Assert.NotNull(spans); - var sizeSpan = spans.FirstOrDefault(s => s.Text == "Big Text"); - Assert.NotNull(sizeSpan); - Assert.Equal(18, sizeSpan.FontSize); - - var fontSpan = spans.FirstOrDefault(s => s.Text == "Modern Font"); - Assert.NotNull(fontSpan); - Assert.Equal("Poppins", fontSpan.FontFamily); - } -} diff --git a/tests/PdfEditorApp.Tests/SignatureAndFormsServiceTests.cs b/tests/PdfEditorApp.Tests/SignatureAndFormsServiceTests.cs deleted file mode 100644 index 7225457..0000000 --- a/tests/PdfEditorApp.Tests/SignatureAndFormsServiceTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class SignatureAndFormsServiceTests -{ - [Fact] - public void SignatureService_ComputeDocumentSha256_ProducesDeterministicHash() - { - // Arrange - var service = new SignatureService(); - var doc1 = new PdfDocumentModel - { - Title = "Contract_Final.pdf", - Author = "Enterprise Signer", - Subject = "Service Agreement" - }; - var page1 = new PdfPageModel { PageNumber = 1 }; - page1.Elements.Add(new PdfTextElement { Text = "Terms & Conditions", X = 50, Y = 50 }); - doc1.Pages.Add(page1); - - var doc2 = new PdfDocumentModel - { - Title = "Contract_Final.pdf", - Author = "Enterprise Signer", - Subject = "Service Agreement" - }; - var page2 = new PdfPageModel { PageNumber = 1 }; - page2.Elements.Add(new PdfTextElement { Text = "Terms & Conditions", X = 50, Y = 50 }); - doc2.Pages.Add(page2); - - // Act - string hash1 = service.ComputeDocumentSha256(doc1); - string hash2 = service.ComputeDocumentSha256(doc2); - - // Assert - Assert.Equal(64, hash1.Length); - Assert.Equal(hash1, hash2); - - // Modify doc2 - doc2.Title = "Contract_Tampered.pdf"; - string hash3 = service.ComputeDocumentSha256(doc2); - Assert.NotEqual(hash1, hash3); - } - - [Fact] - public void SignatureService_CreateCryptographicSignatureSeal_PopulatesMetadataAndDigest() - { - // Arrange - var service = new SignatureService(); - string dummyDigest = "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"; - - // Act - var seal = service.CreateCryptographicSignatureSeal("Dr. Evelyn Reed", "Global Medical Corp", "Clinical Approval", dummyDigest, 120, 250); - - // Assert - Assert.NotNull(seal); - Assert.Equal(120, seal.X); - Assert.Equal(250, seal.Y); - Assert.Equal(320, seal.Width); - Assert.Equal(100, seal.Height); - Assert.Contains("Dr. Evelyn Reed", seal.Label); - Assert.Contains("Global Medical Corp", seal.Label); - Assert.Contains("Clinical Approval", seal.Label); - Assert.Contains("SHA256:", seal.Label); - Assert.Equal("#16A34A", seal.StrokeColorHex); // Emerald verification border - } - - [Fact] - public void SignatureService_CreateCursiveSignatureElement_AppliesFontAndStyleCorrectly() - { - // Arrange - var service = new SignatureService(); - - // Act - var sigExecutive = service.CreateCursiveSignatureElement("Alexander Hamilton", SignatureStyle.CursiveElegance, 50, 100); - var sigScript = service.CreateCursiveSignatureElement("Alexander Hamilton", SignatureStyle.ClassicScript, 50, 100); - - // Assert - Assert.Equal("Georgia", sigExecutive.FontFamily); - Assert.True(sigExecutive.IsItalic); - Assert.Equal("Alexander Hamilton", sigExecutive.Text); - - Assert.Equal("Times New Roman", sigScript.FontFamily); - Assert.Equal("#0F6CBD", sigScript.TextColorHex); - } - - [Fact] - public void SignatureService_CreateFormFieldElement_GeneratesProperFieldTypes() - { - // Arrange - var service = new SignatureService(); - - // Act - var txtField = service.CreateFormFieldElement(FormFieldType.Text, "txt_customer_name", 50, 100, 200, 30); - var chkField = service.CreateFormFieldElement(FormFieldType.Checkbox, "chk_terms_agreed", 50, 150, 24, 24); - - // Assert - Assert.Equal(FormFieldType.Text, txtField.FieldType); - Assert.Equal("txt_customer_name", txtField.FieldName); - Assert.Equal(200, txtField.Width); - - Assert.Equal(FormFieldType.Checkbox, chkField.FieldType); - Assert.Equal("chk_terms_agreed", chkField.FieldName); - Assert.Equal("false", chkField.DefaultValue); - } -} diff --git a/tests/PdfEditorApp.Tests/SmartPlacementTests.cs b/tests/PdfEditorApp.Tests/SmartPlacementTests.cs deleted file mode 100644 index f25c5c7..0000000 --- a/tests/PdfEditorApp.Tests/SmartPlacementTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Linq; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class SmartPlacementTests -{ - [Fact] - public void ViewportCenter_Placement_CalculatesAccurateCoordinates() - { - var service = new SmartPlacementService(); - var page = new PageViewModel { Width = 800, Height = 1131 }; - - // User is scrolled down looking at center (400, 700) - service.UpdateViewport(400, 700, 800, 600); - - // Add 400x80 text box - var (x, y) = service.GetPlacementPosition(page, 400, 80); - - // Expected centered at (400 - 200 = 200, 700 - 40 = 660) - Assert.Equal(200.0, x); - Assert.Equal(660.0, y); - } - - [Fact] - public void ViewportCenter_Clamping_SafelyKeepsElementsInsidePageMargins() - { - var service = new SmartPlacementService(); - var page = new PageViewModel { Width = 800, Height = 1131 }; - - // Viewport is scrolled way past the bottom right (1000, 1500) - service.UpdateViewport(1000, 1500, 800, 600); - - var (x, y) = service.GetPlacementPosition(page, 300, 100); - - // Max X = 800 - 300 - 28 = 472 - // Max Y = 1131 - 100 - 28 = 1003 - Assert.Equal(472.0, x); - Assert.Equal(1003.0, y); - } - - [Fact] - public void ContextMenu_Placement_PositionsNearClickPointer() - { - var service = new SmartPlacementService(); - var page = new PageViewModel { Width = 800, Height = 1131 }; - - service.UpdateViewport(400, 300, 800, 600); - service.SetContextMenuPointer(350, 480); - - // Context menu placed 200x150 sticky note - var (x, y) = service.GetPlacementPosition(page, 200, 150); - - Assert.Equal(320.0, x); // 350 - 30 - Assert.Equal(470.0, y); // 480 - 10 - - // After placement, context pointer is consumed and returns to viewport center - var (nextX, nextY) = service.GetPlacementPosition(page, 200, 150); - Assert.Equal(300.0, nextX); // Viewport center X (400 - 100) - Assert.Equal(225.0, nextY); // Viewport center Y (300 - 75) - } - - [Fact] - public void ConsecutiveInsertions_ApplyCascadingOffset() - { - var service = new SmartPlacementService(); - var page = new PageViewModel { Width = 800, Height = 1131 }; - - service.UpdateViewport(400, 500, 800, 600); - - var (x1, y1) = service.GetPlacementPosition(page, 100, 100); - var (x2, y2) = service.GetPlacementPosition(page, 100, 100); - var (x3, y3) = service.GetPlacementPosition(page, 100, 100); - - Assert.Equal(350.0, x1); - Assert.Equal(450.0, y1); - - // Second should be offset by 22pt - Assert.Equal(372.0, x2); - Assert.Equal(472.0, y2); - - // Third should be offset by 44pt - Assert.Equal(394.0, x3); - Assert.Equal(494.0, y3); - - // Reset cascade - service.ResetCascade(); - var (x4, y4) = service.GetPlacementPosition(page, 100, 100); - Assert.Equal(350.0, x4); - Assert.Equal(450.0, y4); - } - - [Fact] - public void MainViewModel_AddTextElement_UsesViewportPosition() - { - var mainVm = new MainViewModel(); - Assert.NotNull(mainVm.CurrentPage); - mainVm.CurrentPage.Width = 800; - mainVm.CurrentPage.Height = 1131; - - // Update viewport center to lower half of page - mainVm.SmartPlacement.UpdateViewport(400, 800, 800, 600); - - int initialCount = mainVm.CurrentPage.Elements.Count; - mainVm.AddTextElement(); - - Assert.Equal(initialCount + 1, mainVm.CurrentPage.Elements.Count); - var addedText = mainVm.CurrentPage.Elements.Last() as TextElementViewModel; - Assert.NotNull(addedText); - - // 400x80 text box centered at (400, 800) -> X=200, Y=760 - Assert.Equal(200.0, addedText.X); - Assert.Equal(760.0, addedText.Y); - Assert.Same(addedText, mainVm.CurrentPage.SelectedElement); - } - - [Fact] - public void MainViewModel_PasteAndDuplicate_UsesSmartPlacement() - { - var mainVm = new MainViewModel(); - Assert.NotNull(mainVm.CurrentPage); - - var firstEl = mainVm.CurrentPage.Elements.First(); - mainVm.CurrentPage.SelectElement(firstEl); - - // Duplicate - mainVm.SmartPlacement.UpdateViewport(400, 600, 800, 600); - mainVm.Duplicate(); - - var duplicated = mainVm.CurrentPage.SelectedElement; - Assert.NotNull(duplicated); - Assert.NotSame(firstEl, duplicated); - - // Copy and paste - mainVm.Copy(); - mainVm.SmartPlacement.UpdateViewport(400, 900, 800, 600); - mainVm.Paste(); - - var pasted = mainVm.CurrentPage.SelectedElement; - Assert.NotNull(pasted); - Assert.NotSame(duplicated, pasted); - Assert.True(pasted.Y > 700); // Placed in the scrolled-down viewport - } -} diff --git a/tests/PdfEditorApp.Tests/SnakeMarketplaceIntegrationTests.cs b/tests/PdfEditorApp.Tests/SnakeMarketplaceIntegrationTests.cs deleted file mode 100644 index 87a3564..0000000 --- a/tests/PdfEditorApp.Tests/SnakeMarketplaceIntegrationTests.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Marketplace; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Plugins.Snake; -using PdfEditorApp.Services.Overlays; -using PdfEditorApp.Services.Palette; -using PdfEditorApp.Services.Plugins; -using PdfEditorApp.Services.Ribbon; -using PdfEditorApp.Services.StatusBar; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class SnakeMarketplaceIntegrationTests -{ - private ServiceProvider CreateTestServices(string? testDir = null) - { - var services = new ServiceCollection(); - App.ConfigureServices(services); - if (testDir != null) - { - var tempFile = Path.Combine(testDir, "installed_plugins.json"); - services.AddSingleton(new FileInstalledPluginStore(tempFile)); - services.AddSingleton(sp => - { - var host = sp.GetRequiredService(); - var overlay = sp.GetRequiredService(); - var store = sp.GetRequiredService(); - var pluginsDir = Path.Combine(testDir, "plugins"); - return new PluginMarketplaceService(host, overlay, store, null, null, pluginsDir); - }); - } - return services.BuildServiceProvider(); - } - - [Fact] - public async Task SnakePlugin_IsNotInstalledByDefault_AndInstallsAndUninstallsViaMarketplace() - { - var tempDir = Path.Combine(AppContext.BaseDirectory, $"frypdf_snake_test_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - try - { - var sp = CreateTestServices(tempDir); - var host = sp.GetRequiredService(); - var overlayReg = sp.GetRequiredService(); - var commandReg = sp.GetRequiredService(); - var statusReg = sp.GetRequiredService(); - var ribbonReg = sp.GetRequiredService(); - var marketplace = sp.GetRequiredService(); - - // 1. Mount standard profile bundles (does NOT include ShellOverlaysBundle) - var bundles = new IFryPluginBundle[] - { - new ToolsOrganizeBundle(), - new StatusBarBundle(), - new CommandPaletteBundle() - }; - - var profile = new PluginProfile - { - ProfileName = "desktop", - Bundles = bundles.Select(b => b.Id).ToList() - }; - ProfileLoader.ApplyProfile(profile, host, bundles); - await host.StartAsync(); - - // 2. Verify Snake Game is NOT installed or active by default - Assert.False(host.IsPluginActive("frypdf.overlay.snake")); - Assert.False(marketplace.IsPluginInstalled("frypdf.overlay.snake")); - Assert.Null(overlayReg.GetOverlay("frypdf.overlay.snake")); - Assert.False(overlayReg.IsOverlayVisible("frypdf.overlay.snake")); - - // 3. Verify Snake Game and companion overlays are available in the Marketplace catalog - await marketplace.FetchRemoteCatalogAsync(); - var catalog = await marketplace.GetCatalogAsync(); - Assert.True(catalog.Count >= 3); - var snakeItem = catalog.First(c => c.Id == "frypdf.overlay.snake"); - Assert.Equal(MarketplacePluginStatus.Available, snakeItem.Status); - Assert.StartsWith("Retro Arcade Snake Game", snakeItem.Name); - - Assert.Contains(catalog, c => c.Id == "frypdf.overlay.scratchpad"); - Assert.Contains(catalog, c => c.Id == "frypdf.overlay.telemetry"); - - // 4. Install plugin via Marketplace - string lastStatus = ""; - bool installed = await marketplace.InstallPluginAsync("frypdf.overlay.snake", statusCallback: s => lastStatus = s); - Assert.True(installed, $"Install failed: {lastStatus}"); - - // 5. Verify plugin is now active in host and overlay is registered and visible - Assert.True(host.IsPluginActive("frypdf.overlay.snake")); - Assert.True(marketplace.IsPluginInstalled("frypdf.overlay.snake")); - Assert.NotNull(overlayReg.GetOverlay("frypdf.overlay.snake")); - Assert.True(overlayReg.IsOverlayVisible("frypdf.overlay.snake")); - Assert.Single(overlayReg.ActiveOverlays); - - // Verify status in catalog updated to Installed - catalog = await marketplace.GetCatalogAsync(); - Assert.Equal(MarketplacePluginStatus.Installed, catalog.First(c => c.Id == "frypdf.overlay.snake").Status); - - // Verify capabilities registered - Assert.Contains(commandReg.GetAllCommands(), c => c.Id == "cmd.overlay.snake"); - Assert.Contains(statusReg.GetWidgets(StatusBarAlignment.Right), w => w.WidgetId == "frypdf.status.snake"); - Assert.Contains(ribbonReg.GetActionsForTab("view"), a => a.Id == "frypdf.ribbon.action.snake"); - - // 6. Test Persistence & Auto-Restore on simulated App Restart - var store = sp.GetRequiredService(); - Assert.True(store.IsInstalled("frypdf.overlay.snake")); - - var newHost = new PluginHost(new FryPluginContext(sp)); - var newMarketplace = new PluginMarketplaceService(newHost, overlayReg, store, null, null, Path.Combine(tempDir, "plugins")); - Assert.True(newMarketplace.IsPluginInstalled("frypdf.overlay.snake")); - Assert.True(newHost.IsPluginActive("frypdf.overlay.snake")); - - // 7. Uninstall plugin via Marketplace - bool uninstalled = await marketplace.UninstallPluginAsync("frypdf.overlay.snake"); - Assert.True(uninstalled); - Assert.False(store.IsInstalled("frypdf.overlay.snake")); - - // 8. Verify plugin deactivated and cleanly unmounted - Assert.False(host.IsPluginActive("frypdf.overlay.snake")); - Assert.False(marketplace.IsPluginInstalled("frypdf.overlay.snake")); - Assert.False(overlayReg.IsOverlayVisible("frypdf.overlay.snake")); - Assert.Empty(overlayReg.ActiveOverlays); - - // Verify status in catalog returned to Available - catalog = await marketplace.GetCatalogAsync(); - Assert.Equal(MarketplacePluginStatus.Available, catalog.First(c => c.Id == "frypdf.overlay.snake").Status); - - // Verify effects rolled back - Assert.DoesNotContain(commandReg.GetAllCommands(), c => c.Id == "cmd.overlay.snake"); - Assert.DoesNotContain(statusReg.GetWidgets(StatusBarAlignment.Right), w => w.WidgetId == "frypdf.status.snake"); - Assert.DoesNotContain(ribbonReg.GetActionsForTab("view"), a => a.Id == "frypdf.ribbon.action.snake"); - - await host.StopAsync(); - } - finally - { - try { Directory.Delete(tempDir, recursive: true); } catch { } - } - } -} diff --git a/tests/PdfEditorApp.Tests/SnakeOverlayPluginTests.cs b/tests/PdfEditorApp.Tests/SnakeOverlayPluginTests.cs deleted file mode 100644 index b03700f..0000000 --- a/tests/PdfEditorApp.Tests/SnakeOverlayPluginTests.cs +++ /dev/null @@ -1,357 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Plugins.Snake; -using PdfEditorApp.Services.Overlays; -using PdfEditorApp.Services.Palette; -using PdfEditorApp.Services.Ribbon; -using PdfEditorApp.Services.StatusBar; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class SnakeOverlayPluginTests -{ - [Fact] - public void OverlayRegistry_Registers_Shows_Toggles_And_Hides_Overlay() - { - var services = new ServiceCollection().BuildServiceProvider(); - var registry = new OverlayRegistry(services); - - bool registryChangedFired = false; - registry.RegistryChanged += () => registryChangedFired = true; - - var desc = new OverlayDescriptor - { - Id = "test.overlay.custom", - Title = "Custom Overlay", - Slot = "shell.overlay", - DefaultWidth = 300, - DefaultHeight = 400, - ViewFactory = _ => "MockView" - }; - - using var unreg = registry.RegisterOverlay(desc); - Assert.True(registryChangedFired); - Assert.NotNull(registry.GetOverlay("test.overlay.custom")); - Assert.Single(registry.GetAllOverlays()); - - // Initially not visible - Assert.False(registry.IsOverlayVisible("test.overlay.custom")); - - // Show - registry.ShowOverlay("test.overlay.custom"); - Assert.True(registry.IsOverlayVisible("test.overlay.custom")); - Assert.Single(registry.ActiveOverlays); - Assert.Equal("Custom Overlay", registry.ActiveOverlays[0].Title); - - // Toggle (hides) - registry.ToggleOverlay("test.overlay.custom"); - Assert.False(registry.IsOverlayVisible("test.overlay.custom")); - Assert.Empty(registry.ActiveOverlays); - - // Toggle (shows again) - registry.ToggleOverlay("test.overlay.custom"); - Assert.True(registry.IsOverlayVisible("test.overlay.custom")); - Assert.Single(registry.ActiveOverlays); - - // Hide - registry.HideOverlay("test.overlay.custom"); - Assert.False(registry.IsOverlayVisible("test.overlay.custom")); - Assert.Empty(registry.ActiveOverlays); - } - - [Fact] - public void SnakeGameViewModel_Initializes_With_Three_Segments() - { - using var vm = new SnakeGameViewModel(); - - Assert.Equal(3, vm.SnakeBody.Count); - Assert.Equal(SnakeGameState.Ready, vm.State); - Assert.Equal(0, vm.Score); - Assert.True(vm.SnakeBody.First!.Value.X > 0); - Assert.True(vm.SnakeBody.First!.Value.Y > 0); - } - - [Fact] - public void SnakeGameViewModel_Tick_MovesSnake_In_Current_Direction() - { - using var vm = new SnakeGameViewModel(); - vm.StartGame(); - - var initialHead = vm.SnakeBody.First!.Value; - - // Trigger private tick via reflection to test deterministic step - var tickMethod = typeof(SnakeGameViewModel).GetMethod("OnGameTick", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(tickMethod); - - tickMethod.Invoke(vm, new object?[] { null, EventArgs.Empty }); - - var newHead = vm.SnakeBody.First!.Value; - // Default direction is Right (+1 on X) - Assert.Equal(initialHead.X + 1, newHead.X); - Assert.Equal(initialHead.Y, newHead.Y); - Assert.Equal(3, vm.SnakeBody.Count); - } - - [Fact] - public void SnakeGameViewModel_ChangeDirection_Prevents_180Degree_Turn() - { - using var vm = new SnakeGameViewModel(); - vm.StartGame(); - - // Currently Right. Attempting Left should be ignored. - vm.ChangeDirection(SnakeDirection.Left); - - var tickMethod = typeof(SnakeGameViewModel).GetMethod("OnGameTick", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(tickMethod); - tickMethod.Invoke(vm, new object?[] { null, EventArgs.Empty }); - - // Snake still went right - var head = vm.SnakeBody.First!.Value; - Assert.Equal(SnakeGameViewModel.GridWidth / 2 + 1, head.X); - } - - [Fact] - public void SnakeGameViewModel_Eating_Food_Increments_Score_And_Length() - { - using var vm = new SnakeGameViewModel(); - vm.StartGame(); - - var head = vm.SnakeBody.First!.Value; - - // Place food directly in front of snake (head.X + 1, head.Y) - var foodProp = typeof(SnakeGameViewModel).GetProperty("Food"); - Assert.NotNull(foodProp); - var targetFood = new GridPoint(head.X + 1, head.Y); - foodProp.SetValue(vm, targetFood); - - var tickMethod = typeof(SnakeGameViewModel).GetMethod("OnGameTick", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(tickMethod); - tickMethod.Invoke(vm, new object?[] { null, EventArgs.Empty }); - - Assert.Equal(10, vm.Score); - Assert.Equal(4, vm.SnakeBody.Count); // Length increased from 3 to 4 - Assert.Equal(targetFood, vm.SnakeBody.First!.Value); - } - - [Fact] - public void SnakeGameViewModel_WallCollision_TriggersGameOver_When_Enabled() - { - using var vm = new SnakeGameViewModel - { - WallCollision = true - }; - vm.StartGame(); - - // Place head at right edge - vm.SnakeBody.Clear(); - vm.SnakeBody.AddFirst(new GridPoint(SnakeGameViewModel.GridWidth - 1, 5)); - vm.SnakeBody.AddLast(new GridPoint(SnakeGameViewModel.GridWidth - 2, 5)); - - var tickMethod = typeof(SnakeGameViewModel).GetMethod("OnGameTick", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(tickMethod); - - // Moving right from edge hits wall - tickMethod.Invoke(vm, new object?[] { null, EventArgs.Empty }); - - Assert.Equal(SnakeGameState.GameOver, vm.State); - Assert.Contains("wall", vm.StatusText, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void SnakeGameViewModel_WallWrap_WrapsAround_When_Collision_Disabled() - { - using var vm = new SnakeGameViewModel - { - WallCollision = false - }; - vm.StartGame(); - - // Place head at right edge - vm.SnakeBody.Clear(); - vm.SnakeBody.AddFirst(new GridPoint(SnakeGameViewModel.GridWidth - 1, 5)); - vm.SnakeBody.AddLast(new GridPoint(SnakeGameViewModel.GridWidth - 2, 5)); - - var tickMethod = typeof(SnakeGameViewModel).GetMethod("OnGameTick", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(tickMethod); - - // Moving right wraps to 0 - tickMethod.Invoke(vm, new object?[] { null, EventArgs.Empty }); - - Assert.Equal(SnakeGameState.Playing, vm.State); - Assert.Equal(0, vm.SnakeBody.First!.Value.X); - } - - [Fact] - public async Task SnakeGamePlugin_Registers_All_Capabilities_And_Rolls_Back_Cleanly() - { - var services = new ServiceCollection(); - var overlayReg = new OverlayRegistry(services.BuildServiceProvider()); - var commandReg = new CommandPaletteRegistry(); - var statusReg = new StatusBarRegistry(seedDefaults: false); - var ribbonReg = new RibbonRegistry(); - - var rootContext = new FryPluginContext(); - rootContext.RegisterService(overlayReg); - rootContext.RegisterService(commandReg); - rootContext.RegisterService(statusReg); - rootContext.RegisterService(ribbonReg); - - var plugin = new SnakeGamePlugin(); - Assert.Equal("frypdf.overlay.snake", plugin.Id); - Assert.NotNull(plugin.SettingsSchema); - Assert.True(plugin.SettingsSchema.ContainsKey("InitialSpeed")); - Assert.True(plugin.SettingsSchema.ContainsKey("WallCollision")); - - using var scope = new PluginScope(); - var scopedContext = rootContext.CreateScopedContext(scope); - - await plugin.ApplyAsync(scopedContext); - - // 1. Verify Overlay registered - Assert.NotNull(overlayReg.GetOverlay("frypdf.overlay.snake")); - - // 2. Verify Command Palette registered - Assert.Contains(commandReg.GetAllCommands(), c => c.Id == "cmd.overlay.snake"); - - // 3. Verify Status Bar Widget registered - var rightWidgets = statusReg.GetWidgets(StatusBarAlignment.Right); - Assert.Contains(rightWidgets, w => w.WidgetId == "frypdf.status.snake"); - - // 4. Verify Ribbon Action registered - var ribbonActions = ribbonReg.GetActionsForTab("view"); - Assert.Contains(ribbonActions, a => a.Id == "frypdf.ribbon.action.snake"); - - // 5. Test LIFO Rollback when scope is disposed - scope.Dispose(); - - Assert.Null(overlayReg.GetOverlay("frypdf.overlay.snake")); - Assert.DoesNotContain(commandReg.GetAllCommands(), c => c.Id == "cmd.overlay.snake"); - Assert.DoesNotContain(statusReg.GetWidgets(StatusBarAlignment.Right), w => w.WidgetId == "frypdf.status.snake"); - Assert.DoesNotContain(ribbonReg.GetActionsForTab("view"), a => a.Id == "frypdf.ribbon.action.snake"); - } - - [Fact] - public void SnakeGamePlugin_IsStandaloneOverlayPlugin() - { - var plugin = new SnakeGamePlugin(); - Assert.Equal("frypdf.overlay.snake", plugin.Id); - Assert.Equal("Playable Snake Game", plugin.Name); - Assert.Equal(new Version(1, 0, 0), plugin.Version); - } - - [Fact] - public void ShellOverlayHost_ClampsAndPositionsOverlays_WithinViewportBounds() - { - var services = new ServiceCollection(); - App.ConfigureServices(services); - var sp = services.BuildServiceProvider(); - - var mainVm = sp.GetRequiredService(); - var host = new PdfEditorApp.Views.Overlays.ShellOverlayHost - { - DataContext = mainVm - }; - - var overlayReg = sp.GetRequiredService(); - var desc = new OverlayDescriptor - { - Id = "test.overlay.positioning", - Title = "Positioning Test", - Slot = "shell.overlay", - DefaultWidth = 320, - DefaultHeight = 420, - InitialX = null, - InitialY = 100, - ViewFactory = _ => new Avalonia.Controls.Border() - }; - - overlayReg.RegisterOverlay(desc); - overlayReg.ShowOverlay(desc.Id); - - Assert.Single(mainVm.ActiveOverlays); - var instance = mainVm.ActiveOverlays[0]; - - // Measure and Arrange host with 1200x800 - host.Measure(new Avalonia.Size(1200, 800)); - host.Arrange(new Avalonia.Rect(0, 0, 1200, 800)); - - // Clamping docks within 1200x800 - Assert.True(instance.X > 0); - Assert.True(instance.X + instance.Width <= 1200); - Assert.True(instance.Y >= 60); - Assert.True(instance.Y + instance.Height <= 800); - } - - [Fact] - public void OverlayInstanceViewModel_SupportsResizing_AndRespectsConstraints() - { - var desc = new OverlayDescriptor - { - Id = "test.overlay.resizing", - Title = "Resizing Test", - DefaultWidth = 360, - DefaultHeight = 480, - MinWidth = 300, - MinHeight = 400, - MaxWidth = 800, - MaxHeight = 900, - IsResizable = true - }; - - var vm = new OverlayInstanceViewModel(desc); - - Assert.True(vm.IsResizable); - Assert.True(vm.CanResize); - Assert.Equal(360, vm.Width); - Assert.Equal(480, vm.Height); - Assert.Equal(480, vm.EffectiveHeight); - Assert.Equal(400, vm.EffectiveMinHeight); - - // Test normal resize - vm.Resize(500, 600); - Assert.Equal(500, vm.Width); - Assert.Equal(600, vm.Height); - Assert.Equal(600, vm.EffectiveHeight); - - // Test below MinWidth / MinHeight - vm.Resize(200, 250); - Assert.Equal(300, vm.Width); - Assert.Equal(400, vm.Height); - - // Test above MaxWidth / MaxHeight - vm.Resize(1200, 1500); - Assert.Equal(800, vm.Width); - Assert.Equal(900, vm.Height); - - // Test canvas boundary clamping - vm.X = 100; - vm.Y = 50; - vm.Resize(750, 850, maxCanvasWidth: 500, maxCanvasHeight: 600); - Assert.Equal(500, vm.Width); - Assert.Equal(600, vm.Height); - - // Test Minimize behavior - vm.ToggleMinimize(); - Assert.True(vm.IsMinimized); - Assert.False(vm.CanResize); - Assert.True(double.IsNaN(vm.EffectiveHeight)); - Assert.Equal(42, vm.EffectiveMinHeight); - - // Restore behavior - vm.ToggleMinimize(); - Assert.False(vm.IsMinimized); - Assert.True(vm.CanResize); - Assert.Equal(600, vm.EffectiveHeight); - Assert.Equal(400, vm.EffectiveMinHeight); - } -} diff --git a/tests/PdfEditorApp.Tests/TesseractManagerTests.cs b/tests/PdfEditorApp.Tests/TesseractManagerTests.cs deleted file mode 100644 index 37d7ec8..0000000 --- a/tests/PdfEditorApp.Tests/TesseractManagerTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Ocr; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class TesseractManagerTests -{ - private readonly TesseractModelService _modelService = new(); - - [Fact] - public void TesseractCatalog_ContainsAllMajorWorldwideLanguagePacks() - { - var languages = _modelService.AvailableLanguages; - - Assert.NotNull(languages); - Assert.True(languages.Count >= 30, "Catalog must contain at least 30 major language and script models"); - - // European / Latin - Assert.Contains(languages, l => l.Code == "eng" && l.Category == "Latin & European"); - Assert.Contains(languages, l => l.Code == "spa" && l.Category == "Latin & European"); - Assert.Contains(languages, l => l.Code == "fra" && l.Category == "Latin & European"); - Assert.Contains(languages, l => l.Code == "deu" && l.Category == "Latin & European"); - Assert.Contains(languages, l => l.Code == "rus" && l.Category == "Latin & European"); - - // East Asia (CJK) - Assert.Contains(languages, l => l.Code == "chi_sim" && l.Category == "East Asia (CJK)"); - Assert.Contains(languages, l => l.Code == "chi_tra" && l.Category == "East Asia (CJK)"); - Assert.Contains(languages, l => l.Code == "jpn" && l.Category == "East Asia (CJK)"); - Assert.Contains(languages, l => l.Code == "kor" && l.Category == "East Asia (CJK)"); - - // South Asia (Indic) - Assert.Contains(languages, l => l.Code == "hin" && l.Category == "South Asia (Indic)"); - Assert.Contains(languages, l => l.Code == "ben" && l.Category == "South Asia (Indic)"); - Assert.Contains(languages, l => l.Code == "tam" && l.Category == "South Asia (Indic)"); - Assert.Contains(languages, l => l.Code == "tel" && l.Category == "South Asia (Indic)"); - Assert.Contains(languages, l => l.Code == "san" && l.Category == "South Asia (Indic)"); - - // Middle East & RTL - Assert.Contains(languages, l => l.Code == "ara" && l.Category == "Middle East & RTL"); - Assert.Contains(languages, l => l.Code == "heb" && l.Category == "Middle East & RTL"); - Assert.Contains(languages, l => l.Code == "fas" && l.Category == "Middle East & RTL"); - - // Southeast Asia - Assert.Contains(languages, l => l.Code == "vie" && l.Category == "Southeast Asia"); - Assert.Contains(languages, l => l.Code == "tha" && l.Category == "Southeast Asia"); - Assert.Contains(languages, l => l.Code == "ind" && l.Category == "Southeast Asia"); - - // Specialized - Assert.Contains(languages, l => l.Code == "osd" && l.Category == "Specialized & Other"); - Assert.Contains(languages, l => l.Code == "equ" && l.Category == "Specialized & Other"); - } - - [Fact] - public void TesseractCatalog_AllFilesUseTessdataFastGitHubUrlPattern() - { - var languages = _modelService.AvailableLanguages; - Assert.NotEmpty(languages); - - foreach (var lang in languages) - { - Assert.False(string.IsNullOrWhiteSpace(lang.Code)); - Assert.False(string.IsNullOrWhiteSpace(lang.DisplayName)); - Assert.False(string.IsNullOrWhiteSpace(lang.NativeName)); - Assert.False(string.IsNullOrWhiteSpace(lang.FlagEmoji)); - Assert.False(string.IsNullOrWhiteSpace(lang.Description)); - Assert.False(string.IsNullOrWhiteSpace(lang.SampleText)); - Assert.StartsWith("https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/main/", lang.DownloadUrl); - Assert.EndsWith($"{lang.Code}.traineddata", lang.DownloadUrl); - Assert.True(lang.EstimatedSizeBytes > 0, $"Estimated size for {lang.Code} must be positive"); - Assert.Equal($"{lang.Code}.traineddata", lang.FileName); - } - } - - [Fact] - public void TesseractLanguagePackageInfo_FormatBytes_ReturnsHumanReadableSize() - { - Assert.Equal("0 KB", TesseractLanguagePackageInfo.FormatBytes(0)); - Assert.Equal("500.0 KB", TesseractLanguagePackageInfo.FormatBytes(500 * 1024)); - Assert.Equal("15.0 MB", TesseractLanguagePackageInfo.FormatBytes((long)(15.0 * 1024 * 1024))); - } - - [Fact] - public void TesseractManagerViewModel_FilterAndCategorySelection_FiltersCorrectly() - { - var vm = new TesseractManagerViewModel(_modelService); - - Assert.Equal(vm.TotalLanguagesCount, vm.FilteredLanguages.Count); - - // Filter by East Asia - vm.SelectCategory("EastAsia"); - Assert.All(vm.FilteredLanguages, l => Assert.Contains("East Asia", l.Category)); - Assert.Equal(4, vm.FilteredLanguages.Count); // chi_sim, chi_tra, jpn, kor - - // Filter by South Asia (Indic) - vm.SelectCategory("SouthAsia"); - Assert.All(vm.FilteredLanguages, l => Assert.Contains("South Asia", l.Category)); - Assert.True(vm.FilteredLanguages.Count >= 6); - - // Filter by Search Query - vm.SelectCategory("All"); - vm.SearchQuery = "hindi"; - Assert.Single(vm.FilteredLanguages); - Assert.Equal("hin", vm.FilteredLanguages[0].Code); - - // Search by code - vm.SearchQuery = "jpn"; - Assert.Single(vm.FilteredLanguages); - Assert.Equal("jpn", vm.FilteredLanguages[0].Code); - - // Clear filter - vm.SearchQuery = ""; - Assert.Equal(vm.TotalLanguagesCount, vm.FilteredLanguages.Count); - } - - [Fact] - public void HomeViewModel_NavigatesToTesseractDataSection() - { - var home = new HomeViewModel(); - - home.SelectNavSection("TesseractData"); - - Assert.Equal(HomeNavSection.TesseractData, home.SelectedNavSection); - Assert.True(home.IsTesseractDataSection); - Assert.NotNull(home.TesseractManager); - Assert.True(home.TesseractManager.TotalLanguagesCount >= 30); - } - - [Fact] - public async Task TesseractModelService_MockDirectoryLifecycle_HandlesInstallDeleteAndClear() - { - string tempDir = Path.Combine(Path.GetTempPath(), "frypdf_test_tessdata_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(tempDir); - - try - { - var service = new TesseractModelService(tempDir); - Assert.False(service.IsLanguageInstalled("eng")); - - // Create a fake traineddata file - string engPath = Path.Combine(tempDir, "eng.traineddata"); - await File.WriteAllBytesAsync(engPath, new byte[2048]); - - Assert.True(service.IsLanguageInstalled("eng")); - Assert.Equal(engPath, service.GetInstalledLanguagePath("eng")); - - long totalBytes = await service.GetTotalCacheSizeBytesAsync(); - Assert.Equal(2048, totalBytes); - - // Delete language - bool deleted = await service.DeleteLanguageAsync("eng"); - Assert.True(deleted); - Assert.False(service.IsLanguageInstalled("eng")); - Assert.False(File.Exists(engPath)); - - // Create two fake files and clear all - await File.WriteAllBytesAsync(Path.Combine(tempDir, "fra.traineddata"), new byte[1500]); - await File.WriteAllBytesAsync(Path.Combine(tempDir, "deu.traineddata"), new byte[1500]); - Assert.True(service.IsLanguageInstalled("fra")); - Assert.True(service.IsLanguageInstalled("deu")); - - await service.ClearAllCacheAsync(); - Assert.False(service.IsLanguageInstalled("fra")); - Assert.False(service.IsLanguageInstalled("deu")); - } - finally - { - try { Directory.Delete(tempDir, recursive: true); } catch { } - } - } -} diff --git a/tests/PdfEditorApp.Tests/TextSelectionAndOcrTests.cs b/tests/PdfEditorApp.Tests/TextSelectionAndOcrTests.cs deleted file mode 100644 index e2ad516..0000000 --- a/tests/PdfEditorApp.Tests/TextSelectionAndOcrTests.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Input; -using PdfEditorApp.Services; -using PdfEditorApp.Templates; -using PdfEditorApp.ViewModels; -using PdfEditorApp.Views.Controls; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class TextSelectionAndOcrTests -{ - private readonly IPdfExportService _exportService = new PdfExportService(); - private readonly ITemplateService _templateService = new TemplateService(); - - private byte[] CreateSamplePdfBytes() - { - var model = _templateService.CreateAnnualReportTemplate(); - return _exportService.GeneratePdfBytes(model); - } - - [Fact] - public void SelectionMode_DefaultIsText_CanToggleToAreaMode() - { - var vm = new PdfViewerViewModel(); - - Assert.Equal(PdfViewerSelectionMode.Text, vm.SelectionMode); - Assert.True(vm.IsTextSelectionMode); - Assert.False(vm.IsAreaSelectionMode); - - vm.SetAreaSelectionModeCommand.Execute(null); - Assert.Equal(PdfViewerSelectionMode.Area, vm.SelectionMode); - Assert.False(vm.IsTextSelectionMode); - Assert.True(vm.IsAreaSelectionMode); - - vm.SetTextSelectionModeCommand.Execute(null); - Assert.Equal(PdfViewerSelectionMode.Text, vm.SelectionMode); - Assert.True(vm.IsTextSelectionMode); - Assert.False(vm.IsAreaSelectionMode); - - vm.ToggleSelectionModeCommand.Execute(null); - Assert.Equal(PdfViewerSelectionMode.Area, vm.SelectionMode); - Assert.True(vm.IsAreaSelectionMode); - } - - [Fact] - public void ScannedDocumentDetection_BannerState_TransitionsProperly() - { - var vm = new PdfViewerViewModel(); - - // Initially no document - Assert.False(vm.ShowScannedDocumentBanner); - - // When document is loaded and page has 0 words - vm.IsScannedDocument = true; - vm.IsCurrentPageScanned = true; - vm.HasDocument = true; - - Assert.True(vm.ShowScannedDocumentBanner); - - // User dismisses banner - vm.DismissScannedBannerCommand.Execute(null); - Assert.False(vm.ShowScannedDocumentBanner); - Assert.True(vm.IsScannedBannerDismissed); - - // Re-enabling or OCR running suppresses banner - vm.IsScannedBannerDismissed = false; - vm.IsOcrRunning = true; - Assert.False(vm.ShowScannedDocumentBanner); - } - - [Fact] - public async Task AreaSelection_ExtractsWordsWithinRegion() - { - var vm = new PdfViewerViewModel(); - byte[] pdfBytes = CreateSamplePdfBytes(); - await vm.LoadDocumentBytesAsync(pdfBytes, "Test.pdf"); - - Assert.NotNull(vm.SelectedPage); - var page = vm.SelectedPage!; - - // Clear any auto-extracted template words and set specific test words - page.Words.Clear(); - page.Words.Add(new PdfViewerWordItem - { - Text = "Hello", - Bounds = new Rect(10, 10, 40, 15), - WordIndex = 0 - }); - page.Words.Add(new PdfViewerWordItem - { - Text = "World", - Bounds = new Rect(60, 10, 45, 15), - WordIndex = 1 - }); - page.Words.Add(new PdfViewerWordItem - { - Text = "FarAway", - Bounds = new Rect(300, 300, 60, 15), - WordIndex = 2 - }); - - // Region enclosing only "Hello World" - var region = new Rect(5, 5, 120, 30); - string extracted = await vm.ExtractOcrTextFromRegionAsync(page, region); - - Assert.Contains("Hello", extracted); - Assert.Contains("World", extracted); - Assert.DoesNotContain("FarAway", extracted); - } - - [Fact] - public void MarqueeSelection_IntersectsWords_CalculatesBoundingBoxCorrectly() - { - var word1 = new PdfViewerWordItem { Text = "Revenue", Bounds = new Rect(50, 100, 60, 16) }; - var word2 = new PdfViewerWordItem { Text = "Growth", Bounds = new Rect(115, 100, 50, 16) }; - var word3 = new PdfViewerWordItem { Text = "Footer", Bounds = new Rect(50, 700, 40, 12) }; - - var words = new List { word1, word2, word3 }; - var marqueeBox = new Rect(40, 90, 150, 30); - - var selected = words.Where(w => w.Bounds.Intersects(marqueeBox)).ToList(); - - Assert.Equal(2, selected.Count); - Assert.Equal("Revenue", selected[0].Text); - Assert.Equal("Growth", selected[1].Text); - } - - [Fact] - public async Task ScannedPdf_LoadsAndInitializesOcrCapabilities() - { - string? samplePdf = FindTestPdf(); - if (samplePdf == null || !File.Exists(samplePdf)) return; - - var vm = new PdfViewerViewModel(); - await vm.LoadDocumentAsync(samplePdf); - - Assert.True(vm.HasDocument); - Assert.NotEmpty(vm.Pages); - - var firstPage = vm.Pages[0]; - Assert.True(firstPage.WidthPoints > 0); - Assert.True(firstPage.HeightPoints > 0); - - // Ensure page geometry can be populated via OCR if needed - if (OperatingSystem.IsMacOS() && firstPage.Words.Count == 0) - { - await vm.RecognizeActivePageOcrAsync(); - Assert.NotEmpty(firstPage.Words); - Assert.False(string.IsNullOrWhiteSpace(firstPage.ExtractedText)); - Assert.False(vm.IsCurrentPageScanned); - } - } - - [Fact] - public void ShiftClick_ExtendsSelectionRange_OnPageItem() - { - var page = new PdfViewerPageItem - { - PageNumber = 1, - WidthPoints = 612, - HeightPoints = 792 - }; - - var w1 = new PdfViewerWordItem { Text = "First", Bounds = new Rect(50, 50, 30, 12), LineIndex = 0, WordIndex = 0 }; - var w2 = new PdfViewerWordItem { Text = "Second", Bounds = new Rect(85, 50, 40, 12), LineIndex = 0, WordIndex = 1 }; - var w3 = new PdfViewerWordItem { Text = "Third", Bounds = new Rect(130, 50, 30, 12), LineIndex = 0, WordIndex = 2 }; - - var line = new PdfViewerTextLineItem { LineIndex = 0, Bounds = new Rect(50, 50, 110, 12) }; - line.Words.AddRange(new[] { w1, w2, w3 }); - - page.Words.AddRange(new[] { w1, w2, w3 }); - page.TextLines.Add(line); - - // Select from start of First to end of Third - page.SetSelectionRange(new Point(50, 55), new Point(160, 55)); - - Assert.True(page.HasSelection); - Assert.Contains("First", page.SelectedText); - Assert.Contains("Third", page.SelectedText); - Assert.NotEmpty(page.SelectionRects); - } - - [Fact] - public void GarbledText_Detection_IdentifiesCorruptedFontsCorrectly() - { - // Unmapped / corrupt font encodings from custom PDF fonts - Assert.True(PdfViewerViewModel.IsGarbledText("? FT * FtJlTiur")); - Assert.True(PdfViewerViewModel.IsGarbledText("s | l | r | i | a | a | t | a | e | h | d | D | a | A")); - Assert.True(PdfViewerViewModel.IsGarbledText("??? ?? * ??")); - - // Clean normal texts (Latin, Indic Unicode, CJK) - Assert.False(PdfViewerViewModel.IsGarbledText("Hello World, this is a clean financial report.")); - Assert.False(PdfViewerViewModel.IsGarbledText("इस आधार पत्र को यूआईडीएआई द्वारा नियुक्त प्रमाणीकरण")); - Assert.False(PdfViewerViewModel.IsGarbledText("Annual Revenue Growth 2026")); - Assert.False(PdfViewerViewModel.IsGarbledText("")); - Assert.False(PdfViewerViewModel.IsGarbledText(null)); - } - - [Fact] - public void IsSelectionTextGarbled_UpdatesAutomatically_WhenSelectionChanges() - { - var vm = new PdfViewerViewModel(); - - Assert.False(vm.IsSelectionTextGarbled); - - vm.ActiveSelectedText = "? FT * FtJlTiur"; - Assert.True(vm.IsSelectionTextGarbled); - - vm.ActiveSelectedText = "Standard Heading Text"; - Assert.False(vm.IsSelectionTextGarbled); - } - - private static string? FindTestPdf() - { - var dir = new DirectoryInfo(AppContext.BaseDirectory); - while (dir != null) - { - string candidate = Path.Combine(dir.FullName, "Arduino Cheat Sheet_Redacted_Redacted.pdf"); - if (File.Exists(candidate)) return candidate; - dir = dir.Parent; - } - return null; - } -} diff --git a/tests/PdfEditorApp.Tests/ThemeServiceTests.cs b/tests/PdfEditorApp.Tests/ThemeServiceTests.cs deleted file mode 100644 index 3e99668..0000000 --- a/tests/PdfEditorApp.Tests/ThemeServiceTests.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Tests.Mocks; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -[Collection("ThemeServiceCollection")] -public class ThemeServiceTests : IDisposable -{ - private readonly string _settingsFilePath; - private readonly string? _originalBackup; - - public ThemeServiceTests() - { - _settingsFilePath = ThemeService.GetSettingsFilePath(); - if (File.Exists(_settingsFilePath)) - { - _originalBackup = File.ReadAllText(_settingsFilePath); - } - } - - public void Dispose() - { - try - { - if (_originalBackup != null) - { - File.WriteAllText(_settingsFilePath, _originalBackup); - } - else if (File.Exists(_settingsFilePath)) - { - File.Delete(_settingsFilePath); - } - } - catch - { - // Clean up best effort - } - } - - [Fact] - public void ThemeService_InitialState_LoadsFromDiskOrDefaultsToLight() - { - // Arrange: remove file if present - if (File.Exists(_settingsFilePath)) File.Delete(_settingsFilePath); - - // Act - var service = new ThemeService(); - - // Assert - Assert.Equal(AppThemeMode.Light, service.CurrentTheme); - Assert.Equal(PdfReaderTheme.Default, service.ReadingTheme); - Assert.False(service.IsDarkMode); - } - - [Fact] - public void ThemeService_SetTheme_ChangesThemeAndRaisesEvent() - { - // Arrange - var service = new ThemeService(); - var targetTheme = service.CurrentTheme == AppThemeMode.Dark ? AppThemeMode.Light : AppThemeMode.Dark; - AppThemeMode? receivedTheme = null; - service.ThemeChanged += (t) => receivedTheme = t; - - // Act - service.SetTheme(targetTheme); - - // Assert - Assert.Equal(targetTheme, service.CurrentTheme); - Assert.Equal(targetTheme == AppThemeMode.Dark, service.IsDarkMode); - Assert.Equal(targetTheme, receivedTheme); - } - - [Fact] - public void ThemeService_ToggleTheme_CyclesBetweenLightAndDark() - { - // Arrange - var service = new ThemeService(); - service.SetTheme(AppThemeMode.Light); - Assert.Equal(AppThemeMode.Light, service.CurrentTheme); - - // Act 1: Toggle to Dark - service.ToggleTheme(); - Assert.Equal(AppThemeMode.Dark, service.CurrentTheme); - Assert.True(service.IsDarkMode); - - // Act 2: Toggle back to Light - service.ToggleTheme(); - Assert.Equal(AppThemeMode.Light, service.CurrentTheme); - Assert.False(service.IsDarkMode); - } - - [Fact] - public void ThemeService_PersistsTheme_ToDisk_AndRestoresUponReopening() - { - // Arrange: instantiate service and save Dark mode - var service1 = new ThemeService(); - service1.SetTheme(AppThemeMode.Dark); - - // Verify file was written - Assert.True(File.Exists(_settingsFilePath)); - string savedJson = File.ReadAllText(_settingsFilePath); - Assert.Contains("Dark", savedJson); - - // Act: create brand new ThemeService instance (simulating app restart) - var service2 = new ThemeService(); - - // Assert: should restore Dark theme immediately from disk - Assert.Equal(AppThemeMode.Dark, service2.CurrentTheme); - Assert.True(service2.IsDarkMode); - } - - [Fact] - public void ThemeService_PersistsReadingTheme_ToDisk_AndRestoresUponReopening() - { - // Arrange: instantiate service and set reading theme to Sepia - var service1 = new ThemeService(); - service1.SetReadingTheme(PdfReaderTheme.Sepia); - - // Verify file was written - Assert.True(File.Exists(_settingsFilePath)); - string savedJson = File.ReadAllText(_settingsFilePath); - Assert.Contains("Sepia", savedJson); - - // Act: create brand new ThemeService instance (simulating app restart) - var service2 = new ThemeService(); - - // Assert: should restore Sepia reading theme immediately from disk - Assert.Equal(PdfReaderTheme.Sepia, service2.ReadingTheme); - } - - [Fact] - public void ThemeService_HandlesCaseInsensitiveAndLegacyJson_Gracefully() - { - // Arrange: manually write lowercase / legacy json - var legacyJson = "{\n \"theme\": \"dark\",\n \"readingTheme\": \"highcontrast\",\n \"isDarkMode\": true\n}"; - var dir = ThemeService.GetSettingsDirectory(); - Directory.CreateDirectory(dir); - File.WriteAllText(_settingsFilePath, legacyJson); - - // Act: instantiate new ThemeService - var service = new ThemeService(); - - // Assert - Assert.Equal(AppThemeMode.Dark, service.CurrentTheme); - Assert.Equal(PdfReaderTheme.HighContrast, service.ReadingTheme); - Assert.True(service.IsDarkMode); - } - - [Fact] - public void ThemeService_HandlesNumericEnumJson_Gracefully() - { - // Arrange: numeric enum values (0 = System, 1 = Light, 2 = Dark) - var numericJson = "{\n \"Theme\": 2,\n \"ReadingTheme\": 1\n}"; - var dir = ThemeService.GetSettingsDirectory(); - Directory.CreateDirectory(dir); - File.WriteAllText(_settingsFilePath, numericJson); - - // Act: instantiate new ThemeService - var service = new ThemeService(); - - // Assert - Assert.Equal(AppThemeMode.Dark, service.CurrentTheme); - Assert.Equal(PdfReaderTheme.Sepia, service.ReadingTheme); - } - - [Fact] - public void ThemeService_HandlesCorruptedJson_FallsBackToDefaults() - { - // Arrange: corrupt json - var corruptJson = "{ INVALID JSON !! @@ }}"; - var dir = ThemeService.GetSettingsDirectory(); - Directory.CreateDirectory(dir); - File.WriteAllText(_settingsFilePath, corruptJson); - - // Act: instantiate new ThemeService - var service = new ThemeService(); - - // Assert - Assert.Equal(AppThemeMode.Light, service.CurrentTheme); - Assert.Equal(PdfReaderTheme.Default, service.ReadingTheme); - } - - [Fact] - public void HomeViewModel_ToggleThemeCommand_SynchronizesWithThemeService() - { - // Arrange - var themeService = new ThemeService(); - themeService.SetTheme(AppThemeMode.Light); - - var homeVm = new HomeViewModel( - new MockRecentDocumentsService(), - new TemplateService(), - new ProjectPersistenceService(), - new PdfToolRegistry(), - themeService: themeService); - - Assert.False(homeVm.IsDarkMode); - - // Act 1: Toggle via HomeViewModel - homeVm.ToggleThemeCommand.Execute(null); - - // Assert - Assert.True(homeVm.IsDarkMode); - Assert.True(themeService.IsDarkMode); - - // Act 2: Toggle back - homeVm.ToggleThemeCommand.Execute(null); - - // Assert - Assert.False(homeVm.IsDarkMode); - Assert.False(themeService.IsDarkMode); - } - - [Fact] - public void MainViewModel_ThemeCommands_SwitchThemesAndSyncHomeVm() - { - // Arrange - var themeService = new ThemeService(); - themeService.SetTheme(AppThemeMode.Light); - - var mainVm = new MainViewModel( - new PdfExportService(), - new TemplateService(), - new ProjectPersistenceService(), - themeService: themeService); - - Assert.False(mainVm.IsDarkMode); - Assert.False(mainVm.Home.IsDarkMode); - - // Act 1: Set Dark Theme - mainVm.SetDarkThemeCommand.Execute(null); - Assert.True(mainVm.IsDarkMode); - Assert.True(mainVm.Home.IsDarkMode); - Assert.Equal(AppThemeMode.Dark, themeService.CurrentTheme); - - // Act 2: Set Light Theme - mainVm.SetLightThemeCommand.Execute(null); - Assert.False(mainVm.IsDarkMode); - Assert.False(mainVm.Home.IsDarkMode); - Assert.Equal(AppThemeMode.Light, themeService.CurrentTheme); - - // Act 3: Toggle Theme - mainVm.ToggleThemeCommand.Execute(null); - Assert.True(mainVm.IsDarkMode); - Assert.True(mainVm.Home.IsDarkMode); - } - - [Fact] - public void MainViewModel_PdfViewer_SyncsAndPersistsReadingTheme() - { - // Arrange - var themeService = new ThemeService(); - var mainVm = new MainViewModel( - new PdfExportService(), - new TemplateService(), - new ProjectPersistenceService(), - themeService: themeService); - - // Act: User selects Dark reading mode in PDF viewer - mainVm.PdfViewer.SetReadingThemeCommand.Execute("Dark"); - - // Assert: MainViewModel, ThemeService and PdfViewer are all synced - Assert.Equal(PdfReaderTheme.Dark, mainVm.PdfViewer.ReadingTheme); - Assert.Equal(PdfReaderTheme.Dark, themeService.ReadingTheme); - - // Simulating reopen - var reopenedService = new ThemeService(); - Assert.Equal(PdfReaderTheme.Dark, reopenedService.ReadingTheme); - } - - [Fact] - public void CommandPalette_ContainsThemeCommands() - { - // Arrange - var mainVm = new MainViewModel( - new PdfExportService(), - new TemplateService(), - new ProjectPersistenceService()); - - // Act - mainVm.FilterPaletteCommands("Theme"); - - // Assert - Assert.NotEmpty(mainVm.FilteredPaletteCommands); - Assert.Contains(mainVm.FilteredPaletteCommands, c => c.Title.Contains("Toggle Dark / Light Theme")); - Assert.Contains(mainVm.FilteredPaletteCommands, c => c.Title.Contains("Switch to Dark Theme")); - Assert.Contains(mainVm.FilteredPaletteCommands, c => c.Title.Contains("Switch to Light Theme")); - } - - [Fact] - public void AppAxaml_And_Styles_DefineChubbySliderResourcesAndRules() - { - // Find project root - var currentDir = AppContext.BaseDirectory; - string? projectRoot = null; - var dir = new DirectoryInfo(currentDir); - while (dir != null) - { - if (File.Exists(Path.Combine(dir.FullName, "FryPDF.sln")) || Directory.Exists(Path.Combine(dir.FullName, "src", "PdfEditorApp"))) - { - projectRoot = dir.FullName; - break; - } - dir = dir.Parent; - } - - Assert.NotNull(projectRoot); - - var appAxamlPath = Path.Combine(projectRoot, "src", "PdfEditorApp", "App.axaml"); - Assert.True(File.Exists(appAxamlPath), "App.axaml must exist"); - var appAxaml = File.ReadAllText(appAxamlPath); - - // Verify modern chubby slider resources are configured - Assert.Contains("SliderTrackThemeHeight", appAxaml); - Assert.Contains("8", appAxaml); - Assert.Contains("0", appAxaml); - Assert.Contains("0", appAxaml); - Assert.Contains("20", appAxaml); - - // Verify styles file has chubby slider selectors - var stylesPath = Path.Combine(projectRoot, "src", "PdfEditorApp", "Styles", "FluentOfficeStyles.axaml"); - Assert.True(File.Exists(stylesPath), "FluentOfficeStyles.axaml must exist"); - var stylesText = File.ReadAllText(stylesPath); - - Assert.Contains("Modern Chubby Tactile Slider Styling", stylesText); - Assert.Contains("RepeatButton#PART_DecreaseButton", stylesText); - Assert.Contains("RepeatButton#PART_IncreaseButton", stylesText); - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/ExcelToPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/ExcelToPdfToolTests.cs deleted file mode 100644 index 24600e3..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/ExcelToPdfToolTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class ExcelToPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public ExcelToPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void ExcelToPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (ExcelToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.ExcelToPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.ExcelToPdf, vm.Tool.Id); - Assert.Equal(PageOrientation.Landscape, vm.Orientation); - } - - [Fact] - public async Task ExcelToPdfTool_ConvertsDocumentSuccessfully() - { - string sampleXlsx = ToolTestFixture.CreateSampleXlsx("TestExcel"); - try - { - var vm = (ExcelToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.ExcelToPdf); - vm.SelectedFiles.Add(sampleXlsx); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sampleXlsx)) File.Delete(sampleXlsx); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/HtmlToPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/HtmlToPdfToolTests.cs deleted file mode 100644 index 131561e..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/HtmlToPdfToolTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class HtmlToPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public HtmlToPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void HtmlToPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (HtmlToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.HtmlToPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.HtmlToPdf, vm.Tool.Id); - Assert.Equal(PageFormat.A4, vm.Format); - Assert.Equal(PageOrientation.Portrait, vm.Orientation); - } - - [Fact] - public async Task HtmlToPdfTool_ConvertsHtmlContentSuccessfully() - { - var vm = (HtmlToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.HtmlToPdf); - vm.HtmlContentOrUrl = "

    Sample Document

    Test HTML body content.

    "; - - try - { - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete, $"Error: {vm.ErrorMessage}"); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (!string.IsNullOrEmpty(vm.LastOutputFilePath) && File.Exists(vm.LastOutputFilePath)) - { - try { File.Delete(vm.LastOutputFilePath); } catch { } - } - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/JpgToPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/JpgToPdfToolTests.cs deleted file mode 100644 index 1d0ff61..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/JpgToPdfToolTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class JpgToPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public JpgToPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void JpgToPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (JpgToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.JpgToPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.JpgToPdf, vm.Tool.Id); - Assert.Equal(PageFormat.A4, vm.PageFormat); - Assert.True(vm.AutoOrientation); - } - - [Fact] - public async Task JpgToPdfTool_CombinesImagesToPdfSuccessfully() - { - string imgPath = ToolTestFixture.CreateSampleImage("TestJpg"); - try - { - var vm = (JpgToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.JpgToPdf); - vm.SelectedFiles.Add(imgPath); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(imgPath)) File.Delete(imgPath); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToExcelToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToExcelToolTests.cs deleted file mode 100644 index 5fd228d..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToExcelToolTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class PdfToExcelToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PdfToExcelToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PdfToExcelTool_InstantiatesWithCorrectDefaults() - { - var vm = (PdfToExcelToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToExcel); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PdfToExcel, vm.Tool.Id); - Assert.True(vm.DetectAllTables); - } - - [Fact] - public async Task PdfToExcelTool_ConvertsPdfToExcelSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("ExcelSample", 2); - try - { - var vm = (PdfToExcelToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToExcel); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToJpgToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToJpgToolTests.cs deleted file mode 100644 index 7c50955..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToJpgToolTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class PdfToJpgToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PdfToJpgToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PdfToJpgTool_InstantiatesWithCorrectDefaults() - { - var vm = (PdfToJpgToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToJpg); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PdfToJpg, vm.Tool.Id); - Assert.Equal("jpg", vm.OutputFormat); - Assert.Equal(300, vm.Dpi); - } - - [Fact] - public async Task PdfToJpgTool_ExportsPdfToImagesSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("PdfToJpgSample", 2); - try - { - var vm = (PdfToJpgToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToJpg); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task PdfToJpgTool_ExportedImage_ContainsRealPageContent_NotBlankCanvas() - { - string sample = ToolTestFixture.CreateSamplePdf("PdfToJpgContentSample", 1); - string? imagesDir = null; - try - { - var vm = (PdfToJpgToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToJpg); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.OutputFormat = "png"; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - imagesDir = Path.GetDirectoryName(vm.LastOutputFilePath); - - using var bitmap = SkiaSharp.SKBitmap.Decode(vm.LastOutputFilePath); - Assert.NotNull(bitmap); - - // Regression guard: before the fix, every exported page was a blank white - // canvas (the draw call was missing entirely). The sample PDF has a solid - // SteelBlue header bar across its top, so some pixels must be non-white. - bool foundNonWhitePixel = false; - for (int y = 0; y < bitmap!.Height && !foundNonWhitePixel; y += 4) - { - for (int x = 0; x < bitmap.Width; x += 4) - { - var pixel = bitmap.GetPixel(x, y); - if (pixel.Red != 255 || pixel.Green != 255 || pixel.Blue != 255) - { - foundNonWhitePixel = true; - break; - } - } - } - Assert.True(foundNonWhitePixel, "Exported page image was entirely blank white — PDF content was not rendered."); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - if (imagesDir != null && Directory.Exists(imagesDir)) Directory.Delete(imagesDir, recursive: true); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToMarkdownToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToMarkdownToolTests.cs deleted file mode 100644 index a275590..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToMarkdownToolTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class PdfToMarkdownToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PdfToMarkdownToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PdfToMarkdownTool_InstantiatesWithCorrectDefaults() - { - var vm = (PdfToMarkdownToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToMarkdown); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PdfToMarkdown, vm.Tool.Id); - Assert.True(vm.IncludeTables); - Assert.True(vm.IncludeMetadataHeader); - } - - [Fact] - public async Task PdfToMarkdownTool_ConvertsPdfToMarkdownSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("MarkdownSample", 2); - try - { - var vm = (PdfToMarkdownToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToMarkdown); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToPdfAToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToPdfAToolTests.cs deleted file mode 100644 index e29e8d0..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToPdfAToolTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class PdfToPdfAToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PdfToPdfAToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PdfToPdfATool_InstantiatesWithCorrectDefaults() - { - var vm = (PdfToPdfAToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToPdfA); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PdfToPdfA, vm.Tool.Id); - Assert.Equal(PdfAStandard.PdfA2b, vm.Standard); - } - - [Fact] - public async Task PdfToPdfATool_ConvertsToPdfASuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("PdfASample", 1); - try - { - var vm = (PdfToPdfAToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToPdfA); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToPowerPointToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToPowerPointToolTests.cs deleted file mode 100644 index 1605d18..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToPowerPointToolTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class PdfToPowerPointToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PdfToPowerPointToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PdfToPowerPointTool_InstantiatesWithCorrectDefaults() - { - var vm = (PdfToPowerPointToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToPowerPoint); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PdfToPowerPoint, vm.Tool.Id); - Assert.True(vm.EditableText); - } - - [Fact] - public async Task PdfToPowerPointTool_ConvertsPdfToPptxSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("PptSample", 2); - try - { - var vm = (PdfToPowerPointToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToPowerPoint); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToWordToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToWordToolTests.cs deleted file mode 100644 index 53fb381..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/PdfToWordToolTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class PdfToWordToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PdfToWordToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PdfToWordTool_InstantiatesWithCorrectDefaults() - { - var vm = (PdfToWordToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToWord); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PdfToWord, vm.Tool.Id); - Assert.True(vm.ExtractTables); - } - - [Fact] - public async Task PdfToWordTool_ConvertsPdfToWordSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("WordSample", 2); - try - { - var vm = (PdfToWordToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToWord); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task PdfToWordTool_MultiFileBatch_ProcessesEveryFile_NotJustTheFirst() - { - string sample1 = ToolTestFixture.CreateSamplePdf("WordBatch1", 1); - string sample2 = ToolTestFixture.CreateSamplePdf("WordBatch2", 1); - try - { - var vm = (PdfToWordToolViewModel)_fixture.Factory.Create(PdfToolId.PdfToWord); - vm.SelectedFiles.Add(sample1); - vm.SelectedFiles.Add(sample2); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - // Regression guard: before the fix, only the first selected file was ever - // converted and the second was silently dropped with no error. - Assert.Contains("2 of 2", vm.ResultSummaryMessage); - } - finally - { - if (File.Exists(sample1)) File.Delete(sample1); - if (File.Exists(sample2)) File.Delete(sample2); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/PowerPointToPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/PowerPointToPdfToolTests.cs deleted file mode 100644 index 43b9794..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/PowerPointToPdfToolTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class PowerPointToPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PowerPointToPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PowerPointToPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (PowerPointToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.PowerPointToPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PowerPointToPdf, vm.Tool.Id); - Assert.Equal(PageOrientation.Landscape, vm.Orientation); - } - - [Fact] - public async Task PowerPointToPdfTool_ConvertsDocumentSuccessfully() - { - string samplePptx = ToolTestFixture.CreateSamplePptx("TestPpt"); - try - { - var vm = (PowerPointToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.PowerPointToPdf); - vm.SelectedFiles.Add(samplePptx); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(samplePptx)) File.Delete(samplePptx); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/ScanToPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/ScanToPdfToolTests.cs deleted file mode 100644 index 0356843..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/ScanToPdfToolTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class ScanToPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public ScanToPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void ScanToPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (ScanToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.ScanToPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.ScanToPdf, vm.Tool.Id); - Assert.True(vm.AutoDeskew); - Assert.True(vm.EnhanceContrast); - } - - [Fact] - public async Task ScanToPdfTool_ProcessesScanFilesSuccessfully() - { - string sample = ToolTestFixture.CreateSampleImage("ScanSample"); - try - { - var vm = (ScanToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.ScanToPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Conversion/WordToPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Conversion/WordToPdfToolTests.cs deleted file mode 100644 index ad2698e..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Conversion/WordToPdfToolTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Conversion; - -public class WordToPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public WordToPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void WordToPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (WordToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.WordToPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.WordToPdf, vm.Tool.Id); - Assert.Equal(PageOrientation.Portrait, vm.Orientation); - } - - [Fact] - public async Task WordToPdfTool_ConvertsDocumentSuccessfully() - { - string sampleDoc = ToolTestFixture.CreateSampleDocx("TestWord"); - try - { - var vm = (WordToPdfToolViewModel)_fixture.Factory.Create(PdfToolId.WordToPdf); - vm.SelectedFiles.Add(sampleDoc); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sampleDoc)) File.Delete(sampleDoc); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Core/ToolTestFixture.cs b/tests/PdfEditorApp.Tests/Tools/Core/ToolTestFixture.cs deleted file mode 100644 index 96b84bc..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Core/ToolTestFixture.cs +++ /dev/null @@ -1,208 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using System; -using System.IO; -using System.Text; -using DocumentFormat.OpenXml; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Spreadsheet; -using DocumentFormat.OpenXml.Wordprocessing; -using DocumentFormat.OpenXml.Presentation; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Tools; -using PdfSharpCore.Pdf; -using PdfSharpCore.Drawing; -using SkiaSharp; -using UglyToad.PdfPig.Writer; - -namespace PdfEditorApp.Tests.Tools.Core; - -public class ToolTestFixture -{ - public IPdfToolRegistry ToolRegistry { get; } - public IPdfDocumentOperationsService OperationsService { get; } - public IPdfToolViewModelFactory Factory { get; } - - public ToolTestFixture() - { - ToolRegistry = new PdfToolRegistry(); - var pageService = new PdfPageService(); - var optService = new PdfOptimizationService(); - var secService = new PdfSecurityService(); - var convService = new PdfConversionService(); - var ocrService = new PdfOcrService(); - var formService = new PdfFormService(); - var aiService = new AiDocumentService(); - var transService = new DocumentTranslationService(); - var workflowEngine = new PdfWorkflowEngine(pageService, optService, secService, convService, ocrService); - - OperationsService = new PdfDocumentOperationsService( - ToolRegistry, pageService, optService, secService, convService, ocrService, formService, aiService, transService, workflowEngine); - - Factory = new PdfToolViewModelFactory(OperationsService, ToolRegistry); - } - - public static string CreateIsolatedDirectory(string prefix = "PdfTest") - { - string dir = Path.Combine(Path.GetTempPath(), $"{prefix}_{Guid.NewGuid():N}"); - Directory.CreateDirectory(dir); - return dir; - } - - /// - /// Generates a realistic, self-contained multi-page PDF with headers, formatted paragraphs, and shapes. - /// - public static string CreateSamplePdf(string name = "Sample", int pageCount = 2, string? directory = null) - { - string dir = directory ?? Path.GetTempPath(); - string path = Path.Combine(dir, $"{name}_{Guid.NewGuid():N}.pdf"); - using var doc = new PdfDocument(); - for (int i = 0; i < pageCount; i++) - { - var page = doc.AddPage(); - page.Width = XUnit.FromPoint(595); - page.Height = XUnit.FromPoint(842); - - using var gfx = XGraphics.FromPdfPage(page); - var titleFont = new XFont("Helvetica", 18, XFontStyle.Bold); - var bodyFont = new XFont("Helvetica", 11, XFontStyle.Regular); - var headerFont = new XFont("Helvetica", 9, XFontStyle.Italic); - - // Header bar - gfx.DrawRectangle(XBrushes.SteelBlue, 0, 0, page.Width.Point, 30); - gfx.DrawString($"FryPDF Generated Test Suite • Document: {name}", headerFont, XBrushes.White, new XPoint(20, 20)); - - // Document Title - gfx.DrawString($"Test Document: {name} (Page {i + 1} of {pageCount})", titleFont, XBrushes.DarkSlateGray, new XPoint(40, 70)); - - // Paragraph content - string p1 = "This is a real, self-contained PDF document generated dynamically for unit testing."; - string p2 = "It contains multi-page content, paragraphs, metadata, and geometry to test PDF manipulations."; - string p3 = "CONFIDENTIAL: Internal operations verification test suite for FryPDF."; - - gfx.DrawString(p1, bodyFont, XBrushes.Black, new XPoint(40, 110)); - gfx.DrawString(p2, bodyFont, XBrushes.Black, new XPoint(40, 130)); - gfx.DrawString(p3, bodyFont, XBrushes.DarkRed, new XPoint(40, 150)); - - // Simple table - double tableY = 180; - gfx.DrawRectangle(XPens.DarkGray, XBrushes.LightGray, 40, tableY, 500, 20); - gfx.DrawString("Column A", bodyFont, XBrushes.Black, new XPoint(50, tableY + 14)); - gfx.DrawString("Column B", bodyFont, XBrushes.Black, new XPoint(200, tableY + 14)); - gfx.DrawString("Column C", bodyFont, XBrushes.Black, new XPoint(350, tableY + 14)); - - for (int r = 1; r <= 3; r++) - { - double rowY = tableY + (r * 22); - gfx.DrawRectangle(XPens.LightGray, 40, rowY, 500, 22); - gfx.DrawString($"Data {r}-A", bodyFont, XBrushes.Black, new XPoint(50, rowY + 15)); - gfx.DrawString($"Data {r}-B", bodyFont, XBrushes.Black, new XPoint(200, rowY + 15)); - gfx.DrawString($"Data {r}-C", bodyFont, XBrushes.Black, new XPoint(350, rowY + 15)); - } - - // Footer - gfx.DrawLine(XPens.LightGray, 40, page.Height.Point - 40, page.Width.Point - 40, page.Height.Point - 40); - gfx.DrawString($"Page {i + 1} of {pageCount}", headerFont, XBrushes.Gray, new XPoint(page.Width.Point / 2 - 20, page.Height.Point - 25)); - } - - doc.Save(path); - return path; - } - - /// - /// Generates a PDF with web-appended whitespace or trailing comments to test resilience. - /// - public static string CreatePaddedWebPdf(string name = "WebPaddedSample", int pageCount = 3, string? directory = null) - { - string basePdf = CreateSamplePdf(name, pageCount, directory); - byte[] bytes = File.ReadAllBytes(basePdf); - using var ms = new MemoryStream(); - ms.Write(bytes, 0, bytes.Length); - byte[] extra = Encoding.ASCII.GetBytes("\r\n% Trailing web analytics comment\r\n\r\n \r\n"); - ms.Write(extra, 0, extra.Length); - - string dir = directory ?? Path.GetTempPath(); - string paddedPath = Path.Combine(dir, $"{name}_padded_{Guid.NewGuid():N}.pdf"); - File.WriteAllBytes(paddedPath, ms.ToArray()); - - if (File.Exists(basePdf)) File.Delete(basePdf); - return paddedPath; - } - - public static string CreateSampleDocx(string name = "SampleDocx") - { - string path = Path.Combine(Path.GetTempPath(), $"{name}_{Guid.NewGuid():N}.docx"); - using var wordDoc = WordprocessingDocument.Create(path, WordprocessingDocumentType.Document); - var mainPart = wordDoc.AddMainDocumentPart(); - mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document(); - var body = mainPart.Document.AppendChild(new Body()); - var para = body.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Paragraph()); - var run = para.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Run()); - run.AppendChild(new DocumentFormat.OpenXml.Wordprocessing.Text("Sample Word Document Paragraph for Testing")); - mainPart.Document.Save(); - return path; - } - - public static string CreateSampleXlsx(string name = "SampleXlsx") - { - string path = Path.Combine(Path.GetTempPath(), $"{name}_{Guid.NewGuid():N}.xlsx"); - using var spreadsheetDoc = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook); - var wbPart = spreadsheetDoc.AddWorkbookPart(); - wbPart.Workbook = new Workbook(); - var wsPart = wbPart.AddNewPart(); - var sheetData = new SheetData(); - var row = new Row(); - var cell = new Cell { DataType = CellValues.String, CellValue = new CellValue("Header 1") }; - row.Append(cell); - sheetData.Append(row); - wsPart.Worksheet = new Worksheet(sheetData); - wsPart.Worksheet.Save(); - var sheets = wbPart.Workbook.AppendChild(new Sheets()); - sheets.Append(new Sheet { Id = wbPart.GetIdOfPart(wsPart), SheetId = 1, Name = "Sheet1" }); - wbPart.Workbook.Save(); - spreadsheetDoc.Save(); - return path; - } - - public static string CreateSamplePptx(string name = "SamplePptx") - { - string path = Path.Combine(Path.GetTempPath(), $"{name}_{Guid.NewGuid():N}.pptx"); - using var presentationDoc = PresentationDocument.Create(path, PresentationDocumentType.Presentation); - var pPart = presentationDoc.AddPresentationPart(); - pPart.Presentation = new Presentation(); - var slideIdList = pPart.Presentation.AppendChild(new SlideIdList()); - var slidePart = pPart.AddNewPart(); - var slide = new Slide(new CommonSlideData(new ShapeTree())); - slidePart.Slide = slide; - slidePart.Slide.Save(); - slideIdList.Append(new SlideId { Id = 256, RelationshipId = pPart.GetIdOfPart(slidePart) }); - pPart.Presentation.Save(); - return path; - } - - public static string CreateSampleImage(string name = "SampleImage") - { - string path = Path.Combine(Path.GetTempPath(), $"{name}_{Guid.NewGuid():N}.png"); - using var bitmap = new SKBitmap(300, 300); - using (var canvas = new SKCanvas(bitmap)) - { - canvas.Clear(SKColors.AliceBlue); - using var paint = new SKPaint { Color = SKColors.Navy, IsAntialias = true }; - canvas.DrawRect(30, 30, 240, 240, paint); - } - using var image = SKImage.FromBitmap(bitmap); - using var data = image.Encode(SKEncodedImageFormat.Png, 90); - using var stream = File.OpenWrite(path); - data.SaveTo(stream); - return path; - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Intelligence/AiSummarizerToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Intelligence/AiSummarizerToolTests.cs deleted file mode 100644 index 26e9c75..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Intelligence/AiSummarizerToolTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Intelligence; - -public class AiSummarizerToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public AiSummarizerToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void AiSummarizerTool_InstantiatesWithCorrectDefaults() - { - var vm = (AiSummarizerToolViewModel)_fixture.Factory.Create(PdfToolId.AiSummarizer); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.AiSummarizer, vm.Tool.Id); - Assert.True(vm.IncludeActionItems); - Assert.Equal(7, vm.MaxBulletPoints); - } - - [Fact] - public async Task AiSummarizerTool_SummarizesDocumentSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("AiSample", 2); - try - { - var vm = (AiSummarizerToolViewModel)_fixture.Factory.Create(PdfToolId.AiSummarizer); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Intelligence/ComparePdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Intelligence/ComparePdfToolTests.cs deleted file mode 100644 index 2884551..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Intelligence/ComparePdfToolTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Intelligence; - -public class ComparePdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public ComparePdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void ComparePdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (ComparePdfToolViewModel)_fixture.Factory.Create(PdfToolId.ComparePdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.ComparePdf, vm.Tool.Id); - Assert.True(vm.DetectTextDiff); - Assert.True(vm.DetectVisualDiff); - } - - [Fact] - public async Task ComparePdfTool_ExecutesComparisonSuccessfully() - { - string docA = ToolTestFixture.CreateSamplePdf("CompareA", 2); - string docB = ToolTestFixture.CreateSamplePdf("CompareB", 2); - try - { - var vm = (ComparePdfToolViewModel)_fixture.Factory.Create(PdfToolId.ComparePdf); - vm.DocumentAPath = docA; - vm.DocumentBPath = docB; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.NotEmpty(vm.ResultSummaryMessage); - } - finally - { - if (File.Exists(docA)) File.Delete(docA); - if (File.Exists(docB)) File.Delete(docB); - } - } - - [Fact] - public async Task ComparePdfTool_DetectsRealPageCountDifference_AndWritesReportFile() - { - // Regression guard: before the fix, the dispatcher was a stub that only compared - // file byte-sizes and never wrote an output file — a 2-page and a 5-page document - // would "succeed" identically with no indication anything differed. - string docA = ToolTestFixture.CreateSamplePdf("CompareRealA", 2); - string docB = ToolTestFixture.CreateSamplePdf("CompareRealB", 5); - string? reportPath = null; - try - { - var vm = (ComparePdfToolViewModel)_fixture.Factory.Create(PdfToolId.ComparePdf); - vm.DocumentAPath = docA; - vm.DocumentBPath = docB; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - reportPath = vm.LastOutputFilePath; - Assert.True(File.Exists(reportPath), "ComparePdf must produce a real report file, not a silent no-op."); - - string reportContent = await File.ReadAllTextAsync(reportPath); - Assert.Contains("2 pages", reportContent); - Assert.Contains("5 pages", reportContent); - } - finally - { - if (File.Exists(docA)) File.Delete(docA); - if (File.Exists(docB)) File.Delete(docB); - if (reportPath != null && File.Exists(reportPath)) File.Delete(reportPath); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Intelligence/EditPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Intelligence/EditPdfToolTests.cs deleted file mode 100644 index ba2f0ce..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Intelligence/EditPdfToolTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Tests.Tools.Core; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Intelligence; - -public class EditPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public EditPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void EditPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (EditPdfToolViewModel)_fixture.Factory.Create(PdfToolId.EditPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.EditPdf, vm.Tool.Id); - } - - [Fact] - public async Task EditPdfTool_OpensEditorSessionSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("EditSample", 1); - try - { - var vm = (EditPdfToolViewModel)_fixture.Factory.Create(PdfToolId.EditPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - bool openInEditorTriggered = false; - string targetPath = ""; - WeakReferenceMessenger.Default.Register(this, (r, m) => - { - openInEditorTriggered = true; - targetPath = m.FilePath; - }); - - try - { - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(openInEditorTriggered); - Assert.Equal(sample, targetPath); - } - finally - { - WeakReferenceMessenger.Default.Unregister(this); - } - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Intelligence/OcrPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Intelligence/OcrPdfToolTests.cs deleted file mode 100644 index 51a3fad..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Intelligence/OcrPdfToolTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Services; -using PdfEditorApp.Tests.Tools.Core; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using PdfSharpCore.Drawing; -using PdfSharpCore.Pdf; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Intelligence; - -[Collection("OcrTests")] -public class OcrPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public OcrPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - /// Creates a PDF page containing only an image and no drawn text, simulating a scan. - private static string CreateImageOnlyPdf(string name) - { - string path = Path.Combine(Path.GetTempPath(), $"{name}_{System.Guid.NewGuid():N}.pdf"); - string image = ToolTestFixture.CreateSampleImage(name + "_img"); - try - { - using var doc = new PdfDocument(); - var page = doc.AddPage(); - using var gfx = XGraphics.FromPdfPage(page); - using var xImage = XImage.FromFile(image); - gfx.DrawImage(xImage, 0, 0, page.Width.Point, page.Height.Point); - doc.Save(path); - } - finally - { - if (File.Exists(image)) File.Delete(image); - } - return path; - } - - [Fact] - public void OcrPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (OcrPdfToolViewModel)_fixture.Factory.Create(PdfToolId.OcrPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.OcrPdf, vm.Tool.Id); - Assert.Equal("eng", vm.Language); - Assert.True(vm.GenerateSearchablePdf); - } - - [Fact] - public async Task OcrPdfTool_ExecutesOcrSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("OcrSample", 1); - try - { - var vm = (OcrPdfToolViewModel)_fixture.Factory.Create(PdfToolId.OcrPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task OcrPdfTool_ScannedImageOnlyPdf_ReportsHonestFailure_NotFalseSuccess() - { - // Regression guard: before the fix, a page with zero recognized words still - // produced a "Completed... Output is fully searchable" success message — this - // tool has no real image-to-text engine, so that claim was always false for - // genuinely scanned input. It must now say so clearly instead of claiming success. - string sample = CreateImageOnlyPdf("OcrScanned"); - try - { - var customOcr = new PdfEditorApp.Services.Ocr.CompositeOcrProvider - { - PreferredEngine = PdfEditorApp.Core.Models.OcrEngineType.Tesseract - }; - var ops = (PdfDocumentOperationsService)_fixture.OperationsService; - var customOps = new PdfDocumentOperationsService( - ops.ToolRegistry, - ops.PageService, - ops.OptimizationService, - ops.SecurityService, - ops.ConversionService, - new PdfOcrService(customOcr), - ops.FormService, - ops.AiService, - ops.TranslationService, - ops.WorkflowEngine); - var toolDef = _fixture.ToolRegistry.GetTool(PdfToolId.OcrPdf)!; - var vm = new OcrPdfToolViewModel(customOps, toolDef, customOcr) - { - SelectedEngine = PdfEditorApp.Core.Models.OcrEngineType.Tesseract - }; - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.HasError); - Assert.False(vm.IsComplete); - Assert.Contains("does not yet perform true image-to-text OCR", vm.ErrorMessage); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Intelligence/PdfFormsToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Intelligence/PdfFormsToolTests.cs deleted file mode 100644 index d3a76b7..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Intelligence/PdfFormsToolTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Intelligence; - -public class PdfFormsToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PdfFormsToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PdfFormsTool_InstantiatesWithCorrectDefaults() - { - var vm = (PdfFormsToolViewModel)_fixture.Factory.Create(PdfToolId.PdfForms); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PdfForms, vm.Tool.Id); - Assert.False(vm.FlattenFields); - } - - [Fact] - public async Task PdfFormsTool_FlattensFormsSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("FormSample", 1); - try - { - var vm = (PdfFormsToolViewModel)_fixture.Factory.Create(PdfToolId.PdfForms); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.FlattenFields = true; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Intelligence/TranslatePdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Intelligence/TranslatePdfToolTests.cs deleted file mode 100644 index 0b3fb48..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Intelligence/TranslatePdfToolTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Intelligence; - -public class TranslatePdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public TranslatePdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void TranslatePdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (TranslatePdfToolViewModel)_fixture.Factory.Create(PdfToolId.TranslatePdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.TranslatePdf, vm.Tool.Id); - Assert.Equal("Auto", vm.SourceLanguage); - Assert.Equal("Spanish", vm.TargetLanguage); - Assert.True(vm.PreserveLayout); - } - - [Fact] - public async Task TranslatePdfTool_TranslatesDocumentSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("TranslateSample", 1); - try - { - var vm = (TranslatePdfToolViewModel)_fixture.Factory.Create(PdfToolId.TranslatePdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task TranslatePdfTool_UnsupportedLanguage_ReportsHonestFailure_NotSilentNoOp() - { - // Regression guard: only Spanish/French/German have a glossary. Before the fix, - // any other language (e.g. Japanese, suggested by the tool's own UI placeholder) - // silently passed every line through unchanged while still reporting success. - string sample = ToolTestFixture.CreateSamplePdf("TranslateUnsupported", 1); - try - { - var vm = (TranslatePdfToolViewModel)_fixture.Factory.Create(PdfToolId.TranslatePdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.TargetLanguage = "Japanese"; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.HasError); - Assert.False(vm.IsComplete); - Assert.Contains("not supported yet", vm.ErrorMessage); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Organize/CropPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Organize/CropPdfToolTests.cs deleted file mode 100644 index db1a83e..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Organize/CropPdfToolTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Organize; - -public class CropPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public CropPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void CropPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (CropPdfToolViewModel)_fixture.Factory.Create(PdfToolId.CropPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.CropPdf, vm.Tool.Id); - Assert.Equal(36, vm.CropMarginTop); - Assert.Equal(36, vm.CropMarginBottom); - } - - [Fact] - public async Task CropPdfTool_CropsPdfPagesSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("CropSample", 2); - try - { - var vm = (CropPdfToolViewModel)_fixture.Factory.Create(PdfToolId.CropPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.CropMarginTop = 15; - vm.CropMarginBottom = 15; - vm.CropMarginLeft = 10; - vm.CropMarginRight = 10; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Organize/MergePdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Organize/MergePdfToolTests.cs deleted file mode 100644 index 036fd8e..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Organize/MergePdfToolTests.cs +++ /dev/null @@ -1,212 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Organize; - -public class MergePdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public MergePdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void MergePdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (MergePdfToolViewModel)_fixture.Factory.Create(PdfToolId.MergePdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.MergePdf, vm.Tool.Id); - Assert.True(vm.PreserveBookmarks); - Assert.False(vm.NormalizePageSizes); - Assert.Empty(vm.SelectedFiles); - } - - [Fact] - public void MergePdfTool_ReordersAndSyncsPreviewItems() - { - var vm = (MergePdfToolViewModel)_fixture.Factory.Create(PdfToolId.MergePdf); - vm.SelectedFiles.Add("fileA.pdf"); - vm.SelectedFiles.Add("fileB.pdf"); - vm.SelectedFiles.Add("fileC.pdf"); - vm.SyncPreviewItems(); - - Assert.Equal(3, vm.SelectedFilePreviewItems.Count); - Assert.Equal("fileA.pdf", vm.SelectedFilePreviewItems[0].FileName); - Assert.Equal("#1", vm.SelectedFilePreviewItems[0].OrderIndexText); - - vm.MoveFileDownCommand.Execute("fileA.pdf"); - Assert.Equal("fileB.pdf", vm.SelectedFilePreviewItems[0].FileName); - Assert.Equal("fileA.pdf", vm.SelectedFilePreviewItems[1].FileName); - - vm.MoveFileUpCommand.Execute("fileC.pdf"); - Assert.Equal("fileC.pdf", vm.SelectedFilePreviewItems[1].FileName); - Assert.Equal("fileA.pdf", vm.SelectedFilePreviewItems[2].FileName); - - vm.RemoveFileCommand.Execute("fileC.pdf"); - Assert.Equal(2, vm.SelectedFilePreviewItems.Count); - - vm.ClearFilesCommand.Execute(null); - Assert.Empty(vm.SelectedFilePreviewItems); - Assert.Empty(vm.SelectedFiles); - } - - [Fact] - public async Task MergePdfTool_ExecutesMergeSuccessfully() - { - string tempDir = ToolTestFixture.CreateIsolatedDirectory("MergeExec"); - try - { - string file1 = ToolTestFixture.CreateSamplePdf("Doc1", 2, tempDir); - string file2 = ToolTestFixture.CreateSamplePdf("Doc2", 3, tempDir); - - var vm = (MergePdfToolViewModel)_fixture.Factory.Create(PdfToolId.MergePdf); - vm.SelectedFiles.Add(file1); - vm.SelectedFiles.Add(file2); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - - // Verify in-app output preview generation - Assert.NotNull(vm.OutputFilePreview); - Assert.Equal(5, vm.OutputFilePreview.PageCount); - Assert.Equal(5, vm.OutputPageThumbnails.Count); - Assert.Equal("Page 1 of 5", vm.OutputPageThumbnails[0].PageLabel); - Assert.Equal(1, vm.OutputPageThumbnails[0].PageNumber); - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public async Task MergePdfTool_MergesPaddedAndWebPdfsSuccessfully() - { - string tempDir = ToolTestFixture.CreateIsolatedDirectory("MergePadded"); - try - { - string file1 = ToolTestFixture.CreatePaddedWebPdf("WebDoc1", 2, tempDir); - string file2 = ToolTestFixture.CreatePaddedWebPdf("WebDoc2", 3, tempDir); - - var vm = (MergePdfToolViewModel)_fixture.Factory.Create(PdfToolId.MergePdf); - vm.SelectedFiles.Add(file1); - vm.SelectedFiles.Add(file2); - vm.SyncPreviewItems(); - - Assert.Equal(2, vm.SelectedFilePreviewItems.Count); - Assert.Equal(5, vm.TotalSelectedPages); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - Assert.Equal(5, vm.OutputPageThumbnails.Count); - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public void MergePdfTool_StartOver_ResetsStateAndPreviews() - { - var vm = (MergePdfToolViewModel)_fixture.Factory.Create(PdfToolId.MergePdf); - vm.SelectedFiles.Add("sample.pdf"); - vm.SyncPreviewItems(); - Assert.True(vm.HasSelectedFiles); - - vm.StartOverCommand.Execute(null); - Assert.False(vm.HasSelectedFiles); - Assert.Empty(vm.SelectedFilePreviewItems); - Assert.Empty(vm.OutputPageThumbnails); - Assert.Null(vm.OutputFilePreview); - Assert.False(vm.IsComplete); - } - - [Fact] - public async Task MergePdfTool_SaveOutputFileAs_DoesNotThrowWhenSavingToSamePath() - { - string tempDir = ToolTestFixture.CreateIsolatedDirectory("MergeSamePath"); - try - { - string file1 = ToolTestFixture.CreateSamplePdf("Doc1", 1, tempDir); - string file2 = ToolTestFixture.CreateSamplePdf("Doc2", 1, tempDir); - - var vm = (MergePdfToolViewModel)_fixture.Factory.Create(PdfToolId.MergePdf); - vm.SelectedFiles.Add(file1); - vm.SelectedFiles.Add(file2); - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.True(File.Exists(vm.LastOutputFilePath)); - - // Saving to the exact same path must NOT throw IOException (self-copy) - vm.SaveOutputFileToPath(vm.LastOutputFilePath); - - Assert.True(vm.HasSavedNotification); - Assert.False(vm.HasError); - Assert.Contains("Saved successfully", vm.SavedNotificationMessage); - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public async Task MergePdfTool_SaveOutputFileAs_HandlesLockedTargetGracefully() - { - string tempDir = ToolTestFixture.CreateIsolatedDirectory("MergeLocked"); - string file1 = ToolTestFixture.CreateSamplePdf("Doc1", 1, tempDir); - string file2 = ToolTestFixture.CreateSamplePdf("Doc2", 1, tempDir); - string lockedTarget = Path.Combine(tempDir, $"locked_target_{Guid.NewGuid():N}.pdf"); - File.WriteAllText(lockedTarget, "locked content"); - - try - { - using var lockStream = new FileStream(lockedTarget, FileMode.Open, FileAccess.ReadWrite, FileShare.None); - - var vm = (MergePdfToolViewModel)_fixture.Factory.Create(PdfToolId.MergePdf); - vm.SelectedFiles.Add(file1); - vm.SelectedFiles.Add(file2); - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - - // Saving to an externally locked file must NOT crash with an unhandled exception - vm.SaveOutputFileToPath(lockedTarget); - - Assert.False(vm.HasSavedNotification); - Assert.True(vm.HasError); - Assert.Contains("currently open or in use", vm.ErrorMessage); - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Organize/OrganizePdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Organize/OrganizePdfToolTests.cs deleted file mode 100644 index a72fcc5..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Organize/OrganizePdfToolTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Organize; - -public class OrganizePdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public OrganizePdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void OrganizePdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (OrganizePdfToolViewModel)_fixture.Factory.Create(PdfToolId.OrganizePdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.OrganizePdf, vm.Tool.Id); - } - - [Fact] - public async Task OrganizePdfTool_ReordersPagesSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("OrganizeSample", 3); - try - { - var vm = (OrganizePdfToolViewModel)_fixture.Factory.Create(PdfToolId.OrganizePdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - // This standalone view has no reorder UI yet — set a real order directly to - // exercise the underlying reorder logic (see the honesty-fix test below for - // what happens without one). - vm.PageOrder = new System.Collections.Generic.List { 2, 1, 0 }; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task OrganizePdfTool_NoOrderOrDeletionSet_ReportsHonestFailure_NotFakeSuccess() - { - // Regression guard: this standalone view has no reorder/delete/rotate controls, so - // PageOrder/PagesToDelete are always empty here. Before the fix, running the tool - // still claimed "Organized PDF to N pages" success while silently saving an - // unchanged copy of the input. - string sample = ToolTestFixture.CreateSamplePdf("OrganizeNoOpSample", 3); - try - { - var vm = (OrganizePdfToolViewModel)_fixture.Factory.Create(PdfToolId.OrganizePdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.HasError); - Assert.False(vm.IsComplete); - Assert.Contains("Pages Sidebar", vm.ErrorMessage); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Organize/PageNumbersToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Organize/PageNumbersToolTests.cs deleted file mode 100644 index e1284da..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Organize/PageNumbersToolTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Organize; - -public class PageNumbersToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public PageNumbersToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void PageNumbersTool_InstantiatesWithCorrectDefaults() - { - var vm = (PageNumbersToolViewModel)_fixture.Factory.Create(PdfToolId.PageNumbers); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.PageNumbers, vm.Tool.Id); - Assert.Equal(PageNumberPosition.BottomCenter, vm.Position); - Assert.Equal("Page {n} of {total}", vm.Template); - } - - [Fact] - public async Task PageNumbersTool_StampsPageNumbersSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("PageNumSample", 3); - try - { - var vm = (PageNumbersToolViewModel)_fixture.Factory.Create(PdfToolId.PageNumbers); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Organize/RotatePdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Organize/RotatePdfToolTests.cs deleted file mode 100644 index c7f0b58..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Organize/RotatePdfToolTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Organize; - -public class RotatePdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public RotatePdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void RotatePdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (RotatePdfToolViewModel)_fixture.Factory.Create(PdfToolId.RotatePdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.RotatePdf, vm.Tool.Id); - Assert.Equal(90, vm.RotationDegrees); - Assert.Equal(PageFilterTarget.All, vm.TargetFilter); - } - - [Fact] - public async Task RotatePdfTool_ExecutesRotationSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("RotateSample", 2); - try - { - var vm = (RotatePdfToolViewModel)_fixture.Factory.Create(PdfToolId.RotatePdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.RotationDegrees = 180; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RotatePdfTool_MultiFileBatch_ProcessesEveryFile_NotJustTheFirst() - { - string sample1 = ToolTestFixture.CreateSamplePdf("RotateBatch1", 1); - string sample2 = ToolTestFixture.CreateSamplePdf("RotateBatch2", 1); - try - { - var vm = (RotatePdfToolViewModel)_fixture.Factory.Create(PdfToolId.RotatePdf); - vm.SelectedFiles.Add(sample1); - vm.SelectedFiles.Add(sample2); - vm.SyncPreviewItems(); - vm.RotationDegrees = 90; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - // Regression guard: before the fix, only the first selected file was ever - // processed and the batch outcome was indistinguishable from a single-file run. - Assert.Contains("2 of 2", vm.ResultSummaryMessage); - } - finally - { - if (File.Exists(sample1)) File.Delete(sample1); - if (File.Exists(sample2)) File.Delete(sample2); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Organize/SplitPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Organize/SplitPdfToolTests.cs deleted file mode 100644 index 883ba13..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Organize/SplitPdfToolTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Organize; - -public class SplitPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public SplitPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void SplitPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (SplitPdfToolViewModel)_fixture.Factory.Create(PdfToolId.SplitPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.SplitPdf, vm.Tool.Id); - Assert.Equal(SplitExtractMode.SplitEveryNPages, vm.SplitMode); - Assert.Equal(1, vm.SplitPagesInterval); - } - - [Fact] - public async Task SplitPdfTool_SplitsEveryPageSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("SplitSample", 4); - try - { - var vm = (SplitPdfToolViewModel)_fixture.Factory.Create(PdfToolId.SplitPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SplitPagesInterval = 2; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task SplitPdfTool_SplitByPageRangesMode_HonorsRangeExpression() - { - // Regression guard: SplitMode had no UI control to set it, so the tool always - // fell back to "every N pages" regardless of the (also-visible) range textbox. - string sample = ToolTestFixture.CreateSamplePdf("SplitRangeSample", 10); - try - { - var vm = (SplitPdfToolViewModel)_fixture.Factory.Create(PdfToolId.SplitPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SplitMode = SplitExtractMode.SplitByPageRanges; - vm.SplitRangeExpression = "1-3, 5, 7-10"; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - // Three distinct range groups (1-3 / 5 / 7-10) => 3 output files, not 10. - Assert.Contains("into 3 files", vm.ResultSummaryMessage); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Security/CompressPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Security/CompressPdfToolTests.cs deleted file mode 100644 index 0ff57ca..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Security/CompressPdfToolTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Security; - -public class CompressPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public CompressPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void CompressPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (CompressPdfToolViewModel)_fixture.Factory.Create(PdfToolId.CompressPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.CompressPdf, vm.Tool.Id); - Assert.Equal(PdfCompressionLevel.Balanced, vm.CompressionLevel); - Assert.Equal(150, vm.ImageQualityDpi); - } - - [Fact] - public async Task CompressPdfTool_ExecutesCompressionSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("CompressSample", 3); - try - { - var vm = (CompressPdfToolViewModel)_fixture.Factory.Create(PdfToolId.CompressPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.CompressionLevel = PdfCompressionLevel.MaximumCompression; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public void CompressPdfTool_CustomMode_ClampsValuesAndProvidesHelpfulFeedback() - { - var vm = (CompressPdfToolViewModel)_fixture.Factory.Create(PdfToolId.CompressPdf); - - // Test Preset Selection - vm.SelectPreset("extreme"); - Assert.True(vm.IsExtremeCompression); - Assert.Equal(96, vm.ImageQualityDpi); - Assert.Equal(52, vm.JpegQuality); - - vm.SelectPreset("recommended"); - Assert.True(vm.IsRecommendedCompression); - Assert.Equal(125, vm.ImageQualityDpi); - Assert.Equal(66, vm.JpegQuality); - - // Test Custom Mode & Dynamic Warning on low DPI - vm.SelectPreset("custom"); - Assert.True(vm.IsCustomMode); - - vm.ImageQualityDpi = 40; // Should be clamped to 50 - Assert.Equal(50, vm.ImageQualityDpi); - Assert.Equal("Warning", vm.QualityFeedbackSeverity); - Assert.Contains("Low DPI", vm.QualityFeedbackMessage); - - // Test Low JPEG Quality Warning - vm.ImageQualityDpi = 150; - vm.JpegQuality = 30; - Assert.Equal("Warning", vm.QualityFeedbackSeverity); - Assert.Contains("Low JPEG Quality", vm.QualityFeedbackMessage); - - // Test Grayscale feedback - vm.JpegQuality = 75; - vm.ConvertToGrayscale = true; - Assert.Equal("Success", vm.QualityFeedbackSeverity); - Assert.Contains("Grayscale", vm.QualityFeedbackMessage); - } - - [Fact] - public async Task CompressPdfTool_ExecutesCustomCompressionWithGrayscaleSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("CompressCustomSample", 2); - try - { - var vm = (CompressPdfToolViewModel)_fixture.Factory.Create(PdfToolId.CompressPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SelectPreset("custom"); - vm.ImageQualityDpi = 100; - vm.JpegQuality = 55; - vm.ConvertToGrayscale = true; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Security/ProtectPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Security/ProtectPdfToolTests.cs deleted file mode 100644 index 8d6d42e..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Security/ProtectPdfToolTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using PdfSharpCore.Pdf.IO; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Security; - -public class ProtectPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public ProtectPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void ProtectPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (ProtectPdfToolViewModel)_fixture.Factory.Create(PdfToolId.ProtectPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.ProtectPdf, vm.Tool.Id); - Assert.True(vm.AllowPrinting); - Assert.False(vm.AllowCopying); - } - - [Fact] - public async Task ProtectPdfTool_ProtectsWithPasswordSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("ProtectSample", 2); - try - { - var vm = (ProtectPdfToolViewModel)_fixture.Factory.Create(PdfToolId.ProtectPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.UserPassword = "Password123"; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task ProtectPdfTool_OwnerPasswordIsNotDerivedFromUserPassword() - { - // Regression guard: the owner password used to be `{userPassword}_admin` — a fixed, - // guessable pattern. Anyone who had the "open" (user) password could compute full - // owner rights and strip every permission restriction. It must now be unguessable. - string sample = ToolTestFixture.CreateSamplePdf("ProtectSecuritySample", 1); - string? outputPath = null; - try - { - var vm = (ProtectPdfToolViewModel)_fixture.Factory.Create(PdfToolId.ProtectPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.UserPassword = "OpenMe123"; - vm.AllowModifying = false; - vm.AllowCopying = false; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - outputPath = vm.LastOutputFilePath; - Assert.True(File.Exists(outputPath)); - - // The old vulnerable derivation must no longer grant owner access. - string guessedOwnerPassword = "OpenMe123_admin"; - Assert.Throws(() => - PdfReader.Open(outputPath, guessedOwnerPassword, PdfDocumentOpenMode.Modify)); - - // The genuinely-generated owner password (surfaced in the result message) must work. - string realOwnerPassword = vm.ResultSummaryMessage.Split(':').Last().Trim(); - Assert.NotEmpty(realOwnerPassword); - Assert.NotEqual(guessedOwnerPassword, realOwnerPassword); - using var doc = PdfReader.Open(outputPath, realOwnerPassword, PdfDocumentOpenMode.Modify); - Assert.True(doc.SecuritySettings.HasOwnerPermissions); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - if (outputPath != null && File.Exists(outputPath)) File.Delete(outputPath); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Security/QuestPdfOperationsTests.cs b/tests/PdfEditorApp.Tests/Tools/Security/QuestPdfOperationsTests.cs deleted file mode 100644 index b961457..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Security/QuestPdfOperationsTests.cs +++ /dev/null @@ -1,264 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.Services.Tools; -using UglyToad.PdfPig; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Security; - -public class QuestPdfOperationsTests -{ - private readonly IQuestPdfOperationsEngine _questEngine = new QuestPdfOperationsEngine(); - - [Fact] - public async Task MergeAsync_CombinesMultiplePdfs_Successfully() - { - string tempDir = ToolTestFixture.CreateIsolatedDirectory("QuestMerge"); - try - { - string file1 = ToolTestFixture.CreateSamplePdf("QuestMerge1", 2, tempDir); - string file2 = ToolTestFixture.CreateSamplePdf("QuestMerge2", 3, tempDir); - - var options = new MergeToolOptions - { - InputFiles = new List { file1, file2 }, - Engine = PdfProcessingEngine.QuestPdfNative - }; - - var result = await _questEngine.MergeAsync(options); - - Assert.True(result.Success); - Assert.NotNull(result.OutputFilePath); - string outFile = result.OutputFilePath; - Assert.True(File.Exists(outFile)); - - using var doc = PdfDocument.Open(outFile); - Assert.Equal(5, doc.NumberOfPages); - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public async Task MergeAsync_WithPerFilePageRanges_ExtractsAndMergesCorrectPages() - { - string tempDir = ToolTestFixture.CreateIsolatedDirectory("QuestRange"); - try - { - string file1 = ToolTestFixture.CreateSamplePdf("QuestRange1", 3, tempDir); - string file2 = ToolTestFixture.CreateSamplePdf("QuestRange2", 2, tempDir); - - var options = new MergeToolOptions - { - InputFiles = new List { file1, file2 }, - Engine = PdfProcessingEngine.QuestPdfNative, - FilePageRanges = new Dictionary - { - [file1] = "1,3", // Take pages 1 and 3 from doc 1 - [file2] = "2" // Take page 2 from doc 2 - } - }; - - var result = await _questEngine.MergeAsync(options); - - Assert.True(result.Success); - Assert.NotNull(result.OutputFilePath); - string outFile = result.OutputFilePath; - Assert.True(File.Exists(outFile)); - - using var doc = PdfDocument.Open(outFile); - Assert.Equal(3, doc.NumberOfPages); - } - finally - { - if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true); - } - } - - [Fact] - public async Task EncryptAndDecrypt_Aes256Bit_ProtectsAndUnlocksDocument() - { - string sample = ToolTestFixture.CreateSamplePdf("QuestCrypto", 2); - string? protectedFile = null; - string? unlockedFile = null; - const string password = "TestSecurePassword2026!"; - - try - { - var encryptOptions = new SecurityToolOptions - { - InputFilePath = sample, - UserPassword = password, - EncryptionLevel = PdfEncryptionLevel.Aes256Bit, - Engine = PdfProcessingEngine.QuestPdfNative, - AllowPrinting = true, - AllowModifying = false - }; - - var encryptResult = await _questEngine.EncryptAsync(encryptOptions); - Assert.True(encryptResult.Success); - protectedFile = encryptResult.OutputFilePath; - Assert.True(File.Exists(protectedFile)); - - // Verify document requires password to open - Assert.ThrowsAny(() => - { - using var _ = PdfDocument.Open(protectedFile); - }); - - // Decrypt and remove restrictions using QuestPDF - var unlockOptions = new UnlockToolOptions - { - InputFilePath = protectedFile, - Password = password, - Engine = PdfProcessingEngine.QuestPdfNative - }; - - var unlockResult = await _questEngine.DecryptAsync(unlockOptions); - Assert.True(unlockResult.Success); - unlockedFile = unlockResult.OutputFilePath; - Assert.True(File.Exists(unlockedFile)); - - // Verify unlocked file opens cleanly without password - using var unlockedDoc = PdfDocument.Open(unlockedFile); - Assert.Equal(2, unlockedDoc.NumberOfPages); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - if (protectedFile != null && File.Exists(protectedFile)) File.Delete(protectedFile); - if (unlockedFile != null && File.Exists(unlockedFile)) File.Delete(unlockedFile); - } - } - - [Fact] - public async Task LinearizeAsync_OptimizesDocumentForFastWebView() - { - string sample = ToolTestFixture.CreateSamplePdf("QuestLinearize", 2); - string? linFile = null; - - try - { - linFile = Path.Combine(Path.GetTempPath(), $"Linearized_{Guid.NewGuid():N}.pdf"); - var result = await _questEngine.LinearizeAsync(sample, linFile); - - Assert.True(result.Success); - Assert.True(File.Exists(linFile)); - Assert.True(new FileInfo(linFile).Length > 0); - - // Document must remain fully valid and openable - using var doc = PdfDocument.Open(linFile); - Assert.Equal(2, doc.NumberOfPages); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - if (linFile != null && File.Exists(linFile)) File.Delete(linFile); - } - } - - [Fact] - public async Task ApplyLayerAsync_LayersOverlayPdf_Successfully() - { - string basePdf = ToolTestFixture.CreateSamplePdf("QuestBase", 2); - string overlayPdf = ToolTestFixture.CreateSamplePdf("QuestOverlay", 1); - string? layeredFile = null; - - try - { - var result = await _questEngine.ApplyLayerAsync( - inputPath: basePdf, - layerPdfPath: overlayPdf, - isOverlay: true, - targetPages: "1-z"); - - Assert.True(result.Success); - layeredFile = result.OutputFilePath; - Assert.NotNull(layeredFile); - Assert.True(File.Exists(layeredFile)); - - using var doc = PdfDocument.Open(layeredFile); - Assert.Equal(2, doc.NumberOfPages); - } - finally - { - if (File.Exists(basePdf)) File.Delete(basePdf); - if (File.Exists(overlayPdf)) File.Delete(overlayPdf); - if (layeredFile != null && File.Exists(layeredFile)) File.Delete(layeredFile); - } - } - - [Fact] - public async Task AddAttachmentAsync_EmbedsFileAttachment_Successfully() - { - string basePdf = ToolTestFixture.CreateSamplePdf("QuestAttachmentDoc", 1); - string tempAttachment = Path.Combine(Path.GetTempPath(), $"invoice_{Guid.NewGuid():N}.xml"); - File.WriteAllText(tempAttachment, "1234599.99"); - string? attachedFile = null; - - try - { - var result = await _questEngine.AddAttachmentAsync( - inputPath: basePdf, - attachmentFilePath: tempAttachment, - description: "ZUGFeRD Electronic Invoice XML", - mimeType: "text/xml"); - - Assert.True(result.Success); - attachedFile = result.OutputFilePath; - Assert.NotNull(attachedFile); - Assert.True(File.Exists(attachedFile)); - - using var doc = PdfDocument.Open(attachedFile); - Assert.Equal(1, doc.NumberOfPages); - } - finally - { - if (File.Exists(basePdf)) File.Delete(basePdf); - if (File.Exists(tempAttachment)) File.Delete(tempAttachment); - if (attachedFile != null && File.Exists(attachedFile)) File.Delete(attachedFile); - } - } - - [Fact] - public async Task ExtendMetadataAsync_ExtendsXmpMetadata_Successfully() - { - string basePdf = ToolTestFixture.CreateSamplePdf("QuestMetadataDoc", 1); - string customXmp = ""; - string? metaFile = null; - - try - { - var result = await _questEngine.ExtendMetadataAsync(basePdf, customXmp); - - Assert.True(result.Success); - metaFile = result.OutputFilePath; - Assert.NotNull(metaFile); - Assert.True(File.Exists(metaFile)); - - using var doc = PdfDocument.Open(metaFile); - Assert.Equal(1, doc.NumberOfPages); - } - finally - { - if (File.Exists(basePdf)) File.Delete(basePdf); - if (metaFile != null && File.Exists(metaFile)) File.Delete(metaFile); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Security/RedactPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Security/RedactPdfToolTests.cs deleted file mode 100644 index 5799bd5..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Security/RedactPdfToolTests.cs +++ /dev/null @@ -1,423 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System; -using System.IO; -using System.Threading.Tasks; -using Avalonia; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Security; - -public class RedactPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public RedactPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void RedactPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.RedactPdf, vm.Tool.Id); - Assert.True(vm.PermanentScrubText); - Assert.Equal("CONFIDENTIAL", vm.SearchPattern); - Assert.Empty(vm.Marks); - Assert.False(vm.HasMarks); - Assert.Equal(1.0, vm.Preview.ZoomLevel); - } - - [Fact] - public void RedactPdfTool_ZoomIn_And_ZoomOut_Clamp() - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - - vm.Preview.ZoomInCommand.Execute(null); - Assert.True(vm.Preview.ZoomLevel > 1.0); - - // Clamp at the top. - for (int i = 0; i < 20; i++) vm.Preview.ZoomInCommand.Execute(null); - Assert.Equal(3.0, vm.Preview.ZoomLevel); - Assert.False(vm.Preview.CanZoomIn); - - for (int i = 0; i < 20; i++) vm.Preview.ZoomOutCommand.Execute(null); - Assert.Equal(0.5, vm.Preview.ZoomLevel); - Assert.False(vm.Preview.CanZoomOut); - } - - [Fact] - public async Task RedactPdfTool_ZoomChange_RepositionsExistingMarkHighlights() - { - string sample = ToolTestFixture.CreateSamplePdf("RedactZoomRepositionSample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - await WaitForPreviewAsync(vm); - - vm.SearchPattern = "CONFIDENTIAL"; - await vm.FindAndMarkCommand.ExecuteAsync(null); - Assert.True(vm.CurrentPageMarks.Count > 0); - double originalDisplayX = vm.CurrentPageMarks[0].DisplayX; - - vm.Preview.ZoomInCommand.Execute(null); - - // Recomputed synchronously as soon as ZoomLevel changes, ahead of the - // debounced re-render — the highlight must track the new scale immediately. - Assert.True(vm.CurrentPageMarks.Count > 0); - Assert.NotEqual(originalDisplayX, vm.CurrentPageMarks[0].DisplayX); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RedactPdfTool_FindAndMark_PopulatesMarksList() - { - string sample = ToolTestFixture.CreateSamplePdf("RedactFindMarkSample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SearchPattern = "CONFIDENTIAL"; - - await vm.FindAndMarkCommand.ExecuteAsync(null); - - Assert.True(vm.HasMarks); - Assert.NotEmpty(vm.Marks); - Assert.Contains("Marked", vm.SearchStatusMessage); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RedactPdfTool_FindAndMark_DoesNotDuplicateOnRepeatedSearch() - { - string sample = ToolTestFixture.CreateSamplePdf("RedactNoDupSample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SearchPattern = "CONFIDENTIAL"; - - await vm.FindAndMarkCommand.ExecuteAsync(null); - int firstCount = vm.Marks.Count; - Assert.True(firstCount > 0); - - await vm.FindAndMarkCommand.ExecuteAsync(null); - - Assert.Equal(firstCount, vm.Marks.Count); - Assert.Contains("already marked", vm.SearchStatusMessage); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RedactPdfTool_RemoveMark_And_ClearMarks_Work() - { - string sample = ToolTestFixture.CreateSamplePdf("RedactRemoveClearSample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SearchPattern = "CONFIDENTIAL"; - await vm.FindAndMarkCommand.ExecuteAsync(null); - Assert.True(vm.Marks.Count > 0); - - var firstMark = vm.Marks[0]; - vm.RemoveMarkCommand.Execute(firstMark); - Assert.DoesNotContain(firstMark, vm.Marks); - - vm.ClearMarksCommand.Execute(null); - Assert.Empty(vm.Marks); - Assert.False(vm.HasMarks); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RedactPdfTool_NoMarks_BlocksExecution_ViaValidation() - { - // Regression guard for the new interactive flow: searching alone no longer - // commits anything — the user must review matches (now in Marks) before - // Execute is allowed to run at all. - string sample = ToolTestFixture.CreateSamplePdf("RedactNoMarksSample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.HasError); - Assert.False(vm.IsComplete); - Assert.Contains("Search for text to redact", vm.ErrorMessage); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RedactPdfTool_SearchWithNoMatches_SetsStatusMessage_DoesNotMark() - { - string sample = ToolTestFixture.CreateSamplePdf("RedactSearchNoMatchSample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SearchPattern = "ThisTextDoesNotExistAnywhereInTheDocument"; - - await vm.FindAndMarkCommand.ExecuteAsync(null); - - Assert.Empty(vm.Marks); - Assert.False(vm.HasMarks); - Assert.Contains("No matches", vm.SearchStatusMessage); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RedactPdfTool_FullFlow_MarkThenExecute_ActuallyRemovesMatchedText() - { - // End-to-end regression guard: search finds matches → user reviews them in - // Marks → Execute commits exactly those marks. Proves the matched text is - // genuinely gone from the output, not just visually covered. - string sample = ToolTestFixture.CreateSamplePdf("RedactFullFlowSample", 1); - string? outputPath = null; - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SearchPattern = "CONFIDENTIAL"; - vm.PermanentScrubText = true; - - await vm.FindAndMarkCommand.ExecuteAsync(null); - Assert.True(vm.HasMarks); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - outputPath = vm.LastOutputFilePath; - Assert.True(File.Exists(outputPath)); - - using var outputDoc = UglyToad.PdfPig.PdfDocument.Open(outputPath); - string extractedText = outputDoc.GetPage(1).Text; - Assert.DoesNotContain("CONFIDENTIAL", extractedText, StringComparison.OrdinalIgnoreCase); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - if (outputPath != null && File.Exists(outputPath)) File.Delete(outputPath); - } - } - - [Fact] - public async Task RedactPdfTool_LoadingDocument_ResolvesPageCountAndDimensions() - { - // Note: actual Bitmap decoding needs Avalonia's platform rendering services, - // which this headless xUnit host doesn't bootstrap — so the Bitmap itself isn't - // asserted here. Page count/dimensions come from PdfPig directly and are - // deliberately captured independent of bitmap decode success (see - // PdfPageRenderer.RenderPageAtScale), so they're verifiable in this environment - // and are exactly what the highlight-overlay math depends on. - string sample = ToolTestFixture.CreateSamplePdf("RedactPreviewSample", 3); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - // Loading is fire-and-forget off the SelectedFiles collection change; give it - // a moment to finish (waits on SelectedPage.WidthPoints, set last in the load - // chain — polling TotalPages instead would race ahead of the render actually - // finishing). - await WaitForPreviewAsync(vm); - - Assert.Equal(3, vm.Preview.TotalPages); - Assert.NotNull(vm.Preview.SelectedPage); - Assert.True(vm.Preview.SelectedPage!.WidthPoints > 0); - Assert.True(vm.Preview.SelectedPage!.HeightPoints > 0); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - private static async Task WaitForPreviewAsync(RedactPdfToolViewModel vm) - { - // Word geometry (needed for text-snap marking) refreshes independently of, and - // not necessarily before, Preview's own page render completing — wait for both - // so callers can rely on AddManualMark's word-snapping actually having data. - for (int i = 0; i < 50 && (vm.Preview.SelectedPage == null || vm.Preview.SelectedPage.WidthPoints == 0 || vm.WordsRefreshedCount == 0); i++) - { - await Task.Delay(50); - } - } - - [Fact] - public async Task RedactPdfTool_ForceDrawBox_AddsRawRectangleAsManualMark() - { - string sample = ToolTestFixture.CreateSamplePdf("RedactDrawBoxSample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - await WaitForPreviewAsync(vm); - Assert.NotNull(vm.Preview.SelectedPage); - Assert.True(vm.Preview.SelectedPage!.WidthPoints > 0); - - vm.AddManualMark(new Rect(50, 60, 120, 30), forceDrawBox: true); - - Assert.Single(vm.Marks); - var mark = vm.Marks[0]; - Assert.Equal("Manual selection", mark.Label); - Assert.Equal(0, mark.Region.PageIndex); - Assert.True(mark.Region.Width > 0 && mark.Region.Height > 0); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RedactPdfTool_DefaultDrag_SnapsToTouchedWordsLikeThePdfReader() - { - string sample = ToolTestFixture.CreateSamplePdf("RedactSelectTextSample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - await WaitForPreviewAsync(vm); - Assert.NotNull(vm.Preview.SelectedPage); - Assert.True(vm.Preview.SelectedPage!.WidthPoints > 0); - - // A drag covering the whole page must touch every word on it (default mode - // snaps to text — no mode switch needed, matching the PDF Reader's behavior). - double displayWidth = vm.Preview.SelectedPage!.WidthPoints * vm.Preview.ZoomLevel; - double displayHeight = vm.Preview.SelectedPage!.HeightPoints * vm.Preview.ZoomLevel; - vm.AddManualMark(new Rect(0, 0, displayWidth, displayHeight)); - - Assert.Single(vm.Marks); - var mark = vm.Marks[0]; - Assert.NotEqual("Manual selection", mark.Label); - Assert.Contains("CONFIDENTIAL", mark.Label, StringComparison.OrdinalIgnoreCase); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task RedactPdfTool_DefaultDrag_EmptyAreaAddsNothing() - { - // A drag that touches no words shouldn't create a mark by default — unlike - // forceDrawBox: true, which always uses the raw rectangle. - string sample = ToolTestFixture.CreateSamplePdf("RedactSelectTextEmptySample", 1); - try - { - var vm = (RedactPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RedactPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - await WaitForPreviewAsync(vm); - Assert.NotNull(vm.Preview.SelectedPage); - - // Bottom-right corner is blank in the fixture's generated layout. - double displayWidth = vm.Preview.SelectedPage!.WidthPoints * vm.Preview.ZoomLevel; - double displayHeight = vm.Preview.SelectedPage!.HeightPoints * vm.Preview.ZoomLevel; - vm.AddManualMark(new Rect(displayWidth - 5, displayHeight - 5, 3, 3)); - - Assert.Empty(vm.Marks); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task FindRedactionMatchesAsync_FindsMatchesWithoutWritingAnyFile() - { - // The preview-support backend method: read-only, used by the interactive UI to - // show matches before the user commits to redacting. Must not create/modify any file. - string sample = ToolTestFixture.CreateSamplePdf("RedactFindOnlySample", 2); - try - { - var matches = await _fixture.OperationsService.SecurityService.FindRedactionMatchesAsync(sample, "CONFIDENTIAL", caseSensitive: false); - - Assert.NotEmpty(matches); - Assert.All(matches, m => - { - Assert.True(m.PageIndex >= 0); - Assert.True(m.Width > 0); - Assert.True(m.Height > 0); - }); - - // No sibling output file should exist — this call must be side-effect free. - string dir = Path.GetDirectoryName(sample)!; - string baseName = Path.GetFileNameWithoutExtension(sample); - Assert.False(File.Exists(Path.Combine(dir, $"{baseName}_Redacted.pdf"))); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } - - [Fact] - public async Task FindRedactionMatchesAsync_NoMatches_ReturnsEmptyList_NotAnError() - { - string sample = ToolTestFixture.CreateSamplePdf("RedactFindNoneSample", 1); - try - { - var matches = await _fixture.OperationsService.SecurityService.FindRedactionMatchesAsync(sample, "NothingLikeThisExistsHere", caseSensitive: false); - Assert.Empty(matches); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Security/RepairPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Security/RepairPdfToolTests.cs deleted file mode 100644 index c493d09..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Security/RepairPdfToolTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Security; - -public class RepairPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public RepairPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void RepairPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (RepairPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RepairPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.RepairPdf, vm.Tool.Id); - } - - [Fact] - public async Task RepairPdfTool_RepairsDocumentSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("RepairSample", 2); - try - { - var vm = (RepairPdfToolViewModel)_fixture.Factory.Create(PdfToolId.RepairPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Security/SignPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Security/SignPdfToolTests.cs deleted file mode 100644 index 352a162..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Security/SignPdfToolTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Security; - -public class SignPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public SignPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void SignPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (SignPdfToolViewModel)_fixture.Factory.Create(PdfToolId.SignPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.SignPdf, vm.Tool.Id); - Assert.Equal("Jane Doe", vm.SignerName); - Assert.Equal(SignatureStyle.CursiveElegance, vm.Style); - } - - [Fact] - public async Task SignPdfTool_SignsDocumentSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("SignSample", 1); - try - { - var vm = (SignPdfToolViewModel)_fixture.Factory.Create(PdfToolId.SignPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.SignerName = "Alex Morgan"; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Security/UnlockPdfToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Security/UnlockPdfToolTests.cs deleted file mode 100644 index d4a5a28..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Security/UnlockPdfToolTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Security; - -public class UnlockPdfToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public UnlockPdfToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void UnlockPdfTool_InstantiatesWithCorrectDefaults() - { - var vm = (UnlockPdfToolViewModel)_fixture.Factory.Create(PdfToolId.UnlockPdf); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.UnlockPdf, vm.Tool.Id); - Assert.Empty(vm.Password); - } - - [Fact] - public async Task UnlockPdfTool_UnlocksDocumentSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("UnlockSample", 2); - try - { - var vm = (UnlockPdfToolViewModel)_fixture.Factory.Create(PdfToolId.UnlockPdf); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/Tools/Security/WatermarkToolTests.cs b/tests/PdfEditorApp.Tests/Tools/Security/WatermarkToolTests.cs deleted file mode 100644 index f562b00..0000000 --- a/tests/PdfEditorApp.Tests/Tools/Security/WatermarkToolTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.ViewModels.Tools.Core; -using PdfEditorApp.ViewModels.Tools.Organize; -using PdfEditorApp.ViewModels.Tools.Security; -using PdfEditorApp.ViewModels.Tools.Conversion; -using PdfEditorApp.ViewModels.Tools.Intelligence; -using PdfEditorApp.Tests.Tools.Core; -using System.IO; -using System.Threading.Tasks; -using PdfEditorApp.Models; -using PdfEditorApp.ViewModels.Tools; -using Xunit; - -namespace PdfEditorApp.Tests.Tools.Security; - -public class WatermarkToolTests : IClassFixture -{ - private readonly ToolTestFixture _fixture; - - public WatermarkToolTests(ToolTestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public void WatermarkTool_InstantiatesWithCorrectDefaults() - { - var vm = (WatermarkToolViewModel)_fixture.Factory.Create(PdfToolId.Watermark); - Assert.NotNull(vm); - Assert.Equal(PdfToolId.Watermark, vm.Tool.Id); - Assert.Equal("CONFIDENTIAL", vm.WatermarkText); - Assert.Equal(0.35, vm.Opacity); - Assert.Equal(-45, vm.RotationAngle); - } - - [Fact] - public async Task WatermarkTool_ExecutesWatermarkingSuccessfully() - { - string sample = ToolTestFixture.CreateSamplePdf("WatermarkSample", 2); - try - { - var vm = (WatermarkToolViewModel)_fixture.Factory.Create(PdfToolId.Watermark); - vm.SelectedFiles.Add(sample); - vm.SyncPreviewItems(); - vm.WatermarkText = "INTERNAL ONLY"; - vm.Opacity = 0.5; - - await vm.ExecuteToolCommand.ExecuteAsync(null); - - Assert.True(vm.IsComplete); - Assert.False(vm.HasError); - Assert.True(File.Exists(vm.LastOutputFilePath)); - } - finally - { - if (File.Exists(sample)) File.Delete(sample); - } - } -} diff --git a/tests/PdfEditorApp.Tests/TotalMicrokernelTests.cs b/tests/PdfEditorApp.Tests/TotalMicrokernelTests.cs deleted file mode 100644 index 176bf40..0000000 --- a/tests/PdfEditorApp.Tests/TotalMicrokernelTests.cs +++ /dev/null @@ -1,369 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using PdfEditorApp.Core.Plugins; -using PdfEditorApp.Core.Plugins.Descriptors; -using PdfEditorApp.Core.Plugins.Profiles; -using PdfEditorApp.Plugins.Bundles; -using PdfEditorApp.Services.Dialogs; -using PdfEditorApp.Services.Navigation; -using PdfEditorApp.Services.Ribbon; -using PdfEditorApp.Services.Sidebar; -using PdfEditorApp.Services.Tools.Core; -using PdfEditorApp.Services.Tools.Conversion; -using PdfEditorApp.Services.Tools.Intelligence; -using PdfEditorApp.Services.Tools.Organize; -using PdfEditorApp.Services.Tools.Security; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -public class TotalMicrokernelTests -{ - [Fact] - public void NavigationRegistry_Registers_And_Queries_By_Group() - { - var reg = new NavigationRegistry(); - bool eventFired = false; - reg.RegistryChanged += () => eventFired = true; - - var desc1 = new NavigationItemDescriptor - { - Id = "test.page.a", - Title = "Page A", - Group = "TestGroup", - Order = 20, - ViewFactory = sp => "View A" - }; - var desc2 = new NavigationItemDescriptor - { - Id = "test.page.b", - Title = "Page B", - Group = "TestGroup", - Order = 10, - ViewFactory = sp => "View B" - }; - var desc3 = new NavigationItemDescriptor - { - Id = "test.page.c", - Title = "Page C", - Group = "OtherGroup", - Order = 5 - }; - - using var unreg1 = reg.RegisterNavigationItem(desc1); - using var unreg2 = reg.RegisterNavigationItem(desc2); - using var unreg3 = reg.RegisterNavigationItem(desc3); - - Assert.True(eventFired); - Assert.Equal(3, reg.GetAllItems().Count); - - var testGroupItems = reg.GetItemsByGroup("TestGroup"); - Assert.Equal(2, testGroupItems.Count); - Assert.Equal("test.page.b", testGroupItems[0].Id); // Order 10 before 20 - Assert.Equal("test.page.a", testGroupItems[1].Id); - - var singleItem = reg.GetItem("test.page.a"); - Assert.NotNull(singleItem); - Assert.Equal("View A", singleItem.ViewFactory!(null!)); - } - - [Fact] - public void DialogRegistry_Registers_And_Resolves_Dialogs() - { - var reg = new DialogRegistry(); - bool changed = false; - reg.RegistryChanged += () => changed = true; - - var desc = new DialogDescriptor - { - Id = "custom.dialog.test", - Title = "Test Dialog", - ViewFactory = sp => "Dialog Content" - }; - - var unreg = reg.RegisterDialog(desc); - Assert.True(changed); - - var retrieved = reg.GetDialog("custom.dialog.test"); - Assert.NotNull(retrieved); - Assert.Equal("Test Dialog", retrieved.Title); - Assert.Equal("Dialog Content", retrieved.ViewFactory!(null!)); - - unreg.Dispose(); - Assert.Null(reg.GetDialog("custom.dialog.test")); - } - - [Fact] - public void SidebarRegistry_Registers_And_Orders_Tabs() - { - var reg = new SidebarRegistry(); - bool changed = false; - reg.RegistryChanged += () => changed = true; - - var tab1 = new SidebarTabDescriptor - { - Id = "sidebar.layers", - Title = "Layers", - IconKind = "LayersOutline", - Order = 50 - }; - var tab2 = new SidebarTabDescriptor - { - Id = "sidebar.thumbnails", - Title = "Pages", - IconKind = "FileDocumentOutline", - Order = 10 - }; - - using var u1 = reg.RegisterTab(tab1); - using var u2 = reg.RegisterTab(tab2); - - Assert.True(changed); - var tabs = reg.GetAllTabs(); - Assert.Equal(2, tabs.Count); - Assert.Equal("sidebar.thumbnails", tabs[0].Id); - Assert.Equal("sidebar.layers", tabs[1].Id); - } - - [Fact] - public void Enhanced_RibbonRegistry_Supports_Dynamic_Tabs_And_Groups() - { - var reg = new RibbonRegistry(); - - var tab = new RibbonTabDescriptor - { - Id = "CadEngineering", - Title = "CAD & 3D", - Order = 150 - }; - - var group = new RibbonGroupDescriptor - { - Id = "CadTools", - TabId = "CadEngineering", - Title = "Drawing Tools", - Order = 10 - }; - - var action = new RibbonActionDescriptor - { - Id = "cad.measure.caliper", - TabId = "CadEngineering", - GroupId = "CadTools", - Label = "Caliper", - IconKind = "RulerSquare", - Order = 5 - }; - - using var uTab = reg.RegisterTab(tab); - using var uGroup = reg.RegisterGroup(group); - reg.RegisterAction(action); - - var allTabs = reg.GetAllTabs(); - Assert.Contains(allTabs, t => t.Id == "CadEngineering"); - - var groups = reg.GetGroupsForTab("CadEngineering"); - Assert.Single(groups); - Assert.Equal("CadTools", groups[0].Id); - - var actions = reg.GetActionsForGroup("CadEngineering", "CadTools"); - Assert.Single(actions); - Assert.Equal("cad.measure.caliper", actions[0].Id); - - reg.UnregisterAction("cad.measure.caliper"); - Assert.Empty(reg.GetActionsForGroup("CadEngineering", "CadTools")); - } - - [Fact] - public async Task WorkspacePagesBundle_Mounts_All_12_Workspace_Pages() - { - var navReg = new NavigationRegistry(); - var context = new FryPluginContext(); - context.RegisterService(navReg); - - var host = new PluginHost(context); - var bundle = new WorkspacePagesBundle(); - host.RegisterPlugins(bundle.Plugins); - - await host.StartAsync(); - - var pages = navReg.GetAllItems(); - Assert.True(pages.Count >= 12, $"Expected at least 12 pages registered, found {pages.Count}"); - - Assert.Contains(pages, p => p.Id == "Home"); - Assert.Contains(pages, p => p.Id == "PdfReader"); - Assert.Contains(pages, p => p.Id == "NewDocument"); - Assert.Contains(pages, p => p.Id == "AllTools"); - Assert.Contains(pages, p => p.Id == "Starred"); - Assert.Contains(pages, p => p.Id == "FontPackages"); - Assert.Contains(pages, p => p.Id == "TesseractData"); - Assert.Contains(pages, p => p.Id == "Trash"); - Assert.Contains(pages, p => p.Id == "Help"); - Assert.Contains(pages, p => p.Id == "Licensing"); - Assert.Contains(pages, p => p.Id == "Settings"); - - await host.StopAsync(); - } - - [Fact] - public async Task DialogsBundle_Mounts_Core_And_Studio_Dialogs() - { - var dialogReg = new DialogRegistry(); - var context = new FryPluginContext(); - context.RegisterService(dialogReg); - - var host = new PluginHost(context); - var bundle = new DialogsBundle(); - host.RegisterPlugins(bundle.Plugins); - - await host.StartAsync(); - - var dialogs = dialogReg.GetAllDialogs(); - Assert.True(dialogs.Count >= 15, $"Expected at least 15 dialogs registered, found {dialogs.Count}"); - - Assert.Contains(dialogs, d => d.Id == "frypdf.dialog.about"); - Assert.Contains(dialogs, d => d.Id == "frypdf.dialog.shortcuts"); - Assert.Contains(dialogs, d => d.Id == "frypdf.dialog.plugins"); - Assert.Contains(dialogs, d => d.Id == "frypdf.dialog.signature"); - Assert.Contains(dialogs, d => d.Id == "frypdf.dialog.math"); - Assert.Contains(dialogs, d => d.Id == "frypdf.dialog.datastudio"); - Assert.Contains(dialogs, d => d.Id == "frypdf.dialog.aiassistant"); - - await host.StopAsync(); - } - - [Fact] - public async Task EditorSidebarsBundle_Mounts_Sidebar_Tabs() - { - var sidebarReg = new SidebarRegistry(); - var context = new FryPluginContext(); - context.RegisterService(sidebarReg); - - var host = new PluginHost(context); - var bundle = new EditorSidebarsBundle(); - host.RegisterPlugins(bundle.Plugins); - - await host.StartAsync(); - - var tabs = sidebarReg.GetAllTabs(); - Assert.Equal(3, tabs.Count); - Assert.Equal("Thumbnails", tabs[0].Id); - Assert.Equal("Outline", tabs[1].Id); - Assert.Equal("Comments", tabs[2].Id); - - await host.StopAsync(); - } - - [Fact] - public void HomeViewModel_DynamicNavigation_Activates_Page_View() - { - var navReg = new NavigationRegistry(); - navReg.RegisterNavigationItem(new NavigationItemDescriptor - { - Id = "CustomStudio", - Title = "Custom Studio", - Group = "Extensions", - ViewFactory = sp => "Custom Studio View Instance" - }); - - var vm = new HomeViewModel( - navigationRegistry: navReg, - recentService: new Services.RecentDocumentsService(), - templateService: new Services.TemplateService(), - persistenceService: new Services.ProjectPersistenceService(), - toolRegistry: new Services.Tools.Core.PdfToolRegistry()); - - Assert.Single(vm.ContributedNavigationItems); - Assert.Equal("CustomStudio", vm.ContributedNavigationItems[0].Id); - - vm.SelectNavSection("CustomStudio"); - Assert.True(vm.ContributedNavigationItems[0].IsActive); - Assert.Equal("Custom Studio View Instance", vm.DynamicPageView); - } - - [Fact] - public void Desktop_Profile_Includes_All_16_Bundles() - { - var profilePath = Path.Combine(AppContext.BaseDirectory, "profiles", "desktop.profile.json"); - if (!File.Exists(profilePath)) - { - profilePath = "profiles/desktop.profile.json"; - } - - Assert.True(File.Exists(profilePath), $"desktop.profile.json not found at {profilePath}"); - var profile = ProfileLoader.LoadFromFile(profilePath); - - Assert.Contains("FryPdf.Bundle.WorkspacePages", profile.Bundles); - Assert.Contains("FryPdf.Bundle.Dialogs", profile.Bundles); - Assert.Contains("FryPdf.Bundle.EditorSidebars", profile.Bundles); - Assert.Contains("FryPdf.Bundle.CanvasElements", profile.Bundles); - Assert.Contains("FryPdf.Bundle.CommandPalette", profile.Bundles); - } - - [Fact] - public async Task PdfDocumentOperationsService_ExecutesThroughWaterfallPipeline_InterceptsTool() - { - var context = new FryPluginContext(); - bool pipelineIntercepted = false; - - context.RegisterWaterfall( - "tool:execute", - async (ctx, next) => - { - pipelineIntercepted = true; - ctx.Properties["audit:user"] = "SystemTester"; - await next(); - }); - - var ops = new Services.PdfDocumentOperationsService( - new PdfToolRegistry(), - new PdfPageService(), - new PdfOptimizationService(), - new PdfSecurityService(), - new PdfConversionService(), - new PdfOcrService(), - new PdfFormService(), - new AiDocumentService(), - new DocumentTranslationService(), - new PdfWorkflowEngine(), - context); - - var result = await ops.ExecuteToolAsync(Models.PdfToolId.MergePdf, new Models.MergeToolOptions()); - - Assert.True(pipelineIntercepted); - } - - [Fact] - public async Task PluginItemViewModel_ToggleIsActive_TriggersToggleHandler() - { - bool toggleTriggered = false; - bool toggleTargetValue = false; - - var vm = new PluginItemViewModel - { - Id = "test.plugin.toggle", - Name = "Toggle Test", - IsActive = true, - ToggleHandler = (id, active) => - { - toggleTriggered = true; - toggleTargetValue = active; - return Task.CompletedTask; - } - }; - - // Switch to false - vm.IsActive = false; - - // Give async Task a moment to run - await Task.Delay(20); - - Assert.True(toggleTriggered); - Assert.False(toggleTargetValue); - Assert.False(vm.IsActive); - } -} diff --git a/tests/PdfEditorApp.Tests/TypographyAndTextEngineTests.cs b/tests/PdfEditorApp.Tests/TypographyAndTextEngineTests.cs deleted file mode 100644 index 0e4ed6b..0000000 --- a/tests/PdfEditorApp.Tests/TypographyAndTextEngineTests.cs +++ /dev/null @@ -1,709 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using System.Threading.Tasks; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Core.Models.Elements; -using Xunit; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.Services.Typography; -using PdfEditorApp.ViewModels; -using PdfEditorApp.ViewModels.ElementViewModels; - -namespace PdfEditorApp.Tests; - -public class TypographyAndTextEngineTests -{ - [Fact] - public void PdfTextElement_DefaultValues_AreSensible() - { - var textEl = new PdfTextElement(); - - Assert.Equal("Enter text here", textEl.Text); - Assert.Equal(14.0, textEl.FontSize); - Assert.Equal("#201F1E", textEl.TextColorHex); - Assert.Equal(1.0, textEl.TextOpacity); - Assert.Equal(TextAlignmentMode.Left, textEl.Alignment); - Assert.Equal(TextVerticalAlignment.Top, textEl.VerticalAlignment); - Assert.Equal(TextShapeMode.Normal, textEl.ShapeMode); - Assert.False(textEl.HasStroke); - Assert.False(textEl.HasShadow); - Assert.False(textEl.IsDoubleUnderline); - Assert.Equal(1.0, textEl.ScaleX); - Assert.Equal(1.0, textEl.ScaleY); - Assert.False(textEl.FlipX); - Assert.False(textEl.FlipY); - } - - [Fact] - public void PdfTextElement_JsonSerialization_RoundTripPreservesAllAdvancedProperties() - { - var original = new PdfTextElement - { - Id = Guid.NewGuid().ToString(), - X = 50, - Y = 100, - Width = 300, - Height = 150, - Text = "Curved Badge Typography", - FontFamily = "Inter", - FontSize = 24, - IsBold = true, - IsItalic = true, - IsUnderline = true, - IsDoubleUnderline = true, - IsStrikethrough = false, - TextColorHex = "#0F6CBD", - TextOpacity = 0.9, - Alignment = TextAlignmentMode.Center, - VerticalAlignment = TextVerticalAlignment.Center, - LineHeight = 1.4, - CharacterSpacing = 2.5, - WordSpacing = 5.0, - ParagraphSpacing = 8.0, - TextWrap = true, - HasStroke = true, - StrokeColorHex = "#1E293B", - StrokeWidth = 2.0, - HasShadow = true, - ShadowColorHex = "#80000000", - ShadowOffsetX = 3.0, - ShadowOffsetY = 4.0, - ShadowBlurRadius = 6.0, - ShadowOpacity = 0.75, - BackgroundColorHex = "#FEF08A", - CornerRadius = 8.0, - BorderColorHex = "#CA8A04", - BorderThickness = 1.5, - Padding = 12.0, - ShapeMode = TextShapeMode.Curved, - CurveRadius = 150.0, - CurveArcAngle = 180.0, - CurveStartAngle = -90.0, - CurveClockwise = true, - CurveInvert = false, - CircularPlacement = CircularTextPlacement.TopArc, - BaselineShift = 5.0, - CharacterRotation = 10.0, - ScaleX = 1.2, - ScaleY = 0.9, - FlipX = false, - FlipY = false - }; - - var options = new JsonSerializerOptions { WriteIndented = true }; - string json = JsonSerializer.Serialize(original, options); - var deserialized = JsonSerializer.Deserialize(json, options); - - Assert.NotNull(deserialized); - Assert.Equal(original.Id, deserialized.Id); - Assert.Equal(original.Text, deserialized.Text); - Assert.Equal(original.FontSize, deserialized.FontSize); - Assert.True(deserialized.IsBold); - Assert.True(deserialized.IsDoubleUnderline); - Assert.Equal(TextShapeMode.Curved, deserialized.ShapeMode); - Assert.Equal(150.0, deserialized.CurveRadius); - Assert.Equal(180.0, deserialized.CurveArcAngle); - Assert.True(deserialized.HasStroke); - Assert.Equal("#1E293B", deserialized.StrokeColorHex); - Assert.True(deserialized.HasShadow); - Assert.Equal(3.0, deserialized.ShadowOffsetX); - Assert.Equal(1.2, deserialized.ScaleX); - } - - [Fact] - public void PdfTextElement_BackwardCompatibility_LoadsLegacyJsonWithoutError() - { - // Legacy JSON schema with only basic fields - string legacyJson = """ - { - "Id": "legacy-text-1", - "X": 10, - "Y": 20, - "Width": 200, - "Height": 50, - "Text": "Legacy Text", - "FontFamily": "Arial", - "FontSize": 14, - "TextColorHex": "#000000", - "Alignment": 0, - "IsBold": false, - "IsItalic": false - } - """; - - var el = JsonSerializer.Deserialize(legacyJson); - Assert.NotNull(el); - Assert.Equal("legacy-text-1", el.Id); - Assert.Equal("Legacy Text", el.Text); - Assert.Equal(TextShapeMode.Normal, el.ShapeMode); - Assert.False(el.HasStroke); - Assert.False(el.HasShadow); - Assert.Equal(1.0, el.ScaleX); - } - - [Fact] - public void PdfTextElement_Clone_CreatesDeepCopy() - { - var original = new PdfTextElement - { - Text = "Master Copy", - ShapeMode = TextShapeMode.Circular, - CurveRadius = 120, - HasStroke = true, - StrokeWidth = 3.0 - }; - - var clone = (PdfTextElement)original.Clone(); - Assert.NotEqual(original.Id, clone.Id); - Assert.Equal(original.Text, clone.Text); - Assert.Equal(original.ShapeMode, clone.ShapeMode); - Assert.Equal(original.CurveRadius, clone.CurveRadius); - Assert.Equal(original.HasStroke, clone.HasStroke); - Assert.Equal(original.StrokeWidth, clone.StrokeWidth); - - clone.Text = "Modified Copy"; - Assert.Equal("Master Copy", original.Text); - } - - [Fact] - public void TextLayoutEngine_MeasureGlyphWidth_ReturnsExpectedProportions() - { - double widthW = TextLayoutEngine.MeasureGlyphWidth('W', "Arial", 20, isBold: false, isItalic: false); - double widthI = TextLayoutEngine.MeasureGlyphWidth('I', "Arial", 20, isBold: false, isItalic: false); - - Assert.True(widthW > widthI, "W should be significantly wider than I in proportional fonts"); - Assert.True(widthW > 0); - Assert.True(widthI > 0); - - double widthBold = TextLayoutEngine.MeasureGlyphWidth('W', "Arial", 20, isBold: true, isItalic: false); - Assert.True(widthBold >= widthW, "Bold glyphs should be slightly wider or equal to regular glyphs"); - } - - [Fact] - public void TextLayoutEngine_CalculateNormalLayout_WordWrapsCorrectly() - { - string text = "First line here\nSecond line is quite long and should wrap when width is small enough"; - var result = TextLayoutEngine.CalculateNormalLayout( - text, - "Arial", - 16, - isBold: false, - isItalic: false, - availableWidth: 120, - lineHeightMultiplier: 1.2, - charSpacing: 0, - wordSpacing: 0, - paragraphSpacing: 0, - alignment: TextAlignmentMode.Left, - vAlign: TextVerticalAlignment.Top, - boxHeight: 200, - wrap: true, - padding: 4 - ); - - Assert.True(result.Lines.Count >= 2, "Expected multiple lines from newline and word wrapping"); - - foreach (var line in result.Lines) - { - Assert.False(string.IsNullOrEmpty(line.Text)); - Assert.True(line.Width > 0); - Assert.True(line.Height > 0); - } - } - - [Fact] - public void TextLayoutEngine_CalculateCurvedGlyphs_CalculatesValidGlyphTransforms() - { - string text = "CURVED HEADING"; - var layoutResult = TextLayoutEngine.CalculateCurvedGlyphs( - text, - "Arial", - 20, - isBold: true, - isItalic: false, - boxWidth: 300, - boxHeight: 150, - radius: 100, - arcAngleDeg: 120, - startAngleDeg: -60, - clockwise: true, - invert: false, - charSpacing: 0, - circularPlacement: CircularTextPlacement.TopArc, - shapeMode: TextShapeMode.Curved, - baselineShift: 0 - ); - - Assert.Equal(text.Length, layoutResult.Glyphs.Count); - - for (int i = 0; i < layoutResult.Glyphs.Count; i++) - { - var g = layoutResult.Glyphs[i]; - Assert.Equal(text[i].ToString(), g.Text); - Assert.True(!double.IsNaN(g.X) && !double.IsInfinity(g.X)); - Assert.True(!double.IsNaN(g.Y) && !double.IsInfinity(g.Y)); - Assert.True(!double.IsNaN(g.TangentAngleDeg) && !double.IsInfinity(g.TangentAngleDeg)); - } - } - - [Fact] - public void TextLayoutEngine_CalculateCircularGlyphs_PositionsGlyphsAroundCircle() - { - string text = "CIRCULAR BADGE EMBLEM"; - double width = 200; - double height = 200; - double radius = 80; - - var layoutResult = TextLayoutEngine.CalculateCurvedGlyphs( - text, - "Arial", - 14, - isBold: true, - isItalic: false, - boxWidth: width, - boxHeight: height, - radius: radius, - arcAngleDeg: 360, - startAngleDeg: -90, - clockwise: true, - invert: false, - charSpacing: 0, - circularPlacement: CircularTextPlacement.FullCircle, - shapeMode: TextShapeMode.Circular, - baselineShift: 0 - ); - - Assert.Equal(text.Length, layoutResult.Glyphs.Count); - - double centerX = width / 2.0; - double centerY = height / 2.0; - - foreach (var g in layoutResult.Glyphs) - { - if (string.IsNullOrWhiteSpace(g.Text)) continue; - double dx = g.X - centerX; - double dy = g.Y - centerY; - double dist = Math.Sqrt(dx * dx + dy * dy); - // Distance should be close to radius - Assert.InRange(dist, 40, 120); - } - } - - [Fact] - public void TextLayoutEngine_GenerateSvgMarkup_ProducesValidVectorMarkup() - { - var textEl = new PdfTextElement - { - Text = "VECTOR SVG EXPORT", - Width = 250, - Height = 100, - FontSize = 18, - TextColorHex = "#2563EB", - HasStroke = true, - StrokeColorHex = "#1E293B", - StrokeWidth = 1.5, - HasShadow = true, - ShadowColorHex = "#40000000", - ShapeMode = TextShapeMode.Curved, - CurveRadius = 120, - CurveArcAngle = 90 - }; - - string svg = TextLayoutEngine.GenerateSvgMarkup(textEl); - Assert.NotNull(svg); - Assert.StartsWith("", svg); - Assert.Contains("xmlns=\"http://www.w3.org/2000/svg\"", svg); - Assert.Contains("", svg); - Assert.Contains(" 500, "Exported PDF file should be non-empty and well-formed"); - } - finally - { - if (File.Exists(tempPdfPath)) - { - File.Delete(tempPdfPath); - } - } - } - - [Fact] - public void InspectorViewModel_TypographyCommands_SupportUndoRedo() - { - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel - { - UndoRedo = undoRedo - }; - - var textEl = new PdfTextElement - { - Text = "Undoable Text", - ShapeMode = TextShapeMode.Normal, - HasStroke = false, - HasShadow = false, - IsDoubleUnderline = false - }; - var vm = new TextElementViewModel(textEl); - inspector.SelectedElement = vm; - - // 1. Toggle Double Underline - Assert.False(vm.IsDoubleUnderline); - inspector.ToggleDoubleUnderlineCommand.Execute(null); - Assert.True(vm.IsDoubleUnderline); - Assert.True(undoRedo.CanUndo); - - undoRedo.Undo(); - Assert.False(vm.IsDoubleUnderline); - - undoRedo.Redo(); - Assert.True(vm.IsDoubleUnderline); - - // 2. Change Shape Mode - inspector.SetTextShapeModeCommand.Execute("Curved"); - Assert.Equal(TextShapeMode.Curved, vm.ShapeMode); - - undoRedo.Undo(); - Assert.Equal(TextShapeMode.Normal, vm.ShapeMode); - - // 3. Toggle Stroke - inspector.ToggleTextStrokeCommand.Execute(null); - Assert.True(vm.HasStroke); - - undoRedo.Undo(); - Assert.False(vm.HasStroke); - - // 4. Toggle Shadow - inspector.ToggleTextShadowCommand.Execute(null); - Assert.True(vm.HasShadow); - - undoRedo.Undo(); - Assert.False(vm.HasShadow); - - // 5. Apply Preset - inspector.ApplyTypographyPresetCommand.Execute("circlebadge"); - Assert.Equal(TextShapeMode.Circular, vm.ShapeMode); - - undoRedo.Undo(); - Assert.Equal(TextShapeMode.Normal, vm.ShapeMode); - } - - [Fact] - public void TextLayoutEngine_CalculateNormalLayout_AlignmentsAndPadding() - { - string text = "Centered Heading Text"; - var resultLeft = TextLayoutEngine.CalculateNormalLayout( - text, "Arial", 16, false, false, 300, 1.2, 0, 0, 0, TextAlignmentMode.Left, TextVerticalAlignment.Top, 100, false, 10); - var resultCenter = TextLayoutEngine.CalculateNormalLayout( - text, "Arial", 16, false, false, 300, 1.2, 0, 0, 0, TextAlignmentMode.Center, TextVerticalAlignment.Top, 100, false, 10); - var resultRight = TextLayoutEngine.CalculateNormalLayout( - text, "Arial", 16, false, false, 300, 1.2, 0, 0, 0, TextAlignmentMode.Right, TextVerticalAlignment.Top, 100, false, 10); - - Assert.Single(resultLeft.Lines); - Assert.Single(resultCenter.Lines); - Assert.Single(resultRight.Lines); - - Assert.Equal(10.0, resultLeft.Lines[0].X); // starts at padding - Assert.True(resultCenter.Lines[0].X > resultLeft.Lines[0].X, "Center alignment X should be greater than Left alignment X"); - Assert.True(resultRight.Lines[0].X > resultCenter.Lines[0].X, "Right alignment X should be greater than Center alignment X"); - } - - [Fact] - public void TextLayoutEngine_CalculateBezierGlyphs_PositionsGlyphsAlongCubicBezier() - { - string text = "BEZIER WAVE CURVE"; - var pts = TextLayoutEngine.GetPresetBezierControlPoints(BezierCurvePreset.Wave); - - var result = TextLayoutEngine.CalculateBezierGlyphs( - text, "Arial", 16, false, false, 400, 150, - pts.P0, pts.P1, pts.P2, pts.P3, - invert: false, charSpacing: 0, baselineShift: 0); - - Assert.Equal(text.Length, result.Glyphs.Count); - Assert.True(result.BoundingBox.Width > 100); - Assert.True(result.BoundingBox.Height > 20); - - // Check that glyphs progress along X from left to right - for (int i = 1; i < result.Glyphs.Count; i++) - { - Assert.True(result.Glyphs[i].X >= result.Glyphs[i - 1].X, "Glyphs should advance along X"); - } - } - - [Fact] - public void TextLayoutEngine_BezierPresets_ProvideDistinctControlPoints() - { - var wave = TextLayoutEngine.GetPresetBezierControlPoints(BezierCurvePreset.Wave); - var scurve = TextLayoutEngine.GetPresetBezierControlPoints(BezierCurvePreset.SCurve); - var bridge = TextLayoutEngine.GetPresetBezierControlPoints(BezierCurvePreset.Bridge); - var valley = TextLayoutEngine.GetPresetBezierControlPoints(BezierCurvePreset.Valley); - var rise = TextLayoutEngine.GetPresetBezierControlPoints(BezierCurvePreset.Rise); - - Assert.NotEqual(wave.P1.Y, bridge.P1.Y); - Assert.NotEqual(bridge.P1.Y, valley.P1.Y); - Assert.NotEqual(scurve.P0.Y, rise.P0.Y); - } - - [Fact] - public void TextLayoutEngine_GenerateSvgMarkup_SupportsBezierCurve() - { - var textEl = new PdfTextElement - { - Text = "SMOOTH BEZIER TEXT", - FontSize = 18, - ShapeMode = TextShapeMode.BezierCurve, - BezierPreset = BezierCurvePreset.SCurve, - Width = 350, - Height = 120, - TextColorHex = "#4F46E5", - IsBold = true - }; - - string svg = TextLayoutEngine.GenerateSvgMarkup(textEl); - - Assert.NotNull(svg); - Assert.StartsWith("", svg); - Assert.Contains("fill=\"#4F46E5\"", svg); - Assert.Contains("font-size=\"18.0\"", svg); - Assert.Contains("transform=\"rotate(", svg); - } - - [Fact] - public void TextElementViewModel_BezierPresets_ApplyCorrectly() - { - var vm = new TextElementViewModel(); - - vm.ApplyTypographyPreset("wave"); - Assert.Equal(TextShapeMode.BezierCurve, vm.ShapeMode); - Assert.Equal(BezierCurvePreset.Wave, vm.BezierPreset); - - vm.ApplyTypographyPreset("scurve"); - Assert.Equal(TextShapeMode.BezierCurve, vm.ShapeMode); - Assert.Equal(BezierCurvePreset.SCurve, vm.BezierPreset); - - vm.ApplyTypographyPreset("bridge"); - Assert.Equal(TextShapeMode.BezierCurve, vm.ShapeMode); - Assert.Equal(BezierCurvePreset.Bridge, vm.BezierPreset); - } - - [Fact] - public void InspectorViewModel_SetBezierPresetCommand_SupportsUndoRedo() - { - var undoRedo = new UndoRedoService(); - var inspector = new InspectorViewModel { UndoRedo = undoRedo }; - var vm = new TextElementViewModel(new PdfTextElement { ShapeMode = TextShapeMode.BezierCurve }); - inspector.SelectedElement = vm; - - inspector.SetBezierPresetCommand.Execute("SCurve"); - Assert.Equal(BezierCurvePreset.SCurve, vm.BezierPreset); - Assert.True(undoRedo.CanUndo); - - undoRedo.Undo(); - Assert.Equal(BezierCurvePreset.Wave, vm.BezierPreset); - - undoRedo.Redo(); - Assert.Equal(BezierCurvePreset.SCurve, vm.BezierPreset); - } - - [Fact] - public async Task PdfExportService_ExportsBezierTypographyToPdf_Successfully() - { - var exportService = new PdfExportService(); - var doc = new PdfDocumentModel { Title = "Bezier Export Test" }; - var page = new PdfPageModel { PageNumber = 1, Width = 595, Height = 842 }; - - page.Elements.Add(new PdfTextElement - { - X = 50, - Y = 100, - Width = 400, - Height = 150, - Text = "ELEGANT BEZIER WAVE TYPOGRAPHY", - FontSize = 18, - ShapeMode = TextShapeMode.BezierCurve, - BezierPreset = BezierCurvePreset.Wave, - TextColorHex = "#2563EB", - IsBold = true - }); - - doc.Pages.Add(page); - - string tempPdfPath = Path.Combine(Path.GetTempPath(), $"bezier_test_{Guid.NewGuid():N}.pdf"); - try - { - await exportService.ExportToFileAsync(doc, tempPdfPath); - Assert.True(File.Exists(tempPdfPath)); - var fileInfo = new FileInfo(tempPdfPath); - Assert.True(fileInfo.Length > 500); - } - finally - { - if (File.Exists(tempPdfPath)) - { - File.Delete(tempPdfPath); - } - } - } -} - - diff --git a/tests/PdfEditorApp.Tests/UiSettingsTests.cs b/tests/PdfEditorApp.Tests/UiSettingsTests.cs deleted file mode 100644 index f668609..0000000 --- a/tests/PdfEditorApp.Tests/UiSettingsTests.cs +++ /dev/null @@ -1,425 +0,0 @@ -using System; -using System.IO; -using Avalonia.Layout; -using CommunityToolkit.Mvvm.Messaging; -using PdfEditorApp.Core.Models; -using PdfEditorApp.Messages; -using PdfEditorApp.Models; -using PdfEditorApp.Services; -using PdfEditorApp.ViewModels; -using Xunit; - -namespace PdfEditorApp.Tests; - -[Collection("ThemeServiceCollection")] -public class UiSettingsTests : IDisposable -{ - private readonly string _tempSettingsDir; - private readonly string _tempSettingsFile; - - public UiSettingsTests() - { - _tempSettingsDir = Path.Combine(Path.GetTempPath(), "FryPdf_UiSettingsTests_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempSettingsDir); - _tempSettingsFile = Path.Combine(_tempSettingsDir, "test_ui_settings.json"); - } - - public void Dispose() - { - try - { - if (Directory.Exists(_tempSettingsDir)) - { - Directory.Delete(_tempSettingsDir, true); - } - } - catch - { - // Best effort cleanup - } - } - - [Fact] - public void UiSettingsService_InitializesWithDefaults_WhenFileDoesNotExist() - { - var service = new UiSettingsService(_tempSettingsFile); - - Assert.Equal(ToastPosition.BottomCenter, service.Settings.ToastPosition); - Assert.Equal(ToastStyleVariant.Solid, service.Settings.ToastStyleVariant); - Assert.Equal(3500, service.Settings.ToastDurationMs); - Assert.True(service.Settings.ToastShowCloseButton); - Assert.False(service.Settings.ToastSoundEnabled); - Assert.Equal(AppThemeMode.Light, service.Settings.ThemeMode); - Assert.Equal(PdfReaderTheme.Default, service.Settings.ReadingTheme); - Assert.False(service.Settings.ShowGridByDefault); - Assert.False(service.Settings.SnapToGridByDefault); - } - - [Fact] - public void UiSettingsService_UpdateSettings_PersistsToDiskAndFiresEvent() - { - var service = new UiSettingsService(_tempSettingsFile); - UiSettingsModel? received = null; - service.SettingsChanged += (s) => received = s; - - service.UpdateSettings(s => - { - s.ToastPosition = ToastPosition.TopRight; - s.ToastStyleVariant = ToastStyleVariant.Subtle; - s.ToastDurationMs = 6000; - s.ToastShowCloseButton = false; - }); - - Assert.True(File.Exists(_tempSettingsFile)); - Assert.NotNull(received); - Assert.Equal(ToastPosition.TopRight, received.ToastPosition); - Assert.Equal(ToastStyleVariant.Subtle, received.ToastStyleVariant); - Assert.Equal(6000, received.ToastDurationMs); - Assert.False(received.ToastShowCloseButton); - - // Verify persistence by loading in new instance - var reloadedService = new UiSettingsService(_tempSettingsFile); - Assert.Equal(ToastPosition.TopRight, reloadedService.Settings.ToastPosition); - Assert.Equal(ToastStyleVariant.Subtle, reloadedService.Settings.ToastStyleVariant); - Assert.Equal(6000, reloadedService.Settings.ToastDurationMs); - Assert.False(reloadedService.Settings.ToastShowCloseButton); - } - - [Fact] - public void UiSettingsService_ResetToDefaults_RestoresDefaultsAndSaves() - { - var service = new UiSettingsService(_tempSettingsFile); - service.UpdateSettings(s => - { - s.ToastPosition = ToastPosition.TopLeft; - s.ToastDurationMs = 2000; - }); - - Assert.Equal(ToastPosition.TopLeft, service.Settings.ToastPosition); - - service.ResetToDefaults(); - - Assert.Equal(ToastPosition.BottomCenter, service.Settings.ToastPosition); - Assert.Equal(3500, service.Settings.ToastDurationMs); - - var reloaded = new UiSettingsService(_tempSettingsFile); - Assert.Equal(ToastPosition.BottomCenter, reloaded.Settings.ToastPosition); - } - - [Fact] - public void UiSettingsService_CorruptJson_GracefullyFallsBackToDefaults() - { - File.WriteAllText(_tempSettingsFile, "{ broken-json: invalid-content"); - - var service = new UiSettingsService(_tempSettingsFile); - - Assert.NotNull(service.Settings); - Assert.Equal(ToastPosition.BottomCenter, service.Settings.ToastPosition); - } - - [Theory] - [InlineData("Could not open file: unrecognized format", null, ToastNotificationType.Danger)] - [InlineData("Failed to export document", null, ToastNotificationType.Danger)] - [InlineData("Corrupt or invalid PDF file", null, ToastNotificationType.Danger)] - [InlineData("Something broke", "AlertCircleOutline", ToastNotificationType.Danger)] - [InlineData("Warning: document contains unsaved edits", null, ToastNotificationType.Warning)] - [InlineData("Nothing to undo", null, ToastNotificationType.Warning)] - [InlineData("Saved successfully to /path/test.pdf", null, ToastNotificationType.Success)] - [InlineData("Copied 3 Elements", null, ToastNotificationType.Success)] - [InlineData("Pasted 1 Element", null, ToastNotificationType.Success)] - [InlineData("Created new document from Blank template", null, ToastNotificationType.Success)] - [InlineData("Switched to Dark Studio Theme", null, ToastNotificationType.Primary)] - [InlineData("Active Tool: Text", null, ToastNotificationType.Primary)] - [InlineData("Zoom: 125%", null, ToastNotificationType.Primary)] - [InlineData("Fit to Width", null, ToastNotificationType.Primary)] - [InlineData("General studio notice", null, ToastNotificationType.General)] - public void MainViewModel_InferToastType_ClassifiesCorrectly(string message, string? icon, ToastNotificationType expected) - { - var result = MainViewModel.InferToastType(message, icon); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData(ToastPosition.TopLeft, HorizontalAlignment.Left, VerticalAlignment.Top, 32, 54, 0, 0)] - [InlineData(ToastPosition.TopCenter, HorizontalAlignment.Center, VerticalAlignment.Top, 0, 54, 0, 0)] - [InlineData(ToastPosition.TopRight, HorizontalAlignment.Right, VerticalAlignment.Top, 0, 54, 32, 0)] - [InlineData(ToastPosition.BottomLeft, HorizontalAlignment.Left, VerticalAlignment.Bottom, 32, 0, 0, 48)] - [InlineData(ToastPosition.BottomCenter, HorizontalAlignment.Center, VerticalAlignment.Bottom, 0, 0, 0, 48)] - [InlineData(ToastPosition.BottomRight, HorizontalAlignment.Right, VerticalAlignment.Bottom, 0, 0, 32, 48)] - public void MainViewModel_ToastPlacement_CalculatesCoordinatesProperly( - ToastPosition position, - HorizontalAlignment expectedHoriz, - VerticalAlignment expectedVert, - double left, double top, double right, double bottom) - { - var vm = new MainViewModel(); - vm.ToastPosition = position; - - Assert.Equal(expectedHoriz, vm.ToastHorizontalAlignment); - Assert.Equal(expectedVert, vm.ToastVerticalAlignment); - Assert.Equal(left, vm.ToastMargin.Left); - Assert.Equal(top, vm.ToastMargin.Top); - Assert.Equal(right, vm.ToastMargin.Right); - Assert.Equal(bottom, vm.ToastMargin.Bottom); - } - - [Fact] - public void MainViewModel_DismissToast_HidesNotification() - { - var vm = new MainViewModel(); - vm.ShowToast("Test message", ToastNotificationType.Success); - Assert.True(vm.IsToastVisible); - - vm.DismissToastCommand.Execute(null); - - Assert.False(vm.IsToastVisible); - } - - [Fact] - public void SettingsViewModel_TestPlaygroundCommands_TriggerCorrectNotifications() - { - var service = new UiSettingsService(_tempSettingsFile); - var vm = new SettingsViewModel(service); - - string? receivedMsg = null; - ToastNotificationType? receivedType = null; - string? receivedIcon = null; - - WeakReferenceMessenger.Default.Register(this, (r, m) => - { - receivedMsg = m.Message; - receivedType = m.Type; - receivedIcon = m.ActionLabel; - }); - - try - { - // Test Primary - vm.TestPrimaryNotificationCommand.Execute(null); - Assert.Equal("Primary message comes here", receivedMsg); - Assert.Equal(ToastNotificationType.Primary, receivedType); - Assert.Equal("InformationOutline", receivedIcon); - - // Test Success - vm.TestSuccessNotificationCommand.Execute(null); - Assert.Equal("Success message comes here", receivedMsg); - Assert.Equal(ToastNotificationType.Success, receivedType); - Assert.Equal("CheckCircleOutline", receivedIcon); - - // Test Danger - vm.TestDangerNotificationCommand.Execute(null); - Assert.Equal("Danger message comes here", receivedMsg); - Assert.Equal(ToastNotificationType.Danger, receivedType); - Assert.Equal("AlertOctagonOutline", receivedIcon); - - // Test Warning - vm.TestWarningNotificationCommand.Execute(null); - Assert.Equal("Warning message comes here", receivedMsg); - Assert.Equal(ToastNotificationType.Warning, receivedType); - Assert.Equal("AlertOutline", receivedIcon); - - // Test General - vm.TestGeneralNotificationCommand.Execute(null); - Assert.Equal("General message comes here", receivedMsg); - Assert.Equal(ToastNotificationType.General, receivedType); - Assert.Equal("InformationOutline", receivedIcon); - } - finally - { - WeakReferenceMessenger.Default.Unregister(this); - } - } - - [Fact] - public void SettingsViewModel_PositionAndStyleCommands_MutateStateAndService() - { - var service = new UiSettingsService(_tempSettingsFile); - var vm = new SettingsViewModel(service); - - vm.SetToastPositionCommand.Execute(ToastPosition.TopCenter); - Assert.Equal(ToastPosition.TopCenter, vm.ToastPosition); - Assert.Equal(ToastPosition.TopCenter, service.Settings.ToastPosition); - - vm.SetToastStyleVariantCommand.Execute(ToastStyleVariant.Subtle); - Assert.Equal(ToastStyleVariant.Subtle, vm.ToastStyleVariant); - Assert.Equal(ToastStyleVariant.Subtle, service.Settings.ToastStyleVariant); - - vm.SetToastDurationCommand.Execute(6000); - Assert.Equal(6000, vm.ToastDurationMs); - Assert.Equal(6000, service.Settings.ToastDurationMs); - - vm.ToggleToastCloseButtonCommand.Execute(null); - Assert.False(vm.ToastShowCloseButton); - Assert.False(service.Settings.ToastShowCloseButton); - } - - [Fact] - public void SettingsViewModel_Commands_AcceptStringParametersFromXaml() - { - var service = new UiSettingsService(_tempSettingsFile); - var vm = new SettingsViewModel(service); - - // Position via string (from XAML CommandParameter="TopLeft") - Assert.True(vm.SetToastPositionCommand.CanExecute("TopLeft")); - vm.SetToastPositionCommand.Execute("TopLeft"); - Assert.Equal(ToastPosition.TopLeft, vm.ToastPosition); - Assert.Equal(ToastPosition.TopLeft, service.Settings.ToastPosition); - - Assert.True(vm.SetToastPositionCommand.CanExecute("BottomRight")); - vm.SetToastPositionCommand.Execute("BottomRight"); - Assert.Equal(ToastPosition.BottomRight, vm.ToastPosition); - - // Style variant via string (from XAML CommandParameter="Subtle") - Assert.True(vm.SetToastStyleVariantCommand.CanExecute("Subtle")); - vm.SetToastStyleVariantCommand.Execute("Subtle"); - Assert.Equal(ToastStyleVariant.Subtle, vm.ToastStyleVariant); - Assert.False(vm.PreviewIsSolid); - - // Duration via string (from XAML CommandParameter="2000") - Assert.True(vm.SetToastDurationCommand.CanExecute("2000")); - vm.SetToastDurationCommand.Execute("2000"); - Assert.Equal(2000, vm.ToastDurationMs); - - // Theme mode via string - Assert.True(vm.SetThemeModeCommand.CanExecute("Dark")); - vm.SetThemeModeCommand.Execute("Dark"); - Assert.Equal(AppThemeMode.Dark, vm.ThemeMode); - - // Reading theme via string - Assert.True(vm.SetReadingThemeCommand.CanExecute("Sepia")); - vm.SetReadingThemeCommand.Execute("Sepia"); - Assert.Equal(PdfReaderTheme.Sepia, vm.ReadingTheme); - - // Zoom mode via string - Assert.True(vm.SetDefaultZoomModeCommand.CanExecute("FitWidth")); - vm.SetDefaultZoomModeCommand.Execute("FitWidth"); - Assert.Equal(PdfViewerZoomMode.FitWidth, vm.DefaultZoomMode); - } - - [Fact] - public void SettingsViewModel_LivePreviewBrushes_UpdateCorrectly() - { - var service = new UiSettingsService(_tempSettingsFile); - var vm = new SettingsViewModel(service); - - // Solid Primary - vm.SetToastStyleVariantCommand.Execute(ToastStyleVariant.Solid); - vm.TestPrimaryNotificationCommand.Execute(null); - Assert.True(vm.PreviewIsSolid); - Assert.NotNull(vm.PreviewBackgroundBrush); - Assert.NotNull(vm.PreviewForegroundBrush); - - // Subtle Danger - vm.SetToastStyleVariantCommand.Execute(ToastStyleVariant.Subtle); - vm.TestDangerNotificationCommand.Execute(null); - Assert.False(vm.PreviewIsSolid); - Assert.Equal(ToastNotificationType.Danger, vm.PreviewToastType); - Assert.NotNull(vm.PreviewBackgroundBrush); - Assert.NotNull(vm.PreviewBorderBrush); - } - - [Fact] - public void MainViewModel_FullPipeline_PlaygroundButtonTriggersMainToast() - { - var mainVm = new MainViewModel(); - - // Trigger Success test from settings playground - mainVm.Home.Settings.TestSuccessNotificationCommand.Execute(null); - - Assert.True(mainVm.IsToastVisible); - Assert.Equal(ToastNotificationType.Success, mainVm.ToastType); - Assert.Equal("Success message comes here", mainVm.ToastMessage); - Assert.Equal("CheckCircleOutline", mainVm.ToastIcon); - } - - [Fact] - public void HomeViewModel_SettingsNavigation_WorksCleanly() - { - var home = new HomeViewModel(); - Assert.False(home.IsSettingsSection); - - home.SelectNavSectionCommand.Execute("Settings"); - - Assert.Equal(HomeNavSection.Settings, home.SelectedNavSection); - Assert.True(home.IsSettingsSection); - Assert.False(home.IsHomeSection); - Assert.NotNull(home.Settings); - } - - [Fact] - public void HomeViewModel_SelectNavSection_PassesSelfAsServiceProviderToViewFactory() - { - var navReg = new PdfEditorApp.Services.Navigation.NavigationRegistry(); - IServiceProvider? capturedSp = null; - object expectedView = new object(); - - navReg.RegisterNavigationItem(new PdfEditorApp.Core.Plugins.Descriptors.NavigationItemDescriptor - { - Id = "Settings", - Title = "Settings", - Group = "Preferences", - ViewFactory = sp => - { - capturedSp = sp; - return expectedView; - } - }); - - var home = new HomeViewModel( - new RecentDocumentsService(), - new TemplateService(), - new ProjectPersistenceService(), - new PdfEditorApp.Services.Tools.Core.PdfToolRegistry(), - null, - null, - new ThemeService(), - new UiSettingsService(), - navReg); - - // Act: Navigate to Settings - home.SelectNavSectionCommand.Execute("Settings"); - - // Assert - Assert.Equal(HomeNavSection.Settings, home.SelectedNavSection); - Assert.True(home.IsSettingsSection); - Assert.Same(expectedView, home.DynamicPageView); - Assert.NotNull(capturedSp); - Assert.Same(home, capturedSp.GetService(typeof(HomeViewModel))); - Assert.Same(home.Settings, capturedSp.GetService(typeof(SettingsViewModel))); - Assert.Same(home.FontManager, capturedSp.GetService(typeof(FontManagerViewModel))); - Assert.Same(home.TesseractManager, capturedSp.GetService(typeof(TesseractManagerViewModel))); - Assert.Same(home.HelpGuide, capturedSp.GetService(typeof(HelpGuideViewModel))); - } - - [Fact] - public void HomeViewModel_SelectNavSection_WhenViewFactoryThrows_GracefullyFallsBackWithoutCrashing() - { - var navReg = new PdfEditorApp.Services.Navigation.NavigationRegistry(); - - navReg.RegisterNavigationItem(new PdfEditorApp.Core.Plugins.Descriptors.NavigationItemDescriptor - { - Id = "Settings", - Title = "Settings", - Group = "Preferences", - ViewFactory = sp => throw new InvalidOperationException("Simulated plugin view failure") - }); - - var home = new HomeViewModel( - new RecentDocumentsService(), - new TemplateService(), - new ProjectPersistenceService(), - new PdfEditorApp.Services.Tools.Core.PdfToolRegistry(), - null, - null, - new ThemeService(), - new UiSettingsService(), - navReg); - - // Must not throw an unhandled exception! - var ex = Record.Exception(() => home.SelectNavSectionCommand.Execute("Settings")); - Assert.Null(ex); - Assert.Null(home.DynamicPageView); - Assert.Equal(HomeNavSection.Settings, home.SelectedNavSection); - Assert.True(home.IsSettingsSection); - } -} diff --git a/watch b/watch deleted file mode 100755 index 8b904dc..0000000 --- a/watch +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -dotnet clean -dotnet build - -dotnet watch --project src/PdfEditorApp "$@"