Built test page for testing tray output for debugging in main app.

This commit is contained in:
Jason
2026-06-11 10:05:48 -05:00
parent 28e06b2f39
commit 7c501d3e44
11 changed files with 598 additions and 25 deletions
+241
View File
@@ -0,0 +1,241 @@
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
}
+78 -19
View File
@@ -12,11 +12,16 @@ public class PrinterService
{
private readonly AppSettings _settings;
private readonly ILogger<PrinterService> _logger;
private readonly ContentTransformer _transformer;
public PrinterService(IOptions<AppSettings> settings, ILogger<PrinterService> logger)
public PrinterService(
IOptions<AppSettings> settings,
ILogger<PrinterService> logger,
ContentTransformer transformer)
{
_settings = settings.Value;
_logger = logger;
_transformer = transformer;
}
/// <summary>
@@ -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.
/// </summary>
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
}
/// <summary>
/// Render page content using Graphics API
/// Render page content using Graphics API with styled text and transformations
/// </summary>
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();
}
/// <summary>