From 7c501d3e44e8e87d20d8825c68211b6de06d509b Mon Sep 17 00:00:00 2001 From: Jason <17367223+jsoltys@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:05:48 -0500 Subject: [PATCH] Built test page for testing tray output for debugging in main app. --- PrintService.sln | 6 +- PrintService/Models/PrinterConfig.cs | 51 ++++- PrintService/Program.cs | 1 + PrintService/Services/ContentTransformer.cs | 241 ++++++++++++++++++++ PrintService/Services/PrinterService.cs | 97 ++++++-- PrintService/Worker.cs | 4 +- PrintServiceTray/Models/PrinterConfig.cs | 17 ++ Test/TrayTest.cs | 69 ++++++ Test/TrayTest.csproj | 16 ++ printer-config.example.json | 120 ++++++++++ test-page.txt | 1 + 11 files changed, 598 insertions(+), 25 deletions(-) create mode 100644 PrintService/Services/ContentTransformer.cs create mode 100644 Test/TrayTest.cs create mode 100644 Test/TrayTest.csproj create mode 100644 printer-config.example.json create mode 100644 test-page.txt diff --git a/PrintService.sln b/PrintService.sln index 9ba5ffd..524e377 100644 --- a/PrintService.sln +++ b/PrintService.sln @@ -14,9 +14,6 @@ Global Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - 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 @@ -31,4 +28,7 @@ Global {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 diff --git a/PrintService/Models/PrinterConfig.cs b/PrintService/Models/PrinterConfig.cs index 1961fef..bec9269 100644 --- a/PrintService/Models/PrinterConfig.cs +++ b/PrintService/Models/PrinterConfig.cs @@ -30,6 +30,39 @@ public class DocumentTypeConfig // Text transformation rules public List Transformations { get; set; } = new(); + + // Rust transformation properties + /// + /// Left margin/indent for all lines (in 1/100 inch units) + /// + public int LinePadding { get; set; } = 0; + + /// + /// Remove both "Order #: " label and the order number entirely + /// + public bool RemoveOrderNumber { get; set; } = false; + + /// + /// Remove "Order #: " label but keep the order number (and make it bold) + /// + public bool RemoveOrderNumberLabel { get; set; } = false; + + /// + /// Add spacing before the order number (shifts it right) + /// + public string IndentOrderNumber { get; set; } = string.Empty; + + /// + /// Vertical position adjustments per line (line number → adjustment in 1/100 inch) + /// Negative values move up, positive values move down + /// + public Dictionary RowShift { get; set; } = new(); + + /// + /// Horizontal character trimming per line (line number → trim configuration) + /// Removes characters from Start to End indices + /// + public Dictionary RowTrim { get; set; } = new(); } /// @@ -41,4 +74,20 @@ public class PageConfig public string PrinterName { get; set; } = string.Empty; public int TrayNumber { get; set; } public string? TrayLabel { get; set; } -} +} + +/// +/// Configuration for row trimming (horizontal character removal) +/// +public class RowTrimConfig +{ + /// + /// Start index of characters to remove (0-based) + /// + public int Start { get; set; } + + /// + /// End index of characters to remove (exclusive) + /// + public int End { get; set; } +} diff --git a/PrintService/Program.cs b/PrintService/Program.cs index f5276cd..9e31bff 100644 --- a/PrintService/Program.cs +++ b/PrintService/Program.cs @@ -17,6 +17,7 @@ IHost host = Host.CreateDefaultBuilder(args) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/PrintService/Services/ContentTransformer.cs b/PrintService/Services/ContentTransformer.cs new file mode 100644 index 0000000..4662e8a --- /dev/null +++ b/PrintService/Services/ContentTransformer.cs @@ -0,0 +1,241 @@ +using PrintService.Models; +using System.Text; +using System.Text.RegularExpressions; + +namespace PrintService.Services; + +/// +/// Handles content transformations from Rust CLI (cgwprint) logic +/// +public class ContentTransformer +{ + private readonly ILogger _logger; + + public ContentTransformer(ILogger logger) + { + _logger = logger; + } + + /// + /// Transform document content according to configuration + /// + 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 + }; + } + + /// + /// Step 1: Normalize font codes (pre-processing) + /// + 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; + } + + /// + /// Step 2: Order number manipulation + /// + 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; + } + + /// + /// Step 3: Apply row trim (horizontal character removal) + /// + private string[] ApplyRowTrim(string[] lines, Dictionary 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; + } + + /// + /// Step 4: Parse lines into segments with font styling + /// + private List ParseStyledLines(string[] lines) + { + var styledLines = new List(); + + foreach (var line in lines) + { + var segments = new List(); + 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; + } +} + +/// +/// Transformed document with styled lines and layout adjustments +/// +public class TransformedDocument +{ + public List Lines { get; set; } = new(); + public Dictionary RowShifts { get; set; } = new(); + public int LinePadding { get; set; } +} + +/// +/// A line with styled text segments +/// +public class StyledLine +{ + public List Segments { get; set; } = new(); +} + +/// +/// A text segment with font styling +/// +public class TextSegment +{ + public string Text { get; set; } = string.Empty; + public FontStyle Style { get; set; } = FontStyle.Regular; +} + +public enum FontStyle +{ + Regular, + Bold +} diff --git a/PrintService/Services/PrinterService.cs b/PrintService/Services/PrinterService.cs index 86799d2..4bbc1c5 100644 --- a/PrintService/Services/PrinterService.cs +++ b/PrintService/Services/PrinterService.cs @@ -12,11 +12,16 @@ public class PrinterService { private readonly AppSettings _settings; private readonly ILogger _logger; + private readonly ContentTransformer _transformer; - public PrinterService(IOptions settings, ILogger logger) + public PrinterService( + IOptions settings, + ILogger logger, + ContentTransformer transformer) { _settings = settings.Value; _logger = logger; + _transformer = transformer; } /// @@ -24,16 +29,18 @@ public class PrinterService /// 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, DocumentTypeConfig config, string originalFileName) + 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"); } - var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.None); + // 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(lines.Length / (double)linesPerPage)); + 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) @@ -44,7 +51,10 @@ public class PrinterService var printDoc = new PrintDocument { - PrinterSettings = { PrinterName = primaryPrinterName } + PrinterSettings = { + PrinterName = primaryPrinterName, + Duplex = Duplex.Simplex // Force single-sided printing (no duplexing) + } }; // Handle PDF output if using Microsoft Print to PDF @@ -67,9 +77,10 @@ public class PrinterService throw new InvalidOperationException($"Printer '{primaryPrinterName}' not found"); } - printDoc.PrintPage += (sender, e) => + // QueryPageSettings fires BEFORE PrintPage - set tray here + printDoc.QueryPageSettings += (sender, e) => { - if (e.Graphics == null || e.PageSettings == null) + if (e.PageSettings == null) return; var pageConfig = config.Pages[copyIndex]; @@ -96,9 +107,15 @@ public class PrinterService { _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, lines, originalPageIndex * linesPerPage, linesPerPage, + RenderPage(e.Graphics, transformed, originalPageIndex * linesPerPage, linesPerPage, config.FontName, config.FontSize, config.HorizontalOffset, config.VerticalOffset); copyIndex++; @@ -122,28 +139,70 @@ public class PrinterService } /// - /// Render page content using Graphics API + /// Render page content using Graphics API with styled text and transformations /// - private void RenderPage(Graphics graphics, string[] lines, int startLine, int linesPerPage, + private void RenderPage(Graphics graphics, TransformedDocument transformed, int startLine, int linesPerPage, string fontName, float fontSize, int horizontalOffset, int verticalOffset) { - var font = new Font(fontName, fontSize); + 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 = font.GetHeight(graphics); + var lineHeight = normalFont.GetHeight(graphics); - var x = (float)horizontalOffset; - var y = (float)verticalOffset; + // 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, lines.Length); + var endLine = Math.Min(startLine + linesPerPage, transformed.Lines.Count); - for (int i = startLine; i < endLine; i++) + // Track cumulative vertical shift + float cumulativeShift = 0; + + for (int lineIndex = startLine; lineIndex < endLine; lineIndex++) { - if (i < lines.Length) + 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)) { - graphics.DrawString(lines[i], font, brush, x, y); - y += lineHeight; + // 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(); } /// diff --git a/PrintService/Worker.cs b/PrintService/Worker.cs index f119fa6..f26c538 100644 --- a/PrintService/Worker.cs +++ b/PrintService/Worker.cs @@ -130,8 +130,8 @@ public class Worker : BackgroundService // Process/transform content var processedContent = _documentProcessor.ProcessDocument(content, config); - // Print to Windows queue - await Task.Run(() => _printerService.Print(processedContent, config, job.OriginalFilename), cancellationToken); + // 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); diff --git a/PrintServiceTray/Models/PrinterConfig.cs b/PrintServiceTray/Models/PrinterConfig.cs index 01839e5..58f2494 100644 --- a/PrintServiceTray/Models/PrinterConfig.cs +++ b/PrintServiceTray/Models/PrinterConfig.cs @@ -15,6 +15,14 @@ public class DocumentTypeConfig { public string Name { get; set; } = string.Empty; public List 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 RowShift { get; set; } = new(); + public Dictionary RowTrim { get; set; } = new(); } /// @@ -27,3 +35,12 @@ public class PageConfig public int TrayNumber { get; set; } public string? TrayLabel { get; set; } // Optional: "Pink Paper", "Green Paper", etc. } + +/// +/// Configuration for row trimming (horizontal character removal) +/// +public class RowTrimConfig +{ + public int Start { get; set; } + public int End { get; set; } +} diff --git a/Test/TrayTest.cs b/Test/TrayTest.cs new file mode 100644 index 0000000..6f987b6 --- /dev/null +++ b/Test/TrayTest.cs @@ -0,0 +1,69 @@ +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!"); + } +} diff --git a/Test/TrayTest.csproj b/Test/TrayTest.csproj new file mode 100644 index 0000000..0c090cc --- /dev/null +++ b/Test/TrayTest.csproj @@ -0,0 +1,16 @@ + + + Exe + net7.0-windows + enable + true + + + + + + + + + + diff --git a/printer-config.example.json b/printer-config.example.json new file mode 100644 index 0000000..18965e9 --- /dev/null +++ b/printer-config.example.json @@ -0,0 +1,120 @@ +{ + "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": {} + } + } +} diff --git a/test-page.txt b/test-page.txt new file mode 100644 index 0000000..5a9508a --- /dev/null +++ b/test-page.txt @@ -0,0 +1 @@ +Page 1 of 1