From 28e06b2f39d8a4c732f95cfec7627072e5fe0c16 Mon Sep 17 00:00:00 2001 From: Jason <17367223+jsoltys@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:46:35 -0500 Subject: [PATCH] Add printer-config integration and Rust transformation flow docs --- PrintService/Models/PrinterConfig.cs | 44 +++ PrintService/Program.cs | 1 + PrintService/Services/ConfigurationService.cs | 87 ++++++ PrintService/Services/DocumentProcessor.cs | 18 +- PrintService/Services/PrinterService.cs | 95 ++++--- PrintService/Worker.cs | 4 +- RUST_TRANSFORMATION_FLOWS.md | 263 ++++++++++++++++++ 7 files changed, 467 insertions(+), 45 deletions(-) create mode 100644 PrintService/Models/PrinterConfig.cs create mode 100644 PrintService/Services/ConfigurationService.cs create mode 100644 RUST_TRANSFORMATION_FLOWS.md diff --git a/PrintService/Models/PrinterConfig.cs b/PrintService/Models/PrinterConfig.cs new file mode 100644 index 0000000..1961fef --- /dev/null +++ b/PrintService/Models/PrinterConfig.cs @@ -0,0 +1,44 @@ +namespace PrintService.Models; + +/// +/// Root configuration loaded from printer-config.json +/// +public class PrinterConfiguration +{ + public Dictionary DocumentTypes { get; set; } = new(); +} + +/// +/// Configuration for a single document type (replaces DocumentConfig for printer/tray mapping) +/// +public class DocumentTypeConfig +{ + public string Name { get; set; } = string.Empty; + public List 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 Transformations { get; set; } = new(); +} + +/// +/// Configuration for a single page (printer and tray assignment) +/// +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; } +} diff --git a/PrintService/Program.cs b/PrintService/Program.cs index c9ac78d..f5276cd 100644 --- a/PrintService/Program.cs +++ b/PrintService/Program.cs @@ -14,6 +14,7 @@ IHost host = Host.CreateDefaultBuilder(args) services.Configure(hostContext.Configuration.GetSection("AppSettings")); // Register services + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/PrintService/Services/ConfigurationService.cs b/PrintService/Services/ConfigurationService.cs new file mode 100644 index 0000000..3ff94c5 --- /dev/null +++ b/PrintService/Services/ConfigurationService.cs @@ -0,0 +1,87 @@ +using System.Text.Json; +using PrintService.Models; + +namespace PrintService.Services; + +/// +/// Loads printer-config.json (global and local), merging local over global. +/// Mirrors the same logic used in PrintServiceTray. +/// +public class ConfigurationService +{ + private readonly string _globalConfigPath; + private readonly string _localConfigPath; + private readonly ILogger _logger; + + public ConfigurationService(ILogger 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"); + } + + /// + /// Load merged configuration. Returns an empty config (not null) if no files exist. + /// + public PrinterConfiguration Load() + { + var config = new PrinterConfiguration(); + + if (File.Exists(_globalConfigPath)) + { + try + { + var json = File.ReadAllText(_globalConfigPath); + var global = JsonSerializer.Deserialize(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(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; + } + + /// + /// Get a single document type config by name (case-insensitive). + /// + public DocumentTypeConfig? GetDocumentType(string documentType) + { + var config = Load(); + return config.DocumentTypes + .FirstOrDefault(kvp => kvp.Key.Equals(documentType, StringComparison.OrdinalIgnoreCase)) + .Value; + } +} diff --git a/PrintService/Services/DocumentProcessor.cs b/PrintService/Services/DocumentProcessor.cs index 432d7d5..408107c 100644 --- a/PrintService/Services/DocumentProcessor.cs +++ b/PrintService/Services/DocumentProcessor.cs @@ -12,18 +12,23 @@ namespace PrintService.Services; public class DocumentProcessor { private readonly AppSettings _settings; + private readonly ConfigurationService _configService; private readonly ILogger _logger; - public DocumentProcessor(IOptions settings, ILogger logger) + public DocumentProcessor( + IOptions settings, + ConfigurationService configService, + ILogger logger) { _settings = settings.Value; + _configService = configService; _logger = logger; } /// /// Process and transform document content /// - public string ProcessDocument(string content, DocumentConfig config) + public string ProcessDocument(string content, DocumentTypeConfig config) { var processed = content; @@ -48,12 +53,11 @@ public class DocumentProcessor } /// - /// Get document configuration by type + /// Get document configuration by type from printer-config.json /// - 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); } /// @@ -67,7 +71,7 @@ public class DocumentProcessor /// /// Archive or delete file after processing /// - public void PostProcess(PrintJob job, DocumentConfig config) + public void PostProcess(PrintJob job, DocumentTypeConfig config) { try { diff --git a/PrintService/Services/PrinterService.cs b/PrintService/Services/PrinterService.cs index 291dc64..86799d2 100644 --- a/PrintService/Services/PrinterService.cs +++ b/PrintService/Services/PrinterService.cs @@ -20,42 +20,51 @@ public class PrinterService } /// - /// 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. /// - 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().Contains(config.PrinterName)) + // Verify primary printer exists + if (!PrinterSettings.InstalledPrinters.Cast().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(); } /// /// Render page content using Graphics API /// - 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 } /// - /// Calculate lines per page based on font and page size + /// Calculate lines per page based on font size /// - 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); } diff --git a/PrintService/Worker.cs b/PrintService/Worker.cs index fa4fe2a..f119fa6 100644 --- a/PrintService/Worker.cs +++ b/PrintService/Worker.cs @@ -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 diff --git a/RUST_TRANSFORMATION_FLOWS.md b/RUST_TRANSFORMATION_FLOWS.md new file mode 100644 index 0000000..dd61a27 --- /dev/null +++ b/RUST_TRANSFORMATION_FLOWS.md @@ -0,0 +1,263 @@ +# 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. \ No newline at end of file