diff StoneWebViewer/WebAssembly/Test.cpp @ 1495:fb74ed5d8c22

initial commit of the Stone Web viewer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 25 Jun 2020 16:51:10 +0200
parents
children 244ad1e4e76a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/StoneWebViewer/WebAssembly/Test.cpp	Thu Jun 25 16:51:10 2020 +0200
@@ -0,0 +1,2246 @@
+/**
+ * Stone of Orthanc
+ * 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 <emscripten.h>
+
+
+#define DISPATCH_JAVASCRIPT_EVENT(name)                         \
+  EM_ASM(                                                       \
+    const customEvent = document.createEvent("CustomEvent");    \
+    customEvent.initCustomEvent(name, false, false, undefined); \
+    window.dispatchEvent(customEvent);                          \
+    );
+
+
+#define EXTERN_CATCH_EXCEPTIONS                         \
+  catch (Orthanc::OrthancException& e)                  \
+  {                                                     \
+    LOG(ERROR) << "OrthancException: " << e.What();     \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (OrthancStone::StoneException& e)               \
+  {                                                     \
+    LOG(ERROR) << "StoneException: " << e.What();       \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (std::exception& e)                             \
+  {                                                     \
+    LOG(ERROR) << "Runtime error: " << e.what();        \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }                                                     \
+  catch (...)                                           \
+  {                                                     \
+    LOG(ERROR) << "Native exception";                   \
+    DISPATCH_JAVASCRIPT_EVENT("StoneException");        \
+  }
+
+
+#include <Cache/MemoryObjectCache.h>
+#include <DicomFormat/DicomArray.h>
+#include <DicomParsing/Internals/DicomImageDecoder.h>
+#include <Images/Image.h>
+#include <Images/ImageProcessing.h>
+#include <Images/JpegReader.h>
+#include <Logging.h>
+
+#include "../../Framework/Loaders/DicomResourcesLoader.h"
+#include "../../Framework/Loaders/SeriesMetadataLoader.h"
+#include "../../Framework/Loaders/SeriesThumbnailsLoader.h"
+#include "../../Framework/Loaders/WebAssemblyLoadersContext.h"
+#include "../../Framework/Messages/ObserverBase.h"
+#include "../../Framework/Oracle/ParseDicomFromWadoCommand.h"
+#include "../../Framework/Scene2D/ColorTextureSceneLayer.h"
+#include "../../Framework/Scene2D/FloatTextureSceneLayer.h"
+#include "../../Framework/Scene2D/PolylineSceneLayer.h"
+#include "../../Framework/StoneException.h"
+#include "../../Framework/Toolbox/DicomInstanceParameters.h"
+#include "../../Framework/Toolbox/GeometryToolbox.h"
+#include "../../Framework/Toolbox/SortedFrames.h"
+#include "../../Framework/Viewport/WebGLViewport.h"
+
+#include <boost/make_shared.hpp>
+#include <stdio.h>
+
+
+enum EMSCRIPTEN_KEEPALIVE ThumbnailType
+{
+  ThumbnailType_Image,
+    ThumbnailType_NoPreview,
+    ThumbnailType_Pdf,
+    ThumbnailType_Video,
+    ThumbnailType_Loading,
+    ThumbnailType_Unknown
+};
+
+
+enum EMSCRIPTEN_KEEPALIVE DisplayedFrameQuality
+{
+DisplayedFrameQuality_None,
+  DisplayedFrameQuality_Low,
+  DisplayedFrameQuality_High
+  };
+  
+
+
+static const int PRIORITY_HIGH = -100;
+static const int PRIORITY_LOW = 100;
+static const int PRIORITY_NORMAL = 0;
+
+static const unsigned int QUALITY_JPEG = 0;
+static const unsigned int QUALITY_FULL = 1;
+
+class ResourcesLoader : public OrthancStone::ObserverBase<ResourcesLoader>
+{
+public:
+  class IObserver : public boost::noncopyable
+  {
+  public:
+    virtual ~IObserver()
+    {
+    }
+
+    virtual void SignalResourcesLoaded() = 0;
+
+    virtual void SignalSeriesThumbnailLoaded(const std::string& studyInstanceUid,
+                                             const std::string& seriesInstanceUid) = 0;
+
+    virtual void SignalSeriesMetadataLoaded(const std::string& studyInstanceUid,
+                                            const std::string& seriesInstanceUid) = 0;
+  };
+  
+private:
+  std::unique_ptr<IObserver>                               observer_;
+  OrthancStone::DicomSource                                source_;
+  size_t                                                   pending_;
+  boost::shared_ptr<OrthancStone::LoadedDicomResources>    studies_;
+  boost::shared_ptr<OrthancStone::LoadedDicomResources>    series_;
+  boost::shared_ptr<OrthancStone::DicomResourcesLoader>    resourcesLoader_;
+  boost::shared_ptr<OrthancStone::SeriesThumbnailsLoader>  thumbnailsLoader_;
+  boost::shared_ptr<OrthancStone::SeriesMetadataLoader>    metadataLoader_;
+
+  ResourcesLoader(const OrthancStone::DicomSource& source) :
+    source_(source),
+    pending_(0),
+    studies_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID)),
+    series_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID))
+  {
+  }
+
+  void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
+  {
+    const Orthanc::SingleValueObject<Orthanc::ResourceType>& payload =
+      dynamic_cast<const Orthanc::SingleValueObject<Orthanc::ResourceType>&>(message.GetUserPayload());
+    
+    OrthancStone::LoadedDicomResources& dicom = *message.GetResources();
+    
+    LOG(INFO) << "resources loaded: " << dicom.GetSize()
+              << ", " << Orthanc::EnumerationToString(payload.GetValue());
+
+    if (payload.GetValue() == Orthanc::ResourceType_Series)
+    {
+      for (size_t i = 0; i < dicom.GetSize(); i++)
+      {
+        std::string studyInstanceUid, seriesInstanceUid;
+        if (dicom.GetResource(i).LookupStringValue(
+              studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+            dicom.GetResource(i).LookupStringValue(
+              seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+        {
+          thumbnailsLoader_->ScheduleLoadThumbnail(source_, "", studyInstanceUid, seriesInstanceUid);
+          metadataLoader_->ScheduleLoadSeries(PRIORITY_LOW + 1, source_, studyInstanceUid, seriesInstanceUid);
+        }
+      }
+    }
+
+    if (pending_ == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      pending_ --;
+      if (pending_ == 0 &&
+          observer_.get() != NULL)
+      {
+        observer_->SignalResourcesLoaded();
+      }
+    }
+  }
+
+  void Handle(const OrthancStone::SeriesThumbnailsLoader::SuccessMessage& message)
+  {
+    if (observer_.get() != NULL)
+    {
+      observer_->SignalSeriesThumbnailLoaded(
+        message.GetStudyInstanceUid(), message.GetSeriesInstanceUid());
+    }
+  }
+
+  void Handle(const OrthancStone::SeriesMetadataLoader::SuccessMessage& message)
+  {
+    if (observer_.get() != NULL)
+    {
+      observer_->SignalSeriesMetadataLoaded(
+        message.GetStudyInstanceUid(), message.GetSeriesInstanceUid());
+    }
+  }
+
+  void FetchInternal(const std::string& studyInstanceUid,
+                     const std::string& seriesInstanceUid)
+  {
+    // Firstly, load the study
+    Orthanc::DicomMap filter;
+    filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false);
+
+    std::set<Orthanc::DicomTag> tags;
+    tags.insert(Orthanc::DICOM_TAG_STUDY_DESCRIPTION);  // Necessary for Orthanc DICOMweb plugin
+
+    resourcesLoader_->ScheduleQido(
+      studies_, PRIORITY_HIGH, source_, Orthanc::ResourceType_Study, filter, tags,
+      new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Study));
+
+    // Secondly, load the series
+    if (!seriesInstanceUid.empty())
+    {
+      filter.SetValue(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, seriesInstanceUid, false);
+    }
+    
+    tags.insert(Orthanc::DICOM_TAG_SERIES_NUMBER);  // Necessary for Google Cloud Platform
+    
+    resourcesLoader_->ScheduleQido(
+      series_, PRIORITY_HIGH, source_, Orthanc::ResourceType_Series, filter, tags,
+      new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Series));
+
+    pending_ += 2;
+  }
+
+public:
+  static boost::shared_ptr<ResourcesLoader> Create(OrthancStone::ILoadersContext::ILock& lock,
+                                                   const OrthancStone::DicomSource& source)
+  {
+    boost::shared_ptr<ResourcesLoader> loader(new ResourcesLoader(source));
+
+    loader->resourcesLoader_ = OrthancStone::DicomResourcesLoader::Create(lock);
+    loader->thumbnailsLoader_ = OrthancStone::SeriesThumbnailsLoader::Create(lock, PRIORITY_LOW);
+    loader->metadataLoader_ = OrthancStone::SeriesMetadataLoader::Create(lock);
+    
+    loader->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
+      *loader->resourcesLoader_, &ResourcesLoader::Handle);
+
+    loader->Register<OrthancStone::SeriesThumbnailsLoader::SuccessMessage>(
+      *loader->thumbnailsLoader_, &ResourcesLoader::Handle);
+
+    loader->Register<OrthancStone::SeriesMetadataLoader::SuccessMessage>(
+      *loader->metadataLoader_, &ResourcesLoader::Handle);
+    
+    return loader;
+  }
+  
+  void FetchAllStudies()
+  {
+    FetchInternal("", "");
+  }
+  
+  void FetchStudy(const std::string& studyInstanceUid)
+  {
+    FetchInternal(studyInstanceUid, "");
+  }
+  
+  void FetchSeries(const std::string& studyInstanceUid,
+                   const std::string& seriesInstanceUid)
+  {
+    FetchInternal(studyInstanceUid, seriesInstanceUid);
+  }
+
+  size_t GetStudiesCount() const
+  {
+    return studies_->GetSize();
+  }
+
+  size_t GetSeriesCount() const
+  {
+    return series_->GetSize();
+  }
+
+  void GetStudy(Orthanc::DicomMap& target,
+                size_t i)
+  {
+    target.Assign(studies_->GetResource(i));
+  }
+
+  void GetSeries(Orthanc::DicomMap& target,
+                 size_t i)
+  {
+    target.Assign(series_->GetResource(i));
+
+    // Complement with the study-level tags
+    std::string studyInstanceUid;
+    if (target.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+        studies_->HasResource(studyInstanceUid))
+    {
+      studies_->MergeResource(target, studyInstanceUid);
+    }
+  }
+
+  OrthancStone::SeriesThumbnailType GetSeriesThumbnail(std::string& image,
+                                                       std::string& mime,
+                                                       const std::string& seriesInstanceUid)
+  {
+    return thumbnailsLoader_->GetSeriesThumbnail(image, mime, seriesInstanceUid);
+  }
+
+  void FetchSeriesMetadata(int priority,
+                           const std::string& studyInstanceUid,
+                           const std::string& seriesInstanceUid)
+  {
+    metadataLoader_->ScheduleLoadSeries(priority, source_, studyInstanceUid, seriesInstanceUid);
+  }
+
+  bool IsSeriesComplete(const std::string& seriesInstanceUid)
+  {
+    OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid);
+    return accessor.IsComplete();
+  }
+
+  bool SortSeriesFrames(OrthancStone::SortedFrames& target,
+                        const std::string& seriesInstanceUid)
+  {
+    OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid);
+    
+    if (accessor.IsComplete())
+    {
+      target.Clear();
+
+      for (size_t i = 0; i < accessor.GetInstancesCount(); i++)
+      {
+        target.AddInstance(accessor.GetInstance(i));
+      }
+
+      target.Sort();
+      
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  void AcquireObserver(IObserver* observer)
+  {  
+    observer_.reset(observer);
+  }
+};
+
+
+
+class FramesCache : public boost::noncopyable
+{
+private:
+  class CachedImage : public Orthanc::ICacheable
+  {
+  private:
+    std::unique_ptr<Orthanc::ImageAccessor>  image_;
+    unsigned int                             quality_;
+
+  public:
+    CachedImage(Orthanc::ImageAccessor* image,
+                unsigned int quality) :
+      image_(image),
+      quality_(quality)
+    {
+      assert(image != NULL);
+    }
+
+    virtual size_t GetMemoryUsage() const
+    {    
+      assert(image_.get() != NULL);
+      return (image_->GetBytesPerPixel() * image_->GetPitch() * image_->GetHeight());
+    }
+
+    const Orthanc::ImageAccessor& GetImage() const
+    {
+      assert(image_.get() != NULL);
+      return *image_;
+    }
+
+    unsigned int GetQuality() const
+    {
+      return quality_;
+    }
+  };
+
+
+  static std::string GetKey(const std::string& sopInstanceUid,
+                            size_t frameIndex)
+  {
+    return sopInstanceUid + "|" + boost::lexical_cast<std::string>(frameIndex);
+  }
+  
+
+  Orthanc::MemoryObjectCache  cache_;
+  
+public:
+  FramesCache()
+  {
+    SetMaximumSize(100 * 1024 * 1024);  // 100 MB
+  }
+  
+  size_t GetMaximumSize()
+  {
+    return cache_.GetMaximumSize();
+  }
+    
+  void SetMaximumSize(size_t size)
+  {
+    cache_.SetMaximumSize(size);
+  }
+
+  /**
+   * Returns "true" iff the provided image has better quality than the
+   * previously cached one, or if no cache was previously available.
+   **/
+  bool Acquire(const std::string& sopInstanceUid,
+               size_t frameIndex,
+               Orthanc::ImageAccessor* image /* transfer ownership */,
+               unsigned int quality)
+  {
+    std::unique_ptr<Orthanc::ImageAccessor> protection(image);
+    
+    if (image == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else if (image->GetFormat() != Orthanc::PixelFormat_Float32 &&
+             image->GetFormat() != Orthanc::PixelFormat_RGB24)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    }
+
+    const std::string& key = GetKey(sopInstanceUid, frameIndex);
+
+    bool invalidate = false;
+    
+    {
+      /**
+       * Access the previous cached entry, with side effect of tagging
+       * it as the most recently accessed frame (update of LRU recycling)
+       **/
+      Orthanc::MemoryObjectCache::Accessor accessor(cache_, key, false /* unique lock */);
+
+      if (accessor.IsValid())
+      {
+        const CachedImage& previous = dynamic_cast<const CachedImage&>(accessor.GetValue());
+        
+        // There is already a cached image for this frame
+        if (previous.GetQuality() < quality)
+        {
+          // The previously stored image has poorer quality
+          invalidate = true;
+        }
+        else
+        {
+          // No update in the quality, don't change the cache
+          return false;   
+        }
+      }
+      else
+      {
+        invalidate = false;
+      }
+    }
+
+    if (invalidate)
+    {
+      cache_.Invalidate(key);
+    }
+        
+    cache_.Acquire(key, new CachedImage(protection.release(), quality));
+    return true;
+  }
+
+  class Accessor : public boost::noncopyable
+  {
+  private:
+    Orthanc::MemoryObjectCache::Accessor accessor_;
+
+    const CachedImage& GetCachedImage() const
+    {
+      if (IsValid())
+      {
+        return dynamic_cast<CachedImage&>(accessor_.GetValue());
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+    
+  public:
+    Accessor(FramesCache& that,
+             const std::string& sopInstanceUid,
+             size_t frameIndex) :
+      accessor_(that.cache_, GetKey(sopInstanceUid, frameIndex), false /* shared lock */)
+    {
+    }
+
+    bool IsValid() const
+    {
+      return accessor_.IsValid();
+    }
+
+    const Orthanc::ImageAccessor& GetImage() const
+    {
+      return GetCachedImage().GetImage();
+    }
+
+    unsigned int GetQuality() const
+    {
+      return GetCachedImage().GetQuality();
+    }
+  };
+};
+
+
+
+class SeriesCursor : public boost::noncopyable
+{
+public:
+  enum Action
+  {
+    Action_FastPlus,
+    Action_Plus,
+    Action_None,
+    Action_Minus,
+    Action_FastMinus
+  };
+  
+private:
+  std::vector<size_t>  prefetch_;
+  int                  framesCount_;
+  int                  currentFrame_;
+  bool                 isCircular_;
+  int                  fastDelta_;
+  Action               lastAction_;
+
+  int ComputeNextFrame(int currentFrame,
+                       Action action) const
+  {
+    if (framesCount_ == 0)
+    {
+      assert(currentFrame == 0);
+      return 0;
+    }
+
+    int nextFrame = currentFrame;
+    
+    switch (action)
+    {
+      case Action_FastPlus:
+        nextFrame += fastDelta_;
+        break;
+
+      case Action_Plus:
+        nextFrame += 1;
+        break;
+
+      case Action_None:
+        break;
+
+      case Action_Minus:
+        nextFrame -= 1;
+        break;
+
+      case Action_FastMinus:
+        nextFrame -= fastDelta_;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    if (isCircular_)
+    {
+      while (nextFrame < 0)
+      {
+        nextFrame += framesCount_;
+      }
+
+      while (nextFrame >= framesCount_)
+      {
+        nextFrame -= framesCount_;
+      }
+    }
+    else
+    {
+      if (nextFrame < 0)
+      {
+        nextFrame = 0;
+      }
+      else if (nextFrame >= framesCount_)
+      {
+        nextFrame = framesCount_ - 1;
+      }
+    }
+
+    return nextFrame;
+  }
+  
+  void UpdatePrefetch()
+  {
+    /**
+     * This method will order the frames of the series according to
+     * the number of "actions" (i.e. mouse wheels) that are necessary
+     * to reach them, starting from the current frame. It is assumed
+     * that once one action is done, it is more likely that the user
+     * will do the same action just afterwards.
+     **/
+    
+    prefetch_.clear();
+
+    if (framesCount_ == 0)
+    {
+      return;
+    }
+
+    prefetch_.reserve(framesCount_);
+    
+    // Breadth-first search using a FIFO. The queue associates a frame
+    // and the action that is the most likely in this frame
+    typedef std::list< std::pair<int, Action> >  Queue;
+
+    Queue queue;
+    std::set<int>  visited;  // Frames that have already been visited
+
+    queue.push_back(std::make_pair(currentFrame_, lastAction_));
+
+    while (!queue.empty())
+    {
+      int frame = queue.front().first;
+      Action previousAction = queue.front().second;
+      queue.pop_front();
+
+      if (visited.find(frame) == visited.end())
+      {
+        visited.insert(frame);
+        prefetch_.push_back(frame);
+
+        switch (previousAction)
+        {
+          case Action_None:
+          case Action_Plus:
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
+            break;
+          
+          case Action_Minus:
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
+            break;
+
+          case Action_FastPlus:
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
+            break;
+              
+          case Action_FastMinus:
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+      }
+    }
+
+    assert(prefetch_.size() == framesCount_);
+  }
+
+  bool CheckFrameIndex(int frame) const
+  {
+    return ((framesCount_ == 0 && frame == 0) ||
+            (framesCount_ > 0 && frame >= 0 && frame < framesCount_));
+  }
+  
+public:
+  SeriesCursor(size_t framesCount) :
+    framesCount_(framesCount),
+    currentFrame_(framesCount / 2),  // Start at the middle frame    
+    isCircular_(false),
+    lastAction_(Action_None)
+  {
+    SetFastDelta(framesCount / 20);
+    UpdatePrefetch();
+  }
+
+  void SetCircular(bool isCircular)
+  {
+    isCircular_ = isCircular;
+    UpdatePrefetch();
+  }
+
+  void SetFastDelta(int delta)
+  {
+    fastDelta_ = (delta < 0 ? -delta : delta);
+
+    if (fastDelta_ <= 0)
+    {
+      fastDelta_ = 1;
+    }
+  }
+
+  void SetCurrentIndex(size_t frame)
+  {
+    if (frame >= framesCount_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      currentFrame_ = frame;
+      lastAction_ = Action_None;
+      UpdatePrefetch();
+    }
+  }
+
+  size_t GetCurrentIndex() const
+  {
+    assert(CheckFrameIndex(currentFrame_));
+    return static_cast<size_t>(currentFrame_);
+  }
+
+  void Apply(Action action)
+  {
+    currentFrame_ = ComputeNextFrame(currentFrame_, action);
+    lastAction_ = action;
+    UpdatePrefetch();
+  }
+
+  size_t GetPrefetchSize() const
+  {
+    assert(prefetch_.size() == framesCount_);
+    return prefetch_.size();
+  }
+
+  size_t GetPrefetchFrameIndex(size_t i) const
+  {
+    if (i >= prefetch_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(CheckFrameIndex(prefetch_[i]));
+      return static_cast<size_t>(prefetch_[i]);
+    }
+  }
+};
+
+
+
+
+class FrameGeometry
+{
+private:
+  bool                              isValid_;
+  std::string                       frameOfReferenceUid_;
+  OrthancStone::CoordinateSystem3D  coordinates_;
+  double                            pixelSpacingX_;
+  double                            pixelSpacingY_;
+  OrthancStone::Extent2D            extent_;
+
+public:
+  FrameGeometry() :
+    isValid_(false)
+  {
+  }
+    
+  FrameGeometry(const Orthanc::DicomMap& tags) :
+    isValid_(false),
+    coordinates_(tags)
+  {
+    if (!tags.LookupStringValue(
+          frameOfReferenceUid_, Orthanc::DICOM_TAG_FRAME_OF_REFERENCE_UID, false))
+    {
+      frameOfReferenceUid_.clear();
+    }
+
+    OrthancStone::GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, tags);
+
+    unsigned int rows, columns;
+    if (tags.HasTag(Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT) &&
+        tags.HasTag(Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT) &&
+        tags.ParseUnsignedInteger32(rows, Orthanc::DICOM_TAG_ROWS) &&
+        tags.ParseUnsignedInteger32(columns, Orthanc::DICOM_TAG_COLUMNS))
+    {
+      double ox = -pixelSpacingX_ / 2.0;
+      double oy = -pixelSpacingY_ / 2.0;
+      extent_.AddPoint(ox, oy);
+      extent_.AddPoint(ox + pixelSpacingX_ * static_cast<double>(columns),
+                       oy + pixelSpacingY_ * static_cast<double>(rows));
+
+      isValid_ = true;
+    }
+  }
+
+  bool IsValid() const
+  {
+    return isValid_;
+  }
+
+  const std::string& GetFrameOfReferenceUid() const
+  {
+    if (isValid_)
+    {
+      return frameOfReferenceUid_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  const OrthancStone::CoordinateSystem3D& GetCoordinates() const
+  {
+    if (isValid_)
+    {
+      return coordinates_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  double GetPixelSpacingX() const
+  {
+    if (isValid_)
+    {
+      return pixelSpacingX_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  double GetPixelSpacingY() const
+  {
+    if (isValid_)
+    {
+      return pixelSpacingY_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  bool Intersect(double& x1,  // Coordinates of the clipped line (out)
+                 double& y1,
+                 double& x2,
+                 double& y2,
+                 const FrameGeometry& other) const
+  {
+    if (this == &other)
+    {
+      return false;
+    }
+    
+    OrthancStone::Vector direction, origin;
+        
+    if (IsValid() &&
+        other.IsValid() &&
+        !extent_.IsEmpty() &&
+        frameOfReferenceUid_ == other.frameOfReferenceUid_ &&
+        OrthancStone::GeometryToolbox::IntersectTwoPlanes(
+          origin, direction,
+          coordinates_.GetOrigin(), coordinates_.GetNormal(),
+          other.coordinates_.GetOrigin(), other.coordinates_.GetNormal()))
+    {
+      double ax, ay, bx, by;
+      coordinates_.ProjectPoint(ax, ay, origin);
+      coordinates_.ProjectPoint(bx, by, origin + 100.0 * direction);
+      
+      return OrthancStone::GeometryToolbox::ClipLineToRectangle(
+        x1, y1, x2, y2,
+        ax, ay, bx, by,
+        extent_.GetX1(), extent_.GetY1(), extent_.GetX2(), extent_.GetY2());
+    }
+    else
+    {
+      return false;
+    }
+  }
+};
+  
+
+
+class ViewerViewport : public OrthancStone::ObserverBase<ViewerViewport>
+{
+public:
+  class IObserver : public boost::noncopyable
+  {
+  public:
+    virtual ~IObserver()
+    {
+    }
+
+    virtual void SignalFrameUpdated(const ViewerViewport& viewport,
+                                    size_t currentFrame,
+                                    size_t countFrames,
+                                    DisplayedFrameQuality quality) = 0;
+  };
+
+private:
+  static const int LAYER_TEXTURE = 0;
+  static const int LAYER_REFERENCE_LINES = 1;
+  
+  
+  class ICommand : public Orthanc::IDynamicObject
+  {
+  private:
+    boost::shared_ptr<ViewerViewport>  viewport_;
+    
+  public:
+    ICommand(boost::shared_ptr<ViewerViewport> viewport) :
+      viewport_(viewport)
+    {
+      if (viewport == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+    
+    virtual ~ICommand()
+    {
+    }
+
+    ViewerViewport& GetViewport() const
+    {
+      assert(viewport_ != NULL);
+      return *viewport_;
+    }
+    
+    virtual void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message) const
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+    
+    virtual void Handle(const OrthancStone::HttpCommand::SuccessMessage& message) const
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    virtual void Handle(const OrthancStone::ParseDicomSuccessMessage& message) const
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  };
+
+  class SetDefaultWindowingCommand : public ICommand
+  {
+  public:
+    SetDefaultWindowingCommand(boost::shared_ptr<ViewerViewport> viewport) :
+      ICommand(viewport)
+    {
+    }
+    
+    virtual void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message) const ORTHANC_OVERRIDE
+    {
+      if (message.GetResources()->GetSize() != 1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+
+      const Orthanc::DicomMap& dicom = message.GetResources()->GetResource(0);
+      
+      {
+        OrthancStone::DicomInstanceParameters params(dicom);
+
+        if (params.HasDefaultWindowing())
+        {
+          GetViewport().defaultWindowingCenter_ = params.GetDefaultWindowingCenter();
+          GetViewport().defaultWindowingWidth_ = params.GetDefaultWindowingWidth();
+          LOG(INFO) << "Default windowing: " << params.GetDefaultWindowingCenter()
+                    << "," << params.GetDefaultWindowingWidth();
+
+          GetViewport().windowingCenter_ = params.GetDefaultWindowingCenter();
+          GetViewport().windowingWidth_ = params.GetDefaultWindowingWidth();
+        }
+        else
+        {
+          LOG(INFO) << "No default windowing";
+          GetViewport().ResetDefaultWindowing();
+        }
+      }
+
+      GetViewport().DisplayCurrentFrame();
+    }
+  };
+
+  class SetLowQualityFrame : public ICommand
+  {
+  private:
+    std::string   sopInstanceUid_;
+    unsigned int  frameIndex_;
+    float         windowCenter_;
+    float         windowWidth_;
+    bool          isMonochrome1_;
+    bool          isPrefetch_;
+    
+  public:
+    SetLowQualityFrame(boost::shared_ptr<ViewerViewport> viewport,
+                       const std::string& sopInstanceUid,
+                       unsigned int frameIndex,
+                       float windowCenter,
+                       float windowWidth,
+                       bool isMonochrome1,
+                       bool isPrefetch) :
+      ICommand(viewport),
+      sopInstanceUid_(sopInstanceUid),
+      frameIndex_(frameIndex),
+      windowCenter_(windowCenter),
+      windowWidth_(windowWidth),
+      isMonochrome1_(isMonochrome1),
+      isPrefetch_(isPrefetch)
+    {
+    }
+    
+    virtual void Handle(const OrthancStone::HttpCommand::SuccessMessage& message) const ORTHANC_OVERRIDE
+    {
+      std::unique_ptr<Orthanc::JpegReader> jpeg(new Orthanc::JpegReader);
+      jpeg->ReadFromMemory(message.GetAnswer());
+
+      bool updatedCache;
+      
+      switch (jpeg->GetFormat())
+      {
+        case Orthanc::PixelFormat_RGB24:
+          updatedCache = GetViewport().cache_->Acquire(
+            sopInstanceUid_, frameIndex_, jpeg.release(), QUALITY_JPEG);
+          break;
+
+        case Orthanc::PixelFormat_Grayscale8:
+        {
+          if (isMonochrome1_)
+          {
+            Orthanc::ImageProcessing::Invert(*jpeg);
+          }
+
+          std::unique_ptr<Orthanc::Image> converted(
+            new Orthanc::Image(Orthanc::PixelFormat_Float32, jpeg->GetWidth(),
+                               jpeg->GetHeight(), false));
+
+          Orthanc::ImageProcessing::Convert(*converted, *jpeg);
+
+          /**
+
+             Orthanc::ImageProcessing::ShiftScale() computes "(x + offset) * scaling".
+             The system to solve is thus:           
+
+             (0 + offset) * scaling = windowingCenter - windowingWidth / 2     [a]
+             (255 + offset) * scaling = windowingCenter + windowingWidth / 2   [b]
+
+             Resolution:
+
+             [b - a] => 255 * scaling = windowingWidth
+             [a] => offset = (windowingCenter - windowingWidth / 2) / scaling
+
+          **/
+
+          const float scaling = windowWidth_ / 255.0f;
+          const float offset = (OrthancStone::LinearAlgebra::IsCloseToZero(scaling) ? 0 :
+                                (windowCenter_ - windowWidth_ / 2.0f) / scaling);
+
+          Orthanc::ImageProcessing::ShiftScale(*converted, offset, scaling, false);
+          updatedCache = GetViewport().cache_->Acquire(
+            sopInstanceUid_, frameIndex_, converted.release(), QUALITY_JPEG);
+          break;
+        }
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      if (updatedCache)
+      {
+        GetViewport().SignalUpdatedFrame(sopInstanceUid_, frameIndex_);
+      }
+
+      if (isPrefetch_)
+      {
+        GetViewport().ScheduleNextPrefetch();
+      }
+    }
+  };
+
+
+  class SetFullDicomFrame : public ICommand
+  {
+  private:
+    std::string   sopInstanceUid_;
+    unsigned int  frameIndex_;
+    bool          isPrefetch_;
+    
+  public:
+    SetFullDicomFrame(boost::shared_ptr<ViewerViewport> viewport,
+                      const std::string& sopInstanceUid,
+                      unsigned int frameIndex,
+                      bool isPrefetch) :
+      ICommand(viewport),
+      sopInstanceUid_(sopInstanceUid),
+      frameIndex_(frameIndex),
+      isPrefetch_(isPrefetch)
+    {
+    }
+    
+    virtual void Handle(const OrthancStone::ParseDicomSuccessMessage& message) const ORTHANC_OVERRIDE
+    {
+      Orthanc::DicomMap tags;
+      message.GetDicom().ExtractDicomSummary(tags);
+
+      std::string s;
+      if (!tags.LookupStringValue(s, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
+      {
+        // Safety check
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }      
+      
+      std::unique_ptr<Orthanc::ImageAccessor> frame(
+        Orthanc::DicomImageDecoder::Decode(message.GetDicom(), frameIndex_));
+
+      if (frame.get() == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      bool updatedCache;
+
+      if (frame->GetFormat() == Orthanc::PixelFormat_RGB24)
+      {
+        updatedCache = GetViewport().cache_->Acquire(
+          sopInstanceUid_, frameIndex_, frame.release(), QUALITY_FULL);
+      }
+      else
+      {
+        double a = 1;
+        double b = 0;
+
+        double doseScaling;
+        if (tags.ParseDouble(doseScaling, Orthanc::DICOM_TAG_DOSE_GRID_SCALING))
+        {
+          a = doseScaling;
+        }
+      
+        double rescaleIntercept, rescaleSlope;
+        if (tags.ParseDouble(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT) &&
+            tags.ParseDouble(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE))
+        {
+          a *= rescaleSlope;
+          b = rescaleIntercept;
+        }
+
+        std::unique_ptr<Orthanc::ImageAccessor> converted(
+          new Orthanc::Image(Orthanc::PixelFormat_Float32, frame->GetWidth(), frame->GetHeight(), false));
+        Orthanc::ImageProcessing::Convert(*converted, *frame);
+        Orthanc::ImageProcessing::ShiftScale2(*converted, b, a, false);
+
+        updatedCache = GetViewport().cache_->Acquire(
+          sopInstanceUid_, frameIndex_, converted.release(), QUALITY_FULL);
+      }
+      
+      if (updatedCache)
+      {
+        GetViewport().SignalUpdatedFrame(sopInstanceUid_, frameIndex_);
+      }
+
+      if (isPrefetch_)
+      {
+        GetViewport().ScheduleNextPrefetch();
+      }
+    }
+  };
+
+
+  class PrefetchItem
+  {
+  private:
+    size_t   frameIndex_;
+    bool     isFull_;
+
+  public:
+    PrefetchItem(size_t frameIndex,
+                 bool isFull) :
+      frameIndex_(frameIndex),
+      isFull_(isFull)
+    {
+    }
+
+    size_t GetFrameIndex() const
+    {
+      return frameIndex_;
+    }
+
+    bool IsFull() const
+    {
+      return isFull_;
+    }
+  };
+  
+
+  std::unique_ptr<IObserver>                    observer_;
+  OrthancStone::ILoadersContext&               context_;
+  boost::shared_ptr<OrthancStone::WebGLViewport>   viewport_;
+  boost::shared_ptr<OrthancStone::DicomResourcesLoader> loader_;
+  OrthancStone::DicomSource                    source_;
+  boost::shared_ptr<FramesCache>               cache_;  
+  std::unique_ptr<OrthancStone::SortedFrames>  frames_;
+  std::unique_ptr<SeriesCursor>                cursor_;
+  float                                        windowingCenter_;
+  float                                        windowingWidth_;
+  float                                        defaultWindowingCenter_;
+  float                                        defaultWindowingWidth_;
+  bool                                         inverted_;
+  bool                                         fitNextContent_;
+  bool                                         isCtrlDown_;
+  FrameGeometry                                currentFrameGeometry_;
+  std::list<PrefetchItem>                      prefetchQueue_;
+
+  void ScheduleNextPrefetch()
+  {
+    while (!prefetchQueue_.empty())
+    {
+      size_t index = prefetchQueue_.front().GetFrameIndex();
+      bool isFull = prefetchQueue_.front().IsFull();
+      prefetchQueue_.pop_front();
+      
+      const std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
+      unsigned int frame = frames_->GetFrameIndex(index);
+
+      {
+        FramesCache::Accessor accessor(*cache_, sopInstanceUid, frame);
+        if (!accessor.IsValid() ||
+            (isFull && accessor.GetQuality() == 0))
+        {
+          if (isFull)
+          {
+            ScheduleLoadFullDicomFrame(index, PRIORITY_NORMAL, true);
+          }
+          else
+          {
+            ScheduleLoadRenderedFrame(index, PRIORITY_NORMAL, true);
+          }
+          return;
+        }
+      }
+    }
+  }
+  
+  
+  void ResetDefaultWindowing()
+  {
+    defaultWindowingCenter_ = 128;
+    defaultWindowingWidth_ = 256;
+
+    windowingCenter_ = defaultWindowingCenter_;
+    windowingWidth_ = defaultWindowingWidth_;
+
+    inverted_ = false;
+  }
+
+  void SignalUpdatedFrame(const std::string& sopInstanceUid,
+                          unsigned int frameIndex)
+  {
+    if (cursor_.get() != NULL &&
+        frames_.get() != NULL)
+    {
+      size_t index = cursor_->GetCurrentIndex();
+
+      if (frames_->GetFrameSopInstanceUid(index) == sopInstanceUid &&
+          frames_->GetFrameIndex(index) == frameIndex)
+      {
+        DisplayCurrentFrame();
+      }
+    }
+  }
+
+  void DisplayCurrentFrame()
+  {
+    DisplayedFrameQuality quality = DisplayedFrameQuality_None;
+    
+    if (cursor_.get() != NULL &&
+        frames_.get() != NULL)
+    {
+      const size_t index = cursor_->GetCurrentIndex();
+      
+      unsigned int cachedQuality;
+      if (!DisplayFrame(cachedQuality, index))
+      {
+        // This frame is not cached yet: Load it
+        if (source_.HasDicomWebRendered())
+        {
+          ScheduleLoadRenderedFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
+        }
+        else
+        {
+          ScheduleLoadFullDicomFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
+        }
+      }
+      else if (cachedQuality < QUALITY_FULL)
+      {
+        // This frame is only available in low-res: Download the full DICOM
+        ScheduleLoadFullDicomFrame(index, PRIORITY_HIGH, false /* not a prefetch */);
+        quality = DisplayedFrameQuality_Low;
+      }
+      else
+      {
+        quality = DisplayedFrameQuality_High;
+      }
+
+      currentFrameGeometry_ = FrameGeometry(frames_->GetFrameTags(index));
+
+      {
+        // Prepare prefetching
+        prefetchQueue_.clear();
+        for (size_t i = 0; i < cursor_->GetPrefetchSize() && i < 16; i++)
+        {
+          size_t a = cursor_->GetPrefetchFrameIndex(i);
+          if (a != index)
+          {
+            prefetchQueue_.push_back(PrefetchItem(a, i < 2));
+          }
+        }
+
+        ScheduleNextPrefetch();
+      }      
+
+      if (observer_.get() != NULL)
+      {
+        observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
+                                      frames_->GetFramesCount(), quality);
+      }
+    }
+    else
+    {
+      currentFrameGeometry_ = FrameGeometry();
+    }
+  }
+
+  void ClearViewport()
+  {
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().GetScene().DeleteLayer(LAYER_TEXTURE);
+      //lock->GetCompositor().Refresh(lock->GetController().GetScene());
+      lock->Invalidate();
+    }
+  }
+
+  bool DisplayFrame(unsigned int& quality,
+                    size_t index)
+  {
+    if (frames_.get() == NULL)
+    {
+      return false;
+    }
+
+    const std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
+    unsigned int frame = frames_->GetFrameIndex(index);
+
+    FramesCache::Accessor accessor(*cache_, sopInstanceUid, frame);
+    if (accessor.IsValid())
+    {
+      quality = accessor.GetQuality();
+
+      std::unique_ptr<OrthancStone::TextureBaseSceneLayer> layer;
+
+      switch (accessor.GetImage().GetFormat())
+      {
+        case Orthanc::PixelFormat_RGB24:
+          layer.reset(new OrthancStone::ColorTextureSceneLayer(accessor.GetImage()));
+          break;
+
+        case Orthanc::PixelFormat_Float32:
+        {
+          std::unique_ptr<OrthancStone::FloatTextureSceneLayer> tmp(
+            new OrthancStone::FloatTextureSceneLayer(accessor.GetImage()));
+          tmp->SetCustomWindowing(windowingCenter_, windowingWidth_);
+          tmp->SetInverted(inverted_ ^ frames_->IsFrameMonochrome1(index));
+          layer.reset(tmp.release());
+          break;
+        }
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+      }
+
+      layer->SetLinearInterpolation(true);
+
+      double pixelSpacingX, pixelSpacingY;
+      OrthancStone::GeometryToolbox::GetPixelSpacing(
+        pixelSpacingX, pixelSpacingY, frames_->GetFrameTags(index));
+      layer->SetPixelSpacing(pixelSpacingX, pixelSpacingY);
+
+      if (layer.get() == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+        lock->GetController().GetScene().SetLayer(LAYER_TEXTURE, layer.release());
+
+        if (fitNextContent_)
+        {
+          lock->GetCompositor().RefreshCanvasSize();
+          lock->GetCompositor().FitContent(lock->GetController().GetScene());
+          fitNextContent_ = false;
+        }
+        
+        //lock->GetCompositor().Refresh(lock->GetController().GetScene());
+        lock->Invalidate();
+        return true;
+      }
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  void ScheduleLoadFullDicomFrame(size_t index,
+                                  int priority,
+                                  bool isPrefetch)
+  {
+    if (frames_.get() != NULL)
+    {
+      std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
+      unsigned int frame = frames_->GetFrameIndex(index);
+      
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
+        lock->Schedule(
+          GetSharedObserver(), priority, OrthancStone::ParseDicomFromWadoCommand::Create(
+            source_, frames_->GetStudyInstanceUid(), frames_->GetSeriesInstanceUid(),
+            sopInstanceUid, false /* transcoding (TODO) */,
+            Orthanc::DicomTransferSyntax_LittleEndianExplicit /* TODO */,
+            new SetFullDicomFrame(GetSharedObserver(), sopInstanceUid, frame, isPrefetch)));
+      }
+    }
+  }
+
+  void ScheduleLoadRenderedFrame(size_t index,
+                                 int priority,
+                                 bool isPrefetch)
+  {
+    if (!source_.HasDicomWebRendered())
+    {
+      ScheduleLoadFullDicomFrame(index, priority, isPrefetch);
+    }
+    else if (frames_.get() != NULL)
+    {
+      std::string sopInstanceUid = frames_->GetFrameSopInstanceUid(index);
+      unsigned int frame = frames_->GetFrameIndex(index);
+      bool isMonochrome1 = frames_->IsFrameMonochrome1(index);
+
+      const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
+                               "/series/" + frames_->GetSeriesInstanceUid() +
+                               "/instances/" + sopInstanceUid +
+                               "/frames/" + boost::lexical_cast<std::string>(frame + 1) + "/rendered");
+
+      std::map<std::string, std::string> headers, arguments;
+      arguments["window"] = (
+        boost::lexical_cast<std::string>(defaultWindowingCenter_) + ","  +
+        boost::lexical_cast<std::string>(defaultWindowingWidth_) + ",linear");
+
+      std::unique_ptr<OrthancStone::IOracleCommand> command(
+        source_.CreateDicomWebCommand(
+          uri, arguments, headers, new SetLowQualityFrame(
+            GetSharedObserver(), sopInstanceUid, frame,
+            defaultWindowingCenter_, defaultWindowingWidth_, isMonochrome1, isPrefetch)));
+
+      {
+        std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock());
+        lock->Schedule(GetSharedObserver(), priority, command.release());
+      }
+    }
+  }
+
+  ViewerViewport(OrthancStone::ILoadersContext& context,
+                 const OrthancStone::DicomSource& source,
+                 const std::string& canvas,
+                 boost::shared_ptr<FramesCache> cache) :
+    context_(context),
+    source_(source),
+    viewport_(OrthancStone::WebGLViewport::Create(canvas)),
+    cache_(cache),
+    fitNextContent_(true),
+    isCtrlDown_(false)
+  {
+    if (!cache_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    
+    emscripten_set_wheel_callback(viewport_->GetCanvasCssSelector().c_str(), this, true, OnWheel);
+    emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);
+    emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, OnKey);
+
+    ResetDefaultWindowing();
+  }
+
+  static EM_BOOL OnKey(int eventType,
+                       const EmscriptenKeyboardEvent *event,
+                       void *userData)
+  {
+    /**
+     * WARNING: There is a problem with Firefox 71 that seems to mess
+     * the "ctrlKey" value.
+     **/
+    
+    ViewerViewport& that = *reinterpret_cast<ViewerViewport*>(userData);
+    that.isCtrlDown_ = event->ctrlKey;
+    return false;
+  }
+
+  
+  static EM_BOOL OnWheel(int eventType,
+                         const EmscriptenWheelEvent *wheelEvent,
+                         void *userData)
+  {
+    ViewerViewport& that = *reinterpret_cast<ViewerViewport*>(userData);
+
+    if (that.cursor_.get() != NULL)
+    {
+      if (wheelEvent->deltaY < 0)
+      {
+        that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastMinus : SeriesCursor::Action_Minus);
+      }
+      else if (wheelEvent->deltaY > 0)
+      {
+        that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastPlus : SeriesCursor::Action_Plus);
+      }
+    }
+    
+    return true;
+  }
+
+  void Handle(const OrthancStone::DicomResourcesLoader::SuccessMessage& message)
+  {
+    dynamic_cast<const ICommand&>(message.GetUserPayload()).Handle(message);
+  }
+
+  void Handle(const OrthancStone::HttpCommand::SuccessMessage& message)
+  {
+    dynamic_cast<const ICommand&>(message.GetOrigin().GetPayload()).Handle(message);
+  }
+
+  void Handle(const OrthancStone::ParseDicomSuccessMessage& message)
+  {
+    dynamic_cast<const ICommand&>(message.GetOrigin().GetPayload()).Handle(message);
+  }
+  
+public:
+  static boost::shared_ptr<ViewerViewport> Create(OrthancStone::ILoadersContext::ILock& lock,
+                                                  const OrthancStone::DicomSource& source,
+                                                  const std::string& canvas,
+                                                  boost::shared_ptr<FramesCache> cache)
+  {
+    boost::shared_ptr<ViewerViewport> viewport(
+      new ViewerViewport(lock.GetContext(), source, canvas, cache));
+
+    viewport->loader_ = OrthancStone::DicomResourcesLoader::Create(lock);
+    viewport->Register<OrthancStone::DicomResourcesLoader::SuccessMessage>(
+      *viewport->loader_, &ViewerViewport::Handle);
+
+    viewport->Register<OrthancStone::HttpCommand::SuccessMessage>(
+      lock.GetOracleObservable(), &ViewerViewport::Handle);
+
+    viewport->Register<OrthancStone::ParseDicomSuccessMessage>(
+      lock.GetOracleObservable(), &ViewerViewport::Handle);
+
+    return viewport;    
+  }
+
+  void SetFrames(OrthancStone::SortedFrames* frames)
+  {
+    if (frames == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    fitNextContent_ = true;
+
+    frames_.reset(frames);
+    cursor_.reset(new SeriesCursor(frames_->GetFramesCount()));
+
+    LOG(INFO) << "Number of frames in series: " << frames_->GetFramesCount();
+
+    ResetDefaultWindowing();
+    ClearViewport();
+    prefetchQueue_.clear();
+    currentFrameGeometry_ = FrameGeometry();
+
+    if (observer_.get() != NULL)
+    {
+      observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
+                                    frames_->GetFramesCount(), DisplayedFrameQuality_None);
+    }
+    
+    if (frames_->GetFramesCount() != 0)
+    {
+      const std::string& sopInstanceUid = frames_->GetFrameSopInstanceUid(cursor_->GetCurrentIndex());
+
+      {
+        // Fetch the default windowing for the central instance
+        const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
+                                 "/series/" + frames_->GetSeriesInstanceUid() +
+                                 "/instances/" + sopInstanceUid + "/metadata");
+        
+        loader_->ScheduleGetDicomWeb(
+          boost::make_shared<OrthancStone::LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
+          0, source_, uri, new SetDefaultWindowingCommand(GetSharedObserver()));
+      }
+    }
+  }
+
+  // This method is used when the layout of the HTML page changes,
+  // which does not trigger the "emscripten_set_resize_callback()"
+  void UpdateSize(bool fitContent)
+  {
+    std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+    lock->GetCompositor().RefreshCanvasSize();
+
+    if (fitContent)
+    {
+      lock->GetCompositor().FitContent(lock->GetController().GetScene());
+    }
+
+    lock->Invalidate();
+  }
+
+  void AcquireObserver(IObserver* observer)
+  {  
+    observer_.reset(observer);
+  }
+
+  const std::string& GetCanvasId() const
+  {
+    assert(viewport_);
+    return viewport_->GetCanvasId();
+  }
+
+  void ChangeFrame(SeriesCursor::Action action)
+  {
+    if (cursor_.get() != NULL)
+    {
+      size_t previous = cursor_->GetCurrentIndex();
+      
+      cursor_->Apply(action);
+      
+      size_t current = cursor_->GetCurrentIndex();
+      if (previous != current)
+      {
+        DisplayCurrentFrame();
+      }
+    }
+  }
+
+  const FrameGeometry& GetCurrentFrameGeometry() const
+  {
+    return currentFrameGeometry_;
+  }
+
+  void UpdateReferenceLines(const std::list<const FrameGeometry*>& planes)
+  {
+    std::unique_ptr<OrthancStone::PolylineSceneLayer> layer(new OrthancStone::PolylineSceneLayer);
+    
+    if (GetCurrentFrameGeometry().IsValid())
+    {
+      for (std::list<const FrameGeometry*>::const_iterator
+             it = planes.begin(); it != planes.end(); ++it)
+      {
+        assert(*it != NULL);
+        
+        double x1, y1, x2, y2;
+        if (GetCurrentFrameGeometry().Intersect(x1, y1, x2, y2, **it))
+        {
+          OrthancStone::PolylineSceneLayer::Chain chain;
+          chain.push_back(OrthancStone::ScenePoint2D(x1, y1));
+          chain.push_back(OrthancStone::ScenePoint2D(x2, y2));
+          layer->AddChain(chain, false, 0, 255, 0);
+        }
+      }
+    }
+    
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+
+      if (layer->GetChainsCount() == 0)
+      {
+        lock->GetController().GetScene().DeleteLayer(LAYER_REFERENCE_LINES);
+      }
+      else
+      {
+        lock->GetController().GetScene().SetLayer(LAYER_REFERENCE_LINES, layer.release());
+      }
+      
+      //lock->GetCompositor().Refresh(lock->GetController().GetScene());
+      lock->Invalidate();
+    }
+  }
+
+
+  void ClearReferenceLines()
+  {
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+      lock->GetController().GetScene().DeleteLayer(LAYER_REFERENCE_LINES);
+      lock->Invalidate();
+    }
+  }
+
+
+  void SetDefaultWindowing()
+  {
+    SetWindowing(defaultWindowingCenter_, defaultWindowingWidth_);
+  }
+
+  void SetWindowing(float windowingCenter,
+                    float windowingWidth)
+  {
+    windowingCenter_ = windowingCenter;
+    windowingWidth_ = windowingWidth;
+
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+
+      if (lock->GetController().GetScene().HasLayer(LAYER_TEXTURE) &&
+          lock->GetController().GetScene().GetLayer(LAYER_TEXTURE).GetType() ==
+          OrthancStone::ISceneLayer::Type_FloatTexture)
+      {
+        dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(
+          lock->GetController().GetScene().GetLayer(LAYER_TEXTURE)).
+          SetCustomWindowing(windowingCenter_, windowingWidth_);
+        lock->Invalidate();
+      }
+    }
+  }
+
+  void Invert()
+  {
+    inverted_ = !inverted_;
+    
+    {
+      std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock());
+
+      if (lock->GetController().GetScene().HasLayer(LAYER_TEXTURE) &&
+          lock->GetController().GetScene().GetLayer(LAYER_TEXTURE).GetType() ==
+          OrthancStone::ISceneLayer::Type_FloatTexture)
+      {
+        OrthancStone::FloatTextureSceneLayer& layer = 
+          dynamic_cast<OrthancStone::FloatTextureSceneLayer&>(
+            lock->GetController().GetScene().GetLayer(LAYER_TEXTURE));
+
+        // NB: Using "IsInverted()" instead of "inverted_" is for
+        // compatibility with MONOCHROME1 images
+        layer.SetInverted(!layer.IsInverted());
+        lock->Invalidate();
+      }
+    }
+  }
+};
+
+
+
+
+
+typedef std::map<std::string, boost::shared_ptr<ViewerViewport> >  Viewports;
+static Viewports allViewports_;
+static bool showReferenceLines_ = true;
+
+
+static void UpdateReferenceLines()
+{
+  if (showReferenceLines_)
+  {
+    std::list<const FrameGeometry*> planes;
+    
+    for (Viewports::const_iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      planes.push_back(&it->second->GetCurrentFrameGeometry());
+    }
+
+    for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      it->second->UpdateReferenceLines(planes);
+    }
+  }
+  else
+  {
+    for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      it->second->ClearReferenceLines();
+    }
+  }
+}
+
+
+class WebAssemblyObserver : public ResourcesLoader::IObserver,
+                            public ViewerViewport::IObserver
+{
+public:
+  virtual void SignalResourcesLoaded() ORTHANC_OVERRIDE
+  {
+    DISPATCH_JAVASCRIPT_EVENT("ResourcesLoaded");
+  }
+
+  virtual void SignalSeriesThumbnailLoaded(const std::string& studyInstanceUid,
+                                           const std::string& seriesInstanceUid) ORTHANC_OVERRIDE
+  {
+    EM_ASM({
+        const customEvent = document.createEvent("CustomEvent");
+        customEvent.initCustomEvent("ThumbnailLoaded", false, false,
+                                    { "studyInstanceUid" : UTF8ToString($0),
+                                        "seriesInstanceUid" : UTF8ToString($1) });
+        window.dispatchEvent(customEvent);
+      },
+      studyInstanceUid.c_str(),
+      seriesInstanceUid.c_str());
+  }
+
+  virtual void SignalSeriesMetadataLoaded(const std::string& studyInstanceUid,
+                                          const std::string& seriesInstanceUid) ORTHANC_OVERRIDE
+  {
+    EM_ASM({
+        const customEvent = document.createEvent("CustomEvent");
+        customEvent.initCustomEvent("MetadataLoaded", false, false,
+                                    { "studyInstanceUid" : UTF8ToString($0),
+                                        "seriesInstanceUid" : UTF8ToString($1) });
+        window.dispatchEvent(customEvent);
+      },
+      studyInstanceUid.c_str(),
+      seriesInstanceUid.c_str());
+  }
+
+  virtual void SignalFrameUpdated(const ViewerViewport& viewport,
+                                  size_t currentFrame,
+                                  size_t countFrames,
+                                  DisplayedFrameQuality quality) ORTHANC_OVERRIDE
+  {
+    EM_ASM({
+        const customEvent = document.createEvent("CustomEvent");
+        customEvent.initCustomEvent("FrameUpdated", false, false,
+                                    { "canvasId" : UTF8ToString($0),
+                                        "currentFrame" : $1,
+                                        "framesCount" : $2,
+                                        "quality" : $3 });
+        window.dispatchEvent(customEvent);
+      },
+      viewport.GetCanvasId().c_str(),
+      static_cast<int>(currentFrame),
+      static_cast<int>(countFrames),
+      quality);
+
+
+    UpdateReferenceLines();
+  };
+};
+
+
+
+static OrthancStone::DicomSource source_;
+static boost::shared_ptr<FramesCache> cache_;
+static boost::shared_ptr<OrthancStone::WebAssemblyLoadersContext> context_;
+static std::string stringBuffer_;
+
+
+
+static void FormatTags(std::string& target,
+                       const Orthanc::DicomMap& tags)
+{
+  Orthanc::DicomArray arr(tags);
+  Json::Value v = Json::objectValue;
+
+  for (size_t i = 0; i < arr.GetSize(); i++)
+  {
+    const Orthanc::DicomElement& element = arr.GetElement(i);
+    if (!element.GetValue().IsBinary() &&
+        !element.GetValue().IsNull())
+    {
+      v[element.GetTag().Format()] = element.GetValue().GetContent();
+    }
+  }
+
+  target = v.toStyledString();
+}
+
+
+static ResourcesLoader& GetResourcesLoader()
+{
+  static boost::shared_ptr<ResourcesLoader>  resourcesLoader_;
+
+  if (!resourcesLoader_)
+  {
+    std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_->Lock());
+    resourcesLoader_ = ResourcesLoader::Create(*lock, source_);
+    resourcesLoader_->AcquireObserver(new WebAssemblyObserver);
+  }
+
+  return *resourcesLoader_;
+}
+
+
+static boost::shared_ptr<ViewerViewport> GetViewport(const std::string& canvas)
+{
+  Viewports::iterator found = allViewports_.find(canvas);
+  if (found == allViewports_.end())
+  {
+    std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_->Lock());
+    boost::shared_ptr<ViewerViewport> viewport(ViewerViewport::Create(*lock, source_, canvas, cache_));
+    viewport->AcquireObserver(new WebAssemblyObserver);
+    allViewports_[canvas] = viewport;
+    return viewport;
+  }
+  else
+  {
+    return found->second;
+  }
+}
+
+
+extern "C"
+{
+  int main(int argc, char const *argv[]) 
+  {
+    printf("OK\n");
+    Orthanc::InitializeFramework("", true);
+    Orthanc::Logging::EnableInfoLevel(true);
+    //Orthanc::Logging::EnableTraceLevel(true);
+
+    context_.reset(new OrthancStone::WebAssemblyLoadersContext(1, 4, 1));
+    cache_.reset(new FramesCache);
+    
+    DISPATCH_JAVASCRIPT_EVENT("StoneInitialized");
+  }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void SetOrthancRoot(const char* uri,
+                      int useRendered)
+  {
+    try
+    {
+      context_->SetLocalOrthanc(uri);  // For "source_.SetDicomWebThroughOrthancSource()"
+      source_.SetDicomWebSource(std::string(uri) + "/dicom-web");
+      source_.SetDicomWebRendered(useRendered != 0);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+  
+
+  EMSCRIPTEN_KEEPALIVE
+  void SetDicomWebServer(const char* serverName,
+                         int hasRendered)
+  {
+    try
+    {
+      source_.SetDicomWebThroughOrthancSource(serverName);
+      source_.SetDicomWebRendered(hasRendered != 0);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+  
+
+  EMSCRIPTEN_KEEPALIVE
+  void FetchAllStudies()
+  {
+    try
+    {
+      GetResourcesLoader().FetchAllStudies();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void FetchStudy(const char* studyInstanceUid)
+  {
+    try
+    {
+      GetResourcesLoader().FetchStudy(studyInstanceUid);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  void FetchSeries(const char* studyInstanceUid,
+                   const char* seriesInstanceUid)
+  {
+    try
+    {
+      GetResourcesLoader().FetchSeries(studyInstanceUid, seriesInstanceUid);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+  
+  EMSCRIPTEN_KEEPALIVE
+  int GetStudiesCount()
+  {
+    try
+    {
+      return GetResourcesLoader().GetStudiesCount();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+    return 0;  // on exception
+  }
+  
+  EMSCRIPTEN_KEEPALIVE
+  int GetSeriesCount()
+  {
+    try
+    {
+      return GetResourcesLoader().GetSeriesCount();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+    return 0;  // on exception
+  }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  const char* GetStringBuffer()
+  {
+    return stringBuffer_.c_str();
+  }
+  
+
+  EMSCRIPTEN_KEEPALIVE
+  void LoadStudyTags(int i)
+  {
+    try
+    {
+      if (i < 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      
+      Orthanc::DicomMap dicom;
+      GetResourcesLoader().GetStudy(dicom, i);
+      FormatTags(stringBuffer_, dicom);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+  
+
+  EMSCRIPTEN_KEEPALIVE
+  void LoadSeriesTags(int i)
+  {
+    try
+    {
+      if (i < 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      
+      Orthanc::DicomMap dicom;
+      GetResourcesLoader().GetSeries(dicom, i);
+      FormatTags(stringBuffer_, dicom);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+  
+
+  EMSCRIPTEN_KEEPALIVE
+  int LoadSeriesThumbnail(const char* seriesInstanceUid)
+  {
+    try
+    {
+      std::string image, mime;
+      switch (GetResourcesLoader().GetSeriesThumbnail(image, mime, seriesInstanceUid))
+      {
+        case OrthancStone::SeriesThumbnailType_Image:
+          Orthanc::Toolbox::EncodeDataUriScheme(stringBuffer_, mime, image);
+          return ThumbnailType_Image;
+          
+        case OrthancStone::SeriesThumbnailType_Pdf:
+          return ThumbnailType_Pdf;
+          
+        case OrthancStone::SeriesThumbnailType_Video:
+          return ThumbnailType_Video;
+          
+        case OrthancStone::SeriesThumbnailType_NotLoaded:
+          return ThumbnailType_Loading;
+          
+        case OrthancStone::SeriesThumbnailType_Unsupported:
+          return ThumbnailType_NoPreview;
+
+        default:
+          return ThumbnailType_Unknown;
+      }
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+    return ThumbnailType_Unknown;
+  }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void SpeedUpFetchSeriesMetadata(const char* studyInstanceUid,
+                                  const char* seriesInstanceUid)
+  {
+    try
+    {
+      GetResourcesLoader().FetchSeriesMetadata(PRIORITY_HIGH, studyInstanceUid, seriesInstanceUid);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  int IsSeriesComplete(const char* seriesInstanceUid)
+  {
+    try
+    {
+      return GetResourcesLoader().IsSeriesComplete(seriesInstanceUid) ? 1 : 0;
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+    return 0;
+  }
+
+  EMSCRIPTEN_KEEPALIVE
+  int LoadSeriesInViewport(const char* canvas,
+                           const char* seriesInstanceUid)
+  {
+    try
+    {
+      std::unique_ptr<OrthancStone::SortedFrames> frames(new OrthancStone::SortedFrames);
+      
+      if (GetResourcesLoader().SortSeriesFrames(*frames, seriesInstanceUid))
+      {
+        GetViewport(canvas)->SetFrames(frames.release());
+        return 1;
+      }
+      else
+      {
+        return 0;
+      }
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+    return 0;
+  }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void AllViewportsUpdateSize(int fitContent)
+  {
+    try
+    {
+      for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        it->second->UpdateSize(fitContent != 0);
+      }
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void DecrementFrame(const char* canvas,
+                      int fitContent)
+  {
+    try
+    {
+      GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void IncrementFrame(const char* canvas,
+                      int fitContent)
+  {
+    try
+    {
+      GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }  
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void ShowReferenceLines(int show)
+  {
+    try
+    {
+      showReferenceLines_ = (show != 0);
+      UpdateReferenceLines();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }  
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void SetDefaultWindowing(const char* canvas)
+  {
+    try
+    {
+      GetViewport(canvas)->SetDefaultWindowing();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }  
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void SetWindowing(const char* canvas,
+                    int center,
+                    int width)
+  {
+    try
+    {
+      GetViewport(canvas)->SetWindowing(center, width);
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }  
+
+
+  EMSCRIPTEN_KEEPALIVE
+  void InvertContrast(const char* canvas)
+  {
+    try
+    {
+      GetViewport(canvas)->Invert();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+  }  
+}