259 lines
10 KiB
C#
259 lines
10 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|