changeset 1197:61b71ccac362 db-changes

integration mainline->db-changes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 23 Oct 2014 13:19:18 +0200
parents 669bb978d52e (current diff) 97089aa85b5f (diff)
children 1169528a9a5f
files NEWS
diffstat 26 files changed, 1106 insertions(+), 64 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.h	Thu Oct 23 13:18:26 2014 +0200
+++ b/Core/DicomFormat/DicomIntegerPixelAccessor.h	Thu Oct 23 13:19:18 2014 +0200
@@ -80,5 +80,10 @@
     {
       return pixelData_;
     }
+
+    size_t GetSize() const
+    {
+      return size_;
+    }
   };
 }
--- a/Core/HttpClient.cpp	Thu Oct 23 13:18:26 2014 +0200
+++ b/Core/HttpClient.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -109,6 +109,7 @@
     method_ = HttpMethod_Get;
     lastStatus_ = HttpStatus_200_Ok;
     isVerbose_ = false;
+    timeout_ = 0;
   }
 
 
@@ -171,6 +172,18 @@
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0));
 
+    // Set timeouts
+    if (timeout_ <= 0)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, 10));  /* default: 10 seconds */
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, 10));  /* default: 10 seconds */
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, timeout_));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, timeout_));
+    }
+
     if (credentials_.size() != 0)
     {
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, credentials_.c_str()));
--- a/Core/HttpClient.h	Thu Oct 23 13:18:26 2014 +0200
+++ b/Core/HttpClient.h	Thu Oct 23 13:19:18 2014 +0200
@@ -52,6 +52,7 @@
     HttpStatus lastStatus_;
     std::string postData_;
     bool isVerbose_;
+    long timeout_;
 
     void Setup();
 
@@ -89,6 +90,16 @@
       return method_;
     }
 
+    void SetTimeout(long seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    long GetTimeout() const
+    {
+      return timeout_;
+    }
+
     void SetPostData(const std::string& data)
     {
       postData_ = data;
--- a/Core/RestApi/RestApiOutput.cpp	Thu Oct 23 13:18:26 2014 +0200
+++ b/Core/RestApi/RestApiOutput.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -128,7 +128,8 @@
 
   void RestApiOutput::SignalError(HttpStatus status)
   {
-    if (status != HttpStatus_403_Forbidden &&
+    if (status != HttpStatus_400_BadRequest &&
+        status != HttpStatus_403_Forbidden &&
         status != HttpStatus_500_InternalServerError &&
         status != HttpStatus_415_UnsupportedMediaType)
     {
--- a/NEWS	Thu Oct 23 13:18:26 2014 +0200
+++ b/NEWS	Thu Oct 23 13:19:18 2014 +0200
@@ -1,8 +1,12 @@
 Pending changes in the mainline
 ===============================
 
-* Speed-up thanks to a new database schema
+* Major speed-up thanks to a new database schema
+* Download ZIP + DICOMDIR from Orthanc Explorer
 * Plugins can monitor changes through callbacks
+* Sample plugin framework to serve static resources
+* Fix issue 21 (Microsoft Visual Studio precompiled headers)
+* Fix issue 22 (Error decoding multi-frame instances)
 
 
 Version 0.8.4 (2014/09/19)
--- a/OrthancExplorer/explorer.html	Thu Oct 23 13:18:26 2014 +0200
+++ b/OrthancExplorer/explorer.html	Thu Oct 23 13:19:18 2014 +0200
@@ -105,6 +105,7 @@
                   <a href="#" id="patient-modified-from">Before modification</a>
                 </li>
                 <li data-icon="gear"><a href="#" id="patient-archive">Download ZIP</a></li>
+                <li data-icon="gear"><a href="#" id="patient-media">Download DICOMDIR</a></li>
               </ul>
             </div>
           </div>
@@ -151,6 +152,7 @@
                   <a href="#" id="study-modified-from">Before modification</a>
                 </li>
                 <li data-icon="gear"><a href="#" id="study-archive">Download ZIP</a></li>
+                <li data-icon="gear"><a href="#" id="study-media">Download DICOMDIR</a></li>
               </ul>
             </div>
           </div>
@@ -200,6 +202,7 @@
                 </li>
                 <li data-icon="search"><a href="#" id="series-preview">Preview this series</a></li>
                 <li data-icon="gear"><a href="#" id="series-archive">Download ZIP</a></li>
+                <li data-icon="gear"><a href="#" id="series-media">Download DICOMDIR</a></li>
               </ul>
             </div>
           </div>
--- a/OrthancExplorer/explorer.js	Thu Oct 23 13:18:26 2014 +0200
+++ b/OrthancExplorer/explorer.js	Thu Oct 23 13:19:18 2014 +0200
@@ -935,6 +935,24 @@
   window.location.href = '../series/' + $.mobile.pageData.uuid + '/archive';
 });
 
+
+$('#patient-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../patients/' + $.mobile.pageData.uuid + '/media';
+});
+
+$('#study-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../studies/' + $.mobile.pageData.uuid + '/media';
+});
+
+$('#series-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../series/' + $.mobile.pageData.uuid + '/media';
+});
+
+
+
 $('#protection').live('change', function(e) {
   var isProtected = e.target.value == "on";
   $.ajax({
--- a/OrthancServer/DicomProtocol/DicomServer.cpp	Thu Oct 23 13:18:26 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomServer.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -190,6 +190,11 @@
 
     LOG(INFO) << "DICOM server stopping";
 
+    if (server->isThreaded_)
+    {
+      server->bagOfDispatchers_.StopAll();
+    }
+
     /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */
     /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
     cond = ASC_dropNetwork(&net);
@@ -403,8 +408,6 @@
     {
       pimpl_->thread_.join();
     }
-
-    bagOfDispatchers_.StopAll();
   }
 
   bool DicomServer::IsMyAETitle(const std::string& aet) const
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Thu Oct 23 13:18:26 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -135,6 +135,8 @@
   struct DicomUserConnection::PImpl
   {
     // Connection state
+    uint32_t dimseTimeout_;
+    uint32_t acseTimeout_;
     T_ASC_Network* net_;
     T_ASC_Parameters* params_;
     T_ASC_Association* assoc_;
@@ -325,7 +327,7 @@
     DcmDataset* statusDetail = NULL;
     Check(DIMSE_storeUser(assoc_, presID, &req,
                           NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL,
-                          /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0,
+                          /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_,
                           &rsp, &statusDetail, NULL));
 
     if (statusDetail != NULL) 
@@ -466,7 +468,8 @@
     DcmDataset* statusDetail = NULL;
     OFCondition cond = DIMSE_findUser(pimpl_->assoc_, presID, &request, dataset.get(),
                                       FindCallback, &result,
-                                      /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0,
+                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                      /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
                                       &response, &statusDetail);
 
     if (statusDetail)
@@ -559,7 +562,8 @@
     DcmDataset* responseIdentifiers = NULL;
     OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset.get(),
                                       NULL, NULL,
-                                      /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0,
+                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                      /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
                                       pimpl_->net_, NULL, NULL,
                                       &response, &statusDetail, &responseIdentifiers);
 
@@ -616,6 +620,7 @@
     distantPort_ = 104;
     manufacturer_ = ModalityManufacturer_Generic;
 
+    SetTimeout(10); 
     pimpl_->net_ = NULL;
     pimpl_->params_ = NULL;
     pimpl_->assoc_ = NULL;
@@ -722,7 +727,7 @@
               << GetDistantHost() << ":" << GetDistantPort() 
               << " (manufacturer: " << EnumerationToString(GetDistantManufacturer()) << ")";
 
-    Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ 30, &pimpl_->net_));
+    Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_));
     Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
 
     // Set this application's title and the called application's title in the params
@@ -818,7 +823,8 @@
     CheckIsOpen();
     DIC_US status;
     Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, 
-                         /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0,
+                         /*opt_blockMode*/ DIMSE_BLOCKING, 
+                         /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
                          &status, NULL));
     return status == STATUS_Success;
   }
@@ -865,9 +871,29 @@
     Move(targetAet, map);
   }
 
-  void DicomUserConnection::SetConnectionTimeout(uint32_t seconds)
+
+  void DicomUserConnection::SetTimeout(uint32_t seconds)
   {
+    if (seconds <= 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
     dcmConnectionTimeout.set(seconds);
+    pimpl_->dimseTimeout_ = seconds;
+    pimpl_->acseTimeout_ = 10;
+  }
+
+
+  void DicomUserConnection::DisableTimeout()
+  {
+    /**
+     * Global timeout (seconds) for connecting to remote hosts.
+     * Default value is -1 which selects infinite timeout, i.e. blocking connect().
+     */
+    dcmConnectionTimeout.set(-1);
+    pimpl_->dimseTimeout_ = 0;
+    pimpl_->acseTimeout_ = 10;
   }
 
 
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h	Thu Oct 23 13:18:26 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.h	Thu Oct 23 13:19:18 2014 +0200
@@ -177,6 +177,8 @@
                       const std::string& seriesUid,
                       const std::string& instanceUid);
 
-    static void SetConnectionTimeout(uint32_t seconds);
+    void SetTimeout(uint32_t seconds);
+
+    void DisableTimeout();
   };
 }
--- a/OrthancServer/Internals/DicomImageDecoder.cpp	Thu Oct 23 13:18:26 2014 +0200
+++ b/OrthancServer/Internals/DicomImageDecoder.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -225,7 +225,6 @@
   private:
     std::string psmct_;
     std::auto_ptr<DicomIntegerPixelAccessor> slowAccessor_;
-    std::auto_ptr<ImageAccessor> fastAccessor_;
 
   public:
     void Setup(DcmDataset& dataset,
@@ -233,7 +232,6 @@
     {
       psmct_.clear();
       slowAccessor_.reset(NULL);
-      fastAccessor_.reset(NULL);
 
       // See also: http://support.dcmtk.org/wiki/dcmtk/howto/accessing-compressed-data
 
@@ -272,13 +270,6 @@
       }
 
       slowAccessor_->SetCurrentFrame(frame);
-
-
-      /**
-       * If possible, create a fast ImageAccessor to the image buffer.
-       **/
-
-      
     }
 
     unsigned int GetWidth() const
@@ -305,15 +296,10 @@
       return *slowAccessor_;
     }
 
-    bool HasFastAccessor() const
+    unsigned int GetSize() const
     {
-      return fastAccessor_.get() != NULL;
-    }
-
-    const ImageAccessor& GetFastAccessor() const
-    {
-      assert(HasFastAccessor());
-      return *fastAccessor_;
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetSize();
     }
   };
 
@@ -435,16 +421,22 @@
     {
       try
       {
-        ImageAccessor sourceImage;
-        sourceImage.AssignReadOnly(sourceFormat, 
-                                   info.GetWidth(), 
-                                   info.GetHeight(),
-                                   info.GetWidth() * GetBytesPerPixel(sourceFormat),
-                                   source.GetAccessor().GetPixelData());                                   
+        size_t frameSize = info.GetHeight() * info.GetWidth() * GetBytesPerPixel(sourceFormat);
+        if ((frame + 1) * frameSize <= source.GetSize())
+        {
+          const uint8_t* buffer = reinterpret_cast<const uint8_t*>(source.GetAccessor().GetPixelData());
 
-        ImageProcessing::Convert(targetAccessor, sourceImage);
-        ImageProcessing::ShiftRight(targetAccessor, info.GetShift());
-        fastVersionSuccess = true;
+          ImageAccessor sourceImage;
+          sourceImage.AssignReadOnly(sourceFormat, 
+                                     info.GetWidth(), 
+                                     info.GetHeight(),
+                                     info.GetWidth() * GetBytesPerPixel(sourceFormat),
+                                     buffer + frame * frameSize);
+
+          ImageProcessing::Convert(targetAccessor, sourceImage);
+          ImageProcessing::ShiftRight(targetAccessor, info.GetShift());
+          fastVersionSuccess = true;
+        }
       }
       catch (OrthancException&)
       {
@@ -452,7 +444,6 @@
       }
     }
 
-
     /**
      * Slow version : loop over the DICOM buffer, storing its value
      * into the target image.
--- a/OrthancServer/main.cpp	Thu Oct 23 13:18:26 2014 +0200
+++ b/OrthancServer/main.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -56,6 +56,8 @@
 using namespace Orthanc;
 
 
+#define ENABLE_PLUGINS  1
+
 
 class OrthancStoreRequestHandler : public IStoreRequestHandler
 {
@@ -509,20 +511,22 @@
     FilesystemHttpHandler staticResources("/app", ORTHANC_PATH "/OrthancExplorer");
 #endif
 
+#if ENABLE_PLUGINS == 1
     OrthancPlugins orthancPlugins(context);
     orthancPlugins.SetOrthancRestApi(restApi);
 
     PluginsManager pluginsManager;
     pluginsManager.RegisterServiceProvider(orthancPlugins);
     LoadPlugins(pluginsManager);
+    httpServer.RegisterHandler(orthancPlugins);
+    context.SetOrthancPlugins(orthancPlugins);
+#endif
 
-    httpServer.RegisterHandler(orthancPlugins);
     httpServer.RegisterHandler(staticResources);
     httpServer.RegisterHandler(restApi);
-    orthancPlugins.SetOrthancRestApi(restApi);
-    context.SetOrthancPlugins(orthancPlugins);
 
 
+#if ENABLE_PLUGINS == 1
     // Prepare the storage area
     if (orthancPlugins.HasStorageArea())
     {
@@ -530,6 +534,7 @@
       storage.reset(orthancPlugins.GetStorageArea());
     }
     else
+#endif
     {
       boost::filesystem::path storageDirectory = Configuration::InterpretStringParameterAsPath(storageDirectoryStr);
       LOG(WARNING) << "Storage directory: " << storageDirectory;
@@ -579,6 +584,9 @@
 
     // We're done
     LOG(WARNING) << "Orthanc is stopping";
+
+    dicomServer.Stop();
+    httpServer.Stop();
   }
 
   serverFactory.Done();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/CMakeLists.txt	Thu Oct 23 13:19:18 2014 +0200
@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(WebSkeleton)
+
+SET(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
+SET(RESOURCES_ROOT ${CMAKE_SOURCE_DIR}/StaticResources)
+
+include(Framework/Framework.cmake)
+
+include_directories(${CMAKE_SOURCE_DIR}/../../OrthancCPlugin/)
+
+add_library(WebSkeleton SHARED 
+  ${AUTOGENERATED_SOURCES}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/Configuration.h	Thu Oct 23 13:19:18 2014 +0200
@@ -0,0 +1,34 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
+ * Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#pragma once
+
+#define ORTHANC_PLUGIN_NAME  "web-skeleton"
+
+#define ORTHANC_PLUGIN_VERSION "1.0"
+
+#define ORTHANC_PLUGIN_WEB_ROOT  "/plugin-web-skeleton/"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Thu Oct 23 13:19:18 2014 +0200
@@ -0,0 +1,398 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
+# Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import sys
+import os
+import os.path
+import pprint
+import re
+
+UPCASE_CHECK = True
+ARGS = []
+for i in range(len(sys.argv)):
+    if not sys.argv[i].startswith('--'):
+        ARGS.append(sys.argv[i])
+    elif sys.argv[i].lower() == '--no-upcase-check':
+        UPCASE_CHECK = False
+
+if len(ARGS) < 2 or len(ARGS) % 2 != 0:
+    print ('Usage:')
+    print ('python %s [--no-upcase-check] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0])
+    exit(-1)
+
+TARGET_BASE_FILENAME = ARGS[1]
+SOURCES = ARGS[2:]
+
+try:
+    # Make sure the destination directory exists
+    os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..')))
+except:
+    pass
+
+
+#####################################################################
+## Read each resource file
+#####################################################################
+
+def CheckNoUpcase(s):
+    global UPCASE_CHECK
+    if (UPCASE_CHECK and
+        re.search('[A-Z]', s) != None):
+        raise Exception("Path in a directory with an upcase letter: %s" % s)
+
+resources = {}
+
+counter = 0
+i = 0
+while i < len(SOURCES):
+    resourceName = SOURCES[i].upper()
+    pathName = SOURCES[i + 1]
+
+    if not os.path.exists(pathName):
+        raise Exception("Non existing path: %s" % pathName)
+
+    if resourceName in resources:
+        raise Exception("Twice the same resource: " + resourceName)
+    
+    if os.path.isdir(pathName):
+        # The resource is a directory: Recursively explore its files
+        content = {}
+        for root, dirs, files in os.walk(pathName):
+            base = os.path.relpath(root, pathName)
+            for f in files:
+                if f.find('~') == -1:  # Ignore Emacs backup files
+                    if base == '.':
+                        r = f
+                    else:
+                        r = os.path.join(base, f)
+
+                    CheckNoUpcase(r)
+                    r = '/' + r.replace('\\', '/')
+                    if r in content:
+                        raise Exception("Twice the same filename (check case): " + r)
+
+                    content[r] = {
+                        'Filename' : os.path.join(root, f),
+                        'Index' : counter
+                        }
+                    counter += 1
+
+        resources[resourceName] = {
+            'Type' : 'Directory',
+            'Files' : content
+            }
+
+    elif os.path.isfile(pathName):
+        resources[resourceName] = {
+            'Type' : 'File',
+            'Index' : counter,
+            'Filename' : pathName
+            }
+        counter += 1
+
+    else:
+        raise Exception("Not a regular file, nor a directory: " + pathName)
+
+    i += 2
+
+#pprint.pprint(resources)
+
+
+#####################################################################
+## Write .h header
+#####################################################################
+
+header = open(TARGET_BASE_FILENAME + '.h', 'w')
+
+header.write("""
+#pragma once
+
+#include <string>
+#include <list>
+
+namespace Orthanc
+{
+  namespace EmbeddedResources
+  {
+    enum FileResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    enum DirectoryResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    const void* GetFileResourceBuffer(FileResourceId id);
+    size_t GetFileResourceSize(FileResourceId id);
+    void GetFileResource(std::string& result, FileResourceId id);
+
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path);
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path);
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path);
+
+    void ListResources(std::list<std::string>& result, DirectoryResourceId id);
+  }
+}
+""")
+header.close()
+
+
+
+#####################################################################
+## Write the resource content in the .cpp source
+#####################################################################
+
+PYTHON_MAJOR_VERSION = sys.version_info[0]
+
+def WriteResource(cpp, item):
+    cpp.write('    static const uint8_t resource%dBuffer[] = {' % item['Index'])
+
+    f = open(item['Filename'], "rb")
+    content = f.read()
+    f.close()
+
+    # http://stackoverflow.com/a/1035360
+    pos = 0
+    for b in content:
+        if PYTHON_MAJOR_VERSION == 2:
+            c = ord(b[0])
+        else:
+            c = b
+
+        if pos > 0:
+            cpp.write(', ')
+
+        if (pos % 16) == 0:
+            cpp.write('\n    ')
+
+        if c < 0:
+            raise Exception("Internal error")
+
+        cpp.write("0x%02x" % c)
+        pos += 1
+
+    cpp.write('  };\n')
+    cpp.write('    static const size_t resource%dSize = %d;\n' % (item['Index'], pos))
+
+
+cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
+
+print os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+
+cpp.write("""
+#include "%s.h"
+
+#include <stdint.h>
+#include <string.h>
+#include <stdexcept>
+
+namespace Orthanc
+{
+  namespace EmbeddedResources
+  {
+""" % (os.path.basename(TARGET_BASE_FILENAME)))
+
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        WriteResource(cpp, resources[name])
+    else:
+        for f in resources[name]['Files']:
+            WriteResource(cpp, resources[name]['Files'][f])
+
+
+
+#####################################################################
+## Write the accessors to the file resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetFileResourceBuffer(FileResourceId id)
+    {
+      switch (id)
+      {
+""")
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dBuffer;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+
+    size_t GetFileResourceSize(FileResourceId id)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dSize;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+#####################################################################
+## Write the accessors to the directory resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dBuffer;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw std::runtime_error("Unknown path in a directory resource");\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dSize;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw std::runtime_error("Unknown path in a directory resource");\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+
+#####################################################################
+## List the resources in a directory
+#####################################################################
+
+cpp.write("""
+    void ListResources(std::list<std::string>& result, DirectoryResourceId id)
+    {
+      result.clear();
+
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        for path in sorted(resources[name]['Files']):
+            cpp.write('        result.push_back("%s");\n' % path)
+        cpp.write('        break;\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+
+#####################################################################
+## Write the convenience wrappers in .cpp
+#####################################################################
+
+cpp.write("""
+    void GetFileResource(std::string& result, FileResourceId id)
+    {
+      size_t size = GetFileResourceSize(id);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetFileResourceBuffer(id), size);
+    }
+
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path)
+    {
+      size_t size = GetDirectoryResourceSize(id, path);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size);
+    }
+  }
+}
+""")
+cpp.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Thu Oct 23 13:19:18 2014 +0200
@@ -0,0 +1,76 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
+# Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+if (${CMAKE_COMPILER_IS_GNUCXX})
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Werror -Wno-unused-function")
+endif()
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  # Linking with "pthread" is necessary, otherwise the software might crash
+  # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17
+  link_libraries(pthread dl)
+endif()
+
+if (STANDALONE_BUILD)
+  add_definitions(-DORTHANC_PLUGIN_STANDALONE=1)
+
+  set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED")
+  set(AUTOGENERATED_SOURCES "${AUTOGENERATED_DIR}/EmbeddedResources.cpp")
+
+  file(MAKE_DIRECTORY ${AUTOGENERATED_DIR})
+  include_directories(${AUTOGENERATED_DIR})
+
+  set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources")
+  add_custom_command(
+    OUTPUT
+    "${AUTOGENERATED_DIR}/EmbeddedResources.h"
+    "${AUTOGENERATED_DIR}/EmbeddedResources.cpp"
+    COMMAND 
+    python
+    "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
+    "${AUTOGENERATED_DIR}/EmbeddedResources"
+    STATIC_RESOURCES
+    ${RESOURCES_ROOT}
+    DEPENDS
+    "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
+    ${STATIC_RESOURCES}
+    )
+
+else()
+  add_definitions(
+    -DORTHANC_PLUGIN_STANDALONE=0
+    -DORTHANC_PLUGIN_RESOURCES_ROOT="${RESOURCES_ROOT}"
+    )
+endif()
+
+
+list(APPEND AUTOGENERATED_SOURCES
+  "${CMAKE_CURRENT_SOURCE_DIR}/Framework/Plugin.cpp"
+  )
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Thu Oct 23 13:19:18 2014 +0200
@@ -0,0 +1,276 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
+ * Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#include "../Configuration.h"
+
+#include <OrthancCPlugin.h>
+#include <string>
+#include <stdexcept>
+#include <algorithm>
+#include <sys/stat.h>
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+// This is an auto-generated file for standalone builds
+#include <EmbeddedResources.h>
+#endif
+
+static OrthancPluginContext* context = NULL;
+
+
+static const char* GetMimeType(const std::string& path)
+{
+  size_t dot = path.find_last_of('.');
+
+  std::string extension = (dot == std::string::npos) ? "" : path.substr(dot);
+  std::transform(extension.begin(), extension.end(), extension.begin(), tolower);
+
+  if (extension == ".html")
+  {
+    return "text/html";
+  }
+  else if (extension == ".css")
+  {
+    return "text/css";
+  }
+  else if (extension == ".js")
+  {
+    return "application/javascript";
+  }
+  else if (extension == ".gif")
+  {
+    return "image/gif";
+  }
+  else if (extension == ".json")
+  {
+    return "application/json";
+  }
+  else if (extension == ".xml")
+  {
+    return "application/xml";
+  }
+  else if (extension == ".png")
+  {
+    return "image/png";
+  }
+  else if (extension == ".jpg" || extension == ".jpeg")
+  {
+    return "image/jpeg";
+  }
+  else
+  {
+    std::string s = "Unknown MIME type for extension: " + extension;
+    OrthancPluginLogWarning(context, s.c_str());
+    return "application/octet-stream";
+  }
+}
+
+
+static bool ReadFile(std::string& content,
+                     const std::string& path)
+{
+  struct stat s;
+  if (stat(path.c_str(), &s) != 0 ||
+      !(s.st_mode & S_IFREG))
+  {
+    // Either the path does not exist, or it is not a regular file
+    return false;
+  }
+
+  FILE* fp = fopen(path.c_str(), "rb");
+  if (fp == NULL)
+  {
+    return false;
+  }
+
+  long size;
+
+  if (fseek(fp, 0, SEEK_END) == -1 ||
+      (size = ftell(fp)) < 0)
+  {
+    fclose(fp);
+    return false;
+  }
+
+  content.resize(size);
+      
+  if (fseek(fp, 0, SEEK_SET) == -1)
+  {
+    fclose(fp);
+    return false;
+  }
+
+  bool ok = true;
+
+  if (size > 0 &&
+      fread(&content[0], size, 1, fp) != 1)
+  {
+    ok = false;
+  }
+
+  fclose(fp);
+
+  return ok;
+}
+
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+static int32_t ServeStaticResource(OrthancPluginRestOutput* output,
+                                   const char* url,
+                                   const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+    return 0;
+  }
+
+  std::string path = "/" + std::string(request->groups[0]);
+  const char* mime = GetMimeType(path);
+
+  try
+  {
+    std::string s;
+    Orthanc::EmbeddedResources::GetDirectoryResource
+      (s, Orthanc::EmbeddedResources::STATIC_RESOURCES, path.c_str());
+
+    const char* resource = s.size() ? s.c_str() : NULL;
+    OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
+
+    return 0;
+  }
+  catch (std::runtime_error&)
+  {
+    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
+    OrthancPluginLogError(context, s.c_str());
+    OrthancPluginSendHttpStatusCode(context, output, 404);
+    return 0;
+  }
+}
+#endif
+
+
+#if ORTHANC_PLUGIN_STANDALONE == 0
+static int32_t ServeFolder(OrthancPluginRestOutput* output,
+                           const char* url,
+                           const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+    return 0;
+  }
+
+  std::string path = ORTHANC_PLUGIN_RESOURCES_ROOT "/" + std::string(request->groups[0]);
+  const char* mime = GetMimeType(path);
+
+  std::string s;
+  if (ReadFile(s, path))
+  {
+    const char* resource = s.size() ? s.c_str() : NULL;
+    OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
+
+    return 0;
+  }
+  else
+  {
+    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
+    OrthancPluginLogError(context, s.c_str());
+    OrthancPluginSendHttpStatusCode(context, output, 404);
+    return 0;
+  }
+}
+#endif
+
+
+static int32_t RedirectRoot(OrthancPluginRestOutput* output,
+                            const char* url,
+                            const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    OrthancPluginRedirect(context, output, ORTHANC_PLUGIN_WEB_ROOT "index.html");
+  }
+
+  return 0;
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context = c;
+    
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      char info[256];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              c->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context, info);
+      return -1;
+    }
+
+    /* Register the callbacks */
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+    OrthancPluginLogInfo(context, "Serving static resources (standalone build)");
+    OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeStaticResource);
+#else
+    OrthancPluginLogInfo(context, "Serving resources from folder: " ORTHANC_PLUGIN_RESOURCES_ROOT);
+    OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeFolder);
+#endif
+
+    OrthancPluginRegisterRestCallback(context, "/", RedirectRoot);
+ 
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return ORTHANC_PLUGIN_NAME;
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/NOTES.txt	Thu Oct 23 13:19:18 2014 +0200
@@ -0,0 +1,7 @@
+This is a sample Orthanc plugin that serves static resources (HTML,
+JavaScript, CSS, images...).
+
+The resources to serve must be stored in the folder "StaticResources".
+
+The folder "Framework" contains a reusable framework for any plugin
+whose sole objective is to serve static resources.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/StaticResources/index.html	Thu Oct 23 13:19:18 2014 +0200
@@ -0,0 +1,16 @@
+<!doctype html>
+
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Web Skeleton</title>
+  </head>
+
+  <body>
+    <h1>Web Skeleton</h1>
+    <p>
+      This is a sample skeleton for Orthanc showing how to create a
+      plugin that serves static HTML resources.
+    </p>
+  </body>
+</html>
--- a/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Thu Oct 23 13:18:26 2014 +0200
+++ b/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Thu Oct 23 13:19:18 2014 +0200
@@ -1,6 +1,6 @@
 macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources)
   get_filename_component(PrecompiledBasename ${PrecompiledHeaders} NAME_WE)
-  set(PrecompiledBinary "${CMAKE_CURRENT_BINARY_DIR}/${PrecompiledBasename}.pch")
+  set(PrecompiledBinary "$(IntDir)/${PrecompiledBasename}.pch")
 
   set_source_files_properties(${PrecompiledSource}
     PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Python/AutoClassify.py	Thu Oct 23 13:19:18 2014 +0200
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
+# Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import argparse
+import time
+import os
+import os.path
+import sys
+import RestToolbox
+
+parser = argparse.ArgumentParser(
+    description = 'Automated classification of DICOM files from Orthanc.',
+    formatter_class = argparse.ArgumentDefaultsHelpFormatter)
+
+parser.add_argument('--host', default = 'localhost',
+                    help = 'The host address that runs Orthanc')
+parser.add_argument('--port', type = int, default = '8042',
+                    help = 'The port number to which Orthanc is listening for the REST API')
+parser.add_argument('--target', default = 'OrthancFiles',
+                    help = 'The target directory where to store the DICOM files')
+parser.add_argument('--all', action = 'store_true',
+                    help = 'Replay the entire history on startup (disabled by default)')
+parser.set_defaults(all = False)
+parser.add_argument('--remove', action = 'store_true',
+                    help = 'Remove DICOM files from Orthanc once classified (disabled by default)')
+parser.set_defaults(remove = False)
+
+
+def FixPath(p):
+    return p.encode('ascii', 'ignore').strip().decode()
+
+def GetTag(resource, tag):
+    if ('MainDicomTags' in resource and
+        tag in resource['MainDicomTags']):
+        return resource['MainDicomTags'][tag]
+    else:
+        return 'No' + tag
+
+def ClassifyInstance(instanceId):
+    # Extract the patient, study, series and instance information
+    instance = RestToolbox.DoGet('%s/instances/%s' % (URL, instanceId))
+    series = RestToolbox.DoGet('%s/series/%s' % (URL, instance['ParentSeries']))
+    study = RestToolbox.DoGet('%s/studies/%s' % (URL, series['ParentStudy']))
+    patient = RestToolbox.DoGet('%s/patients/%s' % (URL, study['ParentPatient']))
+
+    # Construct a target path
+    a = '%s - %s' % (GetTag(patient, 'PatientID'),
+                     GetTag(patient, 'PatientName'))
+    b = GetTag(study, 'StudyDescription')
+    c = '%s - %s' % (GetTag(series, 'Modality'),
+                     GetTag(series, 'SeriesDescription'))
+    d = '%s.dcm' % GetTag(instance, 'SOPInstanceUID')
+    
+    p = os.path.join(args.target, FixPath(a), FixPath(b), FixPath(c))
+    f = os.path.join(p, FixPath(d))
+
+    # Copy the DICOM file to the target path
+    print('Writing new DICOM file: %s' % f)
+    
+    try:
+        os.makedirs(p)
+    except:
+        # Already existing directory, ignore the error
+        pass
+    
+    dcm = RestToolbox.DoGet('%s/instances/%s/file' % (URL, instanceId))
+    with open(f, 'wb') as g:
+        g.write(dcm)
+
+
+# Parse the arguments
+args = parser.parse_args()
+URL = 'http://%s:%d' % (args.host, args.port)
+print('Connecting to Orthanc on address: %s' % URL)
+
+# Compute the starting point for the changes loop
+if args.all:
+    current = 0
+else:
+    current = RestToolbox.DoGet(URL + '/changes?last')['Last']
+
+# Polling loop using the 'changes' API of Orthanc, waiting for the
+# incoming of new DICOM files
+while True:
+    r = RestToolbox.DoGet(URL + '/changes', {
+            'since' : current,
+            'limit' : 4   # Retrieve at most 4 changes at once
+            })
+
+    for change in r['Changes']:
+        # We are only interested interested in the arrival of new instances
+        if change['ChangeType'] == 'NewInstance':
+            try:
+                ClassifyInstance(change['ID'])
+            except:
+                print('Unable to write instance %s to the disk' % change['ID'])
+
+            # If requested, remove the instance once it has been copied
+            if args.remove:
+                RestToolbox.DoDelete('%s/instances/%s' % (URL, change['ID']))
+
+    current = r['Last']
+
+    if r['Done']:
+        print('Everything has been processed: Waiting...')
+        time.sleep(1)
--- a/Resources/Samples/Python/ChangesLoop.py	Thu Oct 23 13:18:26 2014 +0200
+++ b/Resources/Samples/Python/ChangesLoop.py	Thu Oct 23 13:19:18 2014 +0200
@@ -54,7 +54,7 @@
     # Remove the possible trailing characters due to DICOM padding
     patientName = patientName.strip()
 
-    print 'New instance received for patient "%s": "%s"' % (patientName, path)
+    print('New instance received for patient "%s": "%s"' % (patientName, path))
 
 
 
@@ -82,5 +82,5 @@
     current = r['Last']
 
     if r['Done']:
-        print "Everything has been processed: Waiting..."
+        print('Everything has been processed: Waiting...')
         time.sleep(1)
--- a/Resources/Samples/Python/DownloadAnonymized.py	Thu Oct 23 13:18:26 2014 +0200
+++ b/Resources/Samples/Python/DownloadAnonymized.py	Thu Oct 23 13:19:18 2014 +0200
@@ -42,7 +42,7 @@
     if name.startswith('anonymized'):
 
         # Trigger the download
-        print 'Downloading %s' % name
+        print('Downloading %s' % name)
         zipContent = RestToolbox.DoGet('%s/patients/%s/archive' % (URL, patient))
         f = open(os.path.join('/tmp', name + '.zip'), 'wb')
         f.write(zipContent)
--- a/Resources/Samples/Python/HighPerformanceAutoRouting.py	Thu Oct 23 13:18:26 2014 +0200
+++ b/Resources/Samples/Python/HighPerformanceAutoRouting.py	Thu Oct 23 13:19:18 2014 +0200
@@ -107,7 +107,7 @@
                 break
 
         if len(instances) > 0:
-            print 'Sending a packet of %d instances' % len(instances)
+            print('Sending a packet of %d instances' % len(instances))
             start = time.time()
 
             # Send all the instances with a single DICOM connexion
@@ -124,7 +124,7 @@
             RestToolbox.DoDelete('%s/exports' % URL)
 
             end = time.time()
-            print 'The packet of %d instances has been sent in %d seconds' % (len(instances), end - start)
+            print('The packet of %d instances has been sent in %d seconds' % (len(instances), end - start))
 
 
 #
@@ -133,7 +133,7 @@
 
 def PrintProgress(queue):
     while True:
-        print 'Current queue size: %d' % (queue.qsize())
+        print('Current queue size: %d' % (queue.qsize()))
         time.sleep(1)
 
 
--- a/Resources/Samples/Python/RestToolbox.py	Thu Oct 23 13:18:26 2014 +0200
+++ b/Resources/Samples/Python/RestToolbox.py	Thu Oct 23 13:19:18 2014 +0200
@@ -18,10 +18,27 @@
 
 import httplib2
 import json
-from urllib import urlencode
+import sys
+
+if (sys.version_info >= (3, 0)):
+    from urllib.parse import urlencode
+else:
+    from urllib import urlencode
+
 
 _credentials = None
 
+
+def _DecodeJson(s):
+    try:
+        if (sys.version_info >= (3, 0)):
+            return json.loads(s.decode())
+        else:
+            return json.loads(s)
+    except:
+        return s
+
+
 def SetCredentials(username, password):
     global _credentials
     _credentials = (username, password)
@@ -42,12 +59,9 @@
     if not (resp.status in [ 200 ]):
         raise Exception(resp.status)
     elif not interpretAsJson:
-        return content
+        return content.decode()
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 
 def _DoPutOrPost(uri, method, data, contentType):
@@ -72,10 +86,7 @@
     if not (resp.status in [ 200, 302 ]):
         raise Exception(resp.status)
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 
 def DoDelete(uri):
@@ -86,10 +97,7 @@
     if not (resp.status in [ 200 ]):
         raise Exception(resp.status)
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 
 def DoPut(uri, data = {}, contentType = ''):
--- a/THANKS	Thu Oct 23 13:18:26 2014 +0200
+++ b/THANKS	Thu Oct 23 13:19:18 2014 +0200
@@ -23,8 +23,12 @@
 * Marek Swiecicki <mswiecicki@archimedic.pl>, for various suggestions
   and sample DICOM files.
 * Chris Hafey <chafey@gmail.com>, for suggesting many features and
-  improvements.
-* Manabu Tokunaga <manabu@lury.net>, for the Windows service and installer.
+  improvements, for a Windows service+installer with .NET/NSIS.
+* Manabu Tokunaga <manabu@lury.net>, for a Windows service with .NET.
+* Vincent Kersten <vincent1234567@gmail.com>, for DICOMDIR in the GUI.
+* Emsy Chan <emlscs@yahoo.com>, for various contributions
+  and sample DICOM files.
+
 
 Thanks also to all the contributors active in our Google Group:
 https://groups.google.com/forum/#!forum/orthanc-users