// Copyright (c) 2012-2021 fo-dicom contributors. // Licensed under the Microsoft Public License (MS-PL). using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using FellowOakDicom; using FellowOakDicom.Log; using FellowOakDicom.Network; using QueryRetrieve_SCP.Model; using FellowOakDicom.Network.Client; using FellowOakDicom.Imaging.Codec; namespace QueryRetrieve_SCP { public class QRService : DicomService, IDicomServiceProvider, IDicomCFindProvider, IDicomCEchoProvider, IDicomCMoveProvider, IDicomCGetProvider { private static readonly DicomTransferSyntax[] _acceptedTransferSyntaxes = new DicomTransferSyntax[] { DicomTransferSyntax.ExplicitVRLittleEndian, DicomTransferSyntax.ExplicitVRBigEndian, DicomTransferSyntax.ImplicitVRLittleEndian }; private static readonly DicomTransferSyntax[] _acceptedImageTransferSyntaxes = new DicomTransferSyntax[] { // Lossless DicomTransferSyntax.JPEGLSLossless, DicomTransferSyntax.JPEG2000Lossless, DicomTransferSyntax.JPEGProcess14SV1, DicomTransferSyntax.JPEGProcess14, DicomTransferSyntax.RLELossless, // Lossy DicomTransferSyntax.JPEGLSNearLossless, DicomTransferSyntax.JPEG2000Lossy, DicomTransferSyntax.JPEGProcess1, DicomTransferSyntax.JPEGProcess2_4, // Uncompressed DicomTransferSyntax.ExplicitVRLittleEndian, DicomTransferSyntax.ExplicitVRBigEndian, DicomTransferSyntax.ImplicitVRLittleEndian }; public string CallingAE { get; protected set; } public string CalledAE { get; protected set; } public QRService(INetworkStream stream, Encoding fallbackEncoding, ILogger log, ILogManager logmanager, INetworkManager network, ITranscoderManager transcoder) : base(stream, fallbackEncoding, log, logmanager, network, transcoder) { /* initialization per association can be done here */ } public async Task OnCEchoRequestAsync(DicomCEchoRequest request) { Logger.Info($"Received verification request from AE {CallingAE} with IP: {Association.RemoteHost}"); return new DicomCEchoResponse(request, DicomStatus.Success); } public void OnConnectionClosed(Exception exception) { Clean(); } public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason) { //log the abort reason Logger.Error($"Received abort from {source}, reason is {reason}"); } public Task OnReceiveAssociationReleaseRequestAsync() { Clean(); return SendAssociationReleaseResponseAsync(); } public Task OnReceiveAssociationRequestAsync(DicomAssociation association) { CallingAE = association.CallingAE; CalledAE = association.CalledAE; Logger.Info($"Received association request from AE: {CallingAE} with IP: {association.RemoteHost} "); if (QRServer.AETitle != CalledAE) { Logger.Error($"Association with {CallingAE} rejected since called aet {CalledAE} is unknown"); return SendAssociationRejectAsync(DicomRejectResult.Permanent, DicomRejectSource.ServiceUser, DicomRejectReason.CalledAENotRecognized); } foreach (var pc in association.PresentationContexts) { if (pc.AbstractSyntax == DicomUID.Verification || pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelFind || pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelMove || pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelFind || pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelMove) { pc.AcceptTransferSyntaxes(_acceptedTransferSyntaxes); } else if (pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelGet || pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelGet) { pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes); } else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None) { pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes); } else { Logger.Warn($"Requested abstract syntax {pc.AbstractSyntax} from {CallingAE} not supported"); pc.SetResult(DicomPresentationContextResult.RejectAbstractSyntaxNotSupported); } } Logger.Info($"Accepted association request from {CallingAE}"); return SendAssociationAcceptAsync(association); } public async IAsyncEnumerable OnCFindRequestAsync(DicomCFindRequest request) { var queryLevel = request.Level; var matchingFiles = new List(); IDicomImageFinderService finderService = QRServer.CreateFinderService; // a QR SCP has to define in a DICOM Conformance Statement for which dicom tags it can query // depending on the level of the query. Below there are only very few parameters evaluated. switch (queryLevel) { case DicomQueryRetrieveLevel.Patient: { var patname = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty); var patid = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty); matchingFiles = finderService.FindPatientFiles(patname, patid); } break; case DicomQueryRetrieveLevel.Study: { var patname = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty); var patid = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty); var accNr = request.Dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty); var studyUID = request.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty); matchingFiles = finderService.FindStudyFiles(patname, patid, accNr, studyUID); } break; case DicomQueryRetrieveLevel.Series: { var patname = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty); var patid = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty); var accNr = request.Dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty); var studyUID = request.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty); var seriesUID = request.Dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty); var modality = request.Dataset.GetSingleValueOrDefault(DicomTag.Modality, string.Empty); matchingFiles = finderService.FindSeriesFiles(patname, patid, accNr, studyUID, seriesUID, modality); } break; case DicomQueryRetrieveLevel.Image: yield return new DicomCFindResponse(request, DicomStatus.QueryRetrieveUnableToProcess); yield break; } // now read the required dicomtags from the matching files and return as results foreach (var matchingFile in matchingFiles) { var dicomFile = DicomFile.Open(matchingFile); var result = new DicomDataset(); foreach (var requestedTag in request.Dataset) { // most of the requested DICOM tags are stored in the DICOM files and therefore saved into a database. // you can fill the responses by selecting the values from the database. // also be aware that there are some requested DicomTags like "ModalitiesInStudy" or "NumberOfStudyRelatedInstances" // or "NumberOfPatientRelatedInstances" and so on which have to be calculated and cannot be read from a DICOM file. if (dicomFile.Dataset.Contains(requestedTag.Tag)) { dicomFile.Dataset.CopyTo(result, requestedTag.Tag); } // else if (requestedTag == DicomTag.NumberOfStudyRelatedInstances) // { // ... somehow calculate how many instances are stored within the study // result.Add(DicomTag.NumberOfStudyRelatedInstances, number); // } .... else { result.Add(requestedTag); } } yield return new DicomCFindResponse(request, DicomStatus.Pending) { Dataset = result }; } yield return new DicomCFindResponse(request, DicomStatus.Success); } public void Clean() { // cleanup, like cancel outstanding move- or get-jobs } public async IAsyncEnumerable OnCMoveRequestAsync(DicomCMoveRequest request) { // the c-move request contains the DestinationAE. the data of this AE should be configured somewhere. if (request.DestinationAE != "STORESCP") { yield return new DicomCMoveResponse(request, DicomStatus.QueryRetrieveMoveDestinationUnknown); yield return new DicomCMoveResponse(request, DicomStatus.ProcessingFailure); yield break; } // this data should come from some data storage! var destinationPort = 11112; var destinationIP = "localhost"; IDicomImageFinderService finderService = QRServer.CreateFinderService; IEnumerable matchingFiles = Enumerable.Empty(); switch (request.Level) { case DicomQueryRetrieveLevel.Patient: matchingFiles = finderService.FindFilesByUID(request.Dataset.GetString(DicomTag.PatientID), string.Empty, string.Empty); break; case DicomQueryRetrieveLevel.Study: matchingFiles = finderService.FindFilesByUID(string.Empty, request.Dataset.GetString(DicomTag.StudyInstanceUID), string.Empty); break; case DicomQueryRetrieveLevel.Series: matchingFiles = finderService.FindFilesByUID(string.Empty, string.Empty, request.Dataset.GetString(DicomTag.SeriesInstanceUID)); break; case DicomQueryRetrieveLevel.Image: yield return new DicomCMoveResponse(request, DicomStatus.QueryRetrieveUnableToPerformSuboperations); yield break; } var client = DicomClientFactory.Create(destinationIP, destinationPort, false, QRServer.AETitle, request.DestinationAE); client.NegotiateAsyncOps(); int storeTotal = matchingFiles.Count(); int storeDone = 0; // this variable stores the number of instances that have already been sent int storeFailure = 0; // this variable stores the number of faulues returned in a OnResponseReceived foreach (string file in matchingFiles) { var storeRequest = new DicomCStoreRequest(file); // !!! there is a Bug in fo-dicom 3.0.2 that the OnResponseReceived handlers are invoked not until the DicomClient has already // sent all the instances. So the counters are not increased image by image sent but only once in a bulk after all storage // has been finished. This bug will be fixed hopefully soon. storeRequest.OnResponseReceived += (req, resp) => { if (resp.Status == DicomStatus.Success) { Logger.Info("Storage of image successfull"); storeDone++; } else { Logger.Error("Storage of image failed"); storeFailure++; } }; client.AddRequestAsync(storeRequest).Wait(); } var sendTask = client.SendAsync(); while (!sendTask.IsCompleted) { // while the send-task is runnin we inform the QR SCU every 2 seconds about the status and how many instances are remaining to send. yield return new DicomCMoveResponse(request, DicomStatus.Pending) { Remaining = storeTotal - storeDone - storeFailure, Completed = storeDone }; Thread.Sleep(TimeSpan.FromSeconds(2)); } Logger.Info("..finished"); yield return new DicomCMoveResponse(request, DicomStatus.Success); } public async IAsyncEnumerable OnCGetRequestAsync(DicomCGetRequest request) { IDicomImageFinderService finderService = QRServer.CreateFinderService; IEnumerable matchingFiles = Enumerable.Empty(); switch (request.Level) { case DicomQueryRetrieveLevel.Patient: matchingFiles = finderService.FindFilesByUID(request.Dataset.GetString(DicomTag.PatientID), string.Empty, string.Empty); break; case DicomQueryRetrieveLevel.Study: matchingFiles = finderService.FindFilesByUID(string.Empty, request.Dataset.GetString(DicomTag.StudyInstanceUID), string.Empty); break; case DicomQueryRetrieveLevel.Series: matchingFiles = finderService.FindFilesByUID(string.Empty, string.Empty, request.Dataset.GetString(DicomTag.SeriesInstanceUID)); break; case DicomQueryRetrieveLevel.Image: yield return new DicomCGetResponse(request, DicomStatus.QueryRetrieveUnableToPerformSuboperations); yield break; } foreach (var matchingFile in matchingFiles) { var storeRequest = new DicomCStoreRequest(matchingFile); SendRequestAsync(storeRequest).Wait(); } yield return new DicomCGetResponse(request, DicomStatus.Success); } } }