1 Commits

Author SHA1 Message Date
cisupport 65d8c75bbe Initial commit 2026-06-15 00:44:10 +00:00
43 changed files with 56 additions and 5512 deletions
+45 -76
View File
@@ -1,85 +1,54 @@
# .NET Build Outputs
bin/
obj/
publish/
# ---> C
# Prerequisites
*.d
# Runtime Data Folders (don't commit capture files or queues)
Captures/
Queue/
Archive/
Error/
Output/
# Object files
*.o
*.ko
*.obj
*.elf
# Rust Project (reference implementation)
RUST/
# Linker output
*.ilk
*.map
*.exp
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Precompiled Headers
*.gch
*.pch
# Visual Studio Code
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Libraries
*.lib
*.a
*.la
*.lo
# Visual Studio
.vs/
*.userprefs
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# NuGet Packages
*.nupkg
*.snupkg
**/packages/*
!**/packages/build/
*.nuget.props
*.nuget.targets
project.lock.json
project.fragment.lock.json
artifacts/
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# Files built by Visual Studio
*.pidb
*.svclog
*.scc
# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Log files
*.log
# Queue state file (contains runtime job queue)
queue-state.json
-502
View File
@@ -1,502 +0,0 @@
{
"version": "2.0.0",
"tasks": [
// ============================================
// BUILD TASKS - Debug
// ============================================
{
"label": "build-service-debug-x86",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintService/PrintService.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Debug",
"-r",
"win-x86"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build-service-debug-x64",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintService/PrintService.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Debug",
"-r",
"win-x64"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build-cli-debug-x86",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Debug",
"-r",
"win-x86"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build-cli-debug-x64",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Debug",
"-r",
"win-x64"
],
"problemMatcher": "$msCompile",
"group": "build"
},
// ============================================
// BUILD TASKS - Release
// ============================================
{
"label": "build-service-release-x86",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintService/PrintService.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Release",
"-r",
"win-x86"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build-service-release-x64",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintService/PrintService.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Release",
"-r",
"win-x64"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build-cli-release-x86",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Release",
"-r",
"win-x86"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build-cli-release-x64",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Release",
"-r",
"win-x64"
],
"problemMatcher": "$msCompile",
"group": "build"
},
// ============================================
// BUILD ALL TASKS
// ============================================
{
"label": "build-all-x86",
"dependsOn": [
"build-service-release-x86",
"build-cli-release-x86",
"build-tray-release-x86"
],
"dependsOrder": "parallel",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
},
{
"label": "build-all-x64",
"dependsOn": [
"build-service-release-x64",
"build-cli-release-x64",
"build-tray-release-x64"
],
"dependsOrder": "parallel",
"group": "build",
"problemMatcher": []
},
{
"label": "build-all-platforms",
"dependsOn": [
"build-all-x86",
"build-all-x64"
],
"dependsOrder": "sequence",
"group": "build",
"problemMatcher": []
},
// ============================================
// PUBLISH TASKS (Self-Contained)
// ============================================
{
"label": "publish-service-x86-self-contained",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/PrintService/PrintService.csproj",
"-c",
"Release",
"-r",
"win-x86",
"--self-contained",
"true",
"/p:PublishSingleFile=true",
"/p:PublishTrimmed=false",
"-o",
"${workspaceFolder}/publish/x86"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "publish-service-x64-self-contained",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/PrintService/PrintService.csproj",
"-c",
"Release",
"-r",
"win-x64",
"--self-contained",
"true",
"/p:PublishSingleFile=true",
"/p:PublishTrimmed=false",
"-o",
"${workspaceFolder}/publish/x64"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "publish-cli-x86-self-contained",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
"-c",
"Release",
"-r",
"win-x86",
"--self-contained",
"true",
"/p:PublishSingleFile=true",
"/p:PublishTrimmed=false",
"-o",
"${workspaceFolder}/publish/x86"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "publish-cli-x64-self-contained",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
"-c",
"Release",
"-r",
"win-x64",
"--self-contained",
"true",
"/p:PublishSingleFile=true",
"/p:PublishTrimmed=false",
"-o",
"${workspaceFolder}/publish/x64"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "publish-all-x86",
"dependsOn": [
"publish-service-x86-self-contained",
"publish-cli-x86-self-contained",
"publish-tray-x86-self-contained"
],
"dependsOrder": "parallel",
"group": "build",
"problemMatcher": []
},
{
"label": "publish-all-x64",
"dependsOn": [
"publish-service-x64-self-contained",
"publish-cli-x64-self-contained",
"publish-tray-x64-self-contained"
],
"dependsOrder": "parallel",
"group": "build",
"problemMatcher": []
},
// ============================================
// RUN/TEST TASKS
// ============================================
{
"label": "run-service",
"command": "dotnet",
"type": "process",
"args": [
"run",
"--project",
"${workspaceFolder}/PrintService/PrintService.csproj"
],
"problemMatcher": "$msCompile",
"group": "test"
},
{
"label": "run-cli",
"command": "dotnet",
"type": "process",
"args": [
"run",
"--project",
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
"--",
// Add CLI arguments after --
// Example: "-f", "${workspaceFolder}/Captures/test.txt", "-t", "invoice"
],
"problemMatcher": "$msCompile",
"group": "test"
},
{
"label": "test-pdf-output",
"command": "dotnet",
"type": "process",
"args": [
"run",
"--project",
"${workspaceFolder}/PrintServiceCLI/PrintServiceCLI.csproj",
"--",
"-f",
"${workspaceFolder}/Captures/~DARLENE~20260514@072912.TXT",
"-t",
"test",
"--skip-service-check"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "test",
"isDefault": true
}
},
// ============================================
// WATCH TASKS (Auto-rebuild on changes)
// ============================================
{
"label": "watch-service",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/PrintService/PrintService.csproj"
],
"problemMatcher": "$msCompile",
"group": "build"
},
// ============================================
// TRAY APP TASKS
// ============================================
{
"label": "build-tray-debug",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Debug"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build-tray-release-x86",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Release",
"-r",
"win-x86"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "build-tray-release-x64",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-c",
"Release",
"-r",
"win-x64"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "run-tray",
"command": "dotnet",
"type": "process",
"args": [
"run",
"--project",
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj"
],
"problemMatcher": "$msCompile",
"group": "test"
},
{
"label": "publish-tray-x86-self-contained",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
"-c",
"Release",
"-r",
"win-x86",
"--self-contained",
"true",
"/p:PublishSingleFile=true",
"/p:PublishTrimmed=false",
"-o",
"${workspaceFolder}/publish/x86"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "publish-tray-x64-self-contained",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj",
"-c",
"Release",
"-r",
"win-x64",
"--self-contained",
"true",
"/p:PublishSingleFile=true",
"/p:PublishTrimmed=false",
"-o",
"${workspaceFolder}/publish/x64"
],
"problemMatcher": "$msCompile",
"group": "build"
},
// ============================================
// CLEAN TASKS
// ============================================
{
"label": "clean",
"command": "dotnet",
"type": "process",
"args": [
"clean",
"${workspaceFolder}/PrintService.sln"
],
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "clean-publish",
"type": "shell",
"command": "Remove-Item",
"args": [
"-Path",
"${workspaceFolder}/publish",
"-Recurse",
"-Force",
"-ErrorAction",
"SilentlyContinue"
],
"windows": {
"options": {
"shell": {
"executable": "powershell.exe"
}
}
},
"problemMatcher": []
}
]
}
-220
View File
@@ -1,220 +0,0 @@
# Harbor/Clipper Print Service - Implementation Plan
**TL;DR**: Build a persistent Windows service that monitors for new print capture files, immediately moves them to prevent overwrites, queues them for ordered processing, and sends multi-tray print jobs to laser printers with document-type-specific configurations.
**Recommended Approach**: C#/.NET 8+ Windows Service with FileSystemWatcher, print queue, and configurable document templates. C# is completely free (.NET SDK), has superior Windows printer APIs, and excellent service infrastructure.
---
## Steps
### Phase 1: Service Foundation (Parallel work possible after step 1)
1. **Create Windows Service project structure** using .NET 8+ Worker Service template
- Use `BackgroundService` base class for hosting
- Configure as Windows Service with `Microsoft.Extensions.Hosting.WindowsServices`
- Set up dependency injection, logging, and configuration (appsettings.json)
2. **Implement file monitoring system** (*parallel with step 3*)
- `FileSystemWatcher` on Captures folder with `Created`, `Changed`, and `Renamed` events
- Debounce logic to handle rapid file creation (buffer 100-200ms)
- Immediate file move to processing queue folder with GUID-based naming to prevent collisions
- Include original filename, timestamp, and metadata in queue
3. **Design configuration system** (*parallel with step 2*)
- Document type definitions (invoice, order, delivery, etc.)
- Per-document-type settings: printer name, tray sequence, content transformations
- Global settings: queue folder paths, archive settings, retry policies
- JSON schema for easy editing
### Phase 2: Command Interface & Queue Management
4. **Create CLI command handler** (*depends on 1*)
- Parse `-f <filepath> -t <doctype> -o <ordernumber>` arguments
- Check if service is running via named pipe or TCP
- If running: send command to service via IPC
- If not running: log error or auto-start service
- Return exit code immediately
5. **Build print job queue** (*depends on 1*)
- Thread-safe queue with priority/ordering (FIFO by default, configurable)
- Persistent queue state (survive service restart)
- Job metadata: source file, document type, order number, timestamp, retry count
- Status tracking: pending, processing, completed, failed
6. **Implement IPC mechanism** (*depends on 4, 5*)
- Named pipe server listening for commands from CLI
- Deserialize command → create job → enqueue
- Return acknowledgment to caller
### Phase 3: Printer Integration
7. **Implement queue-based printer control** (*depends on 5*)
- Use `System.Drawing.Printing.PrintDocument` with Windows print queue
- Multi-page document with dynamic `PageSettings.PaperSource` for tray selection
- Per-printer tray mapping configuration (map logical tray numbers to physical `PaperSource` indexes)
- Fallback handling when tray empty (configurable: fail job, use default tray, alert)
- Optional: Print to PDF for testing/archiving
8. **Build document processor with Graphics rendering** (*depends on 7*)
- Read queued capture file and parse text content
- Apply document-type-specific transformations:
- Line position adjustments using `Graphics.DrawString()` coordinates
- Text removal/replacement via regex patterns
- Font and formatting configuration per document type
- Future: header/footer injection, graphics overlay, logo placement
- Render each page with `Graphics` API for pixel-perfect control
- Generate multi-page `PrintDocument` with correct tray sequence
- Send to Windows print queue
9. **Add printer routing logic** (*depends on 7, 8*)
- Map document types to printer(s) and tray sequences
- Support multiple printer profiles per document type
- Configuration: `{ "invoice": { "printer": "LaserJet-1", "trays": [3,4,1,2] } }`
### Phase 4: Reliability & Operations
10. **Implement error handling & retry** (*depends on 5, 8*)
- Printer offline detection → requeue with exponential backoff
- File read errors → log and move to error folder
- Max retry limit → move to dead letter queue
- Detailed logging with correlation IDs
11. **Add file lifecycle management** (*depends on 2*)
- Post-processing: delete or move to archive folder (configurable per document type)
- Archive folder structure: organized by date, document type, or both
- Retention policy support (future: auto-cleanup old archives)
12. **Create service installer & deployment** (*depends on all previous*)
- MSI installer or PowerShell install script
- Service registration with `sc.exe` or WiX toolset
- Auto-start configuration
- Uninstall/upgrade support
### Phase 5: Testing & Validation
13. **System testing** (*depends on 12*)
- Test rapid file creation (simulate Harbor/Clipper speed)
- Verify no overwrites or missing files
- Confirm correct print order
- Test printer offline scenarios
- Validate tray sequences for each document type
14. **Integration with Harbor/Clipper** (*depends on 13*)
- Replace current Rust CLI calls with new CLI
- Monitor production for 24-48 hours
- Compare output quality and reliability
- Performance tuning if needed
---
## Relevant Files
**To be created**:
- `PrintService/Program.cs` — Service host and startup configuration
- `PrintService/Worker.cs` — Background service implementing FileSystemWatcher and queue processor
- `PrintService/Models/PrintJob.cs` — Job data model
- `PrintService/Models/DocumentConfig.cs` — Document type configuration schema
- `PrintService/Services/FileMonitorService.cs` — File watching and immediate move logic
- `PrintService/Services/PrintQueueService.cs` — Queue management and processing
- `PrintService/Services/PrinterService.cs` — Queue-based printer control with tray commands
- `PrintService/Services/DocumentProcessor.cs` — Text parsing and Graphics rendering
- `PrintService/Services/IpcService.cs` — Named pipe listener for CLI commands
- `PrintService/appsettings.json` — Configuration file for document types, printers, paths
- `PrintServiceCLI/Program.cs` — Command-line tool for sending commands to service
- `PrintService.sln` — Solution file
**Existing**:
- `Captures/` — Source folder for Harbor/Clipper output files (will be monitored)
---
## Verification
1. **Unit tests**: Queue ordering, configuration parsing, tray sequence generation
2. **Integration test**: Create 50 files in 10 seconds → verify all printed in order with correct trays
3. **Stress test**: 1000+ files → no overwrites, no missing files, proper ordering
4. **Manual test**: Print each document type to each configured printer and verify tray usage
5. **Production validation**: Monitor service for 48 hours, compare output count with Harbor/Clipper expected count
6. **Error scenario testing**: Unplug printer mid-job → verify retry → reconnect → verify job completes
---
## Decisions
**Language**: C#/.NET 8+
- **Rationale**: Free, best Windows service/printer APIs, excellent tooling, easier printer control than Rust
- **Alternative**: Could still use Rust with `windows-rs` crate, but printer control is more complex
**Architecture**: Service with IPC (Named Pipes)
- CLI tool sends commands → service processes asynchronously
- Decouples Harbor/Clipper from processing delays
**File handling**: Immediate move with GUID renaming
- **Critical**: Prevents timestamp collision overwrites
- Original metadata preserved in queue state
**Queue persistence**: JSON file or SQLite
- Survives service restart
- Allows inspection/recovery
**Configuration**: JSON file (appsettings.json)
- Editable without recompilation
- Per-document-type printer/tray/transformation settings
**Printer control**: PrintDocument with Windows print queue
- Per-page tray control via `PageSettings.PaperSource`
- Content rendered using `Graphics.DrawString()` for positioning control
- Jobs appear in Windows print queue (pausable, survives restarts)
- Can print to PDF for testing
**Scope included**:
- Windows service with file monitoring
- Command-line interface for Harbor/Clipper integration
- Multi-tray printer control via Windows print queue
- Configurable document routing
- Archive/delete options
- Error handling and retry logic
- Content transformation with Graphics rendering (line positioning, text removal)
**Scope excluded** (future enhancements):
- GUI management console (use config file + logs for now)
- Advanced document parsing/templating (start with regex transformations)
- Web API or remote monitoring
- Database storage (use file-based queue initially)
- Automatic printer discovery
- Print preview or validation UI
---
## Further Considerations
1. **Printer-specific tray mapping**: Need to identify your laser printer models and test `PaperSource` indexes. Do you know the printer makes/models?
2. **Content transformation complexity**: You mentioned looping line items. For Phase 1, suggest simple regex-based transformations. If parsing becomes complex, consider a template engine (Scriban, Handlebars.NET) in Phase 2.
3. **Testing environment**: Do you have a test laser printer or can we print to PDF initially to verify tray selection and content rendering work correctly?
---
## Benefits of Queue-Based Approach
-**Visibility**: Jobs appear in Windows print queue UI
-**Resilience**: Queue survives service restarts
-**Control**: Can pause/cancel jobs through Windows
-**Testing**: Print to PDF for verification without paper waste
-**Content Control**: `Graphics.DrawString()` gives pixel-perfect positioning for your line adjustments
-**Debugging**: Easier to troubleshoot than raw printer commands
-34
View File
@@ -1,34 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintService", "PrintService\PrintService.csproj", "{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintServiceCLI", "PrintServiceCLI\PrintServiceCLI.csproj", "{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintServiceTray", "PrintServiceTray\PrintServiceTray.csproj", "{42CF28FF-90E4-4912-B427-EA5F675F641E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FE05D84-E68E-4D43-AD80-7A5D8A59D975}.Release|Any CPU.Build.0 = Release|Any CPU
{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Debug|Any CPU.Build.0 = Debug|Any CPU
{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Release|Any CPU.ActiveCfg = Release|Any CPU
{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Release|Any CPU.Build.0 = Release|Any CPU
{42CF28FF-90E4-4912-B427-EA5F675F641E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42CF28FF-90E4-4912-B427-EA5F675F641E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42CF28FF-90E4-4912-B427-EA5F675F641E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42CF28FF-90E4-4912-B427-EA5F675F641E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
-57
View File
@@ -1,57 +0,0 @@
namespace PrintService.Models;
/// <summary>
/// Application settings loaded from appsettings.json
/// </summary>
public class AppSettings
{
/// <summary>
/// Path to monitor for incoming capture files
/// </summary>
public string CapturesPath { get; set; } = "Captures";
/// <summary>
/// Path for queued files (after moving from Captures)
/// </summary>
public string QueuePath { get; set; } = "Queue";
/// <summary>
/// Path for failed jobs
/// </summary>
public string ErrorPath { get; set; } = "Errors";
/// <summary>
/// Default archive path
/// </summary>
public string ArchivePath { get; set; } = "Archive";
/// <summary>
/// Maximum retry attempts before moving to error folder
/// </summary>
public int MaxRetryAttempts { get; set; } = 3;
/// <summary>
/// Debounce delay in milliseconds for file system watcher
/// </summary>
public int DebounceDelayMs { get; set; } = 200;
/// <summary>
/// Named pipe name for IPC communication
/// </summary>
public string PipeName { get; set; } = "PrintServicePipe";
/// <summary>
/// Document type configurations
/// </summary>
public List<DocumentConfig> DocumentTypes { get; set; } = new();
/// <summary>
/// Queue state file path
/// </summary>
public string QueueStateFile { get; set; } = "queue_state.json";
/// <summary>
/// Enable detailed logging
/// </summary>
public bool VerboseLogging { get; set; } = false;
}
-91
View File
@@ -1,91 +0,0 @@
namespace PrintService.Models;
/// <summary>
/// Configuration for document types
/// </summary>
public class DocumentConfig
{
/// <summary>
/// Name/identifier of the document type
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Printer name to send to
/// </summary>
public string PrinterName { get; set; } = string.Empty;
/// <summary>
/// Sequence of tray numbers to use (1-based, e.g., [3, 4, 1, 2])
/// These will be mapped to physical PaperSource indexes
/// </summary>
public int[] TraySequence { get; set; } = Array.Empty<int>();
/// <summary>
/// Font name to use for rendering
/// </summary>
public string FontName { get; set; } = "Courier New";
/// <summary>
/// Font size in points
/// </summary>
public float FontSize { get; set; } = 10f;
/// <summary>
/// Vertical offset adjustment (in pixels)
/// </summary>
public int VerticalOffset { get; set; } = 0;
/// <summary>
/// Horizontal offset adjustment (in pixels)
/// </summary>
public int HorizontalOffset { get; set; } = 0;
/// <summary>
/// Text transformation rules (regex patterns to remove/replace)
/// </summary>
public List<TextTransform> Transformations { get; set; } = new();
/// <summary>
/// Whether to archive files after printing (true) or delete them (false)
/// </summary>
public bool ArchiveAfterPrint { get; set; } = true;
/// <summary>
/// Archive folder path (if ArchiveAfterPrint is true)
/// </summary>
public string? ArchivePath { get; set; }
/// <summary>
/// Output folder for PDF files (when using "Microsoft Print to PDF")
/// If specified, PDFs will be saved to this folder automatically
/// </summary>
public string? OutputPath { get; set; }
/// <summary>
/// Skip post-processing (archive/delete) to leave files in queue for repeated testing
/// Useful for development and testing scenarios
/// </summary>
public bool SkipPostProcessing { get; set; } = false;
}
/// <summary>
/// Text transformation rule
/// </summary>
public class TextTransform
{
/// <summary>
/// Regex pattern to match
/// </summary>
public string Pattern { get; set; } = string.Empty;
/// <summary>
/// Replacement text (empty string to remove)
/// </summary>
public string Replacement { get; set; } = string.Empty;
/// <summary>
/// Description of what this transformation does
/// </summary>
public string Description { get; set; } = string.Empty;
}
-74
View File
@@ -1,74 +0,0 @@
namespace PrintService.Models;
/// <summary>
/// Represents a print job in the queue
/// </summary>
public class PrintJob
{
/// <summary>
/// Unique identifier for the job
/// </summary>
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// Path to the original capture file
/// </summary>
public string SourceFilePath { get; set; } = string.Empty;
/// <summary>
/// Path to the queued file (after being moved with GUID name)
/// </summary>
public string QueuedFilePath { get; set; } = string.Empty;
/// <summary>
/// Document type (invoice, order, delivery, etc.)
/// </summary>
public string DocumentType { get; set; } = string.Empty;
/// <summary>
/// Optional order number
/// </summary>
public string? OrderNumber { get; set; }
/// <summary>
/// When the job was created
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>
/// When the job was last updated
/// </summary>
public DateTime UpdatedAt { get; set; } = DateTime.Now;
/// <summary>
/// Current status of the job
/// </summary>
public PrintJobStatus Status { get; set; } = PrintJobStatus.Pending;
/// <summary>
/// Number of times this job has been attempted
/// </summary>
public int RetryCount { get; set; } = 0;
/// <summary>
/// Last error message if failed
/// </summary>
public string? LastError { get; set; }
/// <summary>
/// Original filename before moving to queue
/// </summary>
public string OriginalFilename { get; set; } = string.Empty;
}
/// <summary>
/// Status of a print job
/// </summary>
public enum PrintJobStatus
{
Pending,
Processing,
Completed,
Failed,
RetryScheduled
}
-93
View File
@@ -1,93 +0,0 @@
namespace PrintService.Models;
/// <summary>
/// Root configuration loaded from printer-config.json
/// </summary>
public class PrinterConfiguration
{
public Dictionary<string, DocumentTypeConfig> DocumentTypes { get; set; } = new();
}
/// <summary>
/// Configuration for a single document type (replaces DocumentConfig for printer/tray mapping)
/// </summary>
public class DocumentTypeConfig
{
public string Name { get; set; } = string.Empty;
public List<PageConfig> Pages { get; set; } = new();
// Rendering settings
public string FontName { get; set; } = "Courier New";
public float FontSize { get; set; } = 10f;
public int HorizontalOffset { get; set; } = 0;
public int VerticalOffset { get; set; } = 0;
// Post-processing settings
public bool ArchiveAfterPrint { get; set; } = true;
public string? ArchivePath { get; set; }
public string? OutputPath { get; set; }
public bool SkipPostProcessing { get; set; } = false;
// Text transformation rules
public List<TextTransform> Transformations { get; set; } = new();
// Rust transformation properties
/// <summary>
/// Left margin/indent for all lines (in 1/100 inch units)
/// </summary>
public int LinePadding { get; set; } = 0;
/// <summary>
/// Remove both "Order #: " label and the order number entirely
/// </summary>
public bool RemoveOrderNumber { get; set; } = false;
/// <summary>
/// Remove "Order #: " label but keep the order number (and make it bold)
/// </summary>
public bool RemoveOrderNumberLabel { get; set; } = false;
/// <summary>
/// Add spacing before the order number (shifts it right)
/// </summary>
public string IndentOrderNumber { get; set; } = string.Empty;
/// <summary>
/// Vertical position adjustments per line (line number → adjustment in 1/100 inch)
/// Negative values move up, positive values move down
/// </summary>
public Dictionary<int, int> RowShift { get; set; } = new();
/// <summary>
/// Horizontal character trimming per line (line number → trim configuration)
/// Removes characters from Start to End indices
/// </summary>
public Dictionary<int, RowTrimConfig> RowTrim { get; set; } = new();
}
/// <summary>
/// Configuration for a single page (printer and tray assignment)
/// </summary>
public class PageConfig
{
public int PageNumber { get; set; }
public string PrinterName { get; set; } = string.Empty;
public int TrayNumber { get; set; }
public string? TrayLabel { get; set; }
}
/// <summary>
/// Configuration for row trimming (horizontal character removal)
/// </summary>
public class RowTrimConfig
{
/// <summary>
/// Start index of characters to remove (0-based)
/// </summary>
public int Start { get; set; }
/// <summary>
/// End index of characters to remove (exclusive)
/// </summary>
public int End { get; set; }
}
-21
View File
@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-PrintService-3e06b535-3a68-4806-9c5b-3a32bde82bf9</UserSecretsId>
<OutputType>Exe</OutputType>
</PropertyGroup>
<!-- Use x86 only for Release builds (for 32-bit target machines) -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.0" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
</ItemGroup>
</Project>
-36
View File
@@ -1,36 +0,0 @@
using PrintService;
using PrintService.Models;
using PrintService.Services;
using Microsoft.Extensions.Options;
IHost host = Host.CreateDefaultBuilder(args)
.UseWindowsService(options =>
{
options.ServiceName = "LAAPC Print Service";
})
.ConfigureServices((hostContext, services) =>
{
// Bind configuration
services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));
// Register services
services.AddSingleton<ConfigurationService>();
services.AddSingleton<PrintQueueService>();
services.AddSingleton<FileMonitorService>();
services.AddSingleton<ContentTransformer>();
services.AddSingleton<PrinterService>();
services.AddSingleton<DocumentProcessor>();
services.AddSingleton<IpcService>();
// Register the main worker
services.AddHostedService<Worker>();
})
.Build();
// Ensure directories exist
var config = host.Services.GetRequiredService<IOptions<AppSettings>>().Value;
Directory.CreateDirectory(config.QueuePath);
Directory.CreateDirectory(config.ErrorPath);
Directory.CreateDirectory(config.ArchivePath);
host.Run();
@@ -1,11 +0,0 @@
{
"profiles": {
"PrintService": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
@@ -1,87 +0,0 @@
using System.Text.Json;
using PrintService.Models;
namespace PrintService.Services;
/// <summary>
/// Loads printer-config.json (global and local), merging local over global.
/// Mirrors the same logic used in PrintServiceTray.
/// </summary>
public class ConfigurationService
{
private readonly string _globalConfigPath;
private readonly string _localConfigPath;
private readonly ILogger<ConfigurationService> _logger;
public ConfigurationService(ILogger<ConfigurationService> logger)
{
_logger = logger;
_globalConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"LAAPC", "printer-config.json");
_localConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LAAPC", "printer-config.json");
}
/// <summary>
/// Load merged configuration. Returns an empty config (not null) if no files exist.
/// </summary>
public PrinterConfiguration Load()
{
var config = new PrinterConfiguration();
if (File.Exists(_globalConfigPath))
{
try
{
var json = File.ReadAllText(_globalConfigPath);
var global = JsonSerializer.Deserialize<PrinterConfiguration>(json);
if (global != null)
config = global;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read global printer config from {Path}", _globalConfigPath);
}
}
if (File.Exists(_localConfigPath))
{
try
{
var json = File.ReadAllText(_localConfigPath);
var local = JsonSerializer.Deserialize<PrinterConfiguration>(json);
if (local != null)
{
// Local overrides global per document type
foreach (var kvp in local.DocumentTypes)
config.DocumentTypes[kvp.Key] = kvp.Value;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read local printer config from {Path}", _localConfigPath);
}
}
if (config.DocumentTypes.Count == 0)
_logger.LogWarning("No document types found in printer-config.json. Checked: {Global} and {Local}",
_globalConfigPath, _localConfigPath);
return config;
}
/// <summary>
/// Get a single document type config by name (case-insensitive).
/// </summary>
public DocumentTypeConfig? GetDocumentType(string documentType)
{
var config = Load();
return config.DocumentTypes
.FirstOrDefault(kvp => kvp.Key.Equals(documentType, StringComparison.OrdinalIgnoreCase))
.Value;
}
}
-241
View File
@@ -1,241 +0,0 @@
using PrintService.Models;
using System.Text;
using System.Text.RegularExpressions;
namespace PrintService.Services;
/// <summary>
/// Handles content transformations from Rust CLI (cgwprint) logic
/// </summary>
public class ContentTransformer
{
private readonly ILogger<ContentTransformer> _logger;
public ContentTransformer(ILogger<ContentTransformer> logger)
{
_logger = logger;
}
/// <summary>
/// Transform document content according to configuration
/// </summary>
public TransformedDocument TransformContent(string content, DocumentTypeConfig config, string? orderNumber = null)
{
// Step 1: Normalize font codes
content = NormalizeFontCodes(content);
// Step 2: Order number manipulation (if order number provided)
if (!string.IsNullOrEmpty(orderNumber))
{
content = ManipulateOrderNumber(content, orderNumber, config);
}
// Step 3: Parse font codes to identify bold/normal segments
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
// Step 4: Apply row trim (modifies line content)
lines = ApplyRowTrim(lines, config.RowTrim);
// Step 5: Parse lines into segments with font styling
var styledLines = ParseStyledLines(lines);
return new TransformedDocument
{
Lines = styledLines,
RowShifts = config.RowShift,
LinePadding = config.LinePadding
};
}
/// <summary>
/// Step 1: Normalize font codes (pre-processing)
/// </summary>
private string NormalizeFontCodes(string content)
{
// Prefix with normal font at start
content = "\x1Bw0" + content;
// Normalize uppercase W to lowercase w
content = content.Replace("\x1BW1", "\x1Bw1");
content = content.Replace("\x1BW0", "\x1Bw0");
// Deduplicate consecutive bold sequences
content = Regex.Replace(content, @"(\x1Bw1)+", "\x1Bw1");
// Deduplicate consecutive normal sequences
content = Regex.Replace(content, @"(\x1Bw0)+", "\x1Bw0");
return content;
}
/// <summary>
/// Step 2: Order number manipulation
/// </summary>
private string ManipulateOrderNumber(string content, string orderNumber, DocumentTypeConfig config)
{
var orderPattern = $"Order #: {orderNumber}";
// Only ONE operation executes based on configuration flags
if (config.RemoveOrderNumber)
{
// Replace entire "Order #: 12345" with spaces
var replacement = new string(' ', orderPattern.Length);
content = content.Replace(orderPattern, replacement);
_logger.LogDebug("Removed order number completely: {OrderNumber}", orderNumber);
}
else if (config.RemoveOrderNumberLabel)
{
// Replace "Order #: " with spaces, keep number and make it bold
var labelLength = "Order #: ".Length;
var spaces = new string(' ', labelLength);
var replacement = $"{spaces}\x1Bw1{orderNumber}\x1Bw0";
content = content.Replace(orderPattern, replacement);
_logger.LogDebug("Removed order number label, kept bold number: {OrderNumber}", orderNumber);
}
else if (!string.IsNullOrEmpty(config.IndentOrderNumber))
{
// Find and indent the order number (assumes it's already bold)
var boldPattern = $@"\x1Bw1{Regex.Escape(orderNumber)}\x1Bw0";
content = Regex.Replace(content, boldPattern, $"{config.IndentOrderNumber}\x1Bw1{orderNumber}\x1Bw0");
_logger.LogDebug("Indented order number: {OrderNumber}", orderNumber);
}
return content;
}
/// <summary>
/// Step 3: Apply row trim (horizontal character removal)
/// </summary>
private string[] ApplyRowTrim(string[] lines, Dictionary<int, RowTrimConfig> rowTrim)
{
if (rowTrim.Count == 0)
return lines;
var result = new string[lines.Length];
for (int i = 0; i < lines.Length; i++)
{
if (rowTrim.TryGetValue(i, out var trim))
{
var line = lines[i];
if (trim.Start >= 0 && trim.End <= line.Length && trim.Start < trim.End)
{
// Remove characters from Start to End
result[i] = line.Substring(0, trim.Start) + line.Substring(trim.End);
_logger.LogDebug("Trimmed line {LineNum}: removed chars {Start}-{End}", i, trim.Start, trim.End);
}
else
{
result[i] = line;
_logger.LogWarning("Invalid row trim config for line {LineNum}: Start={Start}, End={End}, LineLength={Length}",
i, trim.Start, trim.End, line.Length);
}
}
else
{
result[i] = lines[i];
}
}
return result;
}
/// <summary>
/// Step 4: Parse lines into segments with font styling
/// </summary>
private List<StyledLine> ParseStyledLines(string[] lines)
{
var styledLines = new List<StyledLine>();
foreach (var line in lines)
{
var segments = new List<TextSegment>();
var currentText = new StringBuilder();
var currentStyle = FontStyle.Regular;
for (int i = 0; i < line.Length; i++)
{
// Check for font code escape sequence
if (i + 2 < line.Length && line[i] == '\x1B' && line[i + 1] == 'w')
{
// Save current segment if any
if (currentText.Length > 0)
{
segments.Add(new TextSegment
{
Text = currentText.ToString(),
Style = currentStyle
});
currentText.Clear();
}
// Parse font code
char code = line[i + 2];
if (code == '1')
{
currentStyle = FontStyle.Bold;
}
else if (code == '0')
{
currentStyle = FontStyle.Regular;
}
// Skip the escape sequence
i += 2;
}
else
{
currentText.Append(line[i]);
}
}
// Add final segment
if (currentText.Length > 0 || segments.Count == 0)
{
segments.Add(new TextSegment
{
Text = currentText.ToString(),
Style = currentStyle
});
}
styledLines.Add(new StyledLine { Segments = segments });
}
return styledLines;
}
}
/// <summary>
/// Transformed document with styled lines and layout adjustments
/// </summary>
public class TransformedDocument
{
public List<StyledLine> Lines { get; set; } = new();
public Dictionary<int, int> RowShifts { get; set; } = new();
public int LinePadding { get; set; }
}
/// <summary>
/// A line with styled text segments
/// </summary>
public class StyledLine
{
public List<TextSegment> Segments { get; set; } = new();
}
/// <summary>
/// A text segment with font styling
/// </summary>
public class TextSegment
{
public string Text { get; set; } = string.Empty;
public FontStyle Style { get; set; } = FontStyle.Regular;
}
public enum FontStyle
{
Regular,
Bold
}
-112
View File
@@ -1,112 +0,0 @@
using Microsoft.Extensions.Options;
using PrintService.Models;
using System.Drawing;
using System.Drawing.Printing;
using System.Text.RegularExpressions;
namespace PrintService.Services;
/// <summary>
/// Processes documents and renders them for printing
/// </summary>
public class DocumentProcessor
{
private readonly AppSettings _settings;
private readonly ConfigurationService _configService;
private readonly ILogger<DocumentProcessor> _logger;
public DocumentProcessor(
IOptions<AppSettings> settings,
ConfigurationService configService,
ILogger<DocumentProcessor> logger)
{
_settings = settings.Value;
_configService = configService;
_logger = logger;
}
/// <summary>
/// Process and transform document content
/// </summary>
public string ProcessDocument(string content, DocumentTypeConfig config)
{
var processed = content;
// Apply text transformations
foreach (var transform in config.Transformations)
{
try
{
processed = Regex.Replace(processed, transform.Pattern, transform.Replacement);
if (_settings.VerboseLogging)
{
_logger.LogDebug("Applied transformation: {Description}", transform.Description);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to apply transformation: {Pattern}", transform.Pattern);
}
}
return processed;
}
/// <summary>
/// Get document configuration by type from printer-config.json
/// </summary>
public DocumentTypeConfig? GetDocumentConfig(string documentType)
{
return _configService.GetDocumentType(documentType);
}
/// <summary>
/// Read file content
/// </summary>
public string ReadFile(string filePath)
{
return File.ReadAllText(filePath);
}
/// <summary>
/// Archive or delete file after processing
/// </summary>
public void PostProcess(PrintJob job, DocumentTypeConfig config)
{
try
{
// Skip post-processing if configured (useful for testing)
if (config.SkipPostProcessing)
{
_logger.LogInformation("Skipping post-processing for job {JobId} (file remains in queue)", job.Id);
return;
}
if (config.ArchiveAfterPrint && !string.IsNullOrEmpty(config.ArchivePath))
{
Directory.CreateDirectory(config.ArchivePath);
var archiveFileName = Path.Combine(config.ArchivePath,
$"{DateTime.Now:yyyyMMdd_HHmmss}_{job.OriginalFilename}");
if (File.Exists(job.QueuedFilePath))
{
File.Move(job.QueuedFilePath, archiveFileName);
_logger.LogInformation("Archived job {JobId} to {Path}", job.Id, archiveFileName);
}
}
else
{
if (File.Exists(job.QueuedFilePath))
{
File.Delete(job.QueuedFilePath);
_logger.LogInformation("Deleted file for job {JobId}", job.Id);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to post-process job {JobId}", job.Id);
}
}
}
-205
View File
@@ -1,205 +0,0 @@
using Microsoft.Extensions.Options;
using PrintService.Models;
namespace PrintService.Services;
/// <summary>
/// Monitors the Captures folder for new files and moves them to queue
/// </summary>
public class FileMonitorService : IDisposable
{
private readonly AppSettings _settings;
private readonly PrintQueueService _queueService;
private readonly ILogger<FileMonitorService> _logger;
private FileSystemWatcher? _watcher;
private readonly Dictionary<string, DateTime> _pendingFiles = new();
private readonly Timer _debounceTimer;
private readonly object _lock = new();
public FileMonitorService(
IOptions<AppSettings> settings,
PrintQueueService queueService,
ILogger<FileMonitorService> logger)
{
_settings = settings.Value;
_queueService = queueService;
_logger = logger;
// Timer for debouncing file system events
_debounceTimer = new Timer(ProcessPendingFiles, null,
TimeSpan.FromMilliseconds(_settings.DebounceDelayMs),
TimeSpan.FromMilliseconds(_settings.DebounceDelayMs));
}
/// <summary>
/// Start monitoring the Captures folder
/// </summary>
public void Start()
{
if (!Directory.Exists(_settings.CapturesPath))
{
Directory.CreateDirectory(_settings.CapturesPath);
_logger.LogInformation("Created captures directory: {Path}", _settings.CapturesPath);
}
_watcher = new FileSystemWatcher(_settings.CapturesPath)
{
Filter = "*.TXT",
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime,
EnableRaisingEvents = true
};
_watcher.Created += OnFileCreated;
_watcher.Changed += OnFileChanged;
_logger.LogInformation("Started monitoring: {Path}", _settings.CapturesPath);
}
/// <summary>
/// Stop monitoring
/// </summary>
public void Stop()
{
if (_watcher != null)
{
_watcher.EnableRaisingEvents = false;
_watcher.Dispose();
_watcher = null;
}
_logger.LogInformation("Stopped monitoring");
}
private void OnFileCreated(object sender, FileSystemEventArgs e)
{
lock (_lock)
{
_pendingFiles[e.FullPath] = DateTime.Now;
if (_settings.VerboseLogging)
{
_logger.LogDebug("File created: {Path}", e.FullPath);
}
}
}
private void OnFileChanged(object sender, FileSystemEventArgs e)
{
lock (_lock)
{
_pendingFiles[e.FullPath] = DateTime.Now;
if (_settings.VerboseLogging)
{
_logger.LogDebug("File changed: {Path}", e.FullPath);
}
}
}
/// <summary>
/// Process files after debounce delay
/// </summary>
private void ProcessPendingFiles(object? state)
{
List<string> filesToProcess;
lock (_lock)
{
var cutoff = DateTime.Now.AddMilliseconds(-_settings.DebounceDelayMs);
filesToProcess = _pendingFiles
.Where(kvp => kvp.Value <= cutoff)
.Select(kvp => kvp.Key)
.ToList();
foreach (var file in filesToProcess)
{
_pendingFiles.Remove(file);
}
}
foreach (var filePath in filesToProcess)
{
ProcessFile(filePath);
}
}
/// <summary>
/// Process a single file: move to queue with GUID name
/// </summary>
private void ProcessFile(string filePath)
{
try
{
if (!File.Exists(filePath))
{
return; // File may have been moved or deleted
}
var fileName = Path.GetFileName(filePath);
var jobId = Guid.NewGuid();
var queuedFileName = $"{jobId}.txt";
var queuedPath = Path.Combine(_settings.QueuePath, queuedFileName);
// Move file to queue with GUID name to prevent collisions
File.Move(filePath, queuedPath);
_logger.LogInformation("Moved {FileName} to queue as {QueuedFile}", fileName, queuedFileName);
// Note: Job will be created when CLI sends command with document type
// For now, we just have the file safely in the queue folder
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process file: {Path}", filePath);
}
}
/// <summary>
/// Manually process a file (called from IPC when CLI provides metadata)
/// </summary>
public PrintJob CreateJobFromFile(string filePath, string documentType, string? orderNumber)
{
var fileName = Path.GetFileName(filePath);
var jobId = Guid.NewGuid();
var queuedFileName = $"{jobId}.txt";
var queuedPath = Path.Combine(_settings.QueuePath, queuedFileName);
// Check if this document type has SkipPostProcessing enabled
var config = _settings.DocumentTypes.FirstOrDefault(dt =>
dt.Name.Equals(documentType, StringComparison.OrdinalIgnoreCase));
if (config?.SkipPostProcessing == true)
{
// Copy instead of move for testing scenarios
File.Copy(filePath, queuedPath, overwrite: true);
_logger.LogInformation("Copied {FileName} to queue (test mode - source file preserved)", fileName);
}
else
{
// Move file to queue (normal operation)
File.Move(filePath, queuedPath, overwrite: true);
_logger.LogInformation("Moved {FileName} to queue", fileName);
}
var job = new PrintJob
{
Id = jobId,
SourceFilePath = filePath,
QueuedFilePath = queuedPath,
DocumentType = documentType,
OrderNumber = orderNumber,
OriginalFilename = fileName,
Status = PrintJobStatus.Pending,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
_logger.LogInformation("Created job {JobId} for {FileName} (type: {DocType})",
job.Id, fileName, documentType);
return job;
}
public void Dispose()
{
_debounceTimer?.Dispose();
Stop();
}
}
-340
View File
@@ -1,340 +0,0 @@
using Microsoft.Extensions.Options;
using PrintService.Models;
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
namespace PrintService.Services;
/// <summary>
/// Named pipe server for IPC communication with CLI
/// </summary>
public class IpcService : IDisposable
{
private readonly AppSettings _settings;
private readonly PrintQueueService _queueService;
private readonly FileMonitorService _fileMonitorService;
private readonly ILogger<IpcService> _logger;
private CancellationTokenSource? _cancellationTokenSource;
private Task? _listenerTask;
public IpcService(
IOptions<AppSettings> settings,
PrintQueueService queueService,
FileMonitorService fileMonitorService,
ILogger<IpcService> logger)
{
_settings = settings.Value;
_queueService = queueService;
_fileMonitorService = fileMonitorService;
_logger = logger;
}
/// <summary>
/// Start listening for IPC commands
/// </summary>
public void Start()
{
_cancellationTokenSource = new CancellationTokenSource();
_listenerTask = Task.Run(() => ListenForConnections(_cancellationTokenSource.Token));
_logger.LogInformation("IPC service started on pipe: {PipeName}", _settings.PipeName);
}
/// <summary>
/// Stop listening
/// </summary>
public void Stop()
{
_cancellationTokenSource?.Cancel();
_listenerTask?.Wait(TimeSpan.FromSeconds(5));
_logger.LogInformation("IPC service stopped");
}
/// <summary>
/// Listen for incoming connections
/// </summary>
private async Task ListenForConnections(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
NamedPipeServerStream? pipeServer = null;
try
{
pipeServer = new NamedPipeServerStream(
_settings.PipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Message,
PipeOptions.Asynchronous);
await pipeServer.WaitForConnectionAsync(cancellationToken);
// Handle client in separate task, passing ownership of pipeServer
var serverToHandle = pipeServer;
pipeServer = null; // Clear reference so we don't dispose it in catch
_ = Task.Run(async () =>
{
using (serverToHandle)
{
await HandleClient(serverToHandle);
}
}, cancellationToken);
}
catch (OperationCanceledException)
{
pipeServer?.Dispose();
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in IPC listener");
pipeServer?.Dispose();
await Task.Delay(1000, cancellationToken);
}
}
}
/// <summary>
/// Handle a client connection
/// </summary>
private async Task HandleClient(NamedPipeServerStream pipeServer)
{
try
{
if (!pipeServer.IsConnected)
{
return;
}
// Read command
var buffer = new byte[4096];
var bytesRead = await pipeServer.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
return; // Client disconnected
}
var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
if (_settings.VerboseLogging)
{
_logger.LogDebug("Received IPC message: {Message}", message);
}
// Try to determine command type
var commandType = DetermineCommandType(message);
if (commandType == "GetQueueStatus")
{
var status = GetQueueStatus();
await SendQueueStatusResponse(pipeServer, status);
return;
}
// Handle print command
var command = JsonSerializer.Deserialize<PrintCommand>(message);
if (command == null)
{
await SendResponse(pipeServer, false, "Invalid command format");
return;
}
// Process command
var result = ProcessCommand(command);
await SendResponse(pipeServer, result.Success, result.Message);
}
catch (ObjectDisposedException)
{
// Client disconnected, this is normal
if (_settings.VerboseLogging)
{
_logger.LogDebug("Client disconnected");
}
}
catch (IOException ex)
{
// Pipe communication error
if (_settings.VerboseLogging)
{
_logger.LogDebug(ex, "Pipe communication error");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling IPC client");
try
{
if (pipeServer.IsConnected)
{
await SendResponse(pipeServer, false, $"Error: {ex.Message}");
}
}
catch { }
}
}
/// <summary>
/// Determine the type of command from JSON message
/// </summary>
private string? DetermineCommandType(string message)
{
try
{
using var doc = JsonDocument.Parse(message);
if (doc.RootElement.TryGetProperty("Command", out var cmdProp))
{
return cmdProp.GetString();
}
}
catch { }
return null;
}
/// <summary>
/// Get current queue status
/// </summary>
private QueueStatusResponse GetQueueStatus()
{
try
{
var jobs = _queueService.GetAllJobs();
var jobStatuses = jobs.Select(j => new JobStatus
{
Id = j.Id.ToString(),
FileName = j.OriginalFilename,
DocumentType = j.DocumentType,
Status = j.Status.ToString(),
PrinterName = null, // Will be set when processing starts
CurrentPage = null,
TotalPages = null
}).ToList();
return new QueueStatusResponse
{
Success = true,
Jobs = jobStatuses
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get queue status");
return new QueueStatusResponse
{
Success = false,
Message = $"Error: {ex.Message}"
};
}
}
/// <summary>
/// Send queue status response
/// </summary>
private async Task SendQueueStatusResponse(NamedPipeServerStream pipeServer, QueueStatusResponse response)
{
var json = JsonSerializer.Serialize(response);
var bytes = Encoding.UTF8.GetBytes(json);
await pipeServer.WriteAsync(bytes, 0, bytes.Length);
await pipeServer.FlushAsync();
}
/// <summary>
/// Process a print command
/// </summary>
private (bool Success, string Message) ProcessCommand(PrintCommand command)
{
try
{
if (string.IsNullOrEmpty(command.FilePath) || !File.Exists(command.FilePath))
{
return (false, $"File not found: {command.FilePath}");
}
if (string.IsNullOrEmpty(command.DocumentType))
{
return (false, "Document type is required");
}
// Create job and enqueue
var job = _fileMonitorService.CreateJobFromFile(
command.FilePath,
command.DocumentType,
command.OrderNumber);
_queueService.Enqueue(job);
return (true, $"Job {job.Id} queued successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process command");
return (false, ex.Message);
}
}
/// <summary>
/// Send response back to client
/// </summary>
private async Task SendResponse(NamedPipeServerStream pipeServer, bool success, string message)
{
var response = new PrintResponse
{
Success = success,
Message = message
};
var json = JsonSerializer.Serialize(response);
var bytes = Encoding.UTF8.GetBytes(json);
await pipeServer.WriteAsync(bytes, 0, bytes.Length);
await pipeServer.FlushAsync();
}
public void Dispose()
{
Stop();
_cancellationTokenSource?.Dispose();
}
}
/// <summary>
/// Command sent from CLI to service
/// </summary>
public class PrintCommand
{
public string FilePath { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
public string? OrderNumber { get; set; }
}
/// <summary>
/// Response from service to CLI
/// </summary>
public class PrintResponse
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
}
/// <summary>
/// Queue status response for tray app
/// </summary>
public class QueueStatusResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public List<JobStatus> Jobs { get; set; } = new();
}
/// <summary>
/// Status of a single job
/// </summary>
public class JobStatus
{
public string Id { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string? PrinterName { get; set; }
public int? CurrentPage { get; set; }
public int? TotalPages { get; set; }
}
-175
View File
@@ -1,175 +0,0 @@
using Microsoft.Extensions.Options;
using PrintService.Models;
using System.Collections.Concurrent;
using System.Text.Json;
namespace PrintService.Services;
/// <summary>
/// Manages the print job queue with persistence
/// </summary>
public class PrintQueueService
{
private readonly ConcurrentQueue<PrintJob> _queue = new();
private readonly AppSettings _settings;
private readonly ILogger<PrintQueueService> _logger;
private readonly SemaphoreSlim _semaphore = new(1, 1);
public PrintQueueService(IOptions<AppSettings> settings, ILogger<PrintQueueService> logger)
{
_settings = settings.Value;
_logger = logger;
LoadQueueState();
}
/// <summary>
/// Enqueue a new print job
/// </summary>
public void Enqueue(PrintJob job)
{
_queue.Enqueue(job);
_logger.LogInformation("Job {JobId} enqueued for document type: {DocType}", job.Id, job.DocumentType);
SaveQueueState();
}
/// <summary>
/// Try to dequeue the next pending job
/// </summary>
public bool TryDequeue(out PrintJob? job)
{
bool result = _queue.TryDequeue(out job);
if (result && job != null)
{
_logger.LogInformation("Job {JobId} dequeued", job.Id);
SaveQueueState();
}
return result;
}
/// <summary>
/// Peek at the next job without removing it
/// </summary>
public bool TryPeek(out PrintJob? job)
{
return _queue.TryPeek(out job);
}
/// <summary>
/// Get count of jobs in queue
/// </summary>
public int Count => _queue.Count;
/// <summary>
/// Get all jobs in the queue (snapshot)
/// </summary>
public PrintJob[] GetAllJobs()
{
return _queue.ToArray();
}
/// <summary>
/// Save queue state to disk
/// </summary>
private void SaveQueueState()
{
try
{
_semaphore.Wait();
var jobs = _queue.ToArray();
var json = JsonSerializer.Serialize(jobs, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_settings.QueueStateFile, json);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save queue state");
}
finally
{
_semaphore.Release();
}
}
/// <summary>
/// Load queue state from disk
/// </summary>
private void LoadQueueState()
{
try
{
if (File.Exists(_settings.QueueStateFile))
{
var json = File.ReadAllText(_settings.QueueStateFile);
var jobs = JsonSerializer.Deserialize<PrintJob[]>(json);
if (jobs != null)
{
foreach (var job in jobs)
{
// Only reload pending or retry-scheduled jobs
if (job.Status == PrintJobStatus.Pending || job.Status == PrintJobStatus.RetryScheduled)
{
_queue.Enqueue(job);
}
}
_logger.LogInformation("Loaded {Count} jobs from queue state", _queue.Count);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load queue state");
}
}
/// <summary>
/// Requeue a job (for retry)
/// </summary>
public void Requeue(PrintJob job)
{
job.RetryCount++;
job.Status = PrintJobStatus.RetryScheduled;
job.UpdatedAt = DateTime.Now;
if (job.RetryCount >= _settings.MaxRetryAttempts)
{
_logger.LogWarning("Job {JobId} exceeded max retries, moving to error folder", job.Id);
MoveToErrorFolder(job);
}
else
{
_logger.LogInformation("Requeueing job {JobId}, attempt {Retry}", job.Id, job.RetryCount);
_queue.Enqueue(job);
SaveQueueState();
}
}
/// <summary>
/// Move a failed job to error folder
/// </summary>
private void MoveToErrorFolder(PrintJob job)
{
try
{
var errorFileName = Path.Combine(_settings.ErrorPath,
$"{Path.GetFileNameWithoutExtension(job.OriginalFilename)}_{job.Id}.txt");
if (File.Exists(job.QueuedFilePath))
{
File.Move(job.QueuedFilePath, errorFileName);
}
// Save error details
var errorInfo = Path.Combine(_settings.ErrorPath,
$"{Path.GetFileNameWithoutExtension(job.OriginalFilename)}_{job.Id}_error.json");
var json = JsonSerializer.Serialize(job, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(errorInfo, json);
job.Status = PrintJobStatus.Failed;
SaveQueueState();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to move job {JobId} to error folder", job.Id);
}
}
}
-258
View File
@@ -1,258 +0,0 @@
using Microsoft.Extensions.Options;
using PrintService.Models;
using System.Drawing;
using System.Drawing.Printing;
namespace PrintService.Services;
/// <summary>
/// Handles printing to Windows print queue with tray control
/// </summary>
public class PrinterService
{
private readonly AppSettings _settings;
private readonly ILogger<PrinterService> _logger;
private readonly ContentTransformer _transformer;
public PrinterService(
IOptions<AppSettings> settings,
ILogger<PrinterService> logger,
ContentTransformer transformer)
{
_settings = settings.Value;
_logger = logger;
_transformer = transformer;
}
/// <summary>
/// Print document using the new DocumentTypeConfig (printer-config.json).
/// Each PageConfig entry defines one output copy per original content page,
/// so a 4-page document with 4 PageConfig entries produces 16 output pages.
/// </summary>
public void Print(string content, DocumentTypeConfig config, string originalFileName, string? orderNumber = null)
{
if (config.Pages.Count == 0)
{
throw new InvalidOperationException("No pages/trays defined for document type");
}
// Transform content according to Rust CLI logic
var transformed = _transformer.TransformContent(content, config, orderNumber);
var linesPerPage = CalculateLinesPerPage(config.FontSize);
var totalOriginalPages = Math.Max(1, (int)Math.Ceiling(transformed.Lines.Count / (double)linesPerPage));
var originalPageIndex = 0;
var copyIndex = 0; // index into config.Pages (one entry per tray copy)
// All pages within one document type share the same printer in typical use,
// but PageConfig supports per-page overrides — use the first page's printer for
// the PrintDocument; tray (PaperSource) is set per page in the event handler.
var primaryPrinterName = config.Pages[0].PrinterName;
var printDoc = new PrintDocument
{
PrinterSettings = {
PrinterName = primaryPrinterName,
Duplex = Duplex.Simplex // Force single-sided printing (no duplexing)
}
};
// Handle PDF output if using Microsoft Print to PDF
if (primaryPrinterName.Equals("Microsoft Print to PDF", StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(config.OutputPath))
{
Directory.CreateDirectory(config.OutputPath);
var pdfFileName = Path.GetFileNameWithoutExtension(originalFileName) + ".pdf";
var pdfPath = Path.Combine(config.OutputPath, pdfFileName);
printDoc.PrinterSettings.PrintToFile = true;
printDoc.PrinterSettings.PrintFileName = pdfPath;
_logger.LogInformation("PDF will be saved to: {Path}", pdfPath);
}
// Verify primary printer exists
if (!PrinterSettings.InstalledPrinters.Cast<string>().Contains(primaryPrinterName))
{
throw new InvalidOperationException($"Printer '{primaryPrinterName}' not found");
}
// QueryPageSettings fires BEFORE PrintPage - set tray here
printDoc.QueryPageSettings += (sender, e) =>
{
if (e.PageSettings == null)
return;
var pageConfig = config.Pages[copyIndex];
try
{
var paperSource = GetPaperSource(printDoc.PrinterSettings, pageConfig.TrayNumber);
if (paperSource != null)
{
e.PageSettings.PaperSource = paperSource;
if (_settings.VerboseLogging)
{
_logger.LogDebug(
"Output page {OutputPage}: original page {OriginalPage}, tray {Tray} ({Label}) ({Source})",
(originalPageIndex * config.Pages.Count) + copyIndex + 1,
originalPageIndex + 1,
pageConfig.TrayNumber,
pageConfig.TrayLabel ?? "?",
paperSource.SourceName);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to set tray {Tray}, using default", pageConfig.TrayNumber);
}
};
printDoc.PrintPage += (sender, e) =>
{
if (e.Graphics == null)
return;
// Render the same original page for every copy before moving to next original page.
RenderPage(e.Graphics, transformed, originalPageIndex * linesPerPage, linesPerPage,
config.FontName, config.FontSize, config.HorizontalOffset, config.VerticalOffset);
copyIndex++;
if (copyIndex >= config.Pages.Count)
{
copyIndex = 0;
originalPageIndex++;
}
e.HasMorePages = originalPageIndex < totalOriginalPages;
};
_logger.LogInformation(
"Printing to {Printer}: {OriginalPages} original page(s) × {Copies} copies = {Total} output pages",
primaryPrinterName,
totalOriginalPages,
config.Pages.Count,
totalOriginalPages * config.Pages.Count);
printDoc.Print();
}
/// <summary>
/// Render page content using Graphics API with styled text and transformations
/// </summary>
private void RenderPage(Graphics graphics, TransformedDocument transformed, int startLine, int linesPerPage,
string fontName, float fontSize, int horizontalOffset, int verticalOffset)
{
var normalFont = new Font(fontName, fontSize, System.Drawing.FontStyle.Regular);
var boldFont = new Font(fontName, fontSize, System.Drawing.FontStyle.Bold);
var brush = Brushes.Black;
var lineHeight = normalFont.GetHeight(graphics);
// Convert line padding from 1/100 inch to pixels (assuming 96 DPI for screen, but printers use their own DPI)
// For printers, Graphics.DpiX will give the actual printer DPI
var linePaddingPixels = (transformed.LinePadding / 100f) * graphics.DpiX;
var endLine = Math.Min(startLine + linesPerPage, transformed.Lines.Count);
// Track cumulative vertical shift
float cumulativeShift = 0;
for (int lineIndex = startLine; lineIndex < endLine; lineIndex++)
{
if (lineIndex >= transformed.Lines.Count)
break;
var styledLine = transformed.Lines[lineIndex];
// Apply row shift if defined for this line
if (transformed.RowShifts.TryGetValue(lineIndex, out var shift))
{
// Convert shift from 1/100 inch to pixels
var shiftPixels = (shift / 100f) * graphics.DpiY;
cumulativeShift += shiftPixels;
if (_settings.VerboseLogging)
{
_logger.LogDebug("Line {LineNum}: applying row shift {Shift} units ({Pixels} px)",
lineIndex, shift, shiftPixels);
}
}
// Calculate Y position with offsets and shifts
var y = verticalOffset + ((lineIndex - startLine) * lineHeight) + cumulativeShift;
// Start X position with horizontal offset and line padding
var x = (float)horizontalOffset + linePaddingPixels;
// Render each text segment with appropriate font style
foreach (var segment in styledLine.Segments)
{
var font = segment.Style == Services.FontStyle.Bold ? boldFont : normalFont;
if (!string.IsNullOrEmpty(segment.Text))
{
graphics.DrawString(segment.Text, font, brush, x, y);
// Measure text width to advance X position for next segment
var textSize = graphics.MeasureString(segment.Text, font);
x += textSize.Width;
}
}
}
normalFont.Dispose();
boldFont.Dispose();
}
/// <summary>
/// Calculate lines per page based on font size
/// </summary>
private int CalculateLinesPerPage(float fontSize)
{
// Estimate: standard letter size is 11 inches, at 10pt font ~66 lines
var estimatedLineHeight = fontSize * 1.2f; // points
var pageHeightInPoints = 11 * 72; // 11 inches * 72 points per inch
return (int)Math.Floor(pageHeightInPoints / estimatedLineHeight);
}
/// <summary>
/// Get PaperSource by logical tray number
/// </summary>
private PaperSource? GetPaperSource(PrinterSettings printerSettings, int trayNumber)
{
// Try to find by RawKind (tray number)
foreach (PaperSource source in printerSettings.PaperSources)
{
// Some printers use RawKind directly as tray number
if (source.RawKind == trayNumber ||
source.RawKind == (trayNumber + 256)) // Some drivers offset by 256
{
return source;
}
}
// Fallback: use by index if available
if (trayNumber > 0 && trayNumber <= printerSettings.PaperSources.Count)
{
return printerSettings.PaperSources[trayNumber - 1];
}
_logger.LogWarning("Could not map tray {Tray} to PaperSource", trayNumber);
return null;
}
/// <summary>
/// List available paper sources for a printer (for debugging/configuration)
/// </summary>
public void ListPaperSources(string printerName)
{
var printDoc = new PrintDocument { PrinterSettings = { PrinterName = printerName } };
_logger.LogInformation("Available paper sources for {Printer}:", printerName);
foreach (PaperSource source in printDoc.PrinterSettings.PaperSources)
{
_logger.LogInformation(" - {Name} (RawKind: {Kind})", source.SourceName, source.RawKind);
}
}
}
-157
View File
@@ -1,157 +0,0 @@
using PrintService.Models;
using PrintService.Services;
namespace PrintService;
/// <summary>
/// Main worker service that coordinates all components
/// </summary>
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly PrintQueueService _queueService;
private readonly FileMonitorService _fileMonitorService;
private readonly PrinterService _printerService;
private readonly DocumentProcessor _documentProcessor;
private readonly IpcService _ipcService;
public Worker(
ILogger<Worker> logger,
PrintQueueService queueService,
FileMonitorService fileMonitorService,
PrinterService printerService,
DocumentProcessor documentProcessor,
IpcService ipcService)
{
_logger = logger;
_queueService = queueService;
_fileMonitorService = fileMonitorService;
_printerService = printerService;
_documentProcessor = documentProcessor;
_ipcService = ipcService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("LAAPC Print Service starting at: {time}", DateTimeOffset.Now);
try
{
// Start IPC server for CLI communication
_ipcService.Start();
// Start file monitoring (optional - mainly using IPC for job creation)
// _fileMonitorService.Start();
_logger.LogInformation("Print service started successfully");
_logger.LogInformation("Queue has {Count} pending jobs", _queueService.Count);
// Main processing loop
while (!stoppingToken.IsCancellationRequested)
{
try
{
if (_queueService.TryDequeue(out var job) && job != null)
{
await ProcessJob(job, stoppingToken);
}
else
{
// No jobs in queue, wait a bit
await Task.Delay(500, stoppingToken);
}
}
catch (OperationCanceledException)
{
// Expected during shutdown, exit gracefully
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in processing loop");
try
{
await Task.Delay(1000, stoppingToken);
}
catch (OperationCanceledException)
{
// Shutdown requested during error delay
break;
}
}
}
}
catch (OperationCanceledException)
{
// Expected during shutdown
_logger.LogInformation("Service shutdown requested");
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Fatal error in worker service");
throw;
}
finally
{
_logger.LogInformation("Print service stopping");
_ipcService.Stop();
_fileMonitorService.Stop();
}
}
/// <summary>
/// Process a single print job
/// </summary>
private async Task ProcessJob(PrintJob job, CancellationToken cancellationToken)
{
_logger.LogInformation("Processing job {JobId} (Type: {DocType}, Order: {OrderNum})",
job.Id, job.DocumentType, job.OrderNumber ?? "N/A");
job.Status = PrintJobStatus.Processing;
job.UpdatedAt = DateTime.Now;
try
{
// Get document configuration from printer-config.json
var config = _documentProcessor.GetDocumentConfig(job.DocumentType);
if (config == null)
{
throw new InvalidOperationException($"No configuration found for document type '{job.DocumentType}' in printer-config.json");
}
// Read file content
if (!File.Exists(job.QueuedFilePath))
{
throw new FileNotFoundException($"Queued file not found: {job.QueuedFilePath}");
}
var content = _documentProcessor.ReadFile(job.QueuedFilePath);
// Process/transform content
var processedContent = _documentProcessor.ProcessDocument(content, config);
// Print to Windows queue (pass order number for transformations)
await Task.Run(() => _printerService.Print(processedContent, config, job.OriginalFilename, job.OrderNumber), cancellationToken);
// Post-process (archive or delete)
_documentProcessor.PostProcess(job, config);
// Mark as completed
job.Status = PrintJobStatus.Completed;
job.UpdatedAt = DateTime.Now;
_logger.LogInformation("Job {JobId} completed successfully", job.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process job {JobId}", job.Id);
job.LastError = ex.Message;
job.UpdatedAt = DateTime.Now;
// Requeue for retry
_queueService.Requeue(job);
}
}
}
@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
-76
View File
@@ -1,76 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AppSettings": {
"CapturesPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Captures",
"QueuePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Queue",
"ErrorPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Errors",
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive",
"MaxRetryAttempts": 3,
"DebounceDelayMs": 200,
"PipeName": "PrintServicePipe",
"QueueStateFile": "C:\\Users\\Work\\Desktop\\LAAPC\\queue_state.json",
"VerboseLogging": true,
"DocumentTypes": [
{
"Name": "invoice",
"PrinterName": "Microsoft Print to PDF",
"TraySequence": [ 3, 4, 1, 2 ],
"FontName": "Courier New",
"FontSize": 10.0,
"VerticalOffset": 0,
"HorizontalOffset": 0,
"ArchiveAfterPrint": true,
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Invoices",
"Transformations": [
{
"Pattern": "W1DUPLICATE INVW0",
"Replacement": "",
"Description": "Remove duplicate invoice marker"
}
]
},
{
"Name": "order",
"PrinterName": "Brother HL-2270DW series Printer",
"TraySequence": [ 4, 2, 1 ],
"FontName": "Courier New",
"FontSize": 10.0,
"VerticalOffset": 0,
"HorizontalOffset": 0,
"ArchiveAfterPrint": true,
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Orders",
"Transformations": []
},
{
"Name": "delivery",
"PrinterName": "Brother HL-2270DW series Printer",
"TraySequence": [ 3, 1 ],
"FontName": "Courier New",
"FontSize": 10.0,
"VerticalOffset": 0,
"HorizontalOffset": 0,
"ArchiveAfterPrint": true,
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Delivery",
"Transformations": []
},
{
"Name": "test",
"PrinterName": "Microsoft Print to PDF",
"TraySequence": [ 1 ],
"FontName": "Courier New",
"FontSize": 10.0,
"VerticalOffset": 0,
"HorizontalOffset": 0,
"ArchiveAfterPrint": false,
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
"SkipPostProcessing": true,
"Transformations": []
}
]
}
}
-20
View File
@@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Use x86 only for Release builds (for 32-bit target machines) -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Pipes" Version="4.3.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="7.0.0" />
</ItemGroup>
</Project>
-232
View File
@@ -1,232 +0,0 @@
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
namespace PrintServiceCLI;
class Program
{
private const string PipeName = "PrintServicePipe";
private const int TimeoutMs = 5000;
static async Task<int> Main(string[] args)
{
try
{
// Handle special --install-service flag (called by elevated process)
if (args.Contains("--install-service"))
{
return ServiceManager.EnsureServiceRunning(silent: false) ? 0 : 1;
}
// Check for skip-service-check flag
bool skipServiceCheck = args.Contains("--skip-service-check");
// Parse arguments
var command = ParseArguments(args, out bool showHelp);
if (showHelp || command == null)
{
ShowUsage();
return command == null ? 1 : 0;
}
// Ensure service is running (unless testing)
if (!skipServiceCheck)
{
if (!ServiceManager.EnsureServiceRunning())
{
Console.Error.WriteLine("ERROR: Service is not available. Cannot process print request.");
return 1;
}
}
var result = await SendCommandToService(command);
if (result.Success)
{
Console.WriteLine($"SUCCESS: {result.Message}");
return 0;
}
else
{
Console.Error.WriteLine($"ERROR: {result.Message}");
return 1;
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"FATAL ERROR: {ex.Message}");
return 1;
}
}
/// <summary>
/// Parse command line arguments
/// </summary>
static PrintCommand? ParseArguments(string[] args, out bool showHelp)
{
showHelp = false;
string? filePath = null;
string? documentType = null;
string? orderNumber = null;
for (int i = 0; i < args.Length; i++)
{
switch (args[i].ToLower())
{
case "-f":
case "--file":
if (i + 1 < args.Length)
{
filePath = args[++i];
}
break;
case "-t":
case "--type":
if (i + 1 < args.Length)
{
documentType = args[++i];
}
break;
case "-o":
case "--order":
if (i + 1 < args.Length)
{
orderNumber = args[++i];
}
break;
case "-h":
case "--help":
case "?":
showHelp = true;
return null;
case "--skip-service-check":
case "--install-service":
// These flags are handled in Main, skip here
break;
}
}
// Validate required arguments
if (string.IsNullOrEmpty(filePath))
{
Console.Error.WriteLine("Error: File path is required (-f)");
return null;
}
if (string.IsNullOrEmpty(documentType))
{
Console.Error.WriteLine("Error: Document type is required (-t)");
return null;
}
if (!File.Exists(filePath))
{
Console.Error.WriteLine($"Error: File not found: {filePath}");
return null;
}
return new PrintCommand
{
FilePath = Path.GetFullPath(filePath),
DocumentType = documentType,
OrderNumber = orderNumber
};
}
/// <summary>
/// Send command to service via named pipe
/// </summary>
static async Task<PrintResponse> SendCommandToService(PrintCommand command)
{
using var pipeClient = new NamedPipeClientStream(
".",
PipeName,
PipeDirection.InOut,
PipeOptions.Asynchronous);
try
{
await pipeClient.ConnectAsync(TimeoutMs);
}
catch (TimeoutException)
{
return new PrintResponse
{
Success = false,
Message = "Service not responding. Is the LAAPC Print Service running?"
};
}
catch (IOException ex)
{
return new PrintResponse
{
Success = false,
Message = $"Cannot connect to service: {ex.Message}"
};
}
// Send command
var json = JsonSerializer.Serialize(command);
var bytes = Encoding.UTF8.GetBytes(json);
await pipeClient.WriteAsync(bytes, 0, bytes.Length);
await pipeClient.FlushAsync();
// Read response
var buffer = new byte[4096];
var bytesRead = await pipeClient.ReadAsync(buffer, 0, buffer.Length);
var responseJson = Encoding.UTF8.GetString(buffer, 0, bytesRead);
var response = JsonSerializer.Deserialize<PrintResponse>(responseJson);
return response ?? new PrintResponse { Success = false, Message = "Invalid response from service" };
}
/// <summary>
/// Show usage information
/// </summary>
static void ShowUsage()
{
Console.WriteLine("LAAPC Print Service CLI");
Console.WriteLine();
Console.WriteLine("Usage: PrintServiceCLI -f <filepath> -t <doctype> [-o <ordernumber>]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" -f, --file <path> Path to the capture file to print (required)");
Console.WriteLine(" -t, --type <type> Document type (required)");
Console.WriteLine(" Examples: invoice, order, delivery");
Console.WriteLine(" -o, --order <number> Optional order number");
Console.WriteLine(" -h, --help Show this help message");
Console.WriteLine(" --skip-service-check Skip service availability check (for testing)");
Console.WriteLine();
Console.WriteLine("Examples:");
Console.WriteLine(" PrintServiceCLI -f \"C:\\Captures\\file.txt\" -t invoice");
Console.WriteLine(" PrintServiceCLI -f \"C:\\Captures\\file.txt\" -t order -o 12345");
Console.WriteLine();
Console.WriteLine("For Development/Testing:");
Console.WriteLine(" PrintServiceCLI -f \"file.txt\" -t test --skip-service-check");
}
}
/// <summary>
/// Command to send to service
/// </summary>
class PrintCommand
{
public string FilePath { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
public string? OrderNumber { get; set; }
}
/// <summary>
/// Response from service
/// </summary>
class PrintResponse
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
}
-290
View File
@@ -1,290 +0,0 @@
using System.Diagnostics;
using System.ServiceProcess;
using System.Security.Principal;
namespace PrintServiceCLI;
/// <summary>
/// Manages Windows Service installation and verification
/// </summary>
public static class ServiceManager
{
private const string ServiceName = "LAAPC Print Service";
private const string ServiceInstallPath = @"C:\ProgramData\LAAPC";
private const string ServiceExeName = "PrintService.exe";
private static bool? _serviceAvailable = null; // Cache result
/// <summary>
/// Check if service is installed and running, with auto-install option
/// </summary>
public static bool EnsureServiceRunning(bool silent = false)
{
// Return cached result if already checked
if (_serviceAvailable.HasValue)
return _serviceAvailable.Value;
try
{
// Check if service exists and is running
using var service = new ServiceController(ServiceName);
service.Refresh();
if (service.Status == ServiceControllerStatus.Running)
{
_serviceAvailable = true;
return true;
}
// Service exists but not running - try to start it
if (service.Status == ServiceControllerStatus.Stopped)
{
if (!silent)
Console.WriteLine("Service is stopped. Starting...");
service.Start();
service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10));
_serviceAvailable = true;
return true;
}
_serviceAvailable = false;
return false;
}
catch (InvalidOperationException)
{
// Service doesn't exist - offer to install
if (silent)
{
_serviceAvailable = false;
return false;
}
return PromptAndInstall();
}
catch (Exception ex)
{
if (!silent)
Console.Error.WriteLine($"Error checking service: {ex.Message}");
_serviceAvailable = false;
return false;
}
}
/// <summary>
/// Prompt user and install service if they agree
/// </summary>
private static bool PromptAndInstall()
{
Console.WriteLine();
Console.WriteLine("====================================================");
Console.WriteLine("LAAPC Print Service is not installed on this PC.");
Console.WriteLine("====================================================");
Console.WriteLine();
Console.WriteLine("The service must be installed locally to process print jobs.");
Console.WriteLine("This is a one-time setup that requires administrator access.");
Console.WriteLine();
Console.Write("Install the service now? [Y/N]: ");
var response = Console.ReadLine()?.Trim().ToUpper();
if (response != "Y" && response != "YES")
{
Console.WriteLine("Installation cancelled. Service is required for printing.");
_serviceAvailable = false;
return false;
}
return InstallService();
}
/// <summary>
/// Install and start the service with UAC elevation
/// </summary>
private static bool InstallService()
{
try
{
Console.WriteLine();
Console.WriteLine("Installing service...");
// Check if running as admin
if (!IsAdministrator())
{
Console.WriteLine("Requesting administrator privileges...");
return InstallWithElevation();
}
// Copy service files to local machine
var cliPath = AppContext.BaseDirectory;
var serviceSourcePath = Path.Combine(Path.GetDirectoryName(cliPath)!, ServiceExeName);
if (!File.Exists(serviceSourcePath))
{
// Try relative path
serviceSourcePath = Path.Combine(cliPath, "..", "PrintService", ServiceExeName);
serviceSourcePath = Path.GetFullPath(serviceSourcePath);
}
if (!File.Exists(serviceSourcePath))
{
Console.Error.WriteLine($"ERROR: Could not find {ServiceExeName}");
Console.Error.WriteLine($"Expected location: {serviceSourcePath}");
_serviceAvailable = false;
return false;
}
// Create installation directory
Directory.CreateDirectory(ServiceInstallPath);
// Copy service executable and dependencies
var serviceDestPath = Path.Combine(ServiceInstallPath, ServiceExeName);
CopyServiceFiles(Path.GetDirectoryName(serviceSourcePath)!, ServiceInstallPath);
Console.WriteLine($"Files copied to: {ServiceInstallPath}");
// Install Windows Service using sc.exe
var scInstall = Process.Start(new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = $"create \"{ServiceName}\" binPath=\"{serviceDestPath}\" start=auto",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
scInstall?.WaitForExit();
if (scInstall?.ExitCode != 0)
{
Console.Error.WriteLine("ERROR: Failed to install service");
_serviceAvailable = false;
return false;
}
Console.WriteLine("Service installed successfully.");
// Start the service
Console.WriteLine("Starting service...");
var scStart = Process.Start(new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = $"start \"{ServiceName}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
scStart?.WaitForExit();
if (scStart?.ExitCode != 0)
{
Console.Error.WriteLine("WARNING: Service installed but failed to start");
Console.Error.WriteLine("Try running: sc start \"LAAPC Print Service\"");
_serviceAvailable = false;
return false;
}
// Wait for service to be ready
Thread.Sleep(2000);
Console.WriteLine("Service started successfully!");
Console.WriteLine();
_serviceAvailable = true;
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"ERROR: Installation failed: {ex.Message}");
_serviceAvailable = false;
return false;
}
}
/// <summary>
/// Restart process with admin elevation
/// </summary>
private static bool InstallWithElevation()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = Process.GetCurrentProcess().MainModule?.FileName ?? "PrintServiceCLI.exe",
Arguments = "--install-service",
UseShellExecute = true,
Verb = "runas" // Trigger UAC prompt
};
var process = Process.Start(startInfo);
process?.WaitForExit();
if (process?.ExitCode == 0)
{
Console.WriteLine("Service installed successfully.");
_serviceAvailable = true;
return true;
}
Console.Error.WriteLine("Installation was cancelled or failed.");
_serviceAvailable = false;
return false;
}
catch (Exception ex)
{
Console.Error.WriteLine($"ERROR: Could not elevate privileges: {ex.Message}");
_serviceAvailable = false;
return false;
}
}
/// <summary>
/// Copy service files from source to destination
/// </summary>
private static void CopyServiceFiles(string sourceDir, string destDir)
{
// Copy main executable
var sourceExe = Path.Combine(sourceDir, ServiceExeName);
var destExe = Path.Combine(destDir, ServiceExeName);
File.Copy(sourceExe, destExe, overwrite: true);
// Copy all DLLs and config files
foreach (var file in Directory.GetFiles(sourceDir))
{
var fileName = Path.GetFileName(file);
var ext = Path.GetExtension(file).ToLower();
if (ext == ".dll" || ext == ".json" || ext == ".config")
{
var destFile = Path.Combine(destDir, fileName);
File.Copy(file, destFile, overwrite: true);
}
}
// Create necessary folders
var capturesPath = Path.Combine(destDir, "Captures");
var queuePath = Path.Combine(destDir, "Queue");
var archivePath = Path.Combine(destDir, "Archive");
var errorPath = Path.Combine(destDir, "Error");
var outputPath = Path.Combine(destDir, "Output");
Directory.CreateDirectory(capturesPath);
Directory.CreateDirectory(queuePath);
Directory.CreateDirectory(archivePath);
Directory.CreateDirectory(errorPath);
Directory.CreateDirectory(outputPath);
}
/// <summary>
/// Check if running with administrator privileges
/// </summary>
private static bool IsAdministrator()
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
-38
View File
@@ -1,38 +0,0 @@
namespace PrintServiceTray;
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Text = "Form1";
}
#endregion
}
-9
View File
@@ -1,9 +0,0 @@
namespace PrintServiceTray;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
-515
View File
@@ -1,515 +0,0 @@
using PrintServiceTray.Models;
using PrintServiceTray.Services;
namespace PrintServiceTray.Forms;
public partial class ConfigurationForm : Form
{
private readonly ConfigurationService _configService;
private PrinterConfiguration _config;
private string? _selectedDocType;
private List<string> _installedPrinters;
// UI Controls
private ListBox _docTypeListBox = null!;
private Button _newDocTypeButton = null!;
private Button _deleteDocTypeButton = null!;
private Panel _pageConfigPanel = null!;
private RadioButton _saveGlobalRadio = null!;
private RadioButton _saveLocalRadio = null!;
private Button _saveButton = null!;
private Button _cancelButton = null!;
private Button _addPageButton = null!;
private Button _removePageButton = null!;
private Label _configLabel = null!;
private List<PageConfigControl> _pageControls = new();
public ConfigurationForm()
{
_configService = new ConfigurationService();
_config = _configService.Load();
_installedPrinters = _configService.GetInstalledPrinters();
InitializeComponent();
LoadDocumentTypes();
}
private void InitializeComponent()
{
Text = "LAAPC Printer Configuration";
Size = new Size(900, 600);
StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
// Left panel - Document Types
var leftPanel = new Panel
{
Location = new Point(10, 10),
Size = new Size(250, 540),
BorderStyle = BorderStyle.FixedSingle
};
var docTypeLabel = new Label
{
Text = "Document Types:",
Location = new Point(10, 10),
Size = new Size(230, 20),
Font = new Font(Font, FontStyle.Bold)
};
_docTypeListBox = new ListBox
{
Location = new Point(10, 35),
Size = new Size(230, 430)
};
_docTypeListBox.SelectedIndexChanged += DocTypeListBox_SelectedIndexChanged;
_newDocTypeButton = new Button
{
Text = "New",
Location = new Point(10, 475),
Size = new Size(110, 30)
};
_newDocTypeButton.Click += NewDocType_Click;
_deleteDocTypeButton = new Button
{
Text = "Delete",
Location = new Point(130, 475),
Size = new Size(110, 30)
};
_deleteDocTypeButton.Click += DeleteDocType_Click;
leftPanel.Controls.AddRange(new Control[] {
docTypeLabel, _docTypeListBox, _newDocTypeButton, _deleteDocTypeButton
});
// Right panel - Page Configuration
var rightPanel = new Panel
{
Location = new Point(270, 10),
Size = new Size(610, 540),
BorderStyle = BorderStyle.FixedSingle
};
_configLabel = new Label
{
Text = "Configuration for: (Select a document type)",
Location = new Point(10, 10),
Size = new Size(590, 20),
Font = new Font(Font, FontStyle.Bold)
};
var pageConfigLabel = new Label
{
Text = "Page Configuration:",
Location = new Point(10, 40),
Size = new Size(590, 20)
};
_pageConfigPanel = new Panel
{
Location = new Point(10, 65),
Size = new Size(590, 350),
AutoScroll = true,
BorderStyle = BorderStyle.FixedSingle
};
_addPageButton = new Button
{
Text = "+ Add Page",
Location = new Point(10, 425),
Size = new Size(120, 30)
};
_addPageButton.Click += AddPage_Click;
_removePageButton = new Button
{
Text = "- Remove Last",
Location = new Point(140, 425),
Size = new Size(120, 30)
};
_removePageButton.Click += RemovePage_Click;
// Save options
var saveToLabel = new Label
{
Text = "Save to:",
Location = new Point(10, 465),
Size = new Size(60, 20)
};
_saveGlobalRadio = new RadioButton
{
Text = "Global (All Users)",
Location = new Point(80, 465),
Size = new Size(150, 20)
};
_saveLocalRadio = new RadioButton
{
Text = "Local (My Computer)",
Location = new Point(240, 465),
Size = new Size(170, 20),
Checked = true
};
_saveButton = new Button
{
Text = "Save",
Location = new Point(430, 495),
Size = new Size(80, 35)
};
_saveButton.Click += Save_Click;
_cancelButton = new Button
{
Text = "Cancel",
Location = new Point(520, 495),
Size = new Size(80, 35)
};
_cancelButton.Click += (s, e) => Close();
rightPanel.Controls.AddRange(new Control[] {
_configLabel, pageConfigLabel, _pageConfigPanel,
_addPageButton, _removePageButton,
saveToLabel, _saveGlobalRadio, _saveLocalRadio,
_saveButton, _cancelButton
});
Controls.AddRange(new Control[] { leftPanel, rightPanel });
}
private void LoadDocumentTypes()
{
_docTypeListBox.Items.Clear();
foreach (var docType in _config.DocumentTypes.Keys.OrderBy(k => k))
{
_docTypeListBox.Items.Add(docType);
}
}
private void DocTypeListBox_SelectedIndexChanged(object? sender, EventArgs e)
{
if (_docTypeListBox.SelectedItem is string docType)
{
_selectedDocType = docType;
LoadPageConfiguration(docType);
}
}
private void LoadPageConfiguration(string docType)
{
_configLabel.Text = $"Configuration for: {docType}";
_pageConfigPanel.Controls.Clear();
_pageControls.Clear();
if (!_config.DocumentTypes.TryGetValue(docType, out var config))
{
config = new DocumentTypeConfig { Name = docType };
_config.DocumentTypes[docType] = config;
}
int yPos = 10;
foreach (var page in config.Pages.OrderBy(p => p.PageNumber))
{
var pageControl = new PageConfigControl(page.PageNumber, _installedPrinters, _configService);
pageControl.Location = new Point(10, yPos);
pageControl.LoadPage(page);
_pageConfigPanel.Controls.Add(pageControl);
_pageControls.Add(pageControl);
yPos += pageControl.Height + 10;
}
if (config.Pages.Count == 0)
{
// Add first page by default
AddPage_Click(null, EventArgs.Empty);
}
}
private void AddPage_Click(object? sender, EventArgs e)
{
if (_selectedDocType == null) return;
var config = _config.DocumentTypes[_selectedDocType];
int nextPageNum = config.Pages.Count > 0 ? config.Pages.Max(p => p.PageNumber) + 1 : 1;
var newPage = new PageConfig
{
PageNumber = nextPageNum,
PrinterName = _installedPrinters.FirstOrDefault() ?? "",
TrayNumber = 1
};
config.Pages.Add(newPage);
LoadPageConfiguration(_selectedDocType);
}
private void RemovePage_Click(object? sender, EventArgs e)
{
if (_selectedDocType == null) return;
var config = _config.DocumentTypes[_selectedDocType];
if (config.Pages.Count > 0)
{
var lastPage = config.Pages.OrderByDescending(p => p.PageNumber).First();
config.Pages.Remove(lastPage);
LoadPageConfiguration(_selectedDocType);
}
}
private void NewDocType_Click(object? sender, EventArgs e)
{
var dialog = new TextInputDialog("New Document Type", "Enter document type name:");
if (dialog.ShowDialog() == DialogResult.OK && !string.IsNullOrWhiteSpace(dialog.InputText))
{
var docType = dialog.InputText.Trim();
if (!_config.DocumentTypes.ContainsKey(docType))
{
_config.DocumentTypes[docType] = new DocumentTypeConfig { Name = docType };
LoadDocumentTypes();
_docTypeListBox.SelectedItem = docType;
}
else
{
MessageBox.Show($"Document type '{docType}' already exists.", "Duplicate", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
}
private void DeleteDocType_Click(object? sender, EventArgs e)
{
if (_selectedDocType == null) return;
var result = MessageBox.Show(
$"Are you sure you want to delete '{_selectedDocType}'?",
"Confirm Delete",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
_config.DocumentTypes.Remove(_selectedDocType);
_selectedDocType = null;
LoadDocumentTypes();
_pageConfigPanel.Controls.Clear();
_configLabel.Text = "Configuration for: (Select a document type)";
}
}
private void Save_Click(object? sender, EventArgs e)
{
try
{
// Save current page configurations
if (_selectedDocType != null && _config.DocumentTypes.TryGetValue(_selectedDocType, out var config))
{
config.Pages.Clear();
foreach (var pageControl in _pageControls)
{
config.Pages.Add(pageControl.GetPageConfig());
}
}
bool saveGlobal = _saveGlobalRadio.Checked;
_configService.Save(_config, saveGlobal);
MessageBox.Show(
$"Configuration saved to {(saveGlobal ? "global" : "local")} file successfully!",
"Success",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex)
{
MessageBox.Show(
$"Failed to save configuration: {ex.Message}",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
}
// Custom control for each page configuration
public class PageConfigControl : UserControl
{
private readonly int _pageNumber;
private readonly List<string> _printers;
private readonly ConfigurationService _configService;
private ComboBox _printerCombo = null!;
private ComboBox _trayCombo = null!;
public PageConfigControl(int pageNumber, List<string> printers, ConfigurationService configService)
{
_pageNumber = pageNumber;
_printers = printers;
_configService = configService;
InitializeComponent();
}
private void InitializeComponent()
{
Size = new Size(560, 60);
BorderStyle = BorderStyle.FixedSingle;
var pageLabel = new Label
{
Text = $"Page {_pageNumber}:",
Location = new Point(10, 10),
Size = new Size(70, 20),
Font = new Font(Font, FontStyle.Bold)
};
var printerLabel = new Label
{
Text = "Printer:",
Location = new Point(10, 32),
Size = new Size(60, 20)
};
_printerCombo = new ComboBox
{
Location = new Point(75, 30),
Size = new Size(250, 25),
DropDownStyle = ComboBoxStyle.DropDownList
};
_printerCombo.Items.AddRange(_printers.ToArray());
_printerCombo.SelectedIndexChanged += PrinterCombo_SelectedIndexChanged;
var trayLabel = new Label
{
Text = "Tray:",
Location = new Point(340, 32),
Size = new Size(40, 20)
};
_trayCombo = new ComboBox
{
Location = new Point(385, 30),
Size = new Size(165, 25),
DropDownStyle = ComboBoxStyle.DropDownList
};
Controls.AddRange(new Control[] {
pageLabel, printerLabel, _printerCombo, trayLabel, _trayCombo
});
}
public void LoadPage(PageConfig page)
{
_printerCombo.SelectedItem = page.PrinterName;
LoadTrays(page.PrinterName);
// Try to find and select the tray
for (int i = 0; i < _trayCombo.Items.Count; i++)
{
if (_trayCombo.Items[i] is TrayItem item && item.TrayNumber == page.TrayNumber)
{
_trayCombo.SelectedIndex = i;
break;
}
}
}
private void PrinterCombo_SelectedIndexChanged(object? sender, EventArgs e)
{
if (_printerCombo.SelectedItem is string printer)
{
LoadTrays(printer);
}
}
private void LoadTrays(string printerName)
{
_trayCombo.Items.Clear();
var trays = _configService.GetPrinterTrays(printerName);
foreach (var (number, name) in trays)
{
_trayCombo.Items.Add(new TrayItem { TrayNumber = number, TrayName = name });
}
if (_trayCombo.Items.Count > 0)
{
_trayCombo.SelectedIndex = 0;
}
}
public PageConfig GetPageConfig()
{
var selectedTray = _trayCombo.SelectedItem as TrayItem;
return new PageConfig
{
PageNumber = _pageNumber,
PrinterName = _printerCombo.SelectedItem?.ToString() ?? "",
TrayNumber = selectedTray?.TrayNumber ?? 1,
TrayLabel = selectedTray?.TrayName
};
}
private class TrayItem
{
public int TrayNumber { get; set; }
public string TrayName { get; set; } = "";
public override string ToString() => $"Tray {TrayNumber} - {TrayName}";
}
}
// Simple text input dialog
public class TextInputDialog : Form
{
private TextBox _textBox = null!;
public string InputText => _textBox.Text;
public TextInputDialog(string title, string prompt)
{
Text = title;
Size = new Size(400, 150);
StartPosition = FormStartPosition.CenterParent;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
var promptLabel = new Label
{
Text = prompt,
Location = new Point(20, 20),
Size = new Size(360, 20)
};
_textBox = new TextBox
{
Location = new Point(20, 50),
Size = new Size(360, 25)
};
var okButton = new Button
{
Text = "OK",
DialogResult = DialogResult.OK,
Location = new Point(220, 85),
Size = new Size(75, 30)
};
var cancelButton = new Button
{
Text = "Cancel",
DialogResult = DialogResult.Cancel,
Location = new Point(305, 85),
Size = new Size(75, 30)
};
AcceptButton = okButton;
CancelButton = cancelButton;
Controls.AddRange(new Control[] { promptLabel, _textBox, okButton, cancelButton });
}
}
-46
View File
@@ -1,46 +0,0 @@
namespace PrintServiceTray.Models;
/// <summary>
/// Configuration for all document types
/// </summary>
public class PrinterConfiguration
{
public Dictionary<string, DocumentTypeConfig> DocumentTypes { get; set; } = new();
}
/// <summary>
/// Configuration for a single document type
/// </summary>
public class DocumentTypeConfig
{
public string Name { get; set; } = string.Empty;
public List<PageConfig> Pages { get; set; } = new();
// Rust transformation properties (optional - can be edited in JSON directly)
public int LinePadding { get; set; } = 0;
public bool RemoveOrderNumber { get; set; } = false;
public bool RemoveOrderNumberLabel { get; set; } = false;
public string IndentOrderNumber { get; set; } = string.Empty;
public Dictionary<int, int> RowShift { get; set; } = new();
public Dictionary<int, RowTrimConfig> RowTrim { get; set; } = new();
}
/// <summary>
/// Configuration for a single page
/// </summary>
public class PageConfig
{
public int PageNumber { get; set; }
public string PrinterName { get; set; } = string.Empty;
public int TrayNumber { get; set; }
public string? TrayLabel { get; set; } // Optional: "Pink Paper", "Green Paper", etc.
}
/// <summary>
/// Configuration for row trimming (horizontal character removal)
/// </summary>
public class RowTrimConfig
{
public int Start { get; set; }
public int End { get; set; }
}
@@ -1,6 +0,0 @@
namespace PrintServiceTray.Models;
public class QueueStatusRequest
{
public string Command { get; set; } = "GetQueueStatus";
}
@@ -1,19 +0,0 @@
namespace PrintServiceTray.Models;
public class QueueStatusResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public List<JobStatus> Jobs { get; set; } = new();
}
public class JobStatus
{
public string Id { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string? PrinterName { get; set; }
public int? CurrentPage { get; set; }
public int? TotalPages { get; set; }
}
-16
View File
@@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Drawing.EnableWindowsSupport" Value="true" />
</ItemGroup>
</Project>
-35
View File
@@ -1,35 +0,0 @@
namespace PrintServiceTray;
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
try
{
// Prevent multiple instances
bool createdNew;
using var mutex = new Mutex(true, "LAAPC_PrintServiceTray_Mutex", out createdNew);
if (!createdNew)
{
MessageBox.Show("LAAPC Print Service Tray is already running.", "Already Running",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
ApplicationConfiguration.Initialize();
Application.Run(new TrayApplicationContext());
GC.KeepAlive(mutex);
}
catch (Exception ex)
{
MessageBox.Show($"Fatal error: {ex.Message}\n\n{ex.StackTrace}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
@@ -1,136 +0,0 @@
using System.Text.Json;
using PrintServiceTray.Models;
namespace PrintServiceTray.Services;
public class ConfigurationService
{
private readonly string _globalConfigPath;
private readonly string _localConfigPath;
public ConfigurationService()
{
// Global config: next to CLI executable (C:\LAAPC or wherever installed)
var cliPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"LAAPC"
);
_globalConfigPath = Path.Combine(cliPath, "printer-config.json");
// Local config: user's AppData
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LAAPC"
);
Directory.CreateDirectory(appDataPath);
_localConfigPath = Path.Combine(appDataPath, "printer-config.json");
}
/// <summary>
/// Load configuration (local overrides global)
/// </summary>
public PrinterConfiguration Load()
{
var config = new PrinterConfiguration();
// Load global first
if (File.Exists(_globalConfigPath))
{
try
{
var json = File.ReadAllText(_globalConfigPath);
var globalConfig = JsonSerializer.Deserialize<PrinterConfiguration>(json);
if (globalConfig != null)
{
config = globalConfig;
}
}
catch { }
}
// Load local and merge/override
if (File.Exists(_localConfigPath))
{
try
{
var json = File.ReadAllText(_localConfigPath);
var localConfig = JsonSerializer.Deserialize<PrinterConfiguration>(json);
if (localConfig != null)
{
// Merge: local overrides global for matching document types
foreach (var kvp in localConfig.DocumentTypes)
{
config.DocumentTypes[kvp.Key] = kvp.Value;
}
}
}
catch { }
}
return config;
}
/// <summary>
/// Save configuration to global or local
/// </summary>
public void Save(PrinterConfiguration config, bool saveGlobal)
{
var path = saveGlobal ? _globalConfigPath : _localConfigPath;
// Ensure directory exists
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var options = new JsonSerializerOptions
{
WriteIndented = true
};
var json = JsonSerializer.Serialize(config, options);
File.WriteAllText(path, json);
}
/// <summary>
/// Get all installed printers
/// </summary>
public List<string> GetInstalledPrinters()
{
var printers = new List<string>();
try
{
foreach (string printerName in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
{
printers.Add(printerName);
}
}
catch { }
return printers;
}
/// <summary>
/// Get available trays for a printer
/// </summary>
public List<(int Number, string Name)> GetPrinterTrays(string printerName)
{
var trays = new List<(int, string)>();
try
{
var printerSettings = new System.Drawing.Printing.PrinterSettings
{
PrinterName = printerName
};
foreach (System.Drawing.Printing.PaperSource source in printerSettings.PaperSources)
{
trays.Add((source.RawKind, source.SourceName));
}
}
catch { }
return trays;
}
public string GlobalConfigPath => _globalConfigPath;
public string LocalConfigPath => _localConfigPath;
}
@@ -1,62 +0,0 @@
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using PrintServiceTray.Models;
namespace PrintServiceTray.Services;
public class ServiceClient
{
private readonly string _pipeName;
private readonly int _timeout;
public ServiceClient(string pipeName = "PrintServicePipe", int timeoutMs = 5000)
{
_pipeName = pipeName;
_timeout = timeoutMs;
}
public async Task<QueueStatusResponse> GetQueueStatusAsync()
{
try
{
using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut);
await pipe.ConnectAsync(_timeout);
var request = new QueueStatusRequest();
var requestJson = JsonSerializer.Serialize(request);
var requestBytes = Encoding.UTF8.GetBytes(requestJson);
await pipe.WriteAsync(requestBytes);
await pipe.FlushAsync();
var buffer = new byte[4096];
var bytesRead = await pipe.ReadAsync(buffer);
var responseJson = Encoding.UTF8.GetString(buffer, 0, bytesRead);
var response = JsonSerializer.Deserialize<QueueStatusResponse>(responseJson);
return response ?? new QueueStatusResponse { Success = false, Message = "Invalid response" };
}
catch (TimeoutException)
{
return new QueueStatusResponse { Success = false, Message = "Service not responding" };
}
catch (Exception ex)
{
return new QueueStatusResponse { Success = false, Message = $"Error: {ex.Message}" };
}
}
public async Task<bool> IsServiceRunningAsync()
{
try
{
var response = await GetQueueStatusAsync();
return response.Success;
}
catch
{
return false;
}
}
}
-206
View File
@@ -1,206 +0,0 @@
using System.Diagnostics;
using PrintServiceTray.Services;
using PrintServiceTray.Models;
namespace PrintServiceTray;
public class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _trayIcon;
private readonly ServiceClient _serviceClient;
private readonly System.Windows.Forms.Timer _refreshTimer;
private readonly ToolStripMenuItem _queueStatusItem;
private readonly ToolStripMenuItem _serviceStatusItem;
public TrayApplicationContext()
{
try
{
_serviceClient = new ServiceClient();
// Create context menu
var contextMenu = new ContextMenuStrip();
// Service status (at top)
_serviceStatusItem = new ToolStripMenuItem("● Service Running")
{
Enabled = false,
Font = new Font(contextMenu.Font, FontStyle.Bold)
};
contextMenu.Items.Add(_serviceStatusItem);
contextMenu.Items.Add(new ToolStripSeparator());
// Queue status section
_queueStatusItem = new ToolStripMenuItem("Queue Status")
{
Enabled = false
};
contextMenu.Items.Add(_queueStatusItem);
var viewQueueItem = new ToolStripMenuItem("View Full Queue...");
viewQueueItem.Click += ViewQueue_Click;
contextMenu.Items.Add(viewQueueItem);
contextMenu.Items.Add(new ToolStripSeparator());
// Configuration
var configureItem = new ToolStripMenuItem("Configure Printers...");
configureItem.Click += Configure_Click;
contextMenu.Items.Add(configureItem);
contextMenu.Items.Add(new ToolStripSeparator());
// Exit
var exitItem = new ToolStripMenuItem("Exit");
exitItem.Click += Exit_Click;
contextMenu.Items.Add(exitItem);
// Create tray icon
_trayIcon = new NotifyIcon
{
Icon = SystemIcons.Information,
ContextMenuStrip = contextMenu,
Visible = true,
Text = "LAAPC Print Service"
};
_trayIcon.DoubleClick += TrayIcon_DoubleClick;
// Show startup notification
_trayIcon.ShowBalloonTip(2000, "LAAPC Print Service", "Tray app started", ToolTipIcon.Info);
// Set up refresh timer (every 2 seconds)
_refreshTimer = new System.Windows.Forms.Timer
{
Interval = 2000
};
_refreshTimer.Tick += RefreshTimer_Tick;
_refreshTimer.Start();
// Initial refresh
_ = RefreshStatusAsync();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to initialize tray app: {ex.Message}\n\n{ex.StackTrace}",
"Initialization Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
throw;
}
}
private async void RefreshTimer_Tick(object? sender, EventArgs e)
{
await RefreshStatusAsync();
}
private async Task RefreshStatusAsync()
{
try
{
var status = await _serviceClient.GetQueueStatusAsync();
if (status.Success)
{
_serviceStatusItem.Text = "● Service Running";
_serviceStatusItem.ForeColor = Color.Green;
// Update queue status
if (status.Jobs.Count == 0)
{
_queueStatusItem.Text = "Queue Status: Empty";
_queueStatusItem.DropDownItems.Clear();
}
else
{
_queueStatusItem.Text = $"Queue Status ({status.Jobs.Count} {(status.Jobs.Count == 1 ? "job" : "jobs")})";
_queueStatusItem.Enabled = true;
// Update dropdown items with job list
_queueStatusItem.DropDownItems.Clear();
foreach (var job in status.Jobs.Take(10)) // Show max 10 in menu
{
var statusIcon = job.Status == "Processing" ? "●" : "○";
var jobText = $"{statusIcon} {job.Status}: {job.FileName}";
if (job.PrinterName != null)
{
jobText += $" ({job.PrinterName})";
}
if (job.CurrentPage.HasValue && job.TotalPages.HasValue)
{
jobText += $" - Page {job.CurrentPage}/{job.TotalPages}";
}
var jobItem = new ToolStripMenuItem(jobText) { Enabled = false };
_queueStatusItem.DropDownItems.Add(jobItem);
}
if (status.Jobs.Count > 10)
{
_queueStatusItem.DropDownItems.Add(new ToolStripSeparator());
var moreItem = new ToolStripMenuItem($"... and {status.Jobs.Count - 10} more")
{
Enabled = false
};
_queueStatusItem.DropDownItems.Add(moreItem);
}
}
// Update tooltip
_trayIcon.Text = status.Jobs.Count == 0
? "LAAPC Print Service - Queue empty"
: $"LAAPC Print Service - {status.Jobs.Count} job(s) in queue";
}
else
{
_serviceStatusItem.Text = "○ Service Not Running";
_serviceStatusItem.ForeColor = Color.Red;
_queueStatusItem.Text = "Queue Status: Unavailable";
_queueStatusItem.DropDownItems.Clear();
_trayIcon.Text = "LAAPC Print Service - Not running";
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error refreshing status: {ex.Message}");
}
}
private void TrayIcon_DoubleClick(object? sender, EventArgs e)
{
// Double-click opens full queue view
ViewQueue_Click(sender, e);
}
private void ViewQueue_Click(object? sender, EventArgs e)
{
// TODO: Open queue view window
MessageBox.Show("Queue view window coming soon!", "LAAPC Print Service", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void Configure_Click(object? sender, EventArgs e)
{
var configForm = new Forms.ConfigurationForm();
configForm.ShowDialog();
}
private void Exit_Click(object? sender, EventArgs e)
{
_refreshTimer.Stop();
_trayIcon.Visible = false;
Application.Exit();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_refreshTimer?.Stop();
_refreshTimer?.Dispose();
_trayIcon?.Dispose();
}
base.Dispose(disposing);
}
}
+1 -282
View File
@@ -1,283 +1,2 @@
# LAAPC Print Service
# CGW-Printing
A Windows service for managing print jobs from Harbor/Clipper legacy applications to modern laser printers with multi-tray support.
## Overview
This service solves the problem of migrating from dot-matrix printers with carbon copy paper to laser printers with colored paper trays. It:
-**Prevents file overwrites** by immediately moving capture files to a queue with GUID-based names
-**Maintains print order** with a persistent queue system
-**Controls printer trays** via Windows print queue API for "carbon copy" simulation
-**Transforms content** with configurable rules per document type
-**Archives prints** for record-keeping
## Architecture
- **PrintService**: Windows service that runs continuously, monitors for print jobs, and processes them
- **PrintServiceCLI**: Command-line tool for Harbor/Clipper to submit print jobs via IPC (named pipes)
## Quick Start
### 1. Configure
Edit `PrintService/appsettings.json`:
```json
{
"AppSettings": {
"CapturesPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Captures",
"DocumentTypes": [
{
"Name": "invoice",
"PrinterName": "Your_Actual_Printer_Name",
"TraySequence": [ 3, 4, 1, 2 ]
}
]
}
}
```
**Important**: Replace `Your_Actual_Printer_Name` with actual Windows printer name(s).
### 2. Build
```powershell
# Build for 32-bit (supports both 32-bit and 64-bit Windows)
dotnet build -c Release -r win-x86
# Or publish self-contained (includes .NET runtime)
dotnet publish -c Release -r win-x86 --self-contained true -o publish/x86
```
### 3. Install Service
```powershell
# Run as Administrator
sc.exe create "LAAPC Print Service" binPath="C:\Path\To\PrintService.exe" start=auto
sc.exe start "LAAPC Print Service"
```
### 4. Test CLI
```powershell
PrintServiceCLI.exe -f "C:\Captures\test.txt" -t invoice -o 12345
```
## Usage
### From Harbor/Clipper
Replace your current Rust CLI calls with:
```
PrintServiceCLI.exe -f <filepath> -t <doctype> [-o <ordernumber>]
```
**Examples**:
```
PrintServiceCLI.exe -f "C:\Captures\inv001.txt" -t invoice
PrintServiceCLI.exe -f "C:\Captures\ord002.txt" -t order -o 600005
PrintServiceCLI.exe -f "C:\Captures\del003.txt" -t delivery
```
### CLI Options
- `-f, --file <path>`: Path to capture file (required)
- `-t, --type <type>`: Document type: invoice, order, delivery, etc. (required)
- `-o, --order <num>`: Optional order number
- `-h, --help`: Show help
## Configuration
### Document Types
Each document type in `appsettings.json` specifies:
```json
{
"Name": "invoice", // Document type identifier
"PrinterName": "HP LaserJet 500", // Windows printer name
"TraySequence": [ 3, 4, 1, 2 ], // Tray order (simulates carbon copy)
"FontName": "Courier New", // Font for rendering
"FontSize": 10.0, // Font size in points
"VerticalOffset": 0, // Adjust vertical positioning (pixels)
"HorizontalOffset": 0, // Adjust horizontal positioning (pixels)
"ArchiveAfterPrint": true, // Archive or delete after printing
"ArchivePath": "C:\\Archive\\Invoices", // Where to archive
"Transformations": [ // Text transformation rules
{
"Pattern": "W1DUPLICATE INVW0", // Regex pattern to find
"Replacement": "", // Replace with (empty = remove)
"Description": "Remove duplicate marker"
}
]
}
```
### Tray Mapping
The `TraySequence` specifies which physical printer trays to use for each page/copy:
- **Example**: `[3, 4, 1, 2]` prints:
- Page 1 → Tray 3 (e.g., Pink paper)
- Page 2 → Tray 4 (e.g., Orange paper)
- Page 3 → Tray 1 (e.g., Blue paper)
- Page 4 → Tray 2 (e.g., Green paper)
**Note**: Tray numbers may need adjustment per printer model. Use the included tray discovery tool (see Troubleshooting).
## Project Structure
```
PrintService/
├── Models/
│ ├── PrintJob.cs - Print job data model
│ ├── DocumentConfig.cs - Document type configuration
│ └── AppSettings.cs - Application settings
├── Services/
│ ├── PrintQueueService.cs - Job queue management
│ ├── FileMonitorService.cs - File system monitoring
│ ├── PrinterService.cs - Windows printer integration
│ ├── DocumentProcessor.cs - Content transformation
│ └── IpcService.cs - Named pipe IPC server
├── Worker.cs - Main service coordinator
├── Program.cs - Service host configuration
└── appsettings.json - Configuration file
PrintServiceCLI/
└── Program.cs - Command-line interface
```
## Troubleshooting
### Service won't start
1. Check Windows Event Viewer → Application logs
2. Verify paths in `appsettings.json` exist
3. Run with elevated privileges
### Printer not found
List installed printers:
```powershell
Get-Printer | Select-Object Name
```
Update `PrinterName` in config to match exactly.
### Wrong trays selected
Tray mapping varies by printer model. To discover available trays, temporarily add logging to `PrinterService.ListPaperSources()` and check service logs.
### Files being overwritten
Ensure Harbor/Clipper is calling the CLI (not writing directly to Captures folder). The service immediately moves files to prevent timestamp collisions.
### Print order issues
Check `queue_state.json` - jobs are processed FIFO. If order is wrong, check file timestamps.
## Monitoring
### Service Status
```powershell
Get-Service "LAAPC Print Service"
sc.exe query "LAAPC Print Service"
```
### Logs
Check Windows Event Viewer or configure file logging in `appsettings.json`.
### Queue State
Inspect `queue_state.json` (location specified in `appsettings.json`) to see pending jobs.
### Folders
- `Queue/` - Files being processed (GUID-named)
- `Archive/` - Completed jobs (if archiving enabled)
- `Errors/` - Failed jobs after max retries
## Uninstall
```powershell
# Run as Administrator
sc.exe stop "LAAPC Print Service"
sc.exe delete "LAAPC Print Service"
```
## Development
### Build for debugging
```powershell
dotnet build -c Debug
```
### Run service locally (not as Windows Service)
```powershell
dotnet run --project PrintService/PrintService.csproj
```
### Watch mode (auto-rebuild on changes)
```powershell
dotnet watch run --project PrintService/PrintService.csproj
```
### VS Code Tasks
Use Command Palette (`Ctrl+Shift+P`) → "Tasks: Run Task":
- `build-all-x86` - Build release for 32-bit
- `publish-all-x86` - Create deployment package
- `run-service` - Run service locally for testing
## Technical Details
### IPC Protocol
The CLI communicates with the service via Windows Named Pipes (`\\.\pipe\PrintServicePipe`):
**Request**:
```json
{
"FilePath": "C:\\Captures\\file.txt",
"DocumentType": "invoice",
"OrderNumber": "12345"
}
```
**Response**:
```json
{
"Success": true,
"Message": "Job {guid} queued successfully"
}
```
### Print Queue Integration
Uses `System.Drawing.Printing.PrintDocument` with per-page `PageSettings.PaperSource` control:
- Content rendered with `Graphics.DrawString()` for pixel-perfect positioning
- Jobs go through Windows print queue (visible in Windows printer UI)
- Survives service restarts via persistent queue state
### File Safety
1. CLI sends command with file path
2. Service immediately moves file to Queue folder with GUID name
3. Original filename preserved in job metadata
4. Prevents timestamp collision overwrites
## License
[Your License Here]
## Support
For issues or questions, contact [Your Contact Info]
-263
View File
@@ -1,263 +0,0 @@
# Rust Content Transformation Flows
This document describes the content transformation steps performed by the Rust CLI (`cgwprint`) that need to be ported to the C# PrintService.
## Configuration Overview
Each job type is defined in `printers.json` under the `process` section with the following possible properties:
- **label**: Display name for the job type
- **line_padding**: Left margin/indent for all lines (in units)
- **remove_ord_num**: Remove both "Order #: " label and the order number entirely
- **remove_ord_num_label**: Remove "Order #: " label but keep the order number (and make it bold)
- **indent_ord_num**: Add spacing before the order number (shifts it right)
- **row_shift**: Dictionary of line numbers → vertical adjustment (in 1/100 inch units)
- **row_trim**: Dictionary of line numbers → {start, end} (removes characters from start to end)
- **printer**: Array of printer/tray assignments
## Universal Transformation Flow (All Job Types)
Every document goes through this sequence:
### Step 1: File Reading & Page Splitting
- Read entire file content as string
- Split content by **form-feed character** (`\x0C` / ASCII 12)
- Filter out empty pages
- Each page becomes an array of lines
### Step 2: Font Code Normalization (Pre-processing)
- Prefix entire content with `\x1Bw0` (ensure normal font at start)
- Replace `\x1BW1``\x1Bw1` (normalize uppercase W to lowercase)
- Replace `\x1BW0``\x1Bw0` (normalize uppercase W to lowercase)
- Deduplicate consecutive `\x1Bw1` sequences → single `\x1Bw1`
- Deduplicate consecutive `\x1Bw0` sequences → single `\x1Bw0`
### Step 3: Order Number Manipulation (If order number provided)
**Note:** Only ONE of these operations executes per job based on configuration flags.
**3a. Remove Order Number Label** (if `remove_ord_num_label: true`)
- Find: `"Order #: {ordnum}"`
- Replace with: `" \x1Bw1{ordnum}\x1Bw0"` (spaces equal to "Order #: " length + bold order number)
**3b. Remove Order Number Completely** (if `remove_ord_num: true`)
- Find: `"Order #: {ordnum}"`
- Replace with: spaces equal to the entire string length
**3c. Indent Order Number** (if `indent_ord_num` is not empty)
- Find: `\x1Bw1{ordnum}\x1Bw0` (regex search)
- Replace with: `{indent_ord_num}\x1Bw1{ordnum}\x1Bw0`
### Step 4: Font Code Replacement
Replace ESC sequences with actual PCL font commands from printer configuration:
- `\x1Bw1` → Bold font PCL code (e.g., `\x1B(0N\x1B(s0p5h0s3b4099T`)
- `\x1Bw0` → Normal font PCL code (e.g., `\x1B(0N\x1B(s0p10h0s0b4099T`)
### Step 5: Process All Hex Escapes
Convert remaining `\xHH` escape sequences to actual bytes throughout content.
### Step 6: Page Layout Construction
For each original page, for each assigned tray:
**6a. Tray Selection**
- Insert PCL tray selection code (e.g., `\x1B&l4H` for tray 1)
**6b. Line-by-Line Rendering**
For each line (index = `pos_v`) in the page:
1. **Check for Row Shift** (Vertical positioning adjustment)
- If `row_shift["{pos_v}"]` exists, add adjustment to cumulative `line_spacing`
- Example: `row_shift["7"]: -200` means line 7 moves UP 200 units (negative = up, positive = down)
- This allows overlaying text or changing vertical line order
2. **Check for Row Trim** (Horizontal character manipulation)
- If `row_trim["{pos_v}"]` exists:
- Extract substring: `line[0..start] + line[end..]` (removes characters from start to end)
- Use trimmed line for rendering
- Example: `row_trim["8"]: {start: 0, end: 65}` removes first 65 characters
- This is used to add/remove horizontal spacing or adjust text positions on a line
3. **Position Cursor & Write Line**
- Insert PCL positioning: `\x1B&a{line_padding}h{vertical_position}V`
- `vertical_position = (pos_v × spacing) + line_spacing`
- `spacing` = printer's spacing value (100 or 150 = 1/100 inch per line)
- Append the line content
**Note:** The Rust implementation wraps content with PCL/PJL commands because it sends raw data via TCP socket. The C# implementation uses Windows printer drivers, so **most of these commands may not be needed**. We'll need to test what's actually required.
**7a. Header (based on PCL version from printer config) - MAY NOT BE NEEDED IN C#**
```
\x1B%-12345X // PCL mode enter
@PJL ENTER LANGUAGE=PCL6 // PJL language (if PCL 6)
@PJL SET RENDERMODE=GRAYSCALE // PJL grayscale (if PCL 6)
@PJL SET RESOLUTION=600 // PJL resolution (if PCL 6)
\x1B&l0O // Portrait orientation (if PCL 6)
{content here}
```
**7b. Footer - MAY NOT BE NEEDED IN C#**
```
\x1B%-12345X // PCL mode exit
```
### Step 8: Send to Printer
**Rust approach:** Raw TCP socket to printer IP:port 9100
**C# approach:** Windows PrintDocument API with Graphics renderingket to printer IP:port (typically 9100)
- Send raw bytes
- Close socket
---
## Job Type Specific Examples
### **DELIVERY**
```json
{
"label": "Delivery",
"indent_ord_num": " ", // Shift order number right 4 spaces
"line_padding": 10, // 10 units left margin
"printer": [{
"name": "HL-L6415DW",
"tray": [3, 2] // Print to tray 3, then tray 2
}],
"row_shift": {
"0": 625, // Move line 0 down 625 units
"7": -200, // Move line 7 up 200 units
"8": -100, // Move line 8 up 100 units
"14": 100, // Move line 14 down 100 units
"19": 172 // Move line 19 down 172 units
}
}
```
**Transformations Applied:**
- Order number gets 4 spaces prepended
- Specific lines get vertical position adjustments
- Each page prints twice (tray 3, then tray 2)
---
### **INVOICE**
```json
{
"label": "Invoice",
"remove_ord_num": true, // Remove "Order #: 12345" entirely
"line_padding": 10,
"printer": [
{
"name": "HL-L6415DW",
"tray": [2, 4, 5] // 3 copies on different trays
},
{
"name": "SAVIN-100",
"tray": [1] // Additional copy on different printer
}
],
"row_shift": {
"0": 625,
"7": -200,
"8": -100,
"14": 100,
"19": 172
}
}
```
**Transformations Applied:**
- "Order #: 12345" → " " (spaces)
- Same row shifts as delivery
- Prints 4 times total: HL-L6415DW tray 2, 4, 5, then SAVIN tray 1
---
### **PRE-BILL**
```json
{
"label": "Pre-bill",
"remove_ord_num": false,
"remove_ord_num_label": true, // Keep number but remove label
"indent_ord_num": "",
"line_padding": 4,
"printer": [{
"name": "HP-LJP4001",
"tray": [1]
}],
"row_shift": {
"8": -2000, // Major upward shift for line 8
"12": 1550 // Major downward shift for line 12
},
"row_trim": {
"8": {
"start": 0,
"end": 65 // Remove chars 0-65 from line 8
},
"12": {
"start": 0,
"end": 6 // Remove chars 0-6 from line 12
}
}
}
```
**Transformations Applied:**
- "Order #: 12345" → " **12345**" (bold number, no label)
- Line 8: Characters 0-65 removed, positioned -2000 units (major upward shift)
- Line 12: Characters 0-6 removed, positioned +1550 units (major downward shift)
---
### **PRODUCTION** / **ORDERDESK** / **BACKORDER** / **GOLDEN** / etc.
Similar patterns with variations in:
- Order number handling
- Line padding amounts
- Tray assignments
- Row shift values (some have none)
---
## Implementation Notes
### Coordinate System
- Horizontal: `\x1B&a{H}h{V}V` where H = horizontal position (1/300 inch), V = vertical (1/300 inch)
- `line_padding` = left margin in 1/300 inch units
- `spacing` = line height in 1/100 inch units (100 or 150)
- `row_shift` = vertical adjustment in 1/100 inch units
### Font Codes
Fonts are printer-specific PCL sequences. Example for HL-L6415DW:
- **Normal**: `\x1B(0N\x1B(s0p10h0s0b4099T`
- **Bold**: `\x1B(0N\x1B(s0p5h0s3b4099T`
### Tray Codes
Tray selection uses PCL commands. Example for HL-L6415DW:
- Tray 1: `\x1B&l4H`
- Tray 2: `\x1B&l5H`
- Tray 3: `\x1B&l8H`
- Tray 4: `\x1B&l9H`
- Tray 5: `\x1B&l10H`
### Multi-Printer Jobs
When multiple printers are specified (like Invoice), the entire transformation process runs separately for each printer with its own font codes, tray codes, and PCL version.
---
## Questions for Review
1. Answers to Review Questions
1. **Order of Operations**: ✅ Only ONE order number manipulation happens per job based on flags, so order doesn't matter.
2. **Row Shift vs Row Trim**: ✅ Independent operations:
- **Row Shift**: Vertical movement (up/down on page) - changes line order or overlays
- **Row Trim**: Horizontal character manipulation (add/remove spacing or characters)
3. **Tray Duplication Pattern**: ✅ Current C# approach is CORRECT:
- Page 1 → Tray 1, Tray 4, Tray 5, Tray 2
- Page 2 → Tray 1, Tray 4, Tray 5, Tray 2
- This maintains carbon copy layer order
- **Future consideration**: Add optional flag to change tray ordering per job type
4. **Empty Row Shift/Trim**: ✅ Skip if not present in configuration.
5. **Hex Escape Timing**: ⚠️ May not be needed in C# since we use Windows drivers instead of raw PCL. Test to determine what's actually required.
6. **Multiple Printers**: ✅ Either sequential or parallel is fine, just maintain page order within each printer.
-246
View File
@@ -1,246 +0,0 @@
# LAAPC Print Service - Remaining Work
## ✅ Completed
- [x] Core service architecture (queue, IPC, file monitoring)
- [x] CLI with self-installation capability
- [x] Basic printing with tray control
- [x] PDF output for testing
- [x] System tray application with status display
- [x] Configuration dialog for printer/tray assignments
- [x] Fixed System.Drawing.Common platform compatibility
## 🅿️ Parking Lot - Future Enhancements
### Configuration Dialog Issues (Non-Critical)
- [ ] **Printer resets on add page:** When adding a new page to a document type, the printer dropdown resets to the first printer in the list if the configuration hasn't been saved yet
- Expected: Should remember previous page's printer selection or default intelligently
- Workaround: Save after configuring each page
- [ ] **Save closes dialog:** Clicking Save closes the configuration dialog, forcing users to reopen it to continue editing
- Expected: Save button should persist changes but keep dialog open
- Suggested: Add "Save & Close" and "Save" as separate buttons, or just make Save not close the dialog
### Printing Enhancements
- [ ] **Skip blank pages:** Add option to skip printing pages with no content
- Use case: Save paper/toner when certain pages in a document are empty
- Implementation: Check if page has content before setting `HasMorePages = true`
- Could be a global setting or per-document-type configuration
## 🔲 Immediate Testing
### Test Configuration Dialog
- [ ] Right-click tray icon → "Configure Printers..."
- [ ] Create new document type
- [ ] Add pages with printer/tray assignments
- [ ] Save configuration (test both local and global)
- [ ] Verify printer-config.json file created correctly
- [ ] Test loading existing configuration
## 🔨 High Priority - Service Integration
### 1. Integrate printer-config.json into PrintService
**Goal:** Service should read printer-config.json instead of appsettings.json
**Files to modify:**
- `PrintService/Services/ConfigurationService.cs` (CREATE NEW)
- Load printer-config.json from C:\ProgramData\LAAPC\ (global) and %LOCALAPPDATA%\LAAPC\ (local)
- Merge local over global (same logic as tray app)
- Return `PrinterConfiguration` object
- `PrintService/Models/PrinterConfig.cs` (COPY from PrintServiceTray)
- Copy the new config models into PrintService project
- `PrintService/Worker.cs` (MODIFY)
- Replace DocumentConfig loading with PrinterConfiguration loading
- Pass PageConfig[] to DocumentProcessor
- `PrintService/Services/DocumentProcessor.cs` (MODIFY - or DELETE if not needed)
- Update to work with new PageConfig format
- May need to refactor logic
- `PrintService/Services/PrinterService.cs` (MODIFY)
- Update Print() method signature to accept PageConfig[]
- Print each page to correct printer/tray based on PageConfig
**Notes:**
- Keep appsettings.json for service-level settings (queue path, retry counts, etc.)
- printer-config.json ONLY for document type → printer/tray mappings
- Support missing config gracefully (log warning, skip job)
## 🔨 High Priority - Page Duplication Pattern
### 2. Implement Multi-Tray Page Duplication
**User Requirement:** Print EACH original page to ALL trays in sequence
**Current Behavior:**
```
TraySequence: [3,4,1,2]
Original document: 4 pages
Output: 4 pages (page 1→tray3, page 2→tray4, page 3→tray1, page 4→tray2)
```
**New Behavior:**
```
TraySequence: [3,4,1,2]
Original document: 4 pages
Output: 16 pages
Page 1 → Tray 3
Page 1 → Tray 4
Page 1 → Tray 1
Page 1 → Tray 2
Page 2 → Tray 3
Page 2 → Tray 4
Page 2 → Tray 1
Page 2 → Tray 2
... (and so on)
```
**Files to modify:**
- `PrintService/Services/PrinterService.cs`
- Modify PrintPage event handler
- Add page duplication logic
- Track: originalPageIndex, trayIndex, currentOutputPage
- For each original page, cycle through all trays before moving to next page
**Algorithm:**
```csharp
int originalPageIndex = 0;
int trayIndex = 0;
int totalOriginalPages = CalculateTotalPages(lines);
bool hasMorePages = true;
PrintPage event:
1. Render lines for originalPageIndex
2. Set tray to pageConfigs[originalPageIndex].TrayNumber
3. Increment trayIndex
4. If trayIndex >= trays.Length:
trayIndex = 0
originalPageIndex++
5. hasMorePages = (originalPageIndex < totalOriginalPages)
```
## 🔨 Medium Priority - Content Modifications
### 3. Port Rust Content Transformation Logic
**Reference:** `RUST/cgwprint/src/background.rs` and `RUST/cgwprint/data/printers.json`
**DO NOT START UNTIL:** Tray config and page duplication are complete and tested
**New config fields needed in PageConfig:**
```json
{
"PageNumber": 1,
"PrinterName": "Brother HL-L6415DW",
"TrayNumber": 3,
"TrayLabel": "Pink",
// NEW FIELDS:
"RemoveOrderNumber": false,
"RemoveOrderNumberLabel": false,
"IndentOrderNumber": 0,
"RowShift": {
"5": -2, // Move line 5 up by 2 positions
"12": 1 // Move line 12 down by 1 position
},
"RowTrim": {
"3": { "Start": 0, "End": 40 }, // Keep only first 40 chars of line 3
"7": { "Start": 10, "End": 50 } // Keep chars 10-50 of line 7
},
"BoldFont": "Courier New Bold",
"NormalFont": "Courier New"
}
```
**Processing steps:**
1. Parse hex escapes (\xHH → bytes)
2. Split on form feed (\x0C) into pages
3. Font replacement (ESC+w+1 → bold, ESC+w+0 → normal)
4. Order number manipulation (remove/indent based on config)
5. Apply row_shift (vertical position adjustments)
6. Apply row_trim (substring extraction)
7. Add PCL/PJL header/footer wrapping
**Files to modify:**
- `PrintService/Models/PageConfig.cs` - Add new properties
- `PrintService/Services/DocumentProcessor.cs` - Add content transformation
- `PrintService/Services/ContentTransformer.cs` (CREATE NEW)
- ParseHexEscapes()
- SplitPages()
- ProcessFontCodes()
- ManipulateOrderNumber()
- ApplyRowShift()
- ApplyRowTrim()
- WrapWithPclHeaders()
**Location-specific variations:**
- "there are 3 locations where this app is used and each has a slight different layout"
- Use document type naming to differentiate: "Invoice_Location1", "Invoice_Location2", etc.
- Or add "Location" field to config and filter by it
## 📋 Testing Checklist
### Before Production Deployment
- [ ] Test service installation on clean 32-bit Windows 10 machine
- [ ] Test CLI from Harbor/Clipper integration
- [ ] Test rapid concurrent print requests (race condition prevention)
- [ ] Test multi-tray printing on Brother HL-L6415DW
- [ ] Test configuration changes without service restart
- [ ] Test queue persistence (stop service mid-job, restart, verify resume)
- [ ] Test error handling (printer offline, invalid tray, etc.)
- [ ] Test tray app startup on Windows boot (add to Startup folder?)
- [ ] Verify file cleanup after successful print
- [ ] Verify error folder gets populated on failures
## 🎯 Future Enhancements (Low Priority)
- [ ] Add logging to file for tray app (currently only service logs)
- [ ] Add "Start/Stop Service" option in tray menu
- [ ] Add "View Logs" option in tray menu
- [ ] Add notification sound for completed jobs
- [ ] Add job history view (last 50 jobs)
- [ ] Support per-page margins configuration
- [ ] Support custom font sizes per document type
- [ ] Add web UI for remote monitoring
## 📝 Important Notes
### Platform Compatibility
- **System.Drawing.Common:** Requires RuntimeHostConfigurationOption in .csproj
- **Remove explicit package reference** to avoid startup errors
- WinForms apps automatically include System.Drawing on Windows
### Configuration Locations
- **Global:** `C:\ProgramData\LAAPC\printer-config.json` (shared, requires admin)
- **Local:** `%LOCALAPPDATA%\LAAPC\printer-config.json` (per-user, no admin)
- **Merge strategy:** Local settings override global settings by document type
### Print Service Config
- Service currently uses: `appsettings.json` (old format with DocumentConfig)
- Service needs to migrate to: `printer-config.json` (new format with PageConfig[])
- Keep `appsettings.json` for non-printer settings (QueuePath, MaxRetries, etc.)
### Page Duplication Logic
- Current: One page per tray (simple mapping)
- Required: All pages to all trays (page duplication with tray cycling)
- Example: 4-page invoice × 5 trays = 20 physical pages printed
- Reason: Simulates carbon copy paper (each page needs copy in each color)
### Content Modifications
- Do LAST (most complex, least critical for initial deployment)
- Three locations have slightly different requirements
- May need per-location configuration or document type naming convention
- PCL/PJL wrapping may only apply to specific printers
### Git Status
- Last commit: cce9248
- `RUST/` folder excluded from version control
- Remember to commit after each major milestone
## 🚀 Next Steps (In Order)
1. **RIGHT NOW:** Test configuration dialog in tray app
2. Integrate printer-config.json into service
3. Implement page duplication pattern
4. Test on production hardware (Brother HL-L6415DW)
5. Deploy to first location
6. Gather feedback
7. Port content modifications from Rust
8. Deploy to remaining two locations
-69
View File
@@ -1,69 +0,0 @@
using System;
using System.Drawing;
using System.Drawing.Printing;
class TrayTest
{
static void Main()
{
var printerName = "Brother HL-L6415DW series Printer";
var trayNumbers = new int[] { 1, 2, 256, 257, 270 }; // Tray 1, 2, 3, 4, 5
var currentPage = 0;
var printDoc = new PrintDocument
{
PrinterSettings = {
PrinterName = printerName,
Duplex = Duplex.Simplex // Force single-sided
}
};
// QueryPageSettings fires BEFORE PrintPage - this is where we set the tray
printDoc.QueryPageSettings += (sender, e) =>
{
if (e.PageSettings == null)
return;
var trayNumber = trayNumbers[currentPage];
// Set the tray for this page
PaperSource? paperSource = null;
foreach (PaperSource source in printDoc.PrinterSettings.PaperSources)
{
if (source.RawKind == trayNumber)
{
paperSource = source;
break;
}
}
if (paperSource != null)
{
e.PageSettings.PaperSource = paperSource;
Console.WriteLine($"Page {currentPage + 1}: Using tray {trayNumber} ({paperSource.SourceName})");
}
else
{
Console.WriteLine($"Page {currentPage + 1}: WARNING - Tray {trayNumber} not found!");
}
};
printDoc.PrintPage += (sender, e) =>
{
if (e.Graphics == null)
return;
// Draw the page number
var font = new Font("Courier New", 24, FontStyle.Bold);
var text = $"Page #{currentPage + 1}";
e.Graphics.DrawString(text, font, Brushes.Black, 100, 100);
currentPage++;
e.HasMorePages = currentPage < trayNumbers.Length;
};
Console.WriteLine($"Printing 5 pages to {printerName}...");
printDoc.Print();
Console.WriteLine("Done!");
}
}
-16
View File
@@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />
</ItemGroup>
</Project>
-120
View File
@@ -1,120 +0,0 @@
{
"DocumentTypes": {
"test": {
"Name": "test",
"Pages": [
{
"PageNumber": 1,
"PrinterName": "Microsoft Print to PDF",
"TrayNumber": 1,
"TrayLabel": "Default"
}
],
"FontName": "Courier New",
"FontSize": 10.0,
"HorizontalOffset": 0,
"VerticalOffset": 0,
"ArchiveAfterPrint": true,
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Test",
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
"SkipPostProcessing": false,
"Transformations": [],
"LinePadding": 10,
"RemoveOrderNumber": false,
"RemoveOrderNumberLabel": true,
"IndentOrderNumber": "",
"RowShift": {
"7": -200,
"8": -100
},
"RowTrim": {
"8": {
"Start": 0,
"End": 10
}
}
},
"delivery": {
"Name": "delivery",
"Pages": [
{
"PageNumber": 1,
"PrinterName": "Microsoft Print to PDF",
"TrayNumber": 3,
"TrayLabel": "Tray 3"
},
{
"PageNumber": 2,
"PrinterName": "Microsoft Print to PDF",
"TrayNumber": 2,
"TrayLabel": "Tray 2"
}
],
"FontName": "Courier New",
"FontSize": 10.0,
"HorizontalOffset": 0,
"VerticalOffset": 0,
"ArchiveAfterPrint": true,
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Orders",
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
"SkipPostProcessing": false,
"Transformations": [],
"LinePadding": 10,
"RemoveOrderNumber": false,
"RemoveOrderNumberLabel": false,
"IndentOrderNumber": " ",
"RowShift": {
"0": 625,
"7": -200,
"8": -100,
"14": 100,
"19": 172
},
"RowTrim": {}
},
"invoice": {
"Name": "invoice",
"Pages": [
{
"PageNumber": 1,
"PrinterName": "Microsoft Print to PDF",
"TrayNumber": 2,
"TrayLabel": "White"
},
{
"PageNumber": 2,
"PrinterName": "Microsoft Print to PDF",
"TrayNumber": 4,
"TrayLabel": "Yellow"
},
{
"PageNumber": 3,
"PrinterName": "Microsoft Print to PDF",
"TrayNumber": 5,
"TrayLabel": "Pink"
}
],
"FontName": "Courier New",
"FontSize": 10.0,
"HorizontalOffset": 0,
"VerticalOffset": 0,
"ArchiveAfterPrint": true,
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Orders",
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
"SkipPostProcessing": false,
"Transformations": [],
"LinePadding": 10,
"RemoveOrderNumber": true,
"RemoveOrderNumberLabel": false,
"IndentOrderNumber": "",
"RowShift": {
"0": 625,
"7": -200,
"8": -100,
"14": 100,
"19": 172
},
"RowTrim": {}
}
}
}
-1
View File
@@ -1 +0,0 @@
[]
-1
View File
@@ -1 +0,0 @@
Page 1 of 1