// 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
{
///
/// main controller for wado implementation
///
/// the current implementation is incomplete
/// more infos in the official specification : http://dicom.nema.org/medical/dicom/current/output/pdf/part18.pdf
[EnableCors(origins: "*", headers: "*", methods: "GET")] //allows access by any host
[RoutePrefix("wado")]
public class WadoUriController : ApiController
{
#region consts
///
/// string representation of the dicom content type
///
private const string AppDicomContentType = "application/dicom";
///
/// string representation of the jpeg content type
///
private const string JpegImageContentType = "image/jpeg";
#endregion
#region fields
///
/// service used to retrieve images by instance Uid
///
private readonly IDicomImageFinderService _dicomImageFinderService;
#endregion
#region constructors
///
/// Initialize a new instance of WadoUriController
///
public WadoUriController()
{
//Put your own IDicomImageFinderService implementation here for real world scenarios
_dicomImageFinderService = new TestDicomImageFinderService();
}
///
/// Testing purposes constructor if you want to inject a custom IDicomImageFinderService
/// in unit tests
///
///
public WadoUriController(IDicomImageFinderService dicomImageFinderService)
{
_dicomImageFinderService = dicomImageFinderService;
}
#endregion
#region methods
///
/// main wado method
///
/// web request
/// always equals to wado in current wado specification, may change in the future
/// study instance UID
/// serie instance UID
/// instance UID
/// 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.
/// character set of the object to be retrieved.
/// The Transfer Syntax to be used within the DICOM image object, as specified in PS 3.6
/// 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
///
[Route("")]
public async Task 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");
}
}
///
/// 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.
///
///
///
///
///
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();
//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;
}
///
/// Choose the final content type given compatibleContentTypesByOrderOfPreference and dicomFile
///
///
///
///
private static string PickFinalContentType(string[] compatibleContentTypesByOrderOfPreference, DicomFile dicomFile)
{
int nbFrames = dicomFile.Dataset.GetSingleValue(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;
}
///
/// extract content type values (may have multiple values according to IETF RFC2616)
///
/// contentype string from wado request
/// extracted content types
/// false if there is a parse error, else true
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;
}
///
/// Get the compatible content types from the Accept Header, by order of preference
///
///
///
///
/// compatible types by order of preference
/// if contentTypes==null, returns null
///
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;
}
///
/// Converts string dicom transfert syntax to DicomTransferSyntax enumeration
///
///
///
private DicomTransferSyntax GetTransferSyntaxFromString(string transferSyntax)
{
try
{
return DicomParseable.Parse(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
}
}