Compare commits
3 Commits
f5976a04c9
...
60530d2874
| Author | SHA1 | Date | |
|---|---|---|---|
| 60530d2874 | |||
| 7c501d3e44 | |||
| 28e06b2f39 |
+3
-3
@@ -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
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
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; }
|
||||
}
|
||||
@@ -14,8 +14,10 @@ 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<ContentTransformer>();
|
||||
services.AddSingleton<PrinterService>();
|
||||
services.AddSingleton<DocumentProcessor>();
|
||||
services.AddSingleton<IpcService>();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -12,124 +12,206 @@ 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>
|
||||
/// 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, string? orderNumber = null)
|
||||
{
|
||||
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;
|
||||
// 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 = config.PrinterName }
|
||||
PrinterSettings = {
|
||||
PrinterName = primaryPrinterName,
|
||||
Duplex = Duplex.Simplex // Force single-sided printing (no duplexing)
|
||||
}
|
||||
};
|
||||
|
||||
// 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) =>
|
||||
// QueryPageSettings fires BEFORE PrintPage - set tray here
|
||||
printDoc.QueryPageSettings += (sender, e) =>
|
||||
{
|
||||
if (e.Graphics == null || e.PageSettings == null)
|
||||
if (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);
|
||||
|
||||
currentPageIndex++;
|
||||
e.HasMorePages = (currentPageIndex < config.TraySequence.Length);
|
||||
};
|
||||
|
||||
_logger.LogInformation("Printing to {Printer} with {Pages} pages",
|
||||
config.PrinterName, config.TraySequence.Length);
|
||||
|
||||
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
|
||||
/// Render page content using Graphics API with styled text and transformations
|
||||
/// </summary>
|
||||
private void RenderPage(Graphics graphics, string[] lines, int startLine, int linesPerPage, DocumentConfig config)
|
||||
private void RenderPage(Graphics graphics, TransformedDocument transformed, int startLine, int linesPerPage,
|
||||
string fontName, float fontSize, int horizontalOffset, int verticalOffset)
|
||||
{
|
||||
var font = new Font(config.FontName, config.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)config.HorizontalOffset;
|
||||
var y = (float)config.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>
|
||||
/// 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -15,6 +15,14 @@ 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>
|
||||
@@ -27,3 +35,12 @@ public class PageConfig
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -9,9 +9,9 @@
|
||||
- [x] Configuration dialog for printer/tray assignments
|
||||
- [x] Fixed System.Drawing.Common platform compatibility
|
||||
|
||||
## 🅿️ Parking Lot - Configuration Dialog Issues
|
||||
## 🅿️ Parking Lot - Future Enhancements
|
||||
|
||||
### Known Issues (Non-Critical)
|
||||
### 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
|
||||
@@ -20,6 +20,12 @@
|
||||
- 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
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Page 1 of 1
|
||||
Reference in New Issue
Block a user