changeset 2308:1bdc4cc68171 issue-46-anonymization

integration mainline->issue-46-anonymization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 12 Jul 2017 09:12:12 +0200
parents c6defdc4c611 (current diff) 06ff7e86638a (diff)
children 4dc313b9a20a
files
diffstat 39 files changed, 589 insertions(+), 191 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomFormat/DicomImageInformation.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Core/DicomFormat/DicomImageInformation.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -200,13 +200,6 @@
       throw OrthancException(ErrorCode_NotImplemented);
     }
 
-    if (bitsAllocated_ > 32 ||
-        bitsStored_ >= 32)
-    {
-      // Not available, as the accessor internally uses int32_t values
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
     if (samplesPerPixel_ == 0)
     {
       throw OrthancException(ErrorCode_NotImplemented);
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Core/DicomFormat/DicomIntegerPixelAccessor.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -54,6 +54,13 @@
     pixelData_(pixelData),
     size_(size)
   {
+    if (information_.GetBitsAllocated() > 32 ||
+        information_.GetBitsStored() >= 32)
+    {
+      // Not available, as the accessor internally uses int32_t values
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
     frame_ = 0;
     frameOffset_ = information_.GetFrameSize();
 
--- a/Core/Enumerations.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Core/Enumerations.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -636,10 +636,10 @@
         return "RGB";
 
       case PhotometricInterpretation_Monochrome1:
-        return "Monochrome1";
+        return "MONOCHROME1";
 
       case PhotometricInterpretation_Monochrome2:
-        return "Monochrome2";
+        return "MONOCHROME2";
 
       case PhotometricInterpretation_ARGB:
         return "ARGB";
@@ -651,25 +651,25 @@
         return "HSV";
 
       case PhotometricInterpretation_Palette:
-        return "Palette color";
+        return "PALETTE COLOR";
 
       case PhotometricInterpretation_YBRFull:
-        return "YBR full";
+        return "YBR_FULL";
 
       case PhotometricInterpretation_YBRFull422:
-        return "YBR full 422";
+        return "YBR_FULL_422";
 
       case PhotometricInterpretation_YBRPartial420:
-        return "YBR partial 420"; 
+        return "YBR_PARTIAL_420"; 
 
       case PhotometricInterpretation_YBRPartial422:
-        return "YBR partial 422"; 
+        return "YBR_PARTIAL_422"; 
 
       case PhotometricInterpretation_YBR_ICT:
-        return "YBR ICT"; 
+        return "YBR_ICT"; 
 
       case PhotometricInterpretation_YBR_RCT:
-        return "YBR RCT"; 
+        return "YBR_RCT"; 
 
       case PhotometricInterpretation_Unknown:
         return "Unknown";
@@ -1053,6 +1053,80 @@
   }
 
 
+  PhotometricInterpretation StringToPhotometricInterpretation(const char* value)
+  {
+    // http://dicom.nema.org/medical/dicom/2017a/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2
+    std::string s(value);
+
+    if (s == "MONOCHROME1")
+    {
+      return PhotometricInterpretation_Monochrome1;
+    }
+    
+    if (s == "MONOCHROME2")
+    {
+      return PhotometricInterpretation_Monochrome2;
+    }
+
+    if (s == "PALETTE COLOR")
+    {
+      return PhotometricInterpretation_Palette;
+    }
+    
+    if (s == "RGB")
+    {
+      return PhotometricInterpretation_RGB;
+    }
+    
+    if (s == "HSV")
+    {
+      return PhotometricInterpretation_HSV;
+    }
+    
+    if (s == "ARGB")
+    {
+      return PhotometricInterpretation_ARGB;
+    }    
+
+    if (s == "CMYK")
+    {
+      return PhotometricInterpretation_CMYK;
+    }    
+
+    if (s == "YBR_FULL")
+    {
+      return PhotometricInterpretation_YBRFull;
+    }
+    
+    if (s == "YBR_FULL_422")
+    {
+      return PhotometricInterpretation_YBRFull422;
+    }
+    
+    if (s == "YBR_PARTIAL_422")
+    {
+      return PhotometricInterpretation_YBRPartial422;
+    }
+    
+    if (s == "YBR_PARTIAL_420")
+    {
+      return PhotometricInterpretation_YBRPartial420;
+    }
+    
+    if (s == "YBR_ICT")
+    {
+      return PhotometricInterpretation_YBR_ICT;
+    }
+    
+    if (s == "YBR_RCT")
+    {
+      return PhotometricInterpretation_YBR_RCT;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+  
+
   unsigned int GetBytesPerPixel(PixelFormat format)
   {
     switch (format)
--- a/Core/Enumerations.h	Wed May 10 22:40:59 2017 +0200
+++ b/Core/Enumerations.h	Wed Jul 12 09:12:12 2017 +0200
@@ -524,6 +524,8 @@
   ValueRepresentation StringToValueRepresentation(const std::string& vr,
                                                   bool throwIfUnsupported);
 
+  PhotometricInterpretation StringToPhotometricInterpretation(const char* value);
+  
   unsigned int GetBytesPerPixel(PixelFormat format);
 
   bool GetDicomEncoding(Encoding& encoding,
--- a/Core/Images/ImageProcessing.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Core/Images/ImageProcessing.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -772,4 +772,29 @@
         throw OrthancException(ErrorCode_NotImplemented);
     }
   }
+
+
+  void ImageProcessing::Invert(ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+      {
+        for (unsigned int y = 0; y < image.GetHeight(); y++)
+        {
+          uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+
+          for (unsigned int x = 0; x < image.GetWidth(); x++, p++)
+          {
+            *p = 255 - (*p);
+          }
+        }
+        
+        return;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }   
+  }
 }
--- a/Core/Images/ImageProcessing.h	Wed May 10 22:40:59 2017 +0200
+++ b/Core/Images/ImageProcessing.h	Wed Jul 12 09:12:12 2017 +0200
@@ -73,5 +73,7 @@
     static void ShiftScale(ImageAccessor& image,
                            float offset,
                            float scaling);
+
+    static void Invert(ImageAccessor& image);
   };
 }
--- a/Core/MultiThreading/RunnableWorkersPool.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Core/MultiThreading/RunnableWorkersPool.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -77,6 +77,10 @@
           {
             LOG(ERROR) << "Not enough memory to handle some runnable object";
           }
+          catch (std::exception& e)
+          {
+            LOG(ERROR) << "std::exception while handling some runnable object: " << e.what();
+          }
           catch (...)
           {
             LOG(ERROR) << "Native exception while handling some runnable object";
--- a/Core/SQLite/Connection.h	Wed May 10 22:40:59 2017 +0200
+++ b/Core/SQLite/Connection.h	Wed Jul 12 09:12:12 2017 +0200
@@ -39,13 +39,11 @@
 
 #include "Statement.h"
 #include "IScalarFunction.h"
+#include "SQLiteTypes.h"
 
 #include <string>
 #include <map>
 
-struct sqlite3;
-struct sqlite3_stmt;
-
 #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__FILE__, __LINE__)
 
 namespace Orthanc
--- a/Core/SQLite/FunctionContext.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Core/SQLite/FunctionContext.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -49,7 +49,7 @@
   {
     FunctionContext::FunctionContext(struct sqlite3_context* context,
                                      int argc,
-                                     struct ::Mem** argv)
+                                     Internals::SQLiteValue** argv)
     {
       assert(context != NULL);
       assert(argc >= 0);
--- a/Core/SQLite/FunctionContext.h	Wed May 10 22:40:59 2017 +0200
+++ b/Core/SQLite/FunctionContext.h	Wed Jul 12 09:12:12 2017 +0200
@@ -36,9 +36,6 @@
 
 #include "Statement.h"
 
-struct sqlite3_context;
-struct Mem;  // This corresponds to the opaque type "sqlite3_value"
- 
 namespace Orthanc
 {
   namespace SQLite
@@ -50,14 +47,14 @@
     private:
       struct sqlite3_context* context_;
       unsigned int argc_;
-      struct ::Mem** argv_;
+      Internals::SQLiteValue** argv_;
 
       void CheckIndex(unsigned int index) const;
 
     public:
       FunctionContext(struct sqlite3_context* context,
                       int argc,
-                      struct ::Mem** argv);
+                      Internals::SQLiteValue** argv);
 
       ColumnType GetColumnType(unsigned int index) const;
  
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SQLite/SQLiteTypes.h	Wed Jul 12 09:12:12 2017 +0200
@@ -0,0 +1,73 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *    * Neither the name of the CHU of Liege, nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+
+#pragma once
+
+struct sqlite3;
+struct sqlite3_context;
+struct sqlite3_stmt;
+
+#if !defined(ORTHANC_SQLITE_VERSION)
+#error  Please define macro ORTHANC_SQLITE_VERSION
+#endif
+
+
+/**
+ * "sqlite3_value" is defined as:
+ * - "typedef struct Mem sqlite3_value;" up to SQLite <= 3.18.2
+ * - "typedef struct sqlite3_value sqlite3_value;" since SQLite >= 3.19.0.
+ * We create our own copy of this typedef to get around this API incompatibility.
+ * https://github.com/mackyle/sqlite/commit/db1d90df06a78264775a14d22c3361eb5b42be17
+ **/
+      
+#if ORTHANC_SQLITE_VERSION < 3019000
+struct Mem;
+#else
+struct sqlite3_value;
+#endif
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    namespace Internals
+    {
+#if ORTHANC_SQLITE_VERSION < 3019000
+      typedef struct ::Mem  SQLiteValue;
+#else
+      typedef struct ::sqlite3_value  SQLiteValue;
+#endif
+    }
+  }
+}
--- a/Core/SQLite/Statement.h	Wed May 10 22:40:59 2017 +0200
+++ b/Core/SQLite/Statement.h	Wed Jul 12 09:12:12 2017 +0200
@@ -49,8 +49,6 @@
 #include <gtest/gtest_prod.h>
 #endif
 
-struct sqlite3_stmt;
-
 
 namespace Orthanc
 {
--- a/Core/SQLite/StatementReference.h	Wed May 10 22:40:59 2017 +0200
+++ b/Core/SQLite/StatementReference.h	Wed Jul 12 09:12:12 2017 +0200
@@ -38,13 +38,12 @@
 #pragma once
 
 #include "NonCopyable.h"
+#include "SQLiteTypes.h"
 
 #include <stdint.h>
 #include <cassert>
 #include <stdlib.h>
 
-struct sqlite3;
-struct sqlite3_stmt;
 
 namespace Orthanc
 {
--- a/NEWS	Wed May 10 22:40:59 2017 +0200
+++ b/NEWS	Wed Jul 12 09:12:12 2017 +0200
@@ -1,6 +1,11 @@
 Pending changes in the mainline
 ===============================
 
+REST API
+--------
+
+* Argument "Since" in URI "/tools/find" (related to issue 53)
+
 Plugins
 -------
 
@@ -11,10 +16,23 @@
 
 * Added HTTP headers support for Lua HttpPost/HttpGet/HttpPut/HttpDelete
 
+Orthanc Explorer
+----------------
+
+* Query/retrieve: Added button for "DR" modality
+
 Maintenance
 -----------
 
+* Ability to retrieve raw frames encoded as unsigned 32-bits integers
+* Fix issue 29 (more consistent handling of the "--upgrade" argument)
 * Fix issue 35 (AET name is not transferred to Orthanc using DCMTK 3.6.0)
+* Fix issue 44 (Bad interpretation of photometric interpretation MONOCHROME1)
+* Fix issue 45 (crash when providing a folder to "--config" command-line option)
+* Fix issue 49 (Worklists: accentuated characters are removed from C-Find responses)
+* Fix Debian #865606 (orthanc FTBFS with libdcmtk-dev 3.6.1~20170228-2)
+* Fix XSS inside DICOM in Orthanc Explorer (as reported by Victor Pasnkel, Morphus Labs)
+* Upgrade forthcoming DCMTK 3.6.1 to snapshot 20170228
 
 
 Version 1.2.0 (2016/12/13)
--- a/OrthancExplorer/explorer.html	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancExplorer/explorer.html	Wed Jul 12 09:12:12 2017 +0200
@@ -349,6 +349,7 @@
 	        <input type="checkbox" name="PT" id="qr-pt" class="custom" /> <label for="qr-pt">PT</label>
 	        <input type="checkbox" name="US" id="qr-us" class="custom" /> <label for="qr-us">US</label>
 	        <input type="checkbox" name="XA" id="qr-xa" class="custom" /> <label for="qr-xa">XA</label>
+	        <input type="checkbox" name="DR" id="qr-dr" class="custom" /> <label for="qr-dr">DR</label>
 	      </fieldset>
             </div>
           </div>
--- a/OrthancExplorer/explorer.js	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancExplorer/explorer.js	Wed Jul 12 09:12:12 2017 +0200
@@ -17,19 +17,6 @@
 var currentUuid = '';
 
 
-// http://stackoverflow.com/a/4673436
-String.prototype.format = function() {
-  var args = arguments;
-  return this.replace(/{(\d+)}/g, function(match, number) { 
-    /*return typeof args[number] != 'undefined'
-      ? args[number]
-      : match;*/
-
-    return args[number];
-  });
-};
-
-
 function DeepCopy(obj)
 {
   return jQuery.extend(true, {}, obj);
@@ -209,29 +196,34 @@
 }
 
 
-function CompleteFormatting(s, link, isReverse)
+function CompleteFormatting(node, link, isReverse, count)
 {
+  if (count != null)
+  {
+    node = node.add($('<span>')
+                    .addClass('ui-li-count')
+                    .text(count));
+  }
+  
   if (link != null)
   {
-    s = 'href="' + link + '">' + s + '</a>';
-    
+    node = $('<a>').attr('href', link).append(node);
+
     if (isReverse)
-      s = 'data-direction="reverse" '+ s;
-
-    s = '<a ' + s;
+      node.attr('data-direction', 'reverse')
   }
 
+  node = $('<li>').append(node);
+
   if (isReverse)
-    return '<li data-icon="back">' + s + '</li>';
-  else
-    return '<li>' + s + '</li>';
+    node.attr('data-icon', 'back');
+
+  return node;
 }
 
 
-function FormatMainDicomTags(tags, tagsToIgnore)
+function FormatMainDicomTags(target, tags, tagsToIgnore)
 {
-  var s = '';
-
   for (var i in tags)
   {
     if (tagsToIgnore.indexOf(i) == -1)
@@ -250,47 +242,38 @@
         v = SplitLongUid(v);
       }
       
-
-      s += ('<p>{0}: <strong>{1}</strong></p>').format(i, v);
+      target.append($('<p>')
+                    .text(i + ': ')
+                    .append($('<strong>').text(v)));
     }
   }
-
-  return s;
 }
 
 
 function FormatPatient(patient, link, isReverse)
 {
-  var s = ('<h3>{0}</h3>{1}' + 
-           '<span class="ui-li-count">{2}</span>'
-          ).format
-  (patient.MainDicomTags.PatientName,
-   FormatMainDicomTags(patient.MainDicomTags, [ 
-     "PatientName"
-     /*"OtherPatientIDs" */
-   ]),
-   patient.Studies.length
-  );
+  var node = $('<div>').append($('<h3>').text(patient.MainDicomTags.PatientName));
 
-  return CompleteFormatting(s, link, isReverse);
+  FormatMainDicomTags(node, patient.MainDicomTags, [ 
+    "PatientName"
+    // "OtherPatientIDs"
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse, patient.Studies.length);
 }
 
 
 
 function FormatStudy(study, link, isReverse)
 {
-  var s = ('<h3>{0}</h3>{1}' +
-           '<span class="ui-li-count">{2}</span>'
-           ).format
-  (study.MainDicomTags.StudyDescription,
-   FormatMainDicomTags(study.MainDicomTags, [
+  var node = $('<div>').append($('<h3>').text(study.MainDicomTags.StudyDescription));
+
+  FormatMainDicomTags(node, study.MainDicomTags, [ 
      "StudyDescription", 
      "StudyTime" 
-   ]),
-   study.Series.length
-  );
-
-  return CompleteFormatting(s, link, isReverse);
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse, study.Series.length);
 }
 
 
@@ -307,41 +290,39 @@
   {
     c = series.Instances.length + '/' + series.ExpectedNumberOfInstances;
   }
+  
+  var node = $('<div>')
+      .append($('<h3>').text(series.MainDicomTags.SeriesDescription))
+      .append($('<p>').append($('<em>')
+                           .text('Status: ')
+                           .append($('<strong>').text(series.Status))));
 
-  var s = ('<h3>{0}</h3>' +
-           '<p><em>Status: <strong>{1}</strong></em></p>{2}' +
-           '<span class="ui-li-count">{3}</span>').format
-  (series.MainDicomTags.SeriesDescription,
-   series.Status,
-   FormatMainDicomTags(series.MainDicomTags, [
+  FormatMainDicomTags(node, series.MainDicomTags, [ 
      "SeriesDescription", 
      "SeriesTime", 
      "Manufacturer",
      "ImagesInAcquisition",
      "SeriesDate",
      "ImageOrientationPatient"
-   ]),
-   c
-  );
-
-  return CompleteFormatting(s, link, isReverse);
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse, c);
 }
 
 
 function FormatInstance(instance, link, isReverse)
 {
-  var s = ('<h3>Instance {0}</h3>{1}').format
-  (instance.IndexInSeries,
-   FormatMainDicomTags(instance.MainDicomTags, [
-     "AcquisitionNumber", 
-     "InstanceNumber", 
-     "InstanceCreationDate", 
-     "InstanceCreationTime",
-     "ImagePositionPatient"
-   ])
-  );
+  var node = $('<div>').append($('<h3>').text('Instance: ' + instance.IndexInSeries));
 
-  return CompleteFormatting(s, link, isReverse);
+  FormatMainDicomTags(node, instance.MainDicomTags, [
+    "AcquisitionNumber", 
+    "InstanceNumber", 
+    "InstanceCreationDate", 
+    "InstanceCreationTime",
+    "ImagePositionPatient"
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse);
 }
 
 
@@ -353,7 +334,11 @@
     cache: false,
     success: function(s) {
       if (s.Name != "") {
-        $('.orthanc-name').html('<a class="ui-link" href="explorer.html">' + s.Name + '</a> &raquo; ');
+        $('.orthanc-name').append($('<a>')
+                                  .addClass('ui-link')
+                                  .attr('href', 'explorer.html')
+                                  .text(s.Name)
+                                  .append(' &raquo; '));
       }
     }
   });
@@ -417,8 +402,9 @@
         for (var i = 0; i < studies.length; i++) {
           if (i == 0 || studies[i].MainDicomTags.StudyDate != studies[i - 1].MainDicomTags.StudyDate)
           {
-            target.append('<li data-role="list-divider">{0}</li>'.format
-                          (FormatDicomDate(studies[i].MainDicomTags.StudyDate)));
+            target.append($('<li>')
+                          .attr('data-role', 'list-divider')
+                          .text(FormatDicomDate(studies[i].MainDicomTags.StudyDate)));
           }
 
           target.append(FormatStudy(studies[i], '#study?uuid=' + studies[i].ID));
@@ -477,9 +463,11 @@
           for (var i = 0; i < series.length; i++) {
             if (i == 0 || series[i].MainDicomTags.SeriesDate != series[i - 1].MainDicomTags.SeriesDate)
             {
-              target.append('<li data-role="list-divider">{0}</li>'.format
-                            (FormatDicomDate(series[i].MainDicomTags.SeriesDate)));
+              target.append($('<li>')
+                            .attr('data-role', 'list-divider')
+                            .text(FormatDicomDate(series[i].MainDicomTags.SeriesDate)));
             }
+            
             target.append(FormatSeries(series[i], '#series?uuid=' + series[i].ID));
           }
           target.listview('refresh');
@@ -537,6 +525,24 @@
 }
 
 
+function EscapeHtml(value)
+{
+  var ENTITY_MAP = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#39;',
+    '/': '&#x2F;',
+    '`': '&#x60;',
+    '=': '&#x3D;'
+  };
+
+  return String(value).replace(/[&<>"'`=\/]/g, function (s) {
+    return ENTITY_MAP[s];
+  });
+}
+
 
 function ConvertForTree(dicom)
 {
@@ -544,12 +550,14 @@
 
   for (var i in dicom) {
     if (dicom[i] != null) {
-      var label = i + '<span class="tag-name"> (<i>' + dicom[i]["Name"] + '</i>)</span>: ';
+      var label = (i + '<span class="tag-name"> (<i>' +
+                   EscapeHtml(dicom[i]["Name"]) +
+                   '</i>)</span>: ');
 
       if (dicom[i]["Type"] == 'String')
       {
         result.push({
-          label: label + '<strong>' + dicom[i]["Value"] + '</strong>',
+          label: label + '<strong>' + EscapeHtml(dicom[i]["Value"]) + '</strong>',
           children: []
         });
       }
@@ -797,7 +805,7 @@
         var images = [];
         for (var i = 0; i < instances.length; i++) {
           images.push([ '../instances/' + instances[i].ID + '/preview',
-                        '{0}/{1}'.format(i + 1, instances.length) ])
+                        (i + 1).toString() + '/' + instances.length.toString() ])
         }
 
         jQuery.slimbox(images, 0, {
--- a/OrthancServer/FromDcmtkBridge.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/FromDcmtkBridge.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -181,6 +181,8 @@
 
   void FromDcmtkBridge::InitializeDictionary(bool loadPrivateDictionary)
   {
+    LOG(INFO) << "Using DCTMK version: " << DCMTK_VERSION_NUMBER;
+    
     {
       DictionaryLocker locker;
 
@@ -1681,7 +1683,7 @@
           throw OrthancException(ErrorCode_BadParameterType);
         }
 
-        DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key, value.size());
+        DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key);
         element.reset(sequence);
         
         for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
--- a/OrthancServer/Internals/DicomImageDecoder.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/Internals/DicomImageDecoder.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -727,7 +727,8 @@
 
 
   void DicomImageDecoder::ApplyExtractionMode(std::auto_ptr<ImageAccessor>& image,
-                                              ImageExtractionMode mode)
+                                              ImageExtractionMode mode,
+                                              bool invert)
   {
     if (image.get() == NULL)
     {
@@ -761,6 +762,11 @@
     if (ok)
     {
       assert(image.get() != NULL);
+
+      if (invert)
+      {
+        Orthanc::ImageProcessing::Invert(*image);
+      }
     }
     else
     {
@@ -771,9 +777,10 @@
 
   void DicomImageDecoder::ExtractPngImage(std::string& result,
                                           std::auto_ptr<ImageAccessor>& image,
-                                          ImageExtractionMode mode)
+                                          ImageExtractionMode mode,
+                                          bool invert)
   {
-    ApplyExtractionMode(image, mode);
+    ApplyExtractionMode(image, mode, invert);
 
     PngWriter writer;
     writer.WriteToMemory(result, *image);
@@ -783,6 +790,7 @@
   void DicomImageDecoder::ExtractJpegImage(std::string& result,
                                            std::auto_ptr<ImageAccessor>& image,
                                            ImageExtractionMode mode,
+                                           bool invert,
                                            uint8_t quality)
   {
     if (mode != ImageExtractionMode_UInt8 &&
@@ -791,7 +799,7 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    ApplyExtractionMode(image, mode);
+    ApplyExtractionMode(image, mode, invert);
 
     JpegWriter writer;
     writer.SetQuality(quality);
--- a/OrthancServer/Internals/DicomImageDecoder.h	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/Internals/DicomImageDecoder.h	Wed Jul 12 09:12:12 2017 +0200
@@ -79,7 +79,8 @@
     static bool PreviewDecodedImage(std::auto_ptr<ImageAccessor>& image);
 
     static void ApplyExtractionMode(std::auto_ptr<ImageAccessor>& image,
-                                    ImageExtractionMode mode);
+                                    ImageExtractionMode mode,
+                                    bool invert);
 
   public:
     static bool IsPsmctRle1(DcmDataset& dataset);
@@ -92,11 +93,13 @@
 
     static void ExtractPngImage(std::string& result,
                                 std::auto_ptr<ImageAccessor>& image,
-                                ImageExtractionMode mode);
+                                ImageExtractionMode mode,
+                                bool invert);
 
     static void ExtractJpegImage(std::string& result,
                                  std::auto_ptr<ImageAccessor>& image,
                                  ImageExtractionMode mode,
+                                 bool invert,
                                  uint8_t quality);
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -267,14 +267,17 @@
     private:
       std::auto_ptr<ImageAccessor>&  image_;
       ImageExtractionMode            mode_;
+      bool                           invert_;
       std::string                    format_;
       std::string                    answer_;
 
     public:
       ImageToEncode(std::auto_ptr<ImageAccessor>& image,
-                    ImageExtractionMode mode) : 
+                    ImageExtractionMode mode,
+                    bool invert) :
         image_(image),
-        mode_(mode)
+        mode_(mode),
+        invert_(invert)
       {
       }
 
@@ -286,13 +289,13 @@
       void EncodeUsingPng()
       {
         format_ = "image/png";
-        DicomImageDecoder::ExtractPngImage(answer_, image_, mode_);
+        DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_);
       }
 
       void EncodeUsingJpeg(uint8_t quality)
       {
         format_ = "image/jpeg";
-        DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, quality);
+        DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, invert_, quality);
       }
     };
 
@@ -373,6 +376,7 @@
       return;
     }
 
+    bool invert = false;
     std::auto_ptr<ImageAccessor> decoded;
 
     try
@@ -393,6 +397,21 @@
          * to decode the image. This allows us to take advantage of
          * the cache below.
          **/
+
+        if (mode == ImageExtractionMode_Preview &&
+            decoded.get() != NULL)
+        {
+          // TODO Optimize this lookup for photometric interpretation:
+          // It should be implemented by the plugin to avoid parsing
+          // twice the DICOM file
+          ParsedDicomFile parsed(dicomContent);
+          
+          PhotometricInterpretation photometric;
+          if (parsed.LookupPhotometricInterpretation(photometric))
+          {
+            invert = (photometric == PhotometricInterpretation_Monochrome1);
+          }
+        }
       }
 #endif
 
@@ -400,8 +419,15 @@
       {
         // Use Orthanc's built-in decoder, using the cache to speed-up
         // things on multi-frame images
-        ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
+        ServerContext::DicomCacheLocker locker(context, publicId);        
         decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame));
+
+        PhotometricInterpretation photometric;
+        if (mode == ImageExtractionMode_Preview &&
+            locker.GetDicom().LookupPhotometricInterpretation(photometric))
+        {
+          invert = (photometric == PhotometricInterpretation_Monochrome1);
+        }
       }
     }
     catch (OrthancException& e)
@@ -423,7 +449,7 @@
       }
     }
 
-    ImageToEncode image(decoded, mode);
+    ImageToEncode image(decoded, mode, invert);
 
     HttpContentNegociation negociation;
     EncodePng png(image);          negociation.Register("image/png", png);
@@ -1128,7 +1154,8 @@
         request["Level"].type() == Json::stringValue &&
         request["Query"].type() == Json::objectValue &&
         (!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue) &&
-        (!request.isMember("Limit") || request["Limit"].type() == Json::intValue))
+        (!request.isMember("Limit") || request["Limit"].type() == Json::intValue) &&
+        (!request.isMember("Since") || request["Since"].type() == Json::intValue))
     {
       bool expand = false;
       if (request.isMember("Expand"))
@@ -1154,6 +1181,18 @@
         limit = static_cast<size_t>(tmp);
       }
 
+      size_t since = 0;
+      if (request.isMember("Since"))
+      {
+        int tmp = request["Since"].asInt();
+        if (tmp < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+        since = static_cast<size_t>(tmp);
+      }
+
       std::string level = request["Level"].asString();
 
       LookupResource query(StringToResourceType(level.c_str()));
@@ -1172,8 +1211,9 @@
       }
       
       std::list<std::string> resources;
-      context.Apply(resources, query, limit);
-      AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, query.GetLevel(), expand);
+      context.Apply(resources, query, since, limit);
+      AnswerListOfResources(call.GetOutput(), context.GetIndex(),
+                            resources, query.GetLevel(), expand);
     }
     else
     {
--- a/OrthancServer/ParsedDicomFile.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/ParsedDicomFile.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -1409,4 +1409,25 @@
   {
     return FromDcmtkBridge::LookupTransferSyntax(result, *pimpl_->file_);
   }
+
+
+  bool ParsedDicomFile::LookupPhotometricInterpretation(PhotometricInterpretation& result) const
+  {
+    DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(),
+                DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement());
+
+    DcmDataset& dataset = *pimpl_->file_->getDataset();
+
+    const char *c = NULL;
+    if (dataset.findAndGetString(k, c).good() &&
+        c != NULL)
+    {
+      result = StringToPhotometricInterpretation(c);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
 }
--- a/OrthancServer/ParsedDicomFile.h	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/ParsedDicomFile.h	Wed Jul 12 09:12:12 2017 +0200
@@ -183,5 +183,7 @@
     void ExtractDicomAsJson(Json::Value& target) const;
 
     bool LookupTransferSyntax(std::string& result);
+
+    bool LookupPhotometricInterpretation(PhotometricInterpretation& result) const;
   };
 }
--- a/OrthancServer/Search/HierarchicalMatcher.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/Search/HierarchicalMatcher.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -333,7 +333,7 @@
                                                       dicom.GetEncoding()));
 
     std::auto_ptr<ParsedDicomFile> result(new ParsedDicomFile(*dataset));
-    result->SetEncoding(Encoding_Utf8);
+    result->SetEncoding(dicom.GetEncoding());
 
     return result.release();
   }
--- a/OrthancServer/ServerContext.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/ServerContext.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -601,9 +601,10 @@
   }
 
 
-  bool ServerContext::Apply(std::list<std::string>& result,
+  void ServerContext::Apply(std::list<std::string>& result,
                             const ::Orthanc::LookupResource& lookup,
-                            size_t maxResults)
+                            size_t since,
+                            size_t limit)
   {
     result.clear();
 
@@ -612,6 +613,7 @@
 
     assert(resources.size() == instances.size());
 
+    size_t skipped = 0;
     for (size_t i = 0; i < instances.size(); i++)
     {
       Json::Value dicom;
@@ -619,10 +621,14 @@
       
       if (lookup.IsMatch(dicom))
       {
-        if (maxResults != 0 &&
-            result.size() >= maxResults)
+        if (skipped < since)
         {
-          return false;  // too many results
+          skipped++;
+        }
+        else if (limit != 0 &&
+                 result.size() >= limit)
+        {
+          return;  // too many results
         }
         else
         {
@@ -630,8 +636,6 @@
         }
       }
     }
-
-    return true;  // finished
   }
 
 }
--- a/OrthancServer/ServerContext.h	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/ServerContext.h	Wed Jul 12 09:12:12 2017 +0200
@@ -258,9 +258,10 @@
 
     void Stop();
 
-    bool Apply(std::list<std::string>& result,
+    void Apply(std::list<std::string>& result,
                const ::Orthanc::LookupResource& lookup,
-               size_t maxResults);
+               size_t since,
+               size_t limit);
 
 
     /**
--- a/OrthancServer/ServerEnumerations.h	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/ServerEnumerations.h	Wed Jul 12 09:12:12 2017 +0200
@@ -146,7 +146,8 @@
   {
     GlobalProperty_DatabaseSchemaVersion = 1,   // Unused in the Orthanc core as of Orthanc 0.9.5
     GlobalProperty_FlushSleep = 2,
-    GlobalProperty_AnonymizationSequence = 3
+    GlobalProperty_AnonymizationSequence = 3,
+    GlobalProperty_DatabasePatchLevel = 4       // Reserved for internal use of the database plugins
   };
 
   enum MetadataType
--- a/OrthancServer/main.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/OrthancServer/main.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -881,30 +881,27 @@
 }
 
 
-static bool UpgradeDatabase(IDatabaseWrapper& database,
-                            IStorageArea& storageArea,
-                            bool allowDatabaseUpgrade)
+static void UpgradeDatabase(IDatabaseWrapper& database,
+                            IStorageArea& storageArea)
 {
   // Upgrade the schema of the database, if needed
   unsigned int currentVersion = database.GetDatabaseVersion();
+
+  LOG(WARNING) << "Starting the upgrade of the database schema";
+  LOG(WARNING) << "Current database version: " << currentVersion;
+  LOG(WARNING) << "Database version expected by Orthanc: " << ORTHANC_DATABASE_VERSION;
+  
   if (currentVersion == ORTHANC_DATABASE_VERSION)
   {
-    return true;
+    LOG(WARNING) << "No upgrade is needed, start Orthanc without the \"--upgrade\" argument";
+    return;
   }
 
   if (currentVersion > ORTHANC_DATABASE_VERSION)
   {
     LOG(ERROR) << "The version of the database schema (" << currentVersion
                << ") is too recent for this version of Orthanc. Please upgrade Orthanc.";
-    return false;
-  }
-
-  if (!allowDatabaseUpgrade)
-  {
-    LOG(ERROR) << "The database schema must be upgraded from version "
-               << currentVersion << " to " << ORTHANC_DATABASE_VERSION 
-               << ": Please run Orthanc with the \"--upgrade\" command-line option";
-    return false;
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
   }
 
   LOG(WARNING) << "Upgrading the database from schema version "
@@ -926,10 +923,13 @@
   if (ORTHANC_DATABASE_VERSION != currentVersion)
   {
     LOG(ERROR) << "The database schema was not properly upgraded, it is still at version " << currentVersion;
-    throw OrthancException(ErrorCode_InternalError);
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
   }
-
-  return true;
+  else
+  {
+    LOG(WARNING) << "The database schema was successfully upgraded, "
+                 << "you can now start Orthanc without the \"--upgrade\" argument";
+  }
 }
 
 
@@ -1015,13 +1015,23 @@
 static bool ConfigureDatabase(IDatabaseWrapper& database,
                               IStorageArea& storageArea,
                               OrthancPlugins *plugins,
-                              bool allowDatabaseUpgrade)
+                              bool upgradeDatabase)
 {
   database.Open();
+
+  unsigned int currentVersion = database.GetDatabaseVersion();
   
-  if (!UpgradeDatabase(database, storageArea, allowDatabaseUpgrade))
+  if (upgradeDatabase)
   {
-    return false;
+    UpgradeDatabase(database, storageArea);
+    return false;  // Stop and don't restart Orthanc (cf. issue 29)
+  }
+  else if (currentVersion != ORTHANC_DATABASE_VERSION)
+  {
+    LOG(ERROR) << "The database schema must be changed from version "
+               << currentVersion << " to " << ORTHANC_DATABASE_VERSION 
+               << ": Please run Orthanc with the \"--upgrade\" argument";
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
   }
 
   bool success = ConfigureServerContext(database, storageArea, plugins);
@@ -1034,7 +1044,7 @@
 
 static bool ConfigurePlugins(int argc, 
                              char* argv[],
-                             bool allowDatabaseUpgrade)
+                             bool upgradeDatabase)
 {
   std::auto_ptr<IDatabaseWrapper>  databasePtr;
   std::auto_ptr<IStorageArea>  storage;
@@ -1069,14 +1079,14 @@
   assert(database != NULL);
   assert(storage.get() != NULL);
 
-  return ConfigureDatabase(*database, *storage, &plugins, allowDatabaseUpgrade);
+  return ConfigureDatabase(*database, *storage, &plugins, upgradeDatabase);
 
 #elif ORTHANC_ENABLE_PLUGINS == 0
   // The plugins are disabled
   databasePtr.reset(Configuration::CreateDatabaseWrapper());
   storage.reset(Configuration::CreateStorageArea());
 
-  return ConfigureDatabase(*databasePtr, *storage, NULL, allowDatabaseUpgrade);
+  return ConfigureDatabase(*databasePtr, *storage, NULL, upgradeDatabase);
 
 #else
 #  error The macro ORTHANC_ENABLE_PLUGINS must be set to 0 or 1
@@ -1086,9 +1096,9 @@
 
 static bool StartOrthanc(int argc, 
                          char* argv[],
-                         bool allowDatabaseUpgrade)
+                         bool upgradeDatabase)
 {
-  return ConfigurePlugins(argc, argv, allowDatabaseUpgrade);
+  return ConfigurePlugins(argc, argv, upgradeDatabase);
 }
 
 
@@ -1104,7 +1114,7 @@
 {
   Logging::Initialize();
 
-  bool allowDatabaseUpgrade = false;
+  bool upgradeDatabase = false;
   const char* configurationFile = NULL;
 
 
@@ -1193,7 +1203,7 @@
     }
     else if (argument == "--upgrade")
     {
-      allowDatabaseUpgrade = true;
+      upgradeDatabase = true;
     }
     else if (boost::starts_with(argument, "--config="))
     {
@@ -1207,8 +1217,17 @@
 #endif
 
       std::string target = argument.substr(9);
-      SystemToolbox::WriteFile(configurationSample, target);
-      return 0;
+
+      try
+      {
+        SystemToolbox::WriteFile(configurationSample, target);
+        return 0;
+      }
+      catch (OrthancException&)
+      {
+        LOG(ERROR) << "Cannot write sample configuration as file \"" << target << "\"";
+        return -1;
+      }
     }
     else
     {
@@ -1249,7 +1268,7 @@
     {
       OrthancInitialize(configurationFile);
 
-      bool restart = StartOrthanc(argc, argv, allowDatabaseUpgrade);
+      bool restart = StartOrthanc(argc, argv, upgradeDatabase);
       if (restart)
       {
         OrthancFinalize();
--- a/Plugins/Samples/Common/FullOrthancDataset.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Plugins/Samples/Common/FullOrthancDataset.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -166,6 +166,21 @@
   }
 
 
+  FullOrthancDataset::FullOrthancDataset(const void* content,
+                                         size_t size)
+  {
+    IOrthancConnection::ParseJson(root_, content, size);
+    CheckRoot();
+  }
+
+
+  FullOrthancDataset::FullOrthancDataset(const Json::Value& root) :
+    root_(root)
+  {
+    CheckRoot();
+  }
+
+
   bool FullOrthancDataset::GetStringValue(std::string& result,
                                           const DicomPath& path) const
   {
--- a/Plugins/Samples/Common/FullOrthancDataset.h	Wed May 10 22:40:59 2017 +0200
+++ b/Plugins/Samples/Common/FullOrthancDataset.h	Wed Jul 12 09:12:12 2017 +0200
@@ -55,6 +55,11 @@
 
     FullOrthancDataset(const std::string& content);
 
+    FullOrthancDataset(const void* content,
+                       size_t size);
+
+    FullOrthancDataset(const Json::Value& root);
+
     virtual bool GetStringValue(std::string& result,
                                 const DicomPath& path) const;
 
--- a/Plugins/Samples/Common/IOrthancConnection.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Plugins/Samples/Common/IOrthancConnection.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -51,6 +51,20 @@
   }
 
 
+  void IOrthancConnection::ParseJson(Json::Value& result,
+                                     const void* content,
+                                     size_t size)
+  {
+    Json::Reader reader;
+    
+    if (!reader.parse(reinterpret_cast<const char*>(content),
+                      reinterpret_cast<const char*>(content) + size, result))
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
   void IOrthancConnection::RestApiGet(Json::Value& result,
                                       IOrthancConnection& orthanc,
                                       const std::string& uri)
--- a/Plugins/Samples/Common/IOrthancConnection.h	Wed May 10 22:40:59 2017 +0200
+++ b/Plugins/Samples/Common/IOrthancConnection.h	Wed Jul 12 09:12:12 2017 +0200
@@ -64,6 +64,10 @@
     static void ParseJson(Json::Value& result,
                           const std::string& content);
 
+    static void ParseJson(Json::Value& result,
+                          const void* content,
+                          size_t size);
+
     static void RestApiGet(Json::Value& result,
                            IOrthancConnection& orthanc,
                            const std::string& uri);
--- a/Plugins/Samples/ModalityWorklists/Plugin.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -8,7 +8,7 @@
  * 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
@@ -35,7 +35,7 @@
 /**
  * This is the main function for matching a DICOM worklist against a query.
  **/
-static void  MatchWorklist(OrthancPluginWorklistAnswers*      answers,
+static bool MatchWorklist(OrthancPluginWorklistAnswers*      answers,
                            const OrthancPluginWorklistQuery*  query,
                            const OrthancPlugins::FindMatcher& matcher,
                            const std::string& path)
@@ -54,7 +54,11 @@
       OrthancPlugins::LogError(context_, "Error while adding an answer to a worklist request");
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
     }
+
+    return true;
   }
+
+  return false;
 }
 
 
@@ -67,10 +71,10 @@
 
   // Convert the DICOM as JSON, and dump it to the user in "--verbose" mode
   Json::Value json;
-  dicom.DicomToJson(json, OrthancPluginDicomToJsonFormat_Short, 
+  dicom.DicomToJson(json, OrthancPluginDicomToJsonFormat_Short,
                     static_cast<OrthancPluginDicomToJsonFlags>(0), 0);
-  
-  OrthancPlugins::LogInfo(context_, "Received worklist query from remote modality " + 
+
+  OrthancPlugins::LogInfo(context_, "Received worklist query from remote modality " +
                           std::string(issuerAet) + ":\n" + json.toStyledString());
 
   if (!filterIssuerAet_)
@@ -140,10 +144,12 @@
     std::auto_ptr<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet));
 
     // Loop over the regular files in the database folder
-    namespace fs = boost::filesystem;  
+    namespace fs = boost::filesystem;
 
     fs::path source(folder_);
     fs::directory_iterator end;
+    int parsedFilesCount = 0;
+    int matchedWorklistCount = 0;
 
     try
     {
@@ -159,11 +165,21 @@
 
           if (extension == ".wl")
           {
+            parsedFilesCount++;
             // We found a worklist (i.e. a DICOM find with extension ".wl"), match it against the query
-            MatchWorklist(answers, query, *matcher, it->path().string());
+            if (MatchWorklist(answers, query, *matcher, it->path().string()))
+            {
+              OrthancPlugins::LogInfo(context_, "Worklist matched: " + it->path().string());
+              matchedWorklistCount++;
+            }
           }
         }
       }
+
+      std::ostringstream message;
+      message << "Worklist C-Find: parsed " << parsedFilesCount << " files, found " << matchedWorklistCount << " match(es)";
+      OrthancPlugins::LogInfo(context_, message.str());
+
     }
     catch (fs::filesystem_error&)
     {
@@ -192,7 +208,7 @@
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(c) == 0)
     {
-      OrthancPlugins::ReportMinimalOrthancVersion(context_, 
+      OrthancPlugins::ReportMinimalOrthancVersion(context_,
                                                   ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
                                                   ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
                                                   ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
--- a/Resources/CMake/DcmtkConfiguration.cmake	Wed May 10 22:40:59 2017 +0200
+++ b/Resources/CMake/DcmtkConfiguration.cmake	Wed Jul 12 09:12:12 2017 +0200
@@ -6,9 +6,9 @@
   if (USE_DCMTK_361)
     SET(DCMTK_VERSION_NUMBER 361)
     SET(DCMTK_PACKAGE_VERSION "3.6.1")
-    SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.1_20160216)
-    SET(DCMTK_URL "http://www.orthanc-server.com/downloads/third-party/dcmtk-3.6.1_20160216.tar.gz")
-    SET(DCMTK_MD5 "273c8a544b9fe09b8a4fb4eb51df8e52")
+    SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.1_20170228)
+    SET(DCMTK_URL "http://www.orthanc-server.com/downloads/third-party/dcmtk-3.6.1_20170228.tar.gz")
+    SET(DCMTK_MD5 "65f3520fce5d084c3530ae7252e39f3e")
     SET(DCMTK_PATCH_SPEED "${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.1-speed.patch")
 
     macro(DCMTK_UNSET)
@@ -270,10 +270,14 @@
     set(DCMTK_CONFIGURATION_FILE "${DCMTK_config_INCLUDE_DIR}/cfunix.h")
   elseif (EXISTS "${DCMTK_config_INCLUDE_DIR}/osconfig.h")  # This is for Arch Linux
     set(DCMTK_CONFIGURATION_FILE "${DCMTK_config_INCLUDE_DIR}/osconfig.h")
+  elseif (EXISTS "${DCMTK_INCLUDE_DIRS}/dcmtk/config/osconfig.h")  # This is for Debian Buster
+    set(DCMTK_CONFIGURATION_FILE "${DCMTK_INCLUDE_DIRS}/dcmtk/config/osconfig.h")
   else()
     message(FATAL_ERROR "Please install libdcmtk*-dev")
   endif()
 
+  message("DCMTK configuration file: ${DCMTK_CONFIGURATION_FILE}")
+  
   # Autodetection of the version of DCMTK
   file(STRINGS
     "${DCMTK_CONFIGURATION_FILE}" 
@@ -309,6 +313,13 @@
       /usr/share/libdcmtk7
       /usr/share/libdcmtk8
       /usr/share/libdcmtk9
+      /usr/share/libdcmtk10
+      /usr/share/libdcmtk11
+      /usr/share/libdcmtk12
+      /usr/share/libdcmtk13
+      /usr/share/libdcmtk14
+      /usr/share/libdcmtk15
+      /usr/share/libdcmtk16
       /usr/local/share/dcmtk
       )
 
--- a/Resources/CMake/SQLiteConfiguration.cmake	Wed May 10 22:40:59 2017 +0200
+++ b/Resources/CMake/SQLiteConfiguration.cmake	Wed Jul 12 09:12:12 2017 +0200
@@ -19,6 +19,8 @@
   SET(SQLITE_MD5 "5fbeff9645ab035a1f580e90b279a16d")
   SET(SQLITE_URL "http://www.orthanc-server.com/downloads/third-party/sqlite-amalgamation-3071300.zip")
 
+  add_definitions(-DORTHANC_SQLITE_VERSION=3007013)
+
   DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}")
 
   set(SQLITE_SOURCES
@@ -52,7 +54,10 @@
 
   # Autodetection of the version of SQLite
   file(STRINGS "${SQLITE_INCLUDE_DIR}/sqlite3.h" SQLITE_VERSION_NUMBER1 REGEX "#define SQLITE_VERSION_NUMBER.*$")    
-  string(REGEX REPLACE "#define SQLITE_VERSION_NUMBER(.*)$" "\\1" SQLITE_VERSION_NUMBER ${SQLITE_VERSION_NUMBER1})
+  string(REGEX REPLACE "#define SQLITE_VERSION_NUMBER(.*)$" "\\1" SQLITE_VERSION_NUMBER2 ${SQLITE_VERSION_NUMBER1})
+
+  # Remove the trailing spaces to convert the string to a proper integer
+  string(STRIP ${SQLITE_VERSION_NUMBER2} SQLITE_VERSION_NUMBER)
 
   message("Detected version of SQLite: ${SQLITE_VERSION_NUMBER}")
 
@@ -61,5 +66,7 @@
     message(FATAL_ERROR "SQLite version must be above 3.7.0. Please set the CMake variable USE_SYSTEM_SQLITE to OFF.")
   ENDIF()
 
+  add_definitions(-DORTHANC_SQLITE_VERSION=${SQLITE_VERSION_NUMBER})
+
   link_libraries(sqlite3)
 endif()
--- a/Resources/Patches/dcmtk-3.6.1-speed.patch	Wed May 10 22:40:59 2017 +0200
+++ b/Resources/Patches/dcmtk-3.6.1-speed.patch	Wed Jul 12 09:12:12 2017 +0200
@@ -1,26 +1,24 @@
-diff -urEb dcmtk-3.6.1_20160216.orig/dcmnet/libsrc/dul.cc dcmtk-3.6.1_20160216/dcmnet/libsrc/dul.cc
---- dcmtk-3.6.1_20160216.orig/dcmnet/libsrc/dul.cc	2016-04-05 12:56:28.962230391 +0200
-+++ dcmtk-3.6.1_20160216/dcmnet/libsrc/dul.cc	2016-04-05 12:57:15.814232296 +0200
-@@ -1841,7 +1841,7 @@
+diff -urEb dcmtk-3.6.1_20170228.orig/dcmnet/libsrc/dul.cc dcmtk-3.6.1_20170228/dcmnet/libsrc/dul.cc
+--- dcmtk-3.6.1_20170228.orig/dcmnet/libsrc/dul.cc	2017-07-05 12:57:20.707214499 +0200
++++ dcmtk-3.6.1_20170228/dcmnet/libsrc/dul.cc	2017-07-05 12:58:03.563590489 +0200
+@@ -1789,7 +1789,7 @@
          return makeDcmnetCondition(DULC_TCPINITERROR, OF_error, msg.c_str());
      }
  #endif
 -    setTCPBufferLength(sock);
 +    //setTCPBufferLength(sock);
  
- #ifndef DONT_DISABLE_NAGLE_ALGORITHM
      /*
-Only in dcmtk-3.6.1_20160216/dcmnet/libsrc: dul.cc~
-diff -urEb dcmtk-3.6.1_20160216.orig/dcmnet/libsrc/dulfsm.cc dcmtk-3.6.1_20160216/dcmnet/libsrc/dulfsm.cc
---- dcmtk-3.6.1_20160216.orig/dcmnet/libsrc/dulfsm.cc	2016-04-05 12:56:28.962230391 +0200
-+++ dcmtk-3.6.1_20160216/dcmnet/libsrc/dulfsm.cc	2016-04-05 12:57:31.946232952 +0200
-@@ -2431,7 +2431,7 @@
+      * Disable the so-called Nagle algorithm (if requested).
+diff -urEb dcmtk-3.6.1_20170228.orig/dcmnet/libsrc/dulfsm.cc dcmtk-3.6.1_20170228/dcmnet/libsrc/dulfsm.cc
+--- dcmtk-3.6.1_20170228.orig/dcmnet/libsrc/dulfsm.cc	2017-07-05 12:57:20.707214499 +0200
++++ dcmtk-3.6.1_20170228/dcmnet/libsrc/dulfsm.cc	2017-07-05 12:58:17.995717258 +0200
+@@ -2419,7 +2419,7 @@
            return makeDcmnetCondition(DULC_TCPINITERROR, OF_error, msg.c_str());
          }
  #endif
 -        setTCPBufferLength(s);
 +        //setTCPBufferLength(s);
  
- #ifndef DONT_DISABLE_NAGLE_ALGORITHM
          /*
-Only in dcmtk-3.6.1_20160216/dcmnet/libsrc: dulfsm.cc~
+          * Disable the so-called Nagle algorithm (if requested).
--- a/Resources/Patches/dcmtk.txt	Wed May 10 22:40:59 2017 +0200
+++ b/Resources/Patches/dcmtk.txt	Wed Jul 12 09:12:12 2017 +0200
@@ -7,4 +7,4 @@
 For "dcmtk-3.6.1-private.dic"
 =============================
 
-# cp ../../ThirdPartyDownloads/dcmtk-3.6.1_20160216/dcmdata/data/private.dic dcmtk-3.6.1-private.dic
+# cp ../../ThirdPartyDownloads/dcmtk-3.6.1_20170228/dcmdata/data/private.dic dcmtk-3.6.1-private.dic
--- a/Resources/Samples/Lua/AutomatedJpeg2kCompression.lua	Wed May 10 22:40:59 2017 +0200
+++ b/Resources/Samples/Lua/AutomatedJpeg2kCompression.lua	Wed Jul 12 09:12:12 2017 +0200
@@ -16,7 +16,7 @@
 
       -- Compress to JPEG2000 using gdcm
       local compressed = instanceId .. '-compressed.dcm'
-      os.execute('gdcmconv --j2k ' .. uncompressed .. ' ' .. compressed)
+      os.execute('gdcmconv -U --j2k ' .. uncompressed .. ' ' .. compressed)
 
       -- Generate a new SOPInstanceUID for the JPEG2000 file, as
       -- gdcmconv does not do this by itself
--- a/TODO	Wed May 10 22:40:59 2017 +0200
+++ b/TODO	Wed Jul 12 09:12:12 2017 +0200
@@ -30,6 +30,13 @@
 
 * Create multi-frame images with /tools/create-dicom (by adding a
   "MultiFrame" flag to avoid creating a series)
+* In the POST /instances, add a ?force=true query arguments to force Orthanc to replace 
+  the uploaded file even if it already exists in Orthanc.  This is useful when transcoding
+  an image outside Orthanc and reuploading it.  Since it has the same ids, its Orthanc ID
+  will be the same as the previous one.
+* In the /studies/{id}/anonymize route, add an option to remove secondary captures.
+  They usually contains Patient info in the image.  The SOPClassUID might be used to
+  identify such secondary captures.
 
 ---------
 Long-term
@@ -79,6 +86,8 @@
 Ideas of plugins
 ----------------
 
+* Complex anonymization, with recursive mapping of UIDs
+  https://bitbucket.org/sjodogne/orthanc/issues/46/
 * DICOM-RT primitives (RT-STRUCT, RT-PLAN, RT-DOSE)
 * Converter to/from NIfTI
 * MySQL database plugin
@@ -87,6 +96,8 @@
   https://groups.google.com/d/msg/orthanc-users/KompazkxRSs/5Rh03mzgDAAJ
 * More generally, expose more callbacks of the plugin SDK in Lua:
   https://groups.google.com/d/msg/orthanc-users/_FbiRHuXPGM/J-OAv7zaCAAJ
+* Authorization plugin for the DICOM protocol:
+  https://groups.google.com/d/msg/orthanc-users/Wv-QEwTE0IA/rvJxoOjcAQAJ
 
 
 ===
--- a/UnitTestsSources/UnitTestsMain.cpp	Wed May 10 22:40:59 2017 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Wed Jul 12 09:12:12 2017 +0200
@@ -631,6 +631,23 @@
   ASSERT_EQ(ResourceType_Instance, StringToResourceType(EnumerationToString(ResourceType_Instance)));
 
   ASSERT_EQ(ImageFormat_Png, StringToImageFormat(EnumerationToString(ImageFormat_Png)));
+
+  ASSERT_EQ(PhotometricInterpretation_ARGB, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_ARGB)));
+  ASSERT_EQ(PhotometricInterpretation_CMYK, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_CMYK)));
+  ASSERT_EQ(PhotometricInterpretation_HSV, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_HSV)));
+  ASSERT_EQ(PhotometricInterpretation_Monochrome1, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Monochrome1)));
+  ASSERT_EQ(PhotometricInterpretation_Monochrome2, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Monochrome2)));
+  ASSERT_EQ(PhotometricInterpretation_Palette, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_Palette)));
+  ASSERT_EQ(PhotometricInterpretation_RGB, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_RGB)));
+  ASSERT_EQ(PhotometricInterpretation_YBRFull, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRFull)));
+  ASSERT_EQ(PhotometricInterpretation_YBRFull422, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRFull422)));
+  ASSERT_EQ(PhotometricInterpretation_YBRPartial420, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRPartial420)));
+  ASSERT_EQ(PhotometricInterpretation_YBRPartial422, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBRPartial422)));
+  ASSERT_EQ(PhotometricInterpretation_YBR_ICT, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBR_ICT)));
+  ASSERT_EQ(PhotometricInterpretation_YBR_RCT, StringToPhotometricInterpretation(EnumerationToString(PhotometricInterpretation_YBR_RCT)));
+
+  ASSERT_STREQ("Unknown", EnumerationToString(PhotometricInterpretation_Unknown));
+  ASSERT_THROW(StringToPhotometricInterpretation("Unknown"), OrthancException);
 }