Files
CGW-Printing/PrintService/Services/PrinterService.cs
T

259 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Options;
using PrintService.Models;
using System.Drawing;
using System.Drawing.Printing;
namespace PrintService.Services;
/// <summary>
/// Handles printing to Windows print queue with tray control
/// </summary>
public class PrinterService
{
private readonly AppSettings _settings;
private readonly ILogger<PrinterService> _logger;
private readonly ContentTransformer _transformer;
public PrinterService(
IOptions<AppSettings> settings,
ILogger<PrinterService> logger,
ContentTransformer transformer)
{
_settings = settings.Value;
_logger = logger;
_transformer = transformer;
}
/// <summary>
/// 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, DocumentTypeConfig config, string originalFileName, string? orderNumber = null)
{
if (config.Pages.Count == 0)
{
throw new InvalidOperationException("No pages/trays defined for document type");
}
// 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 = primaryPrinterName,
Duplex = Duplex.Simplex // Force single-sided printing (no duplexing)
}
};
// Handle PDF output if using Microsoft Print to PDF
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 primary printer exists
if (!PrinterSettings.InstalledPrinters.Cast<string>().Contains(primaryPrinterName))
{
throw new InvalidOperationException($"Printer '{primaryPrinterName}' not found");
}
// QueryPageSettings fires BEFORE PrintPage - set tray here
printDoc.QueryPageSettings += (sender, e) =>
{
if (e.PageSettings == null)
return;
var pageConfig = config.Pages[copyIndex];
try
{
var paperSource = GetPaperSource(printDoc.PrinterSettings, pageConfig.TrayNumber);
if (paperSource != null)
{
e.PageSettings.PaperSource = paperSource;
if (_settings.VerboseLogging)
{
_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", 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, 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 with styled text and transformations
/// </summary>
private void RenderPage(Graphics graphics, TransformedDocument transformed, int startLine, int linesPerPage,
string fontName, float fontSize, int horizontalOffset, int verticalOffset)
{
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 = normalFont.GetHeight(graphics);
// 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, transformed.Lines.Count);
// Track cumulative vertical shift
float cumulativeShift = 0;
for (int lineIndex = startLine; lineIndex < endLine; lineIndex++)
{
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))
{
// 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 size
/// </summary>
private int CalculateLinesPerPage(float fontSize)
{
// Estimate: standard letter size is 11 inches, at 10pt font ~66 lines
var estimatedLineHeight = fontSize * 1.2f; // points
var pageHeightInPoints = 11 * 72; // 11 inches * 72 points per inch
return (int)Math.Floor(pageHeightInPoints / estimatedLineHeight);
}
/// <summary>
/// Get PaperSource by logical tray number
/// </summary>
private PaperSource? GetPaperSource(PrinterSettings printerSettings, int trayNumber)
{
// Try to find by RawKind (tray number)
foreach (PaperSource source in printerSettings.PaperSources)
{
// Some printers use RawKind directly as tray number
if (source.RawKind == trayNumber ||
source.RawKind == (trayNumber + 256)) // Some drivers offset by 256
{
return source;
}
}
// Fallback: use by index if available
if (trayNumber > 0 && trayNumber <= printerSettings.PaperSources.Count)
{
return printerSettings.PaperSources[trayNumber - 1];
}
_logger.LogWarning("Could not map tray {Tray} to PaperSource", trayNumber);
return null;
}
/// <summary>
/// List available paper sources for a printer (for debugging/configuration)
/// </summary>
public void ListPaperSources(string printerName)
{
var printDoc = new PrintDocument { PrinterSettings = { PrinterName = printerName } };
_logger.LogInformation("Available paper sources for {Printer}:", printerName);
foreach (PaperSource source in printDoc.PrinterSettings.PaperSources)
{
_logger.LogInformation(" - {Name} (RawKind: {Kind})", source.SourceName, source.RawKind);
}
}
}