Add printer-config integration and Rust transformation flow docs

This commit is contained in:
Jason
2026-06-02 19:46:35 -05:00
parent f5976a04c9
commit 28e06b2f39
7 changed files with 467 additions and 45 deletions
+44
View File
@@ -0,0 +1,44 @@
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();
}
/// <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; }
}
+1
View File
@@ -14,6 +14,7 @@ IHost host = Host.CreateDefaultBuilder(args)
services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));
// Register services
services.AddSingleton<ConfigurationService>();
services.AddSingleton<PrintQueueService>();
services.AddSingleton<FileMonitorService>();
services.AddSingleton<PrinterService>();
@@ -0,0 +1,87 @@
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;
}
}
+11 -7
View File
@@ -12,18 +12,23 @@ namespace PrintService.Services;
public class DocumentProcessor
{
private readonly AppSettings _settings;
private readonly ConfigurationService _configService;
private readonly ILogger<DocumentProcessor> _logger;
public DocumentProcessor(IOptions<AppSettings> settings, 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, DocumentConfig config)
public string ProcessDocument(string content, DocumentTypeConfig config)
{
var processed = content;
@@ -48,12 +53,11 @@ public class DocumentProcessor
}
/// <summary>
/// Get document configuration by type
/// Get document configuration by type from printer-config.json
/// </summary>
public DocumentConfig? GetDocumentConfig(string documentType)
public DocumentTypeConfig? GetDocumentConfig(string documentType)
{
return _settings.DocumentTypes.FirstOrDefault(dt =>
dt.Name.Equals(documentType, StringComparison.OrdinalIgnoreCase));
return _configService.GetDocumentType(documentType);
}
/// <summary>
@@ -67,7 +71,7 @@ public class DocumentProcessor
/// <summary>
/// Archive or delete file after processing
/// </summary>
public void PostProcess(PrintJob job, DocumentConfig config)
public void PostProcess(PrintJob job, DocumentTypeConfig config)
{
try
{
+59 -36
View File
@@ -20,42 +20,51 @@ public class PrinterService
}
/// <summary>
/// Print document to specified printer with tray sequence
/// 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, DocumentConfig config, string originalFileName)
public void Print(string content, DocumentTypeConfig config, string originalFileName)
{
if (config.TraySequence.Length == 0)
if (config.Pages.Count == 0)
{
throw new InvalidOperationException("No tray sequence defined for document type");
throw new InvalidOperationException("No pages/trays defined for document type");
}
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
var linesPerPage = CalculateLinesPerPage(config);
var currentPageIndex = 0;
var linesPerPage = CalculateLinesPerPage(config.FontSize);
var totalOriginalPages = Math.Max(1, (int)Math.Ceiling(lines.Length / (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 = config.PrinterName }
PrinterSettings = { PrinterName = primaryPrinterName }
};
// Handle PDF output if using Microsoft Print to PDF
if (config.PrinterName.Equals("Microsoft Print to PDF", StringComparison.OrdinalIgnoreCase) &&
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 printer exists
if (!PrinterSettings.InstalledPrinters.Cast<string>().Contains(config.PrinterName))
// Verify primary printer exists
if (!PrinterSettings.InstalledPrinters.Cast<string>().Contains(primaryPrinterName))
{
throw new InvalidOperationException($"Printer '{config.PrinterName}' not found");
throw new InvalidOperationException($"Printer '{primaryPrinterName}' not found");
}
printDoc.PrintPage += (sender, e) =>
@@ -63,52 +72,67 @@ public class PrinterService
if (e.Graphics == null || e.PageSettings == null)
return;
// Select tray for current page
var trayIndex = config.TraySequence[currentPageIndex % config.TraySequence.Length];
var pageConfig = config.Pages[copyIndex];
try
{
// Map logical tray number to PaperSource
var paperSource = GetPaperSource(printDoc.PrinterSettings, trayIndex);
var paperSource = GetPaperSource(printDoc.PrinterSettings, pageConfig.TrayNumber);
if (paperSource != null)
{
e.PageSettings.PaperSource = paperSource;
if (_settings.VerboseLogging)
{
_logger.LogDebug("Page {Page}: Using tray {Tray} ({Source})",
currentPageIndex + 1, trayIndex, paperSource.SourceName);
_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", trayIndex);
_logger.LogWarning(ex, "Failed to set tray {Tray}, using default", pageConfig.TrayNumber);
}
// Render page content
RenderPage(e.Graphics, lines, currentPageIndex * linesPerPage, linesPerPage, config);
// Render the same original page for every copy before moving to next original page.
RenderPage(e.Graphics, lines, originalPageIndex * linesPerPage, linesPerPage,
config.FontName, config.FontSize, config.HorizontalOffset, config.VerticalOffset);
currentPageIndex++;
e.HasMorePages = (currentPageIndex < config.TraySequence.Length);
copyIndex++;
if (copyIndex >= config.Pages.Count)
{
copyIndex = 0;
originalPageIndex++;
}
e.HasMorePages = originalPageIndex < totalOriginalPages;
};
_logger.LogInformation("Printing to {Printer} with {Pages} pages",
config.PrinterName, config.TraySequence.Length);
_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
/// </summary>
private void RenderPage(Graphics graphics, string[] lines, int startLine, int linesPerPage, DocumentConfig config)
private void RenderPage(Graphics graphics, string[] lines, int startLine, int linesPerPage,
string fontName, float fontSize, int horizontalOffset, int verticalOffset)
{
var font = new Font(config.FontName, config.FontSize);
var font = new Font(fontName, fontSize);
var brush = Brushes.Black;
var lineHeight = font.GetHeight(graphics);
var x = (float)config.HorizontalOffset;
var y = (float)config.VerticalOffset;
var x = (float)horizontalOffset;
var y = (float)verticalOffset;
var endLine = Math.Min(startLine + linesPerPage, lines.Length);
@@ -123,13 +147,12 @@ public class PrinterService
}
/// <summary>
/// Calculate lines per page based on font and page size
/// Calculate lines per page based on font size
/// </summary>
private int CalculateLinesPerPage(DocumentConfig config)
private int CalculateLinesPerPage(float fontSize)
{
// Estimate: standard letter size is 11 inches, at 10pt font ~66 lines
// This is simplified; in production you'd calculate based on actual page dimensions
var estimatedLineHeight = config.FontSize * 1.2f; // points
var estimatedLineHeight = fontSize * 1.2f; // points
var pageHeightInPoints = 11 * 72; // 11 inches * 72 points per inch
return (int)Math.Floor(pageHeightInPoints / estimatedLineHeight);
}
+2 -2
View File
@@ -112,11 +112,11 @@ public class Worker : BackgroundService
try
{
// Get document configuration
// 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}");
throw new InvalidOperationException($"No configuration found for document type '{job.DocumentType}' in printer-config.json");
}
// Read file content