123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- // Copyright (c) 2012-2020 fo-dicom contributors.
- // Licensed under the Microsoft Public License (MS-PL).
- using System;
- using System.Diagnostics;
- using System.Drawing;
- using System.Drawing.Imaging;
- using System.IO;
- using System.Linq;
- using System.Net;
- using System.Net.Http;
- using System.Net.Http.Headers;
- using System.Threading.Tasks;
- using System.Web.Http;
- using System.Web.Http.Cors;
- using Dicom;
- using Dicom.Imaging;
- using Dicom.Imaging.Codec;
- using Dicom.IO;
- using Wado.Models;
- namespace Wado.Controllers
- {
- /// <summary>
- /// main controller for wado implementation
- /// </summary>
- /// <remarks>the current implementation is incomplete</remarks>
- /// <remarks>more infos in the official specification : http://dicom.nema.org/medical/dicom/current/output/pdf/part18.pdf </remarks>
- [EnableCors(origins: "*", headers: "*", methods: "GET")] //allows access by any host
- [RoutePrefix("wado")]
- public class WadoUriController : ApiController
- {
- #region consts
- /// <summary>
- /// string representation of the dicom content type
- /// </summary>
- private const string AppDicomContentType = "application/dicom";
- /// <summary>
- /// string representation of the jpeg content type
- /// </summary>
- private const string JpegImageContentType = "image/jpeg";
- #endregion
- #region fields
- /// <summary>
- /// service used to retrieve images by instance Uid
- /// </summary>
- private readonly IDicomImageFinderService _dicomImageFinderService;
- #endregion
- #region constructors
- /// <summary>
- /// Initialize a new instance of WadoUriController
- /// </summary>
- public WadoUriController()
- {
- //Put your own IDicomImageFinderService implementation here for real world scenarios
- _dicomImageFinderService = new TestDicomImageFinderService();
- }
- /// <summary>
- /// Testing purposes constructor if you want to inject a custom IDicomImageFinderService
- /// in unit tests
- /// </summary>
- /// <param name="dicomImageFinderService"></param>
- public WadoUriController(IDicomImageFinderService dicomImageFinderService)
- {
- _dicomImageFinderService = dicomImageFinderService;
- }
- #endregion
- #region methods
- /// <summary>
- /// main wado method
- /// </summary>
- /// <param name="requestMessage">web request</param>
- /// <param name="requestType">always equals to wado in current wado specification, may change in the future</param>
- /// <param name="studyUID">study instance UID</param>
- /// <param name="seriesUID">serie instance UID</param>
- /// <param name="objectUID">instance UID</param>
- /// <param name="contentType">The value shall be a list of MIME types, separated by a "," character, and potentially associated with relative degree of preference, as specified in IETF RFC2616. </param>
- /// <param name="charset">character set of the object to be retrieved.</param>
- /// <param name="transferSyntax">The Transfer Syntax to be used within the DICOM image object, as specified in PS 3.6</param>
- /// <param name="anonymize">if value is "yes", indicates that we should anonymize object.
- /// The Server may return an error if it either cannot or refuses to anonymize that object</param>
- /// <returns></returns>
- [Route("")]
- public async Task<HttpResponseMessage> GetStudyInstances(HttpRequestMessage requestMessage, string requestType,
- string studyUID, string seriesUID, string objectUID, string contentType = null, string charset = null,
- string transferSyntax = null, string anonymize = null)
- {
- //we do not handle anonymization
- if (anonymize == "yes")
- return requestMessage.CreateErrorResponse(HttpStatusCode.NotAcceptable, "anonymise is not supported on the server");
- //we extract the content types from contentType value
- bool canParseContentTypeParameter = ExtractContentTypesFromContentTypeParameter(contentType,
- out string[] contentTypes);
- if (!canParseContentTypeParameter)
- return requestMessage.CreateErrorResponse(HttpStatusCode.NotAcceptable,
- string.Format("contentType parameter (value: {0}) cannot be parsed", contentType));
- //8.1.5 The Web Client shall provide list of content types it supports in the "Accept" field of the GET method. The
- //value of the contentType parameter of the request shall be one of the values specified in that field.
- string[] acceptContentTypesHeader =
- requestMessage.Headers.Accept.Select(header => header.MediaType).ToArray();
- // */* means that we accept everything for the content Header
- bool acceptAllTypesInAcceptHeader = acceptContentTypesHeader.Contains("*/*");
- bool isRequestedContentTypeCompatibleWithAcceptContentHeader = acceptAllTypesInAcceptHeader ||
- contentTypes == null ||
- acceptContentTypesHeader.Intersect(
- contentTypes).Any();
- if (!isRequestedContentTypeCompatibleWithAcceptContentHeader)
- {
- return requestMessage.CreateErrorResponse(HttpStatusCode.NotAcceptable,
- string.Format("content type {0} is not compatible with types specified in Accept Header",
- contentType));
- }
- //6.3.2.1 The MIME type shall be one on the MIME types defined in the contentType parameter, preferably the most
- //desired by the Web Client, and shall be in any case compatible with the ‘Accept’ field of the GET method.
- //Note: The HTTP behavior is that an error (406 – Not Acceptable) is returned if the required content type cannot
- //be served.
- string[] compatibleContentTypesByOrderOfPreference =
- GetCompatibleContentTypesByOrderOfPreference(contentTypes,
- acceptContentTypesHeader);
- //if there is no type that can be handled by our server, we return an error
- if (compatibleContentTypesByOrderOfPreference != null
- && !compatibleContentTypesByOrderOfPreference.Contains(JpegImageContentType)
- && !compatibleContentTypesByOrderOfPreference.Contains(AppDicomContentType))
- {
- return requestMessage.CreateErrorResponse(HttpStatusCode.NotAcceptable,
- string.Format("content type(s) {0} cannot be served",
- string.Join(" - ", compatibleContentTypesByOrderOfPreference)
- ));
- }
- //we now need to handle the case where contentType is not specified, but in this case, the default value
- //depends on the image, so we need to open it
- string dicomImagePath = _dicomImageFinderService.GetImageByInstanceUid(objectUID);
- if (dicomImagePath == null)
- {
- return requestMessage.CreateErrorResponse(HttpStatusCode.NotFound, "no image found");
- }
- try
- {
- IOManager.SetImplementation(DesktopIOManager.Instance);
- DicomFile dicomFile = await DicomFile.OpenAsync(dicomImagePath);
- string finalContentType = PickFinalContentType(compatibleContentTypesByOrderOfPreference, dicomFile);
- return ReturnImageAsHttpResponse(dicomFile,
- finalContentType, transferSyntax);
- }
- catch (Exception ex)
- {
- Trace.TraceError("exception when sending image: " + ex.ToString());
- return requestMessage.CreateErrorResponse(HttpStatusCode.InternalServerError, "server internal error");
- }
- }
- /// <summary>
- /// returns dicomFile in the content type given by finalContentType in a HttpResponseMessage.
- /// If content type is dicom, transfer syntax must be set to the given transferSyntax parameter.
- /// </summary>
- /// <param name="dicomFile"></param>
- /// <param name="finalContentType"></param>
- /// <param name="transferSyntax"></param>
- /// <returns></returns>
- private HttpResponseMessage ReturnImageAsHttpResponse(DicomFile dicomFile, string finalContentType, string transferSyntax)
- {
- MediaTypeHeaderValue header = null;
- Stream streamContent = null;
- if (finalContentType == JpegImageContentType)
- {
- DicomImage image = new DicomImage(dicomFile.Dataset);
- Bitmap bmp = image.RenderImage(0).As<Bitmap>();
- //When an image/jpeg MIME type is returned, the image shall be encoded using the JPEG baseline lossy 8
- //bit Huffman encoded non-hierarchical non-sequential process ISO/IEC 10918.
- //TODO Is it the case with default Jpeg format from Bitmap?
- header = new MediaTypeHeaderValue(JpegImageContentType);
- streamContent = new MemoryStream();
- bmp.Save(streamContent, ImageFormat.Jpeg);
- streamContent.Seek(0, SeekOrigin.Begin);
- }
- else if (finalContentType == AppDicomContentType)
- {
- //By default, the transfer syntax shall be
- //"Explicit VR Little Endian".
- //Note: This implies that retrieved images are sent un-compressed by default.
- DicomTransferSyntax requestedTransferSyntax = DicomTransferSyntax.ExplicitVRLittleEndian;
- if (transferSyntax != null)
- requestedTransferSyntax = GetTransferSyntaxFromString(transferSyntax);
- bool transferSyntaxIsTheSameAsSourceFile =
- dicomFile.FileMetaInfo.TransferSyntax == requestedTransferSyntax;
- //we only change transfer syntax if we need to
- DicomFile dicomFileToStream;
- if (!transferSyntaxIsTheSameAsSourceFile)
- {
- dicomFileToStream = dicomFile.Clone(requestedTransferSyntax);
- }
- else
- {
- dicomFileToStream = dicomFile;
- }
- header = new MediaTypeHeaderValue(AppDicomContentType);
- streamContent = new MemoryStream();
- dicomFileToStream.Save(streamContent);
- streamContent.Seek(0, SeekOrigin.Begin);
- }
- var result = new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StreamContent(streamContent)
- };
- result.Content.Headers.ContentType = header;
- return result;
- }
- /// <summary>
- /// Choose the final content type given compatibleContentTypesByOrderOfPreference and dicomFile
- /// </summary>
- /// <param name="compatibleContentTypesByOrderOfPreference"></param>
- /// <param name="dicomFile"></param>
- /// <returns></returns>
- private static string PickFinalContentType(string[] compatibleContentTypesByOrderOfPreference, DicomFile dicomFile)
- {
- int nbFrames = dicomFile.Dataset.GetSingleValue<int>(DicomTag.NumberOfFrames);
- //if compatibleContentTypesByOrderOfPreference is null,
- //it means we must choose a default content type based on image content:
- // *Single Frame Image Objects
- // If the contentType parameter is not present in the request, the response shall contain an image/jpeg MIME
- // type, if compatible with the ‘Accept’ field of the GET method.
- // *Multi Frame Image Objects
- // If the contentType parameter is not present in the request, the response shall contain a application/dicom
- // MIME type.
- //not sure if this is how we distinguish multi frame objects?
- bool isMultiFrame = nbFrames > 1;
- bool chooseDefaultValue = compatibleContentTypesByOrderOfPreference == null;
- string chosenContentType;
- if (chooseDefaultValue)
- {
- if (isMultiFrame)
- {
- chosenContentType = AppDicomContentType;
- }
- else
- {
- chosenContentType = JpegImageContentType;
- }
- }
- else
- {
- //we need to take the compatible one
- chosenContentType = compatibleContentTypesByOrderOfPreference
- .Intersect(new[] { AppDicomContentType, JpegImageContentType })
- .First();
- }
- return chosenContentType;
- }
- /// <summary>
- /// extract content type values (may have multiple values according to IETF RFC2616)
- /// </summary>
- /// <param name="contentType">contentype string from wado request</param>
- /// <param name="contentTypes">extracted content types</param>
- /// <returns>false if there is a parse error, else true</returns>
- private static bool ExtractContentTypesFromContentTypeParameter(string contentType, out string[] contentTypes)
- {
- //8.1.5 MIME type of the response
- //The value shall be a list of MIME types, separated by a "," character, and potentially associated with
- //relative degree of preference, as specified in IETF RFC2616.
- //so we must split the string
- contentTypes = null;
- if (contentType != null && contentType.Contains(","))
- {
- contentTypes = contentType.Split(',');
- }
- else if (contentType == null)
- {
- contentTypes = null;
- }
- else
- {
- contentTypes = new[] {contentType};
- }
- //we now need to parse each type which follows the RFC2616 syntax
- //it also extracts parameters like jpeg quality but we discard it because we don't need them for now
- try
- {
- if (contentType != null)
- {
- contentTypes =
- contentTypes.Select(contentTypeString => MediaTypeHeaderValue.Parse(contentTypeString))
- .Select(mediaTypeHeader => mediaTypeHeader.MediaType).ToArray();
- }
- }
- catch (FormatException)
- {
- {
- return false;
- }
- }
- return true;
- }
- /// <summary>
- /// Get the compatible content types from the Accept Header, by order of preference
- /// </summary>
- /// <param name="contentTypes"></param>
- /// <param name="acceptContentTypesHeader"></param>
- /// <returns>
- /// compatible types by order of preference
- /// if contentTypes==null, returns null
- /// </returns>
- private static string[] GetCompatibleContentTypesByOrderOfPreference(
- string[] contentTypes, string[] acceptContentTypesHeader)
- {
- //je vérifie tout d'abord que parmis les types demandés, il y en a bien un que je gère.
- //je dois prendre l'intersection des types demandés et acceptés et les trier par ordre de préférence
- bool acceptAllTypesInAcceptHeader = acceptContentTypesHeader.Contains("*/*");
- string[] compatibleContentTypesByOrderOfPreference;
- if (acceptAllTypesInAcceptHeader)
- {
- compatibleContentTypesByOrderOfPreference = contentTypes;
- }
- //null represent the default value
- else if (contentTypes == null)
- {
- compatibleContentTypesByOrderOfPreference = null;
- }
- else
- {
- //intersect should preserve order (so it's already sorted by order of preference)
- compatibleContentTypesByOrderOfPreference = acceptContentTypesHeader.Intersect(contentTypes).ToArray();
- }
- return compatibleContentTypesByOrderOfPreference;
- }
- /// <summary>
- /// Converts string dicom transfert syntax to DicomTransferSyntax enumeration
- /// </summary>
- /// <param name="transferSyntax"></param>
- /// <returns></returns>
- private DicomTransferSyntax GetTransferSyntaxFromString(string transferSyntax)
- {
- try
- {
- return DicomParseable.Parse<DicomTransferSyntax>(transferSyntax);
- }
- catch (Exception)
- {
- //if we have an error, this probably means syntax is not supported
- //so according to 8.2.11 in spec, we use default ExplicitVRLittleEndian
- return DicomTransferSyntax.ExplicitVRLittleEndian;
- }
- }
- #endregion
- }
- }
|