using Microsoft.Extensions.Options; using PrintService.Models; using System.Drawing; using System.Drawing.Printing; namespace PrintService.Services; /// /// Handles printing to Windows print queue with tray control /// public class PrinterService { private readonly AppSettings _settings; private readonly ILogger _logger; private readonly ContentTransformer _transformer; public PrinterService( IOptions settings, ILogger logger, ContentTransformer transformer) { _settings = settings.Value; _logger = logger; _transformer = transformer; } /// /// 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. /// 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().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(); } /// /// Render page content using Graphics API with styled text and transformations /// 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(); } /// /// Calculate lines per page based on font size /// 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); } /// /// Get PaperSource by logical tray number /// 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; } /// /// List available paper sources for a printer (for debugging/configuration) /// 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); } } }