// Copyright (c) 2012-2021 fo-dicom contributors. // Licensed under the Microsoft Public License (MS-PL). using FellowOakDicom.Log; using FellowOakDicom.Printing; using System; using System.Collections.Generic; using System.Drawing.Printing; using System.IO; using System.Linq; using System.Threading; namespace FellowOakDicom.Samples.Printing { public class StatusUpdateEventArgs : EventArgs { public ushort EventTypeId { get; private set; } public string ExecutionStatusInfo { get; private set; } public string FilmSessionLabel { get; private set; } public string PrinterName { get; private set; } public StatusUpdateEventArgs( ushort eventTypeId, string executionStatusInfo, string filmSessionLabel, string printerName) { EventTypeId = eventTypeId; ExecutionStatusInfo = executionStatusInfo; FilmSessionLabel = filmSessionLabel; PrinterName = printerName; } } public enum PrintJobStatus : ushort { Pending = 1, Printing = 2, Done = 3, Failure = 4 } public class PrintJob : DicomDataset { #region Properties and Attributes public bool SendNEventReport { get; set; } public Guid PrintJobGuid { get; private set; } public IList FilmBoxFolderList { get; private set; } public Printer Printer { get; private set; } public PrintJobStatus Status { get; private set; } public string PrintJobFolder { get; private set; } public string FullPrintJobFolder { get; private set; } public Exception Error { get; private set; } public string FilmSessionLabel { get; private set; } private int _currentPage; private FilmBox _currentFilmBox; /// /// Print job SOP class UID /// public readonly DicomUID SOPClassUID = DicomUID.PrintJob; /// /// Print job SOP instance UID /// public DicomUID SOPInstanceUID { get; private set; } /// /// Execution status of print job. /// /// /// Enumerated Values: /// /// PENDING /// PRINTING /// DONE /// FAILURE /// /// public string ExecutionStatus { get => GetSingleValueOrDefault(DicomTag.ExecutionStatus, string.Empty); set => AddOrUpdate(DicomTag.ExecutionStatus, value.ToUpperInvariant()); } /// /// Additional information about Execution Status (2100,0020). /// public string ExecutionStatusInfo { get => GetSingleValueOrDefault(DicomTag.ExecutionStatusInfo, string.Empty); set => AddOrUpdate(DicomTag.ExecutionStatusInfo, value.ToUpperInvariant()); } /// /// Specifies the priority of the print job. /// /// /// Enumerated values: /// /// HIGH /// MED /// LOW /// /// public string PrintPriority { get => GetSingleValueOrDefault(DicomTag.PrintPriority, "MED"); set => AddOrUpdate(DicomTag.PrintPriority, value); } /// /// Date/Time of print job creation. /// public DateTime CreationDateTime { get => this.GetDateTime(DicomTag.CreationDate, DicomTag.CreationTime); set { AddOrUpdate(DicomTag.CreationDate, value); AddOrUpdate(DicomTag.CreationTime, value); } } /// /// User defined name identifying the printer. /// public string PrinterName { get => GetSingleValueOrDefault(DicomTag.PrinterName, string.Empty); set => AddOrUpdate(DicomTag.PrinterName, value); } /// /// DICOM Application Entity Title that issued the print operation. /// public string Originator { get => GetSingleValueOrDefault(DicomTag.Originator, string.Empty); set => AddOrUpdate(DicomTag.Originator, value); } public ILogger Log { get; private set; } public event EventHandler StatusUpdate; #endregion #region Constructors /// /// Construct new print job using specified SOP instance UID. If passed SOP instance UID is missing, new UID will /// be generated /// /// New print job SOP instance uID public PrintJob(DicomUID sopInstance, Printer printer, string originator, ILogger log) : base() { Log = log; SOPInstanceUID = string.IsNullOrEmpty(sopInstance?.UID) ? DicomUID.Generate() : sopInstance; Add(DicomTag.SOPClassUID, SOPClassUID); Add(DicomTag.SOPInstanceUID, SOPInstanceUID); Printer = printer ?? throw new ArgumentNullException(nameof(printer)); Status = PrintJobStatus.Pending; PrinterName = Printer.PrinterAet; Originator = originator; if (CreationDateTime == DateTime.MinValue) { CreationDateTime = DateTime.Now; } PrintJobFolder = SOPInstanceUID.UID; var receivingFolder = Path.Combine(Environment.CurrentDirectory, "PrintJobs"); FullPrintJobFolder = Path.Combine(receivingFolder, PrintJobFolder); FilmBoxFolderList = new List(); } #endregion #region Printing Methods public void Print(IList filmBoxList) { try { Status = PrintJobStatus.Pending; OnStatusUpdate("QUEUED"); var printJobDir = new DirectoryInfo(FullPrintJobFolder); if (!printJobDir.Exists) { printJobDir.Create(); } DicomFile file; int filmsCount = FilmBoxFolderList.Count; for (int i = 0; i < filmBoxList.Count; i++) { var filmBox = filmBoxList[i]; var filmBoxDir = printJobDir.CreateSubdirectory(string.Format("F{0:000000}", i + 1 + filmsCount)); file = new DicomFile(filmBox.FilmSession); file.Save(Path.Combine(filmBoxDir.FullName, "FilmSession.dcm")); FilmBoxFolderList.Add(filmBoxDir.Name); filmBox.Save(filmBoxDir.FullName); } FilmSessionLabel = filmBoxList.First().FilmSession.FilmSessionLabel; var thread = new Thread(new ThreadStart(DoPrint)) { Name = $"PrintJob {SOPInstanceUID.UID}", IsBackground = true }; thread.Start(); } catch (Exception ex) { Error = ex; Status = PrintJobStatus.Failure; OnStatusUpdate("UNKNOWN"); // The exception may be analyzed and a more proper code as defined in "Table C.13.9.1-1. Defined Terms for Printer and Execution Status Info" may be used DeletePrintFolder(); } } private void DoPrint() { PrintDocument printDocument = null; try { Status = PrintJobStatus.Printing; OnStatusUpdate("QUEUED"); var printerSettings = new PrinterSettings { PrinterName = "Microsoft XPS Document Writer", PrintToFile = true, PrintFileName = Path.Combine(FullPrintJobFolder, SOPInstanceUID.UID + ".xps") }; printDocument = new PrintDocument { PrinterSettings = printerSettings, DocumentName = Thread.CurrentThread.Name, PrintController = new StandardPrintController() }; printDocument.QueryPageSettings += OnQueryPageSettings; printDocument.PrintPage += OnPrintPage; printDocument.Print(); Status = PrintJobStatus.Done; OnStatusUpdate("NORMAL"); } catch (Exception) { Status = PrintJobStatus.Failure; OnStatusUpdate("UNKNOWN"); // The exception may be analyzed and a more proper code as defined in "Table C.13.9.1-1. Defined Terms for Printer and Execution Status Info" may be used } finally { if (printDocument != null) { //dispose the print document and unregister events handlers to avoid memory leaks printDocument.QueryPageSettings -= OnQueryPageSettings; printDocument.PrintPage -= OnPrintPage; printDocument.Dispose(); } } } private void OnPrintPage(object sender, PrintPageEventArgs e) { _currentFilmBox.Print(e.Graphics, e.MarginBounds, 100); _currentFilmBox = null; _currentPage++; e.HasMorePages = _currentPage < FilmBoxFolderList.Count; } private void OnQueryPageSettings(object sender, QueryPageSettingsEventArgs e) { OnStatusUpdate($"NORMAL"); var filmBoxFolder = Path.Combine(FullPrintJobFolder, FilmBoxFolderList[_currentPage]); var filmSession = FilmSession.Load(Path.Combine(filmBoxFolder, "FilmSession.dcm")); _currentFilmBox = FilmBox.Load(filmSession, filmBoxFolder); e.PageSettings.Margins.Left = 25; e.PageSettings.Margins.Right = 25; e.PageSettings.Margins.Top = 25; e.PageSettings.Margins.Bottom = 25; e.PageSettings.Landscape = _currentFilmBox.FilmOrientation == "LANDSCAPE"; } private void DeletePrintFolder() { var folderInfo = new DirectoryInfo(FullPrintJobFolder); if (folderInfo.Exists) { folderInfo.Delete(true); } } #endregion #region Notification Methods protected virtual void OnStatusUpdate(string info) { ExecutionStatus = Status.ToString(); ExecutionStatusInfo = info; if (Status != PrintJobStatus.Failure) { Log.Info("Print Job {0} Status {1}: {2}", SOPInstanceUID.UID.Split('.').Last(), Status, info); } else { Log.Error("Print Job {0} Status {1}: {2}", SOPInstanceUID.UID.Split('.').Last(), Status, info); } StatusUpdate?.Invoke(this, new StatusUpdateEventArgs((ushort)Status, info, FilmSessionLabel, PrinterName)); } #endregion } }