view Plugin/WadoRsRetrieveRendered.cpp @ 415:e7b0c7f83fa7 OrthancDicomWeb-1.1

upgrade to Orthanc framework 1.7.0 in releases
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 25 May 2020 11:58:53 +0200
parents 9b0e7a4b6749
children 555a6faaf73f
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 <Core/Images/Image.h>
#include <Core/Images/ImageProcessing.h>
#include <Core/Images/ImageTraits.h>
#include <Core/Toolbox.h>
#include <Plugins/Samples/Common/OrthancPluginCppWrapper.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)
{
  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_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);
  }


  // 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";

  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);

      ApplyRendering(target, source, parameters);
          
      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);

    // In DICOMweb, the "frame" parameter is in the range [1..N],
    // whereas Orthanc uses range [0..N-1], hence the "-1" below.
    if (buffer.RestApiGet("/instances/" + instanceId + "/frames/" +
                          boost::lexical_cast<std::string>(frame - 1) + "/preview", 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");
  }
}