Mercurial > hg > orthanc-dicomweb
view Plugin/WadoRsRetrieveRendered.cpp @ 480:6dc2f79b5579
Fix issue #190 (STOW-RS fails is multipart boundary is surrounded by quotes in headers)
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 27 Nov 2020 06:41:10 +0100 |
parents | 8121a16faf3e |
children | 327acd0232d8 |
line wrap: on
line source
/** * Orthanc - A Lightweight, RESTful DICOM Store * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium * Copyright (C) 2017-2020 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. **/ #include "WadoRs.h" #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" #include <Images/Image.h> #include <Images/ImageProcessing.h> #include <Images/ImageTraits.h> #include <Logging.h> #include <Toolbox.h> #include <boost/algorithm/string/predicate.hpp> #include <boost/math/special_functions/round.hpp> namespace { enum WindowingMode { WindowingMode_WholeDynamics, WindowingMode_Linear, WindowingMode_LinearExact, WindowingMode_Sigmoid }; class RenderingParameters : public boost::noncopyable { private: bool hasViewport_; bool hasQuality_; bool hasWindowing_; bool hasVW_; bool hasVH_; bool hasSW_; bool hasSH_; unsigned int vw_; unsigned int vh_; unsigned int sx_; unsigned int sy_; unsigned int sw_; unsigned int sh_; bool flipX_; bool flipY_; unsigned int quality_; float windowCenter_; float windowWidth_; WindowingMode windowingMode_; float rescaleSlope_; float rescaleIntercept_; static bool GetIntegerValue(int& target, std::vector<std::string>& tokens, size_t index, bool allowNegative, bool allowFloat, const std::string& message) { if (index >= tokens.size() || tokens[index].empty()) { return false; } try { if (allowFloat) { float value = boost::lexical_cast<float>(tokens[index]); target = boost::math::iround(value); } else { target = boost::lexical_cast<int>(tokens[index]); } } catch (boost::bad_lexical_cast&) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Out-of-range value for " + message + ": " + tokens[index]); } if (!allowNegative && target < 0) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Negative values disallowed for " + message + ": " + tokens[index]); } return true; } public: explicit RenderingParameters(const OrthancPluginHttpRequest* request) : hasViewport_(false), hasQuality_(false), hasWindowing_(false), hasVW_(false), hasVH_(false), hasSW_(false), hasSH_(false), vw_(0), vh_(0), sx_(0), sy_(0), sw_(0), sh_(0), flipX_(false), flipY_(false), quality_(90), // Default quality for JPEG previews (the same as in Orthanc core) windowCenter_(128), windowWidth_(256), windowingMode_(WindowingMode_WholeDynamics), rescaleSlope_(1), rescaleIntercept_(0) { static const std::string VIEWPORT("\"viewport\" in WADO-RS Retrieve Rendered Transaction"); static const std::string WINDOW("\"window\" in WADO-RS Retrieve Rendered Transaction"); for (uint32_t i = 0; i < request->getCount; i++) { const std::string key = request->getKeys[i]; const std::string value = request->getValues[i]; if (key == "viewport") { hasViewport_ = true; std::vector<std::string> tokens; Orthanc::Toolbox::TokenizeString(tokens, value, ','); if (tokens.size() != 2 && tokens.size() != 6) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "The number arguments to " + VIEWPORT + " must be 2 or 6"); } int tmp; hasVW_ = GetIntegerValue(tmp, tokens, 0, false, false, VIEWPORT); if (hasVW_) { assert(tmp >= 0); vw_ = static_cast<unsigned int>(tmp); } hasVH_ = GetIntegerValue(tmp, tokens, 1, false, false, VIEWPORT); if (hasVH_) { assert(tmp >= 0); vh_ = static_cast<unsigned int>(tmp); } if (GetIntegerValue(tmp, tokens, 2, true, true, VIEWPORT)) { sx_ = static_cast<unsigned int>(tmp < 0 ? -tmp : tmp); // Take absolute value } else { sx_ = 0; // Default is zero } if (GetIntegerValue(tmp, tokens, 3, true, true, VIEWPORT)) { sy_ = static_cast<unsigned int>(tmp < 0 ? -tmp : tmp); // Take absolute value } else { sy_ = 0; // Default is zero } hasSW_ = GetIntegerValue(tmp, tokens, 4, true, true, VIEWPORT); if (hasSW_) { sw_ = static_cast<unsigned int>(tmp < 0 ? -tmp : tmp); // Take absolute value flipX_ = (tmp < 0); } hasSH_ = GetIntegerValue(tmp, tokens, 5, true, true, VIEWPORT); if (hasSH_) { sh_ = static_cast<unsigned int>(tmp < 0 ? -tmp : tmp); // Take absolute value flipY_ = (tmp < 0); } } else if (key == "quality") { hasQuality_ = true; bool ok = false; int q; try { q = boost::lexical_cast<int>(value); ok = (q >= 1 && q <= 100); } catch (boost::bad_lexical_cast&) { } if (!ok) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "The value of \"quality\" in WADO-RS Retrieve Rendered Transaction " "must be between 1 and 100, found: " + value); } quality_ = static_cast<unsigned int>(q); } else if (key == "window") { hasWindowing_ = true; std::vector<std::string> tokens; Orthanc::Toolbox::TokenizeString(tokens, value, ','); if (tokens.size() != 3) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "The number arguments to " + WINDOW + " must be 3"); } try { windowCenter_ = boost::lexical_cast<float>(tokens[0]); windowWidth_ = boost::lexical_cast<float>(tokens[1]); } catch (boost::bad_lexical_cast&) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "The first and second arguments to " + WINDOW + " must be floats: " + value); } if (tokens[2] == "linear") { windowingMode_ = WindowingMode_Linear; } else if (tokens[2] == "linear-exact") { windowingMode_ = WindowingMode_LinearExact; } else if (tokens[2] == "sigmoid") { windowingMode_ = WindowingMode_Sigmoid; } else { throw Orthanc::OrthancException( Orthanc::ErrorCode_ParameterOutOfRange, "The third argument to " + WINDOW + " must be linear, linear-exact or sigmoid: " + tokens[2]); } } } } bool HasCustomization() const { return (hasViewport_ || hasQuality_ || hasWindowing_); } unsigned int GetTargetWidth(unsigned int sourceWidth) const { if (hasVW_) { return vw_; } else { return sourceWidth; } } unsigned int GetTargetHeight(unsigned int sourceHeight) const { if (hasVH_) { return vh_; } else { return sourceHeight; } } bool IsFlipX() const { return flipX_; } bool IsFlipY() const { return flipY_; } void GetSourceRegion(Orthanc::ImageAccessor& region, const Orthanc::ImageAccessor& source) const { if (sx_ >= source.GetWidth() || sy_ >= source.GetHeight()) { region.AssignEmpty(source.GetFormat()); } else { unsigned int right = source.GetWidth(); if (hasSW_ && sx_ + sw_ < source.GetWidth()) { right = sx_ + sw_; } unsigned int bottom = source.GetHeight(); if (hasSH_ && sy_ + sh_ < source.GetHeight()) { bottom = sy_ + sh_; } assert(sx_ <= right && sy_ <= bottom && right <= source.GetWidth() && bottom <= source.GetHeight()); source.GetRegion(region, sx_, sy_, right - sx_, bottom - sy_); } } unsigned int GetQuality() const { return quality_; } bool IsWindowing() const { return hasWindowing_; } float GetWindowCenter() const { return windowCenter_; } float GetWindowWidth() const { return windowWidth_; } WindowingMode GetWindowingMode() const { return windowingMode_; } void SetRescaleSlope(float v) { rescaleSlope_ = v; } float GetRescaleSlope() const { return rescaleSlope_; } void SetRescaleIntercept(float v) { rescaleIntercept_ = v; } float GetRescaleIntercept() const { return rescaleIntercept_; } }; } static Orthanc::PixelFormat Convert(OrthancPluginPixelFormat format) { switch (format) { case OrthancPluginPixelFormat_BGRA32: return Orthanc::PixelFormat_BGRA32; case OrthancPluginPixelFormat_Float32: return Orthanc::PixelFormat_Float32; case OrthancPluginPixelFormat_Grayscale16: return Orthanc::PixelFormat_Grayscale16; case OrthancPluginPixelFormat_Grayscale32: return Orthanc::PixelFormat_Grayscale32; case OrthancPluginPixelFormat_Grayscale64: return Orthanc::PixelFormat_Grayscale64; case OrthancPluginPixelFormat_Grayscale8: return Orthanc::PixelFormat_Grayscale8; case OrthancPluginPixelFormat_RGB24: return Orthanc::PixelFormat_RGB24; case OrthancPluginPixelFormat_RGB48: return Orthanc::PixelFormat_RGB48; case OrthancPluginPixelFormat_RGBA32: return Orthanc::PixelFormat_RGBA32; case OrthancPluginPixelFormat_SignedGrayscale16: return Orthanc::PixelFormat_SignedGrayscale16; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } template <Orthanc::PixelFormat SourceFormat> static void ApplyWindowing(Orthanc::ImageAccessor& target, const Orthanc::ImageAccessor& source, float c, float w, WindowingMode mode, float rescaleSlope, float rescaleIntercept) { assert(target.GetFormat() == Orthanc::PixelFormat_Grayscale8 && source.GetFormat() == SourceFormat); if (source.GetWidth() != target.GetWidth() || source.GetHeight() != target.GetHeight()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); } const unsigned int width = source.GetWidth(); const unsigned int height = source.GetHeight(); const float ymin = 0; const float ymax = 255; /** LINEAR: http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.2.1 Python ------ import sympy as sym x, c, w, ymin, ymax = sym.symbols('x c w ymin ymax') e = ((x - (c - 0.5)) / (w-1) + 0.5) * (ymax- ymin) + ymin print(sym.simplify(sym.collect(sym.expand(e), [ x, ymin, ymax ]))) Result ------ (x*(ymax - ymin) + ymax*(-c + 0.5*w) + ymin*(c + 0.5*w - 1.0))/(w - 1) **/ const float linearXMin = (c - 0.5f - (w - 1.0f) / 2.0f); const float linearXMax = (c - 0.5f + (w - 1.0f) / 2.0f); const float linearYScaling = (ymax - ymin) / (w - 1.0f); const float linearYOffset = (ymax * (-c + 0.5f * w) + ymin * (c + 0.5f * w - 1.0f)) / (w - 1.0f); /** LINEAR-EXACT: http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.3.2 Python ------ import sympy as sym x, c, w, ymin, ymax = sym.symbols('x c w ymin ymax') e = (x - c) / w * (ymax- ymin) + ymin print(sym.simplify(sym.collect(sym.expand(e), [ x, ymin, ymax ]))) Result ------ (-c*ymax + x*(ymax - ymin) + ymin*(c + w))/w **/ const float exactXMin = (c - w / 2.0f); const float exactXMax = (c + w / 2.0f); const float exactYScaling = (ymax - ymin) / w; const float exactYOffset = (-c * ymax + ymin * (c + w)) / w; float minValue = std::numeric_limits<float>::infinity(); float maxValue = -std::numeric_limits<float>::infinity(); float wholeDynamicsScale = 1; if (mode == WindowingMode_WholeDynamics) { for (unsigned int y = 0; y < height; y++) { for (unsigned int x = 0; x < width; x++) { float a = Orthanc::ImageTraits<SourceFormat>::GetFloatPixel(source, x, y); minValue = std::min(minValue, a); maxValue = std::max(maxValue, a); } } minValue = rescaleSlope * minValue + rescaleIntercept; maxValue = rescaleSlope * maxValue + rescaleIntercept; wholeDynamicsScale = 255.0f / (maxValue - minValue); } for (unsigned int y = 0; y < height; y++) { for (unsigned int x = 0; x < width; x++) { float a = Orthanc::ImageTraits<SourceFormat>::GetFloatPixel(source, x, y); a = rescaleSlope * a + rescaleIntercept; float b; switch (mode) { case WindowingMode_WholeDynamics: b = (a - minValue) * wholeDynamicsScale; break; case WindowingMode_Linear: { if (a <= linearXMin) { b = ymin; } else if (a > linearXMax) { b = ymax; } else { b = a * linearYScaling + linearYOffset; } break; } case WindowingMode_LinearExact: { if (a <= exactXMin) { b = ymin; } else if (a > exactXMax) { b = ymax; } else { b = a * exactYScaling + exactYOffset; } break; } case WindowingMode_Sigmoid: { // http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.3.1 b = ymax / (1.0f + expf(-4.0f * (a - c) / w)); break; } default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } Orthanc::ImageTraits<Orthanc::PixelFormat_Grayscale8>::SetFloatPixel(target, b, x, y); } } } static void ApplyRendering(Orthanc::ImageAccessor& target, const Orthanc::ImageAccessor& source, const RenderingParameters& parameters, bool invert) { Orthanc::ImageProcessing::Set(target, 0); Orthanc::ImageAccessor region; parameters.GetSourceRegion(region, source); Orthanc::Image scaled(target.GetFormat(), region.GetWidth(), region.GetHeight(), false); if (scaled.GetWidth() == 0 || scaled.GetHeight() == 0) { return; } switch (target.GetFormat()) { case Orthanc::PixelFormat_RGB24: // Windowing is not taken into consideration for color images Orthanc::ImageProcessing::Convert(scaled, region); break; case Orthanc::PixelFormat_Grayscale8: { switch (source.GetFormat()) { case Orthanc::PixelFormat_Grayscale8: ApplyWindowing<Orthanc::PixelFormat_Grayscale8>(scaled, region, parameters.GetWindowCenter(), parameters.GetWindowWidth(), parameters.GetWindowingMode(), parameters.GetRescaleSlope(), parameters.GetRescaleIntercept()); break; case Orthanc::PixelFormat_Grayscale16: ApplyWindowing<Orthanc::PixelFormat_Grayscale16>(scaled, region, parameters.GetWindowCenter(), parameters.GetWindowWidth(), parameters.GetWindowingMode(), parameters.GetRescaleSlope(), parameters.GetRescaleIntercept()); break; case Orthanc::PixelFormat_SignedGrayscale16: ApplyWindowing<Orthanc::PixelFormat_SignedGrayscale16>(scaled, region, parameters.GetWindowCenter(), parameters.GetWindowWidth(), parameters.GetWindowingMode(), parameters.GetRescaleSlope(), parameters.GetRescaleIntercept()); break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } break; } default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } if (parameters.IsFlipX()) { Orthanc::ImageProcessing::FlipX(scaled); } if (parameters.IsFlipY()) { Orthanc::ImageProcessing::FlipY(scaled); } if (invert) { Orthanc::ImageProcessing::Invert(scaled); } // TODO => Replace what follows with a call to "Orthanc::ImageProcessing::FitSize()" // Preserve the aspect ratio float cw = static_cast<float>(scaled.GetWidth()); float ch = static_cast<float>(scaled.GetHeight()); float r = std::min( static_cast<float>(target.GetWidth()) / cw, static_cast<float>(target.GetHeight()) / ch); unsigned int sw = std::min(static_cast<unsigned int>(boost::math::iround(cw * r)), target.GetWidth()); unsigned int sh = std::min(static_cast<unsigned int>(boost::math::iround(ch * r)), target.GetHeight()); Orthanc::Image resized(target.GetFormat(), sw, sh, false); //Orthanc::ImageProcessing::SmoothGaussian5x5(scaled); Orthanc::ImageProcessing::Resize(resized, scaled); assert(target.GetWidth() >= resized.GetWidth() && target.GetHeight() >= resized.GetHeight()); unsigned int offsetX = (target.GetWidth() - resized.GetWidth()) / 2; unsigned int offsetY = (target.GetHeight() - resized.GetHeight()) / 2; target.GetRegion(region, offsetX, offsetY, resized.GetWidth(), resized.GetHeight()); Orthanc::ImageProcessing::Copy(region, resized); } static void AnswerFrameRendered(OrthancPluginRestOutput* output, std::string instanceId, int frame, const OrthancPluginHttpRequest* request) { static const char* const RESCALE_INTERCEPT = "0028,1052"; static const char* const RESCALE_SLOPE = "0028,1053"; static const char* const PHOTOMETRIC_INTERPRETATION = "0028,0004"; Orthanc::MimeType mime = Orthanc::MimeType_Jpeg; // This is the default in DICOMweb for (uint32_t i = 0; i < request->headersCount; i++) { if (boost::iequals(request->headersKeys[i], "Accept") && !boost::iequals(request->headersValues[i], "*/*")) { try { // TODO - Support conversion to GIF mime = Orthanc::StringToMimeType(request->headersValues[i]); if (mime != Orthanc::MimeType_Png && mime != Orthanc::MimeType_Jpeg) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } catch (Orthanc::OrthancException&) { LOG(ERROR) << "Unsupported MIME type in WADO-RS rendered frame: " << request->headersValues[i]; throw; } } } RenderingParameters parameters(request); OrthancPlugins::MemoryBuffer buffer; bool badFrameError = false; if (parameters.HasCustomization()) { if (frame <= 0) { badFrameError = true; } else { buffer.GetDicomInstance(instanceId); Json::Value tags; buffer.DicomToJson(tags, OrthancPluginDicomToJsonFormat_Short, OrthancPluginDicomToJsonFlags_None, 255); if (tags.isMember(RESCALE_SLOPE) && tags[RESCALE_SLOPE].type() == Json::stringValue) { try { parameters.SetRescaleSlope (boost::lexical_cast<float> (Orthanc::Toolbox::StripSpaces(tags[RESCALE_SLOPE].asString()))); } catch (boost::bad_lexical_cast&) { } } if (tags.isMember(RESCALE_INTERCEPT) && tags[RESCALE_INTERCEPT].type() == Json::stringValue) { try { parameters.SetRescaleIntercept (boost::lexical_cast<float> (Orthanc::Toolbox::StripSpaces(tags[RESCALE_INTERCEPT].asString()))); } catch (boost::bad_lexical_cast&) { } } OrthancPlugins::OrthancImage dicom; dicom.DecodeDicomImage(buffer.GetData(), buffer.GetSize(), static_cast<unsigned int>(frame - 1)); Orthanc::PixelFormat targetFormat; OrthancPluginPixelFormat sdkFormat; if (dicom.GetPixelFormat() == OrthancPluginPixelFormat_RGB24) { targetFormat = Orthanc::PixelFormat_RGB24; sdkFormat = OrthancPluginPixelFormat_RGB24; } else { targetFormat = Orthanc::PixelFormat_Grayscale8; sdkFormat = OrthancPluginPixelFormat_Grayscale8; } Orthanc::ImageAccessor source; source.AssignReadOnly(Convert(dicom.GetPixelFormat()), dicom.GetWidth(), dicom.GetHeight(), dicom.GetPitch(), dicom.GetBuffer()); Orthanc::Image target(targetFormat, parameters.GetTargetWidth(source.GetWidth()), parameters.GetTargetHeight(source.GetHeight()), false); // New in 1.3: Fix for MONOCHROME1 images bool invert = false; if (target.GetFormat() == Orthanc::PixelFormat_Grayscale8 && tags.isMember(PHOTOMETRIC_INTERPRETATION) && tags[PHOTOMETRIC_INTERPRETATION].type() == Json::stringValue) { std::string s = tags[PHOTOMETRIC_INTERPRETATION].asString(); Orthanc::Toolbox::StripSpaces(s); if (s == "MONOCHROME1") { invert = true; } } ApplyRendering(target, source, parameters, invert); switch (mime) { case Orthanc::MimeType_Png: OrthancPluginCompressAndAnswerPngImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat, target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer()); break; case Orthanc::MimeType_Jpeg: OrthancPluginCompressAndAnswerJpegImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat, target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer(), parameters.GetQuality()); break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } } } else { // No customization of the rendering. Return the default // preview of Orthanc. std::map<std::string, std::string> headers; headers["Accept"] = Orthanc::EnumerationToString(mime); /** * (1) In DICOMweb, the "frame" parameter is in the range [1..N], * whereas Orthanc uses range [0..N-1], hence the "-1" below. * * (2) We can use "/rendered" that was introduced in the REST API * of Orthanc 1.6.0, as since release 1.2 of the DICOMweb plugin, * the minimal SDK version is Orthanc 1.7.0 (in order to be able * to use transcoding primitives). In releases <= 1.2, "/preview" * was used, which caused one issue: * https://groups.google.com/d/msg/orthanc-users/mKgr2QAKTCU/R7u4I1LvBAAJ **/ if (buffer.RestApiGet("/instances/" + instanceId + "/frames/" + boost::lexical_cast<std::string>(frame - 1) + "/rendered", headers, false)) { OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, buffer.GetData(), buffer.GetSize(), Orthanc::EnumerationToString(mime)); } else { badFrameError = true; } } if (badFrameError) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Inexistent frame index in this image: " + boost::lexical_cast<std::string>(frame)); } } static void AnswerFrameRendered(OrthancPluginRestOutput* output, int frame, const OrthancPluginHttpRequest* request) { if (request->method != OrthancPluginHttpMethod_Get) { OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET"); } else { std::string instanceId, studyInstanceUid, seriesInstanceUid, sopInstanceUid; if (LocateInstance(output, instanceId, studyInstanceUid, seriesInstanceUid, sopInstanceUid, request)) { AnswerFrameRendered(output, instanceId, frame, request); } else { throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, "Inexistent instance"); } } } void RetrieveInstanceRendered(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) { assert(request->groupsCount == 3); AnswerFrameRendered(output, 1 /* first frame */, request); } void RetrieveFrameRendered(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) { assert(request->groupsCount == 4); const char* frame = request->groups[3]; AnswerFrameRendered(output, boost::lexical_cast<int>(frame), request); } void RetrieveSeriesRendered(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) { static const char* const INSTANCES = "Instances"; assert(request->groupsCount == 2); if (request->method != OrthancPluginHttpMethod_Get) { OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET"); } else { std::string orthancId, studyInstanceUid, seriesInstanceUid; if (LocateSeries(output, orthancId, studyInstanceUid, seriesInstanceUid, request)) { Json::Value series; if (OrthancPlugins::RestApiGet(series, "/series/" + orthancId, false) && series.type() == Json::objectValue && series.isMember(INSTANCES) && series[INSTANCES].type() == Json::arrayValue && series[INSTANCES].size() > 0) { std::set<std::string> ids; for (Json::Value::ArrayIndex i = 0; i < series[INSTANCES].size(); i++) { if (series[INSTANCES][i].type() != Json::stringValue) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } else { ids.insert(series[INSTANCES][i].asString()); } } // Retrieve the first instance in alphanumeric order, in order // to always return the same instance std::string instanceId = *ids.begin(); AnswerFrameRendered(output, instanceId, 1 /* first frame */, request); return; // Success } } throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, "Inexistent series"); } } void RetrieveStudyRendered(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) { static const char* const ID = "ID"; assert(request->groupsCount == 1); if (request->method != OrthancPluginHttpMethod_Get) { OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET"); } else { std::string orthancId, studyInstanceUid; if (LocateStudy(output, orthancId, studyInstanceUid, request)) { Json::Value instances; if (OrthancPlugins::RestApiGet(instances, "/studies/" + orthancId + "/instances", false) && instances.type() == Json::arrayValue && instances.size() > 0) { std::set<std::string> ids; for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++) { if (instances[i].type() != Json::objectValue || !instances[i].isMember(ID) || instances[i][ID].type() != Json::stringValue) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } else { ids.insert(instances[i][ID].asString()); } } // Retrieve the first instance in alphanumeric order, in order // to always return the same instance std::string instanceId = *ids.begin(); AnswerFrameRendered(output, instanceId, 1 /* first frame */, request); return; // Success } } throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, "Inexistent study"); } }