From 821af417a8ea3bd2c0ed1a6fb73abe4b4fe502bc Mon Sep 17 00:00:00 2001 From: Abdullah Islam <31964511+abdullah248@users.noreply.github.com> Date: Thu, 27 Apr 2023 11:54:14 -0500 Subject: [PATCH] Add support for render png and quality paramater for jpeg (#2576) --- .../Controllers/RetrieveController.cs | 14 +- .../DicomWebClient.Retrieve.cs | 6 +- .../DicomWebConstants.cs | 4 +- .../IDicomWebClient.Retrieve.cs | 4 +- .../Retrieve/RetrieveRenderedHandlerTests.cs | 10 +- .../Retrieve/RetrieveRenderedServiceTests.cs | 162 ++++++++++++++++-- .../Retrieve/RetrieveRenderedRequestsTests.cs | 6 +- .../DicomCoreResource.Designer.cs | 9 + .../DicomCoreResource.resx | 3 + .../Extensions/DicomMediatorExtensions.cs | 4 +- .../Retrieve/RetrieveRenderedService.cs | 38 +++- .../Retrieve/RetrieveRenderedRequest.cs | 11 +- .../AcceptHeaderHelpers.cs | 6 +- swagger/v1-prerelease/swagger.yaml | 36 ++++ swagger/v1/swagger.yaml | 36 ++++ ...trieveTransactionResourceTests.Rendered.cs | 37 +++- 16 files changed, 339 insertions(+), 47 deletions(-) diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs index 8977bf3124..b29c64e495 100644 --- a/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs +++ b/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs @@ -153,7 +153,7 @@ public async Task GetInstanceAsync( return CreateResult(response); } - [Produces(KnownContentTypes.ImageJpeg)] + [Produces(KnownContentTypes.ImageJpeg, KnownContentTypes.ImagePng)] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.NoContent)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] @@ -166,12 +166,13 @@ public async Task GetInstanceAsync( public async Task GetRenderedInstanceAsync( string studyInstanceUid, string seriesInstanceUid, - string sopInstanceUid) + string sopInstanceUid, + [FromQuery] int quality = 100) { _logger.LogInformation("DICOM Web Retrieve Rendered Image Transaction request for instance received"); RetrieveRenderedResponse response = await _mediator.RetrieveRenderedDicomInstanceAsync( - studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, HttpContext.Request.GetAcceptHeaders(), HttpContext.RequestAborted); + studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, HttpContext.Request.GetAcceptHeaders(), quality, HttpContext.RequestAborted); return CreateResult(response); } @@ -223,7 +224,7 @@ public async Task GetFramesAsync( return CreateResult(response); } - [Produces(KnownContentTypes.ImageJpeg)] + [Produces(KnownContentTypes.ImageJpeg, KnownContentTypes.ImagePng)] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.NoContent)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] @@ -237,12 +238,13 @@ public async Task GetRenderedFrameAsync( string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, - int frame) + int frame, + [FromQuery] int quality = 100) { _logger.LogInformation("DICOM Web Retrieve Rendered Image Transaction request for frame received"); RetrieveRenderedResponse response = await _mediator.RetrieveRenderedDicomInstanceAsync( - studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, HttpContext.Request.GetAcceptHeaders(), HttpContext.RequestAborted, frame); + studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, HttpContext.Request.GetAcceptHeaders(), quality, HttpContext.RequestAborted, frame); return CreateResult(response); } diff --git a/src/Microsoft.Health.Dicom.Client/DicomWebClient.Retrieve.cs b/src/Microsoft.Health.Dicom.Client/DicomWebClient.Retrieve.cs index 4c1ec958ac..cbcff33758 100644 --- a/src/Microsoft.Health.Dicom.Client/DicomWebClient.Retrieve.cs +++ b/src/Microsoft.Health.Dicom.Client/DicomWebClient.Retrieve.cs @@ -100,6 +100,7 @@ public async Task> RetrieveRenderedInstanceAsync( string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, + int quality = 100, string mediaType = DicomWebConstants.ImageJpegMediaType, string partitionName = default, CancellationToken cancellationToken = default) @@ -109,7 +110,7 @@ public async Task> RetrieveRenderedInstanceAsync( EnsureArg.IsNotNullOrWhiteSpace(sopInstanceUid, nameof(sopInstanceUid)); return await RetrieveRenderedAsync( - GenerateRequestUri(string.Format(CultureInfo.InvariantCulture, DicomWebConstants.BaseRetrieveInstanceRenderedUriFormat, studyInstanceUid, seriesInstanceUid, sopInstanceUid), partitionName), + GenerateRequestUri(string.Format(CultureInfo.InvariantCulture, DicomWebConstants.BaseRetrieveInstanceRenderedUriFormat, studyInstanceUid, seriesInstanceUid, sopInstanceUid, quality), partitionName), mediaType, cancellationToken).ConfigureAwait(false); } @@ -162,6 +163,7 @@ public async Task> RetrieveRenderedFrameAsync( string seriesInstanceUid, string sopInstanceUid, int frame, + int quality = 100, string mediaType = DicomWebConstants.ImageJpegMediaType, string partitionName = default, CancellationToken cancellationToken = default) @@ -171,7 +173,7 @@ public async Task> RetrieveRenderedFrameAsync( EnsureArg.IsNotNullOrWhiteSpace(sopInstanceUid, nameof(sopInstanceUid)); return await RetrieveRenderedAsync( - GenerateRequestUri(string.Format(CultureInfo.InvariantCulture, DicomWebConstants.BaseRetrieveFrameRenderedUriFormat, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame), partitionName), + GenerateRequestUri(string.Format(CultureInfo.InvariantCulture, DicomWebConstants.BaseRetrieveFrameRenderedUriFormat, studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame, quality), partitionName), mediaType, cancellationToken).ConfigureAwait(false); } diff --git a/src/Microsoft.Health.Dicom.Client/DicomWebConstants.cs b/src/Microsoft.Health.Dicom.Client/DicomWebConstants.cs index 2d068ddb79..f926d0537d 100644 --- a/src/Microsoft.Health.Dicom.Client/DicomWebConstants.cs +++ b/src/Microsoft.Health.Dicom.Client/DicomWebConstants.cs @@ -17,11 +17,11 @@ public static class DicomWebConstants public const string BaseSeriesUriFormat = BaseStudyUriFormat + "/series/{1}"; public const string BaseRetrieveSeriesMetadataUriFormat = BaseSeriesUriFormat + "/metadata"; public const string BaseInstanceUriFormat = BaseSeriesUriFormat + "/instances/{2}"; - public const string BaseRetrieveInstanceRenderedUriFormat = BaseInstanceUriFormat + "/rendered"; + public const string BaseRetrieveInstanceRenderedUriFormat = BaseInstanceUriFormat + "/rendered?quality={3}"; public const string BaseRetrieveInstanceThumbnailUriFormat = BaseInstanceUriFormat + "/thumbnail"; public const string BaseRetrieveInstanceMetadataUriFormat = BaseInstanceUriFormat + "/metadata"; public const string BaseRetrieveFramesUriFormat = BaseInstanceUriFormat + "/frames/{3}"; - public const string BaseRetrieveFrameRenderedUriFormat = BaseRetrieveFramesUriFormat + "/rendered"; + public const string BaseRetrieveFrameRenderedUriFormat = BaseRetrieveFramesUriFormat + "/rendered?quality={4}"; public const string BaseRetrieveFramesThumbnailUriFormat = BaseRetrieveFramesUriFormat + "/thumbnail"; public const string PartitionsUriString = "/partitions"; public const string StudiesUriString = "/studies"; diff --git a/src/Microsoft.Health.Dicom.Client/IDicomWebClient.Retrieve.cs b/src/Microsoft.Health.Dicom.Client/IDicomWebClient.Retrieve.cs index 2f8bda0a63..c4d25c781e 100644 --- a/src/Microsoft.Health.Dicom.Client/IDicomWebClient.Retrieve.cs +++ b/src/Microsoft.Health.Dicom.Client/IDicomWebClient.Retrieve.cs @@ -14,9 +14,9 @@ public partial interface IDicomWebClient { Task> RetrieveSingleFrameAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, int frame, string partitionName = default, CancellationToken cancellationToken = default); Task> RetrieveFramesAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, int[] frames = default, string mediaType = DicomWebConstants.ApplicationOctetStreamMediaType, string dicomTransferSyntax = DicomWebConstants.OriginalDicomTransferSyntax, string partitionName = default, CancellationToken cancellationToken = default); - Task> RetrieveRenderedFrameAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, int frame, string mediaType = DicomWebConstants.ImageJpegMediaType, string partitionName = default, CancellationToken cancellationToken = default); + Task> RetrieveRenderedFrameAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, int frame, int quality = 100, string mediaType = DicomWebConstants.ImageJpegMediaType, string partitionName = default, CancellationToken cancellationToken = default); Task> RetrieveInstanceAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string dicomTransferSyntax = DicomWebConstants.OriginalDicomTransferSyntax, string partitionName = default, CancellationToken cancellationToken = default); - Task> RetrieveRenderedInstanceAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string mediaType = DicomWebConstants.ImageJpegMediaType, string partitionName = default, CancellationToken cancellationToken = default); + Task> RetrieveRenderedInstanceAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, int quality = 100, string mediaType = DicomWebConstants.ImageJpegMediaType, string partitionName = default, CancellationToken cancellationToken = default); Task> RetrieveInstanceMetadataAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string ifNoneMatch = default, string partitionName = default, CancellationToken cancellationToken = default); Task> RetrieveSeriesAsync(string studyInstanceUid, string seriesInstanceUid, string dicomTransferSyntax = DicomWebConstants.OriginalDicomTransferSyntax, string partitionName = default, CancellationToken cancellationToken = default); Task> RetrieveSeriesMetadataAsync(string studyInstanceUid, string seriesInstanceUid, string ifNoneMatch = default, string partitionName = default, CancellationToken cancellationToken = default); diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedHandlerTests.cs index 31e3daac7b..e07b832ca1 100644 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedHandlerTests.cs +++ b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedHandlerTests.cs @@ -42,7 +42,7 @@ public async Task GivenARequestWithInvalidStudyInstanceIdentifier_WhenHandlerIsE string seriesInstanceUid = TestUidGenerator.Generate(); string sopInstanceUid = TestUidGenerator.Generate(); - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); var ex = await Assert.ThrowsAsync(() => _retrieveRenderedHandler.Handle(request, CancellationToken.None)); Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); } @@ -58,7 +58,7 @@ public async Task GivenARequestWithInvalidSeriesInstanceIdentifier_WhenHandlerIs string studyInstanceUid = TestUidGenerator.Generate(); string sopInstanceUid = TestUidGenerator.Generate(); - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); var ex = await Assert.ThrowsAsync(() => _retrieveRenderedHandler.Handle(request, CancellationToken.None)); Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); } @@ -73,7 +73,7 @@ public async Task GivenARequestWithInvalidInstanceInstanceIdentifier_WhenHandler string studyInstanceUid = TestUidGenerator.Generate(); string seriesInstanceUid = TestUidGenerator.Generate(); - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); var ex = await Assert.ThrowsAsync(() => _retrieveRenderedHandler.Handle(request, CancellationToken.None)); Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); } @@ -88,7 +88,7 @@ public async Task GivenARequestWithInvalidFramNumber_WhenHandlerIsExecuted_ThenB string seriesInstanceUid = TestUidGenerator.Generate(); string sopInstanceUid = TestUidGenerator.Generate(); - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, frame, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, frame, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); var ex = await Assert.ThrowsAsync(() => _retrieveRenderedHandler.Handle(request, CancellationToken.None)); Assert.Equal(error, ex.Message); } @@ -100,7 +100,7 @@ public async Task GivenARequestWithValidInstanceInstanceIdentifier_WhenHandlerIs string seriesInstanceUid = TestUidGenerator.Generate(); string sopInstanceUid = TestUidGenerator.Generate(); - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); await _retrieveRenderedHandler.Handle(request, CancellationToken.None); } diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedServiceTests.cs index be867422a6..0f64bc514a 100644 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedServiceTests.cs +++ b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedServiceTests.cs @@ -27,6 +27,8 @@ using NSubstitute; using Xunit; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using Microsoft.Health.Dicom.Core.Web; namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; public class RetrieveRenderedServiceTests @@ -70,7 +72,7 @@ public RetrieveRenderedServiceTests() } [Fact] - public async Task GivenARequestWithMultipleAcceptHeaders_WhenHandlerIsExecuted_ThenNotAcceptableExceptionExceptionIsThrown() + public async Task GivenARequestWithMultipleAcceptHeaders_WhenServiceIsExecuted_ThenNotAcceptableExceptionExceptionIsThrown() { const string expectedErrorMessage = "The request contains multiple accept headers, which is not supported."; @@ -78,13 +80,13 @@ public async Task GivenARequestWithMultipleAcceptHeaders_WhenHandlerIsExecuted_T string seriesInstanceUid = TestUidGenerator.Generate(); string sopInstanceUid = TestUidGenerator.Generate(); - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader(), AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader(), AcceptHeaderHelpers.CreateRenderAcceptHeader() }); var ex = await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync(request, CancellationToken.None)); Assert.Equal(expectedErrorMessage, ex.Message); } [Fact] - public async Task GivenARequestWithInvalidAcceptHeader_WhenHandlerIsExecuted_ThenNotAcceptableExceptionExceptionIsThrown() + public async Task GivenARequestWithInvalidAcceptHeader_WhenServiceIsExecuted_ThenNotAcceptableExceptionExceptionIsThrown() { const string expectedErrorMessage = "The request headers are not acceptable"; @@ -92,7 +94,7 @@ public async Task GivenARequestWithInvalidAcceptHeader_WhenHandlerIsExecuted_The string seriesInstanceUid = TestUidGenerator.Generate(); string sopInstanceUid = TestUidGenerator.Generate(); - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy() }); + RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, 75, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy() }); var ex = await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync(request, CancellationToken.None)); Assert.Equal(expectedErrorMessage, ex.Message); } @@ -103,10 +105,27 @@ public async Task GivenNoStoredInstances_RenderForInstance_ThenNotFoundIsThrown( _instanceStore.GetInstanceIdentifiersInStudyAsync(DefaultPartition.Key, _studyInstanceUid).Returns(new List()); await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync( - new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Instance, 0, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }), + new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Instance, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }), DefaultCancellationToken)); } + [Theory] + [InlineData(0)] + [InlineData(-10)] + [InlineData(101)] + public async Task GivenInvalidQuality_RenderForInstance_ThenBadRequestThrown(int quality) + { + const string expectedErrorMessage = "Image quality must be between 1 and 100 inclusive"; + + string studyInstanceUid = TestUidGenerator.Generate(); + string seriesInstanceUid = TestUidGenerator.Generate(); + string sopInstanceUid = TestUidGenerator.Generate(); + + RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, quality, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); + var ex = await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync(request, CancellationToken.None)); + Assert.Equal(expectedErrorMessage, ex.Message); + } + [Fact] public async Task GivenFileSizeTooLarge_RenderForInstance_ThenNotFoundIsThrown() { @@ -116,7 +135,7 @@ public async Task GivenFileSizeTooLarge_RenderForInstance_ThenNotFoundIsThrown() await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync( - new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 0, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }), + new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }), DefaultCancellationToken)); } @@ -131,7 +150,7 @@ public async Task GivenStoredInstancesWithFrames_WhenRenderRequestForNonExisting _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(streamOfStoredFiles); _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new FileProperties() { ContentLength = streamOfStoredFiles.Length }); - var retrieveRenderRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 4, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + var retrieveRenderRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 5, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync( retrieveRenderRequest, @@ -142,7 +161,7 @@ await Assert.ThrowsAsync(() => _retrieveRenderedService. } [Fact] - public async Task GivenStoredInstancesWithFrames_WhenRetrieveRenderedForFrames_ThenEachFrameRenderedSuccesfully() + public async Task GivenStoredInstancesWithFramesJpeg_WhenRetrieveRenderedForFrames_ThenEachFrameRenderedSuccesfully() { List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(); @@ -160,7 +179,7 @@ public async Task GivenStoredInstancesWithFrames_WhenRetrieveRenderedForFrames_T streamAndStoredFileForFrame2.Position = 0; streamAndStoredFile.Value.Position = 0; - var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 0, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); RetrieveRenderedResponse response = await _retrieveRenderedService.RetrieveRenderedImageAsync( retrieveRenderedRequest, @@ -176,7 +195,7 @@ public async Task GivenStoredInstancesWithFrames_WhenRetrieveRenderedForFrames_T AssertStreamsEqual(resultStream, response.ResponseStream); Assert.Equal("image/jpeg", response.ContentType); - var retrieveRenderedRequest2 = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + var retrieveRenderedRequest2 = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 2, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(streamAndStoredFileForFrame2); RetrieveRenderedResponse response2 = await _retrieveRenderedService.RetrieveRenderedImageAsync( @@ -196,6 +215,127 @@ public async Task GivenStoredInstancesWithFrames_WhenRetrieveRenderedForFrames_T streamAndStoredFileForFrame2.Dispose(); response.ResponseStream.Dispose(); response2.ResponseStream.Dispose(); + + } + + [Fact] + public async Task GivenStoredInstancesWithFramesJpeg_WhenRetrieveRenderedForFramesDifferentQuality_ThenEachFrameRenderedSuccesfully() + { + List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(); + + KeyValuePair streamAndStoredFile = RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3).Result; + _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(streamAndStoredFile.Value); + _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new FileProperties() { ContentLength = streamAndStoredFile.Value.Length }); + + JpegEncoder jpegEncoder = new JpegEncoder(); + jpegEncoder.Quality = 50; + streamAndStoredFile.Value.Position = 0; + + MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); + await streamAndStoredFile.Value.CopyToAsync(copyStream); + copyStream.Position = 0; + streamAndStoredFile.Value.Position = 0; + + MemoryStream streamAndStoredFileForFrame2 = _recyclableMemoryStreamManager.GetStream(); + await streamAndStoredFile.Value.CopyToAsync(streamAndStoredFileForFrame2); + streamAndStoredFileForFrame2.Position = 0; + streamAndStoredFile.Value.Position = 0; + + var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, 50, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); + + RetrieveRenderedResponse response = await _retrieveRenderedService.RetrieveRenderedImageAsync( + retrieveRenderedRequest, + DefaultCancellationToken); + + DicomFile dicomFile = await DicomFile.OpenAsync(copyStream, FileReadOption.ReadLargeOnDemand); + DicomImage dicomImage = new DicomImage(dicomFile.Dataset); + using var img = dicomImage.RenderImage(0); + using var sharpImage = img.AsSharpImage(); + using MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(); + await sharpImage.SaveAsJpegAsync(resultStream, jpegEncoder, DefaultCancellationToken); + resultStream.Position = 0; + AssertStreamsEqual(resultStream, response.ResponseStream); + Assert.Equal("image/jpeg", response.ContentType); + + var retrieveRenderedRequest2 = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 2, 20, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); + + _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(streamAndStoredFileForFrame2); + RetrieveRenderedResponse response2 = await _retrieveRenderedService.RetrieveRenderedImageAsync( + retrieveRenderedRequest2, + DefaultCancellationToken); + + copyStream.Position = 0; + using var img2 = dicomImage.RenderImage(1); + using var sharpImage2 = img2.AsSharpImage(); + using MemoryStream resultStream2 = _recyclableMemoryStreamManager.GetStream(); + jpegEncoder.Quality = 20; + await sharpImage2.SaveAsJpegAsync(resultStream2, jpegEncoder, DefaultCancellationToken); + resultStream2.Position = 0; + AssertStreamsEqual(resultStream2, response2.ResponseStream); + Assert.Equal("image/jpeg", response.ContentType); + + copyStream.Dispose(); + streamAndStoredFileForFrame2.Dispose(); + response.ResponseStream.Dispose(); + response2.ResponseStream.Dispose(); + } + + [Fact] + public async Task GivenStoredInstancesWithFramesPNG_WhenRetrieveRenderedForFrames_ThenEachFrameRenderedSuccesfully() + { + List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(); + + KeyValuePair streamAndStoredFile = RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3).Result; + _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(streamAndStoredFile.Value); + _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new FileProperties() { ContentLength = streamAndStoredFile.Value.Length }); + + MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); + await streamAndStoredFile.Value.CopyToAsync(copyStream); + copyStream.Position = 0; + streamAndStoredFile.Value.Position = 0; + + MemoryStream streamAndStoredFileForFrame2 = _recyclableMemoryStreamManager.GetStream(); + await streamAndStoredFile.Value.CopyToAsync(streamAndStoredFileForFrame2); + streamAndStoredFileForFrame2.Position = 0; + streamAndStoredFile.Value.Position = 0; + + var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader(mediaType: KnownContentTypes.ImagePng) }); + + RetrieveRenderedResponse response = await _retrieveRenderedService.RetrieveRenderedImageAsync( + retrieveRenderedRequest, + DefaultCancellationToken); + + DicomFile dicomFile = await DicomFile.OpenAsync(copyStream, FileReadOption.ReadLargeOnDemand); + DicomImage dicomImage = new DicomImage(dicomFile.Dataset); + using var img = dicomImage.RenderImage(0); + using var sharpImage = img.AsSharpImage(); + using MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(); + await sharpImage.SaveAsPngAsync(resultStream, new SixLabors.ImageSharp.Formats.Png.PngEncoder(), DefaultCancellationToken); + resultStream.Position = 0; + AssertStreamsEqual(resultStream, response.ResponseStream); + Assert.Equal("image/png", response.ContentType); + + var retrieveRenderedRequest2 = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 2, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader(mediaType: KnownContentTypes.ImagePng) }); + + _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(streamAndStoredFileForFrame2); + RetrieveRenderedResponse response2 = await _retrieveRenderedService.RetrieveRenderedImageAsync( + retrieveRenderedRequest2, + DefaultCancellationToken); + + copyStream.Position = 0; + using var img2 = dicomImage.RenderImage(1); + using var sharpImage2 = img2.AsSharpImage(); + using MemoryStream resultStream2 = _recyclableMemoryStreamManager.GetStream(); + await sharpImage2.SaveAsPngAsync(resultStream2, new SixLabors.ImageSharp.Formats.Png.PngEncoder(), DefaultCancellationToken); + resultStream2.Position = 0; + AssertStreamsEqual(resultStream2, response2.ResponseStream); + Assert.Equal("image/png", response.ContentType); + + copyStream.Dispose(); + streamAndStoredFileForFrame2.Dispose(); + response.ResponseStream.Dispose(); + response2.ResponseStream.Dispose(); + } [Fact] @@ -214,7 +354,7 @@ public async Task GivenStoredInstances_WhenRetrieveRenderedWithoutSpecifyingAcce _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(streamAndStoredFile.Value); _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new FileProperties() { ContentLength = streamAndStoredFile.Value.Length }); - var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Instance, 0, new List()); + var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Instance, 1, 75, new List()); RetrieveRenderedResponse response = await _retrieveRenderedService.RetrieveRenderedImageAsync( retrieveRenderedRequest, diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRenderedRequestsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRenderedRequestsTests.cs index 50e2e278f2..c1e18628d9 100644 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRenderedRequestsTests.cs +++ b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRenderedRequestsTests.cs @@ -19,12 +19,13 @@ public void VerifyAllFieldsSetCorrectlyForInstance() string studyInstanceUid = Guid.NewGuid().ToString(); string seriesInstanceUid = Guid.NewGuid().ToString(); string sopInstanceUid = Guid.NewGuid().ToString(); - var request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + var request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); Assert.Equal(studyInstanceUid, request.StudyInstanceUid); Assert.Equal(seriesInstanceUid, request.SeriesInstanceUid); Assert.Equal(sopInstanceUid, request.SopInstanceUid); Assert.Equal(ResourceType.Instance, request.ResourceType); Assert.Equal(0, request.FrameNumber); + Assert.Equal(75, request.Quality); } [Fact] @@ -33,11 +34,12 @@ public void VerifyAllFieldsSetCorrectlyForFrame() string studyInstanceUid = Guid.NewGuid().ToString(); string seriesInstanceUid = Guid.NewGuid().ToString(); string sopInstanceUid = Guid.NewGuid().ToString(); - var request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, new[] { AcceptHeaderHelpers.CreateRenderJpegAcceptHeader() }); + var request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 6, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); Assert.Equal(studyInstanceUid, request.StudyInstanceUid); Assert.Equal(seriesInstanceUid, request.SeriesInstanceUid); Assert.Equal(sopInstanceUid, request.SopInstanceUid); Assert.Equal(ResourceType.Frames, request.ResourceType); Assert.Equal(5, request.FrameNumber); + Assert.Equal(75, request.Quality); } } diff --git a/src/Microsoft.Health.Dicom.Core/DicomCoreResource.Designer.cs b/src/Microsoft.Health.Dicom.Core/DicomCoreResource.Designer.cs index 6f126ac3d9..f297fa8d91 100644 --- a/src/Microsoft.Health.Dicom.Core/DicomCoreResource.Designer.cs +++ b/src/Microsoft.Health.Dicom.Core/DicomCoreResource.Designer.cs @@ -684,6 +684,15 @@ internal static string InvalidFuzzyMatchValue { } } + /// + /// Looks up a localized string similar to Image quality must be between 1 and 100 inclusive. + /// + internal static string InvalidImageQuality { + get { + return ResourceManager.GetString("InvalidImageQuality", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid query: cannot specify included fields in addition to 'all'.. /// diff --git a/src/Microsoft.Health.Dicom.Core/DicomCoreResource.resx b/src/Microsoft.Health.Dicom.Core/DicomCoreResource.resx index 423d227277..ec07e07f63 100644 --- a/src/Microsoft.Health.Dicom.Core/DicomCoreResource.resx +++ b/src/Microsoft.Health.Dicom.Core/DicomCoreResource.resx @@ -665,4 +665,7 @@ For details on valid range queries, please refer to Search Matching section in C StudyInstanceUids count exceeded maximum length '{0}' {0} StudyInstanceUids max allowed count + + Image quality must be between 1 and 100 inclusive + \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/DicomMediatorExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/DicomMediatorExtensions.cs index 431f6075eb..2823704900 100644 --- a/src/Microsoft.Health.Dicom.Core/Extensions/DicomMediatorExtensions.cs +++ b/src/Microsoft.Health.Dicom.Core/Extensions/DicomMediatorExtensions.cs @@ -81,11 +81,11 @@ public static Task RetrieveDicomInstanceAsync( } public static Task RetrieveRenderedDicomInstanceAsync( - this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, ResourceType resourceType, IReadOnlyCollection acceptHeaders, CancellationToken cancellationToken, int frameNumber = 0) + this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, ResourceType resourceType, IReadOnlyCollection acceptHeaders, int quality, CancellationToken cancellationToken, int frameNumber = 1) { EnsureArg.IsNotNull(mediator, nameof(mediator)); return mediator.Send( - new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, resourceType, frameNumber, acceptHeaders), + new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, resourceType, frameNumber, quality, acceptHeaders), cancellationToken); } diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedService.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedService.cs index 6459f69630..829e44c6b4 100644 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedService.cs +++ b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedService.cs @@ -27,6 +27,7 @@ using Microsoft.Health.Dicom.Core.Web; using Microsoft.IO; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; namespace Microsoft.Health.Dicom.Core.Features.Retrieve; public class RetrieveRenderedService : IRetrieveRenderedService @@ -65,6 +66,11 @@ public async Task RetrieveRenderedImageAsync(RetrieveR { EnsureArg.IsNotNull(request, nameof(request)); + if (request.Quality < 1 || request.Quality > 100) + { + throw new BadRequestException(DicomCoreResource.InvalidImageQuality); + } + // To keep track of how long render operation is taking Stopwatch sw = new Stopwatch(); @@ -84,7 +90,7 @@ public async Task RetrieveRenderedImageAsync(RetrieveR DicomFile dicomFile = await DicomFile.OpenAsync(stream, FileReadOption.ReadLargeOnDemand); DicomPixelData dicomPixelData = dicomFile.GetPixelDataAndValidateFrames(new[] { request.FrameNumber }); - Stream resultStream = await ConvertToImage(dicomFile, request.FrameNumber, returnHeader.MediaType.ToString(), cancellationToken); + Stream resultStream = await ConvertToImage(dicomFile, request.FrameNumber, returnHeader.MediaType.ToString(), request.Quality, cancellationToken); string outputContentType = returnHeader.MediaType.ToString(); sw.Stop(); @@ -102,7 +108,7 @@ public async Task RetrieveRenderedImageAsync(RetrieveR } - private async Task ConvertToImage(DicomFile dicomFile, int frameNumber, string mediaType, CancellationToken cancellationToken) + private async Task ConvertToImage(DicomFile dicomFile, int frameNumber, string mediaType, int quality, CancellationToken cancellationToken) { try { @@ -110,7 +116,18 @@ private async Task ConvertToImage(DicomFile dicomFile, int frameNumber, using var img = dicomImage.RenderImage(frameNumber); using var sharpImage = img.AsSharpImage(); MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(); - await sharpImage.SaveAsJpegAsync(resultStream, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder(), cancellationToken: cancellationToken); + + if (mediaType.Equals(KnownContentTypes.ImageJpeg, StringComparison.OrdinalIgnoreCase)) + { + JpegEncoder jpegEncoder = new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder(); + jpegEncoder.Quality = quality; + await sharpImage.SaveAsJpegAsync(resultStream, jpegEncoder, cancellationToken: cancellationToken); + } + else + { + await sharpImage.SaveAsPngAsync(resultStream, new SixLabors.ImageSharp.Formats.Png.PngEncoder(), cancellationToken: cancellationToken); + } + resultStream.Position = 0; return resultStream; @@ -130,9 +147,20 @@ private static AcceptHeader GetValidRenderAcceptHeader(IReadOnlyCollection { - public RetrieveRenderedRequest(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, ResourceType resourceType, int frameNumber, IReadOnlyCollection acceptHeaders) + public RetrieveRenderedRequest(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, ResourceType resourceType, int frameNumber, int quality, IReadOnlyCollection acceptHeaders) { StudyInstanceUid = studyInstanceUid; SeriesInstanceUid = seriesInstanceUid; SopInstanceUid = sopInstanceUid; ResourceType = resourceType; - FrameNumber = frameNumber; + + // Per DICOMWeb spec (http://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_9.5.1.2.1) + // frame number in the URI is 1-based, unlike fo-dicom representation where it's 0-based. + FrameNumber = frameNumber - 1; + + Quality = quality; AcceptHeaders = acceptHeaders; } @@ -31,4 +36,6 @@ public RetrieveRenderedRequest(string studyInstanceUid, string seriesInstanceUid public string SopInstanceUid { get; } public int FrameNumber { get; } + + public int Quality { get; } } diff --git a/src/Microsoft.Health.Dicom.Tests.Common/AcceptHeaderHelpers.cs b/src/Microsoft.Health.Dicom.Tests.Common/AcceptHeaderHelpers.cs index 12f967d324..fa98eb54a8 100644 --- a/src/Microsoft.Health.Dicom.Tests.Common/AcceptHeaderHelpers.cs +++ b/src/Microsoft.Health.Dicom.Tests.Common/AcceptHeaderHelpers.cs @@ -47,16 +47,14 @@ public static AcceptHeader CreateAcceptHeaderForGetFrame(string transferSyntax = quality: quality); } - public static AcceptHeader CreateRenderJpegAcceptHeader(string transferSyntax = "*", string mediaType = KnownContentTypes.ImageJpeg, double? quality = null, PayloadTypes payloadType = PayloadTypes.SinglePart) + public static AcceptHeader CreateRenderAcceptHeader(string transferSyntax = "*", string mediaType = KnownContentTypes.ImageJpeg, PayloadTypes payloadType = PayloadTypes.SinglePart) { return CreateAcceptHeader( transferSyntax: transferSyntax, payloadType: payloadType, - mediaType: mediaType, - quality: quality); + mediaType: mediaType); } - public static AcceptHeader CreateAcceptHeader(string transferSyntax = "*", PayloadTypes payloadType = PayloadTypes.MultipartRelated, string mediaType = KnownContentTypes.ApplicationOctetStream, double? quality = null) { return new AcceptHeader(mediaType, payloadType, transferSyntax, quality.GetValueOrDefault(AcceptHeader.DefaultQuality)); diff --git a/swagger/v1-prerelease/swagger.yaml b/swagger/v1-prerelease/swagger.yaml index 8e7e34061e..a089bcf7d4 100644 --- a/swagger/v1-prerelease/swagger.yaml +++ b/swagger/v1-prerelease/swagger.yaml @@ -1848,6 +1848,12 @@ paths: required: true schema: type: string + - name: quality + in: query + schema: + type: integer + format: int32 + default: 100 - name: partitionName in: path required: true @@ -1862,6 +1868,9 @@ paths: image/jpeg: schema: type: string + image/png: + schema: + type: string '400': description: Bad Request content: @@ -1900,6 +1909,12 @@ paths: required: true schema: type: string + - name: quality + in: query + schema: + type: integer + format: int32 + default: 100 responses: '200': description: Success @@ -1909,6 +1924,9 @@ paths: image/jpeg: schema: type: string + image/png: + schema: + type: string '400': description: Bad Request content: @@ -2177,6 +2195,12 @@ paths: schema: type: integer format: int32 + - name: quality + in: query + schema: + type: integer + format: int32 + default: 100 - name: partitionName in: path required: true @@ -2191,6 +2215,9 @@ paths: image/jpeg: schema: type: string + image/png: + schema: + type: string '400': description: Bad Request content: @@ -2235,6 +2262,12 @@ paths: schema: type: integer format: int32 + - name: quality + in: query + schema: + type: integer + format: int32 + default: 100 responses: '200': description: Success @@ -2244,6 +2277,9 @@ paths: image/jpeg: schema: type: string + image/png: + schema: + type: string '400': description: Bad Request content: diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 2b7730f2a1..cde9069439 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -1892,6 +1892,12 @@ paths: required: true schema: type: string + - name: quality + in: query + schema: + type: integer + format: int32 + default: 100 - name: partitionName in: path required: true @@ -1906,6 +1912,9 @@ paths: image/jpeg: schema: type: string + image/png: + schema: + type: string '400': description: Bad Request content: @@ -1944,6 +1953,12 @@ paths: required: true schema: type: string + - name: quality + in: query + schema: + type: integer + format: int32 + default: 100 responses: '200': description: Success @@ -1953,6 +1968,9 @@ paths: image/jpeg: schema: type: string + image/png: + schema: + type: string '400': description: Bad Request content: @@ -2221,6 +2239,12 @@ paths: schema: type: integer format: int32 + - name: quality + in: query + schema: + type: integer + format: int32 + default: 100 - name: partitionName in: path required: true @@ -2235,6 +2259,9 @@ paths: image/jpeg: schema: type: string + image/png: + schema: + type: string '400': description: Bad Request content: @@ -2279,6 +2306,12 @@ paths: schema: type: integer format: int32 + - name: quality + in: query + schema: + type: integer + format: int32 + default: 100 responses: '200': description: Success @@ -2288,6 +2321,9 @@ paths: image/jpeg: schema: type: string + image/png: + schema: + type: string '400': description: Bad Request content: diff --git a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Rendered.cs b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Rendered.cs index 6fb947287f..821ec3a269 100644 --- a/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Rendered.cs +++ b/test/Microsoft.Health.Dicom.Web.Tests.E2E/Rest/RetrieveTransactionResourceTests.Rendered.cs @@ -20,6 +20,7 @@ public partial class RetrieveTransactionResourceTests { [Theory] [InlineData("image/jpeg")] + [InlineData("image/png")] public async Task GivenValidMediaType_WhenRetrieveRenderedInstance_ThenServerShouldReturnCorrectContentSuccessfully(string mediaType) { string studyInstanceUID = TestUidGenerator.Generate(); @@ -30,7 +31,7 @@ public async Task GivenValidMediaType_WhenRetrieveRenderedInstance_ThenServerSho using DicomWebResponse response1 = await _instancesManager.StoreAsync(new[] { dicomFile }); - using DicomWebResponse response2 = await _client.RetrieveRenderedInstanceAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, mediaType); + using DicomWebResponse response2 = await _client.RetrieveRenderedInstanceAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 100, mediaType); Assert.True(response2.IsSuccessStatusCode); Assert.Equal(mediaType, response2.ContentHeaders.ContentType.MediaType); @@ -47,11 +48,11 @@ public async Task GivenNoMediaType_WhenRetrieveRenderedFrame_ThenServerShouldRet using DicomWebResponse response1 = await _instancesManager.StoreAsync(new[] { dicomFile }); - using DicomWebResponse response2 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 0); + using DicomWebResponse response2 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 1); Assert.True(response2.IsSuccessStatusCode); Assert.Equal("image/jpeg", response2.ContentHeaders.ContentType.MediaType); - using DicomWebResponse response3 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 1); + using DicomWebResponse response3 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 2); Assert.True(response3.IsSuccessStatusCode); Assert.Equal("image/jpeg", response2.ContentHeaders.ContentType.MediaType); } @@ -67,7 +68,7 @@ public async Task GivenNoMediaType_WhenRetrieveInvalidRenderedFrame_ThenServerSh using DicomWebResponse response1 = await _instancesManager.StoreAsync(new[] { dicomFile }); - using DicomWebResponse response2 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 0); + using DicomWebResponse response2 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 1); Assert.True(response2.IsSuccessStatusCode); Assert.Equal("image/jpeg", response2.ContentHeaders.ContentType.MediaType); @@ -75,4 +76,32 @@ public async Task GivenNoMediaType_WhenRetrieveInvalidRenderedFrame_ThenServerSh Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); } + + [Fact] + public async Task GivenDifferentQualities_WhenRetrieveRenderedFrame_ThenServerShouldReturnCorrectContentSuccessfully() + { + string studyInstanceUID = TestUidGenerator.Generate(); + string seriesInstanceUID = TestUidGenerator.Generate(); + string sopInstanceUID = TestUidGenerator.Generate(); + + DicomFile dicomFile = Samples.CreateRandomDicomFileWithPixelData(studyInstanceUID, seriesInstanceUID, sopInstanceUID, frames: 3); + + using DicomWebResponse response1 = await _instancesManager.StoreAsync(new[] { dicomFile }); + + using DicomWebResponse response2 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 1, 100); + Assert.True(response2.IsSuccessStatusCode); + Assert.Equal("image/jpeg", response2.ContentHeaders.ContentType.MediaType); + + using DicomWebResponse response3 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 2, 100); + Assert.True(response3.IsSuccessStatusCode); + Assert.Equal("image/jpeg", response2.ContentHeaders.ContentType.MediaType); + + using DicomWebResponse response4 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 1, 1); + Assert.True(response4.IsSuccessStatusCode); + Assert.Equal("image/jpeg", response4.ContentHeaders.ContentType.MediaType); + + using DicomWebResponse response5 = await _client.RetrieveRenderedFrameAsync(studyInstanceUID, seriesInstanceUID, sopInstanceUID, 2, 1); + Assert.True(response5.IsSuccessStatusCode); + Assert.Equal("image/jpeg", response5.ContentHeaders.ContentType.MediaType); + } }