diff Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp @ 732:c35e98d22764

move Deprecated classes to a separate folder
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 21 May 2019 14:27:35 +0200
parents Framework/Toolbox/OrthancSlicesLoader.cpp@4f2416d519b4
children be9c1530d40a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Deprecated/Toolbox/OrthancSlicesLoader.cpp	Tue May 21 14:27:35 2019 +0200
@@ -0,0 +1,904 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "OrthancSlicesLoader.h"
+
+#include "../../Toolbox/MessagingToolbox.h"
+
+#include <Core/Compression/GzipCompressor.h>
+#include <Core/Endianness.h>
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/JpegReader.h>
+#include <Core/Images/PngReader.h>
+#include <Core/Images/PamReader.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+#include <Plugins/Samples/Common/FullOrthancDataset.h>
+
+#include <boost/lexical_cast.hpp>
+
+
+
+/**
+ * TODO This is a SLOW implementation of base64 decoding, because
+ * "Orthanc::Toolbox::DecodeBase64()" does not work properly with
+ * WASM. UNDERSTAND WHY.
+ * https://stackoverflow.com/a/34571089/881731
+ **/
+static std::string base64_decode(const std::string &in)
+{
+  std::string out;
+  
+  std::vector<int> T(256,-1);
+  for (int i=0; i<64; i++) T["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i;
+  
+  int val=0, valb=-8;
+  for (size_t i = 0; i < in.size(); i++) {
+    unsigned char c = in[i];
+    if (T[c] == -1) break;
+    val = (val<<6) + T[c];
+    valb += 6;
+    if (valb>=0) {
+      out.push_back(char((val>>valb)&0xFF));
+      valb-=8;
+    }
+  }
+  return out;
+}
+
+
+
+namespace Deprecated
+{
+  class OrthancSlicesLoader::Operation : public Orthanc::IDynamicObject
+  {
+  private:
+    Mode               mode_;
+    unsigned int       frame_;
+    unsigned int       sliceIndex_;
+    const Slice*       slice_;
+    std::string        instanceId_;
+    OrthancStone::SliceImageQuality  quality_;
+
+    Operation(Mode mode) :
+      mode_(mode)
+    {
+    }
+
+  public:
+    Mode GetMode() const
+    {
+      return mode_;
+    }
+
+    OrthancStone::SliceImageQuality GetQuality() const
+    {
+      assert(mode_ == Mode_LoadImage ||
+             mode_ == Mode_LoadRawImage);
+      return quality_;
+    }
+
+    unsigned int GetSliceIndex() const
+    {
+      assert(mode_ == Mode_LoadImage ||
+             mode_ == Mode_LoadRawImage);
+      return sliceIndex_;
+    }
+
+    const Slice& GetSlice() const
+    {
+      assert(mode_ == Mode_LoadImage ||
+             mode_ == Mode_LoadRawImage);
+      assert(slice_ != NULL);
+      return *slice_;
+    }
+
+    unsigned int GetFrame() const
+    {
+      assert(mode_ == Mode_FrameGeometry);
+      return frame_;
+    }
+
+    const std::string& GetInstanceId() const
+    {
+      assert(mode_ == Mode_FrameGeometry ||
+             mode_ == Mode_InstanceGeometry);
+      return instanceId_;
+    }
+
+    static Operation* DownloadInstanceGeometry(const std::string& instanceId)
+    {
+      std::auto_ptr<Operation> operation(new Operation(Mode_InstanceGeometry));
+      operation->instanceId_ = instanceId;
+      return operation.release();
+    }
+
+    static Operation* DownloadFrameGeometry(const std::string& instanceId,
+                                            unsigned int frame)
+    {
+      std::auto_ptr<Operation> operation(new Operation(Mode_FrameGeometry));
+      operation->instanceId_ = instanceId;
+      operation->frame_ = frame;
+      return operation.release();
+    }
+
+    static Operation* DownloadSliceImage(unsigned int  sliceIndex,
+                                         const Slice&  slice,
+                                         OrthancStone::SliceImageQuality quality)
+    {
+      std::auto_ptr<Operation> tmp(new Operation(Mode_LoadImage));
+      tmp->sliceIndex_ = sliceIndex;
+      tmp->slice_ = &slice;
+      tmp->quality_ = quality;
+      return tmp.release();
+    }
+
+    static Operation* DownloadSliceRawImage(unsigned int  sliceIndex,
+                                            const Slice&  slice)
+    {
+      std::auto_ptr<Operation> tmp(new Operation(Mode_LoadRawImage));
+      tmp->sliceIndex_ = sliceIndex;
+      tmp->slice_ = &slice;
+      tmp->quality_ = OrthancStone::SliceImageQuality_InternalRaw;
+      return tmp.release();
+    }
+
+    static Operation* DownloadDicomFile(const Slice&  slice)
+    {
+      std::auto_ptr<Operation> tmp(new Operation(Mode_LoadDicomFile));
+      tmp->slice_ = &slice;
+      return tmp.release();
+    }
+
+  };
+
+  void OrthancSlicesLoader::NotifySliceImageSuccess(const Operation& operation,
+                                                    const Orthanc::ImageAccessor& image)
+  {
+    OrthancSlicesLoader::SliceImageReadyMessage msg
+      (*this, operation.GetSliceIndex(), operation.GetSlice(), image, operation.GetQuality());
+    BroadcastMessage(msg);
+  }
+  
+  
+  void OrthancSlicesLoader::NotifySliceImageError(const Operation& operation)
+  {
+    OrthancSlicesLoader::SliceImageErrorMessage msg
+      (*this, operation.GetSliceIndex(), operation.GetSlice(), operation.GetQuality());
+    BroadcastMessage(msg);
+  }
+  
+  
+  void OrthancSlicesLoader::SortAndFinalizeSlices()
+  {
+    bool ok = slices_.Sort();
+    
+    state_ = State_GeometryReady;
+    
+    if (ok)
+    {
+      LOG(INFO) << "Loaded a series with " << slices_.GetSlicesCount() << " slice(s)";
+      BroadcastMessage(SliceGeometryReadyMessage(*this));
+    }
+    else
+    {
+      LOG(ERROR) << "This series is empty";
+      BroadcastMessage(SliceGeometryErrorMessage(*this));
+    }
+  }
+  
+  void OrthancSlicesLoader::OnGeometryError(const IWebService::HttpRequestErrorMessage& message)
+  {
+    BroadcastMessage(SliceGeometryErrorMessage(*this));
+    state_ = State_Error;
+  }
+
+  void OrthancSlicesLoader::OnSliceImageError(const IWebService::HttpRequestErrorMessage& message)
+  {
+    NotifySliceImageError(dynamic_cast<const Operation&>(message.GetPayload()));
+    state_ = State_Error;
+  }
+
+  void OrthancSlicesLoader::ParseSeriesGeometry(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& series = message.GetJson();
+    Json::Value::Members instances = series.getMemberNames();
+    
+    slices_.Reserve(instances.size());
+    
+    for (size_t i = 0; i < instances.size(); i++)
+    {
+      OrthancPlugins::FullOrthancDataset dataset(series[instances[i]]);
+      
+      Orthanc::DicomMap dicom;
+      OrthancStone::MessagingToolbox::ConvertDataset(dicom, dataset);
+      
+      unsigned int frames;
+      if (!dicom.ParseUnsignedInteger32(frames, Orthanc::DICOM_TAG_NUMBER_OF_FRAMES))
+      {
+        frames = 1;
+      }
+      
+      for (unsigned int frame = 0; frame < frames; frame++)
+      {
+        std::auto_ptr<Slice> slice(new Slice);
+        if (slice->ParseOrthancFrame(dicom, instances[i], frame))
+        {
+          OrthancStone::CoordinateSystem3D geometry = slice->GetGeometry();
+          slices_.AddSlice(geometry, slice.release());
+        }
+        else
+        {
+          LOG(WARNING) << "Skipping invalid frame " << frame << " within instance " << instances[i];
+        }
+      }
+    }
+    
+    SortAndFinalizeSlices();
+  }
+  
+  void OrthancSlicesLoader::ParseInstanceGeometry(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& tags = message.GetJson();
+    const std::string& instanceId = dynamic_cast<const OrthancSlicesLoader::Operation&>(message.GetPayload()).GetInstanceId();
+
+    OrthancPlugins::FullOrthancDataset dataset(tags);
+    
+    Orthanc::DicomMap dicom;
+    OrthancStone::MessagingToolbox::ConvertDataset(dicom, dataset);
+
+    unsigned int frames;
+    if (!dicom.ParseUnsignedInteger32(frames, Orthanc::DICOM_TAG_NUMBER_OF_FRAMES))
+    {
+      frames = 1;
+    }
+    
+    LOG(INFO) << "Instance " << instanceId << " contains " << frames << " frame(s)";
+    
+    for (unsigned int frame = 0; frame < frames; frame++)
+    {
+      std::auto_ptr<Slice> slice(new Slice);
+      if (slice->ParseOrthancFrame(dicom, instanceId, frame))
+      {
+        OrthancStone::CoordinateSystem3D geometry = slice->GetGeometry();
+        slices_.AddSlice(geometry, slice.release());
+      }
+      else
+      {
+        LOG(WARNING) << "Skipping invalid multi-frame instance " << instanceId;
+        BroadcastMessage(SliceGeometryErrorMessage(*this));
+        return;
+      }
+    }
+    
+    SortAndFinalizeSlices();
+  }
+  
+  
+  void OrthancSlicesLoader::ParseFrameGeometry(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Json::Value& tags = message.GetJson();
+    const std::string& instanceId = dynamic_cast<const OrthancSlicesLoader::Operation&>(message.GetPayload()).GetInstanceId();
+    unsigned int frame = dynamic_cast<const OrthancSlicesLoader::Operation&>(message.GetPayload()).GetFrame();
+
+    OrthancPlugins::FullOrthancDataset dataset(tags);
+    
+    state_ = State_GeometryReady;
+    
+    Orthanc::DicomMap dicom;
+    OrthancStone::MessagingToolbox::ConvertDataset(dicom, dataset);
+    
+    std::auto_ptr<Slice> slice(new Slice);
+    if (slice->ParseOrthancFrame(dicom, instanceId, frame))
+    {
+      LOG(INFO) << "Loaded instance geometry " << instanceId;
+
+      OrthancStone::CoordinateSystem3D geometry = slice->GetGeometry();
+      slices_.AddSlice(geometry, slice.release());
+      
+      BroadcastMessage(SliceGeometryReadyMessage(*this));
+    }
+    else
+    {
+      LOG(WARNING) << "Skipping invalid instance " << instanceId;
+      BroadcastMessage(SliceGeometryErrorMessage(*this));
+    }
+  }
+  
+  
+  void OrthancSlicesLoader::ParseSliceImagePng(const OrthancApiClient::BinaryResponseReadyMessage& message)
+  {
+    const Operation& operation = dynamic_cast<const OrthancSlicesLoader::Operation&>(message.GetPayload());
+    std::auto_ptr<Orthanc::ImageAccessor>  image;
+    
+    try
+    {
+      image.reset(new Orthanc::PngReader);
+      dynamic_cast<Orthanc::PngReader&>(*image).ReadFromMemory(message.GetAnswer(), message.GetAnswerSize());
+    }
+    catch (Orthanc::OrthancException&)
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+    
+    if (image->GetWidth() != operation.GetSlice().GetWidth() ||
+        image->GetHeight() != operation.GetSlice().GetHeight())
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+
+    if (operation.GetSlice().GetConverter().GetExpectedPixelFormat() ==
+        Orthanc::PixelFormat_SignedGrayscale16)
+    {
+      if (image->GetFormat() == Orthanc::PixelFormat_Grayscale16)
+      {
+        image->SetFormat(Orthanc::PixelFormat_SignedGrayscale16);
+      }
+      else
+      {
+        NotifySliceImageError(operation);
+        return;
+      }
+    }
+    
+    NotifySliceImageSuccess(operation, *image);
+  }
+  
+  void OrthancSlicesLoader::ParseSliceImagePam(const OrthancApiClient::BinaryResponseReadyMessage& message)
+  {
+    const Operation& operation = dynamic_cast<const OrthancSlicesLoader::Operation&>(message.GetPayload());
+    std::auto_ptr<Orthanc::ImageAccessor>  image;
+
+    try
+    {
+      image.reset(new Orthanc::PamReader);
+      dynamic_cast<Orthanc::PamReader&>(*image).ReadFromMemory(message.GetAnswer(), message.GetAnswerSize());
+    }
+    catch (Orthanc::OrthancException&)
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+
+    if (image->GetWidth() != operation.GetSlice().GetWidth() ||
+        image->GetHeight() != operation.GetSlice().GetHeight())
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+
+    if (operation.GetSlice().GetConverter().GetExpectedPixelFormat() ==
+        Orthanc::PixelFormat_SignedGrayscale16)
+    {
+      if (image->GetFormat() == Orthanc::PixelFormat_Grayscale16)
+      {
+        image->SetFormat(Orthanc::PixelFormat_SignedGrayscale16);
+      }
+      else
+      {
+        NotifySliceImageError(operation);
+        return;
+      }
+    }
+
+    NotifySliceImageSuccess(operation, *image);
+  }
+
+
+  void OrthancSlicesLoader::ParseSliceImageJpeg(const OrthancApiClient::JsonResponseReadyMessage& message)
+  {
+    const Operation& operation = dynamic_cast<const OrthancSlicesLoader::Operation&>(message.GetPayload());
+
+    const Json::Value& encoded = message.GetJson();
+    if (encoded.type() != Json::objectValue ||
+        !encoded.isMember("Orthanc") ||
+        encoded["Orthanc"].type() != Json::objectValue)
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+    
+    const Json::Value& info = encoded["Orthanc"];
+    if (!info.isMember("PixelData") ||
+        !info.isMember("Stretched") ||
+        !info.isMember("Compression") ||
+        info["Compression"].type() != Json::stringValue ||
+        info["PixelData"].type() != Json::stringValue ||
+        info["Stretched"].type() != Json::booleanValue ||
+        info["Compression"].asString() != "Jpeg")
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+    
+    bool isSigned = false;
+    bool isStretched = info["Stretched"].asBool();
+    
+    if (info.isMember("IsSigned"))
+    {
+      if (info["IsSigned"].type() != Json::booleanValue)
+      {
+        NotifySliceImageError(operation);
+        return;
+      }
+      else
+      {
+        isSigned = info["IsSigned"].asBool();
+      }
+    }
+    
+    std::auto_ptr<Orthanc::ImageAccessor> reader;
+    
+    {
+      std::string jpeg;
+      //Orthanc::Toolbox::DecodeBase64(jpeg, info["PixelData"].asString());
+      jpeg = base64_decode(info["PixelData"].asString());
+      
+      try
+      {
+        reader.reset(new Orthanc::JpegReader);
+        dynamic_cast<Orthanc::JpegReader&>(*reader).ReadFromMemory(jpeg);
+      }
+      catch (Orthanc::OrthancException&)
+      {
+        NotifySliceImageError(operation);
+        return;
+      }
+    }
+    
+    Orthanc::PixelFormat expectedFormat =
+      operation.GetSlice().GetConverter().GetExpectedPixelFormat();
+    
+    if (reader->GetFormat() == Orthanc::PixelFormat_RGB24)  // This is a color image
+    {
+      if (expectedFormat != Orthanc::PixelFormat_RGB24)
+      {
+        NotifySliceImageError(operation);
+        return;
+      }
+      
+      if (isSigned || isStretched)
+      {
+        NotifySliceImageError(operation);
+        return;
+      }
+      else
+      {
+        NotifySliceImageSuccess(operation, *reader);
+        return;
+      }
+    }
+    
+    if (reader->GetFormat() != Orthanc::PixelFormat_Grayscale8)
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+    
+    if (!isStretched)
+    {
+      if (expectedFormat != reader->GetFormat())
+      {
+        NotifySliceImageError(operation);
+        return;
+      }
+      else
+      {
+        NotifySliceImageSuccess(operation, *reader);
+        return;
+      }
+    }
+    
+    int32_t stretchLow = 0;
+    int32_t stretchHigh = 0;
+    
+    if (!info.isMember("StretchLow") ||
+        !info.isMember("StretchHigh") ||
+        info["StretchLow"].type() != Json::intValue ||
+        info["StretchHigh"].type() != Json::intValue)
+    {
+      NotifySliceImageError(operation);
+      return;
+    }
+    
+    stretchLow = info["StretchLow"].asInt();
+    stretchHigh = info["StretchHigh"].asInt();
+    
+    if (stretchLow < -32768 ||
+        stretchHigh > 65535 ||
+        (stretchLow < 0 && stretchHigh > 32767))
+    {
+      // This range cannot be represented with a uint16_t or an int16_t
+      NotifySliceImageError(operation);
+      return;
+    }
+    
+    // Decode a grayscale JPEG 8bpp image coming from the Web viewer
+    std::auto_ptr<Orthanc::ImageAccessor> image
+      (new Orthanc::Image(expectedFormat, reader->GetWidth(), reader->GetHeight(), false));
+
+    Orthanc::ImageProcessing::Convert(*image, *reader);
+    reader.reset();
+    
+    float scaling = static_cast<float>(stretchHigh - stretchLow) / 255.0f;
+    
+    if (!OrthancStone::LinearAlgebra::IsCloseToZero(scaling))
+    {
+      float offset = static_cast<float>(stretchLow) / scaling;
+      Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling, true);
+    }
+    
+    NotifySliceImageSuccess(operation, *image);
+  }
+
+
+  class StringImage : public Orthanc::ImageAccessor
+  {
+  private:
+    std::string  buffer_;
+    
+  public:
+    StringImage(Orthanc::PixelFormat format,
+                unsigned int width,
+                unsigned int height,
+                std::string& buffer)
+    {
+      if (buffer.size() != Orthanc::GetBytesPerPixel(format) * width * height)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+      
+      buffer_.swap(buffer);  // The source buffer is now empty
+      
+      void* data = (buffer_.empty() ? NULL : &buffer_[0]);
+      
+      AssignWritable(format, width, height,
+                     Orthanc::GetBytesPerPixel(format) * width, data);
+    }
+  };
+  
+  void OrthancSlicesLoader::ParseSliceRawImage(const OrthancApiClient::BinaryResponseReadyMessage& message)
+  {
+    const Operation& operation = dynamic_cast<const OrthancSlicesLoader::Operation&>(message.GetPayload());
+    Orthanc::GzipCompressor compressor;
+    
+    std::string raw;
+    compressor.Uncompress(raw, message.GetAnswer(), message.GetAnswerSize());
+    
+    const Orthanc::DicomImageInformation& info = operation.GetSlice().GetImageInformation();
+    
+    if (info.GetBitsAllocated() == 32 &&
+        info.GetBitsStored() == 32 &&
+        info.GetHighBit() == 31 &&
+        info.GetChannelCount() == 1 &&
+        !info.IsSigned() &&
+        info.GetPhotometricInterpretation() == Orthanc::PhotometricInterpretation_Monochrome2 &&
+        raw.size() == info.GetWidth() * info.GetHeight() * 4)
+    {
+      // This is the case of RT-DOSE (uint32_t values)
+      
+      std::auto_ptr<Orthanc::ImageAccessor> image
+        (new StringImage(Orthanc::PixelFormat_Grayscale32, info.GetWidth(),
+                         info.GetHeight(), raw));
+      
+      // TODO - Only for big endian
+      for (unsigned int y = 0; y < image->GetHeight(); y++)
+      {
+        uint32_t *p = reinterpret_cast<uint32_t*>(image->GetRow(y));
+        for (unsigned int x = 0; x < image->GetWidth(); x++, p++)
+        {
+          *p = le32toh(*p);
+        }
+      }
+      
+      NotifySliceImageSuccess(operation, *image);
+    }
+    else if (info.GetBitsAllocated() == 16 &&
+             info.GetBitsStored() == 16 &&
+             info.GetHighBit() == 15 &&
+             info.GetChannelCount() == 1 &&
+             !info.IsSigned() &&
+             info.GetPhotometricInterpretation() == Orthanc::PhotometricInterpretation_Monochrome2 &&
+             raw.size() == info.GetWidth() * info.GetHeight() * 2)
+    {
+      std::auto_ptr<Orthanc::ImageAccessor> image
+        (new StringImage(Orthanc::PixelFormat_Grayscale16, info.GetWidth(),
+                         info.GetHeight(), raw));
+      
+      // TODO - Big endian ?
+      
+      NotifySliceImageSuccess(operation, *image);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+  }
+  
+  
+  OrthancSlicesLoader::OrthancSlicesLoader(OrthancStone::MessageBroker& broker,
+                                           OrthancApiClient& orthanc) :
+    OrthancStone::IObservable(broker),
+    OrthancStone::IObserver(broker),
+    orthanc_(orthanc),
+    state_(State_Initialization)
+  {
+  }
+  
+  
+  void OrthancSlicesLoader::ScheduleLoadSeries(const std::string& seriesId)
+  {
+    if (state_ != State_Initialization)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      state_ = State_LoadingGeometry;
+      orthanc_.GetJsonAsync("/series/" + seriesId + "/instances-tags",
+                            new OrthancStone::Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseSeriesGeometry),
+                            new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
+                            NULL);
+    }
+  }
+  
+  void OrthancSlicesLoader::ScheduleLoadInstance(const std::string& instanceId)
+  {
+    if (state_ != State_Initialization)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      state_ = State_LoadingGeometry;
+      
+      // Tag "3004-000c" is "Grid Frame Offset Vector", which is
+      // mandatory to read RT DOSE, but is too long to be returned by default
+      orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags?ignore-length=3004-000c",
+                            new OrthancStone::Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseInstanceGeometry),
+                            new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
+                            Operation::DownloadInstanceGeometry(instanceId));
+    }
+  }
+  
+  
+  void OrthancSlicesLoader::ScheduleLoadFrame(const std::string& instanceId,
+                                              unsigned int frame)
+  {
+    if (state_ != State_Initialization)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      state_ = State_LoadingGeometry;
+
+      orthanc_.GetJsonAsync("/instances/" + instanceId + "/tags",
+                            new OrthancStone::Callable<OrthancSlicesLoader, OrthancApiClient::JsonResponseReadyMessage>(*this, &OrthancSlicesLoader::ParseFrameGeometry),
+                            new OrthancStone::Callable<OrthancSlicesLoader, IWebService::HttpRequestErrorMessage>(*this, &OrthancSlicesLoader::OnGeometryError),
+                            Operation::DownloadFrameGeometry(instanceId, frame));
+    }
+  }
+  
+  
+  bool OrthancSlicesLoader::IsGeometryReady() const
+  {
+    return state_ == State_GeometryReady;
+  }
+  
+  
+  size_t OrthancSlicesLoader::GetSlicesCount() const
+  {
+    if (state_ != State_GeometryReady)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    
+    return slices_.GetSlicesCount();
+  }
+  
+  
+  const Slice& OrthancSlicesLoader::GetSlice(size_t index) const
+  {
+    if (state_ != State_GeometryReady)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    return dynamic_cast<const Slice&>(slices_.GetSlicePayload(index));
+  }
+  
+  
+  bool OrthancSlicesLoader::LookupSlice(size_t& index,
+                                        const OrthancStone::CoordinateSystem3D& plane) const
+  {
+    if (state_ != State_GeometryReady)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    double distance;
+    return (slices_.LookupClosestSlice(index, distance, plane) &&
+            distance <= GetSlice(index).GetThickness() / 2.0);
+  }
+  
+  
+  void OrthancSlicesLoader::ScheduleSliceImagePng(const Slice& slice,
+                                                  size_t index)
+  {
+    std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
+                       boost::lexical_cast<std::string>(slice.GetFrame()));
+    
+    switch (slice.GetConverter().GetExpectedPixelFormat())
+    {
+      case Orthanc::PixelFormat_RGB24:
+        uri += "/preview";
+        break;
+      
+      case Orthanc::PixelFormat_Grayscale16:
+        uri += "/image-uint16";
+        break;
+      
+      case Orthanc::PixelFormat_SignedGrayscale16:
+        uri += "/image-int16";
+        break;
+      
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    
+    orthanc_.GetBinaryAsync(uri, "image/png",
+      new OrthancStone::Callable<OrthancSlicesLoader, 
+        OrthancApiClient::BinaryResponseReadyMessage>
+          (*this, &OrthancSlicesLoader::ParseSliceImagePng),
+      new OrthancStone::Callable<OrthancSlicesLoader, 
+        IWebService::HttpRequestErrorMessage>
+          (*this, &OrthancSlicesLoader::OnSliceImageError),
+      Operation::DownloadSliceImage(
+        static_cast<unsigned int>(index), slice, OrthancStone::SliceImageQuality_FullPng));
+}
+  
+  void OrthancSlicesLoader::ScheduleSliceImagePam(const Slice& slice,
+                                                  size_t index)
+  {
+    std::string uri = 
+      ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
+      boost::lexical_cast<std::string>(slice.GetFrame()));
+
+    switch (slice.GetConverter().GetExpectedPixelFormat())
+    {
+      case Orthanc::PixelFormat_RGB24:
+        uri += "/preview";
+        break;
+
+      case Orthanc::PixelFormat_Grayscale16:
+        uri += "/image-uint16";
+        break;
+
+      case Orthanc::PixelFormat_SignedGrayscale16:
+        uri += "/image-int16";
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    orthanc_.GetBinaryAsync(uri, "image/x-portable-arbitrarymap",
+      new OrthancStone::Callable<OrthancSlicesLoader, 
+        OrthancApiClient::BinaryResponseReadyMessage>
+          (*this, &OrthancSlicesLoader::ParseSliceImagePam),
+      new OrthancStone::Callable<OrthancSlicesLoader, 
+        IWebService::HttpRequestErrorMessage>
+          (*this, &OrthancSlicesLoader::OnSliceImageError),
+      Operation::DownloadSliceImage(static_cast<unsigned int>(index), 
+                                    slice, OrthancStone::SliceImageQuality_FullPam));
+  }
+
+
+  
+  void OrthancSlicesLoader::ScheduleSliceImageJpeg(const Slice& slice,
+                                                   size_t index,
+                                                   OrthancStone::SliceImageQuality quality)
+  {
+    unsigned int value;
+    
+    switch (quality)
+    {
+      case OrthancStone::SliceImageQuality_Jpeg50:
+        value = 50;
+        break;
+
+      case OrthancStone::SliceImageQuality_Jpeg90:
+        value = 90;
+        break;
+
+      case OrthancStone::SliceImageQuality_Jpeg95:
+        value = 95;
+        break;
+      
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    
+    // This requires the official Web viewer plugin to be installed!
+    std::string uri = ("/web-viewer/instances/jpeg" +
+                       boost::lexical_cast<std::string>(value) +
+                       "-" + slice.GetOrthancInstanceId() + "_" +
+                       boost::lexical_cast<std::string>(slice.GetFrame()));
+
+    orthanc_.GetJsonAsync(uri,
+      new OrthancStone::Callable<OrthancSlicesLoader, 
+        OrthancApiClient::JsonResponseReadyMessage>
+          (*this, &OrthancSlicesLoader::ParseSliceImageJpeg),
+      new OrthancStone::Callable<OrthancSlicesLoader, 
+        IWebService::HttpRequestErrorMessage>
+          (*this, &OrthancSlicesLoader::OnSliceImageError),
+        Operation::DownloadSliceImage(
+          static_cast<unsigned int>(index), slice, quality));
+  }
+  
+  
+  
+  void OrthancSlicesLoader::ScheduleLoadSliceImage(size_t index,
+                                                   OrthancStone::SliceImageQuality quality)
+  {
+    if (state_ != State_GeometryReady)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    
+    const Slice& slice = GetSlice(index);
+    
+    if (slice.HasOrthancDecoding())
+    {
+      switch (quality)
+      {
+        case OrthancStone::SliceImageQuality_FullPng:
+          ScheduleSliceImagePng(slice, index);
+          break;
+        case OrthancStone::SliceImageQuality_FullPam:
+          ScheduleSliceImagePam(slice, index);
+          break;
+        default:
+          ScheduleSliceImageJpeg(slice, index, quality);
+      }
+    }
+    else
+    {
+      std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
+                         boost::lexical_cast<std::string>(slice.GetFrame()) + "/raw.gz");
+      orthanc_.GetBinaryAsync(uri, IWebService::HttpHeaders(),
+        new OrthancStone::Callable<OrthancSlicesLoader, 
+          OrthancApiClient::BinaryResponseReadyMessage>
+            (*this, &OrthancSlicesLoader::ParseSliceRawImage),
+        new OrthancStone::Callable<OrthancSlicesLoader,
+          IWebService::HttpRequestErrorMessage>
+            (*this, &OrthancSlicesLoader::OnSliceImageError),
+        Operation::DownloadSliceRawImage(
+          static_cast<unsigned int>(index), slice));
+    }
+  }
+}