changeset 3695:898903022836 storage-commitment

integration mainline->storage-commitment
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 26 Feb 2020 10:39:55 +0100
parents 9dac85e807c2 (current diff) 9107cca846b6 (diff)
children 736907ecb626
files Core/DicomFormat/DicomMap.cpp Core/DicomNetworking/Internals/CommandDispatcher.cpp NEWS OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Plugins/Engine/OrthancPlugins.cpp UnitTestsSources/MultiThreadingTests.cpp
diffstat 28 files changed, 1185 insertions(+), 558 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomFormat/DicomMap.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomFormat/DicomMap.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -984,6 +984,21 @@
     }
   }
 
+  bool DicomMap::ParseFirstFloat(float& result,
+                                 const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseFirstFloat(result);
+    }
+  }
+
   bool DicomMap::ParseDouble(double& result,
                              const DicomTag& tag) const
   {
--- a/Core/DicomFormat/DicomMap.h	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomFormat/DicomMap.h	Wed Feb 26 10:39:55 2020 +0100
@@ -205,6 +205,9 @@
     bool ParseFloat(float& result,
                     const DicomTag& tag) const;
 
+    bool ParseFirstFloat(float& result,
+                         const DicomTag& tag) const;
+
     bool ParseDouble(double& result,
                      const DicomTag& tag) const;
 
--- a/Core/DicomFormat/DicomValue.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomFormat/DicomValue.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -231,6 +231,11 @@
     return ParseValue<double, true>(result, *this);
   }
 
+  bool DicomValue::ParseFirstFloat(float& result) const
+  {
+    return ParseFirstValue<float, true>(result, *this);
+  }
+
   bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const
   {
     return ParseFirstValue<unsigned int, true>(result, *this);
--- a/Core/DicomFormat/DicomValue.h	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomFormat/DicomValue.h	Wed Feb 26 10:39:55 2020 +0100
@@ -112,6 +112,8 @@
 
     bool ParseDouble(double& result) const;
 
+    bool ParseFirstFloat(float& result) const;
+
     bool ParseFirstUnsignedInteger(unsigned int& result) const;
 
     void Serialize(Json::Value& target) const;
--- a/Core/DicomParsing/DicomModification.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomParsing/DicomModification.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -347,7 +347,7 @@
 
     dicom.Replace(*tag, mapped, 
                   false /* don't try and decode data URI scheme for UIDs */, 
-                  DicomReplaceMode_InsertIfAbsent);
+                  DicomReplaceMode_InsertIfAbsent, privateCreator_);
   }
 
   
@@ -359,6 +359,7 @@
     keepSeriesInstanceUid_(false),
     updateReferencedRelationships_(true),
     isAnonymization_(false),
+    //privateCreator_("PrivateCreator"),
     identifierGenerator_(NULL)
   {
   }
@@ -1067,7 +1068,8 @@
     for (Replacements::const_iterator it = replacements_.begin(); 
          it != replacements_.end(); ++it)
     {
-      toModify.Replace(it->first, *it->second, true /* decode data URI scheme */, DicomReplaceMode_InsertIfAbsent);
+      toModify.Replace(it->first, *it->second, true /* decode data URI scheme */,
+                       DicomReplaceMode_InsertIfAbsent, privateCreator_);
     }
 
     // (6) Update the DICOM identifiers
@@ -1262,6 +1264,12 @@
     {
       ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force);
     }
+
+    // New in Orthanc 1.6.0
+    if (request.isMember("PrivateCreator"))
+    {
+      privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
+    }
   }
 
 
@@ -1336,6 +1344,7 @@
   static const char* MAP_STUDIES = "MapStudies";
   static const char* MAP_SERIES = "MapSeries";
   static const char* MAP_INSTANCES = "MapInstances";
+  static const char* PRIVATE_CREATOR = "PrivateCreator";  // New in Orthanc 1.6.0
   
   void DicomModification::Serialize(Json::Value& value) const
   {
@@ -1353,6 +1362,7 @@
     value[KEEP_SERIES_INSTANCE_UID] = keepSeriesInstanceUid_;
     value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_;
     value[IS_ANONYMIZATION] = isAnonymization_;
+    value[PRIVATE_CREATOR] = privateCreator_;
 
     SerializationToolbox::WriteSetOfTags(value, removals_, REMOVALS);
     SerializationToolbox::WriteSetOfTags(value, clearings_, CLEARINGS);
@@ -1451,6 +1461,11 @@
       (serialized, UPDATE_REFERENCED_RELATIONSHIPS);
     isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
 
+    if (serialized.isMember(PRIVATE_CREATOR))
+    {
+      privateCreator_ = SerializationToolbox::ReadString(serialized, PRIVATE_CREATOR);
+    }
+
     SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
     SerializationToolbox::ReadSetOfTags(clearings_, serialized, CLEARINGS);
     SerializationToolbox::ReadSetOfTags(privateTagsToKeep_, serialized, PRIVATE_TAGS_TO_KEEP);
--- a/Core/DicomParsing/DicomModification.h	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomParsing/DicomModification.h	Wed Feb 26 10:39:55 2020 +0100
@@ -86,6 +86,7 @@
     bool updateReferencedRelationships_;
     bool isAnonymization_;
     DicomMap currentSource_;
+    std::string privateCreator_;
 
     IDicomIdentifierGenerator* identifierGenerator_;
 
@@ -185,5 +186,15 @@
     }
 
     void Serialize(Json::Value& value) const;
+
+    void SetPrivateCreator(std::string& privateCreator)
+    {
+      privateCreator_ = privateCreator;
+    }
+
+    const std::string& GetPrivateCreator()
+    {
+      return privateCreator_;
+    }
   };
 }
--- a/Core/DicomParsing/FromDcmtkBridge.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -115,6 +115,16 @@
 
 namespace Orthanc
 {
+  static bool IsBinaryTag(const DcmTag& key)
+  {
+    return (key.isUnknownVR() ||
+            key.getEVR() == EVR_OB ||
+            key.getEVR() == EVR_OW ||
+            key.getEVR() == EVR_UN ||
+            key.getEVR() == EVR_ox);
+  }
+
+
 #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
   static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary,
                                      EmbeddedResources::FileResourceId resource)
@@ -938,18 +948,13 @@
       if (!(flags & DicomToJsonFlags_IncludeUnknownTags))
       {
         DictionaryLocker locker;
-        if (locker->findEntry(element->getTag(), NULL) == NULL)
+        if (locker->findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL)
         {
           continue;
         }
       }
 
-      DcmEVR evr = element->getTag().getEVR();
-      if (evr == EVR_OB ||
-          evr == EVR_OF ||
-          evr == EVR_OW ||
-          evr == EVR_UN ||
-          evr == EVR_ox)
+      if (IsBinaryTag(element->getTag()))
       {
         // This is a binary tag
         if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) ||
@@ -1371,189 +1376,49 @@
   }
 
 
-  static bool IsBinaryTag(const DcmTag& key)
+  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag,
+                                                   const std::string& privateCreator)
   {
-    return (key.isUnknownVR() ||
-#if DCMTK_VERSION_NUMBER >= 361
-            key.getEVR() == EVR_OD ||
-#endif
-            
+    if (tag.IsPrivate() &&
+        privateCreator.empty())
+    {
+      // This solves issue 140 (Modifying private tags with REST API
+      // changes VR from LO to UN)
+      // https://bitbucket.org/sjodogne/orthanc/issues/140
+      LOG(WARNING) << "Private creator should not be empty while creating a private tag: " << tag.Format();
+    }
+    
 #if DCMTK_VERSION_NUMBER >= 362
-            key.getEVR() == EVR_OL ||
-#endif            
-            key.getEVR() == EVR_OB ||
-            key.getEVR() == EVR_OF ||
-            key.getEVR() == EVR_OW ||
-            key.getEVR() == EVR_UN ||
-            key.getEVR() == EVR_ox);
-  }
-
-
-  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag)
-  {
     DcmTag key(tag.GetGroup(), tag.GetElement());
-
-    if (tag.IsPrivate() ||
-        IsBinaryTag(key))
+    if (tag.IsPrivate())
+    {
+      return DcmItem::newDicomElement(key, privateCreator.c_str());
+    }
+    else
     {
+      return DcmItem::newDicomElement(key, NULL);
+    }
+    
+#else
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+    if (tag.IsPrivate())
+    {
+      // https://forum.dcmtk.org/viewtopic.php?t=4527
+      LOG(WARNING) << "You are using DCMTK <= 3.6.0: All the private tags "
+        "are considered as having a binary value representation";
+      key.setPrivateCreator(privateCreator.c_str());
       return new DcmOtherByteOtherWord(key);
     }
-
-    switch (key.getEVR())
+    else
     {
-      // http://support.dcmtk.org/docs/dcvr_8h-source.html
-
-      /**
-       * Binary types, handled above
-       **/
-    
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_OD:
-#endif            
-
-#if DCMTK_VERSION_NUMBER >= 362
-      case EVR_OL:
-#endif            
-
-      case EVR_OB:  // other byte
-      case EVR_OF:  // other float
-      case EVR_OW:  // other word
-      case EVR_UN:  // unknown value representation
-      case EVR_ox:  // OB or OW depending on context
-        throw OrthancException(ErrorCode_InternalError);
-
-
-      /**
-       * String types.
-       * http://support.dcmtk.org/docs/classDcmByteString.html
-       **/
-      
-      case EVR_AS:  // age string
-        return new DcmAgeString(key);
-
-      case EVR_AE:  // application entity title
-        return new DcmApplicationEntity(key);
-
-      case EVR_CS:  // code string
-        return new DcmCodeString(key);        
-
-      case EVR_DA:  // date string
-        return new DcmDate(key);
-        
-      case EVR_DT:  // date time string
-        return new DcmDateTime(key);
-
-      case EVR_DS:  // decimal string
-        return new DcmDecimalString(key);
-
-      case EVR_IS:  // integer string
-        return new DcmIntegerString(key);
-
-      case EVR_TM:  // time string
-        return new DcmTime(key);
-
-      case EVR_UI:  // unique identifier
-        return new DcmUniqueIdentifier(key);
-
-      case EVR_ST:  // short text
-        return new DcmShortText(key);
-
-      case EVR_LO:  // long string
-        return new DcmLongString(key);
-
-      case EVR_LT:  // long text
-        return new DcmLongText(key);
-
-      case EVR_UT:  // unlimited text
-        return new DcmUnlimitedText(key);
-
-      case EVR_SH:  // short string
-        return new DcmShortString(key);
-
-      case EVR_PN:  // person name
-        return new DcmPersonName(key);
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_UC:  // unlimited characters
-        return new DcmUnlimitedCharacters(key);
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_UR:  // URI/URL
-        return new DcmUniversalResourceIdentifierOrLocator(key);
-#endif
-          
-        
-      /**
-       * Numerical types
-       **/ 
-      
-      case EVR_SL:  // signed long
-        return new DcmSignedLong(key);
-
-      case EVR_SS:  // signed short
-        return new DcmSignedShort(key);
-
-      case EVR_UL:  // unsigned long
-        return new DcmUnsignedLong(key);
-
-      case EVR_US:  // unsigned short
-        return new DcmUnsignedShort(key);
-
-      case EVR_FL:  // float single-precision
-        return new DcmFloatingPointSingle(key);
-
-      case EVR_FD:  // float double-precision
-        return new DcmFloatingPointDouble(key);
-
-
-      /**
-       * Sequence types, should never occur at this point.
-       **/
-
-      case EVR_SQ:  // sequence of items
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-
-
-      /**
-       * TODO
-       **/
-
-      case EVR_AT:  // attribute tag
-        throw OrthancException(ErrorCode_NotImplemented);
-
-
-      /**
-       * Internal to DCMTK.
-       **/ 
-
-      case EVR_xs:  // SS or US depending on context
-      case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
-      case EVR_na:  // na="not applicable", for data which has no VR
-      case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
-      case EVR_item:  // used internally for items
-      case EVR_metainfo:  // used internally for meta info datasets
-      case EVR_dataset:  // used internally for datasets
-      case EVR_fileFormat:  // used internally for DICOM files
-      case EVR_dicomDir:  // used internally for DICOMDIR objects
-      case EVR_dirRecord:  // used internally for DICOMDIR records
-      case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
-      case EVR_pixelItem:  // used internally for pixel items in a compressed image
-      case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
-      case EVR_PixelData:  // used internally for uncompressed pixeld data
-      case EVR_OverlayData:  // used internally for overlay data
-      case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
-      default:
-        break;
+      return newDicomElement(key);
     }
-
-    throw OrthancException(ErrorCode_InternalError);          
+#endif      
   }
 
 
 
   void FromDcmtkBridge::FillElementWithString(DcmElement& element,
-                                              const DicomTag& tag,
                                               const std::string& utf8Value,
                                               bool decodeDataUriScheme,
                                               Encoding dicomEncoding)
@@ -1578,14 +1443,11 @@
       decoded = &binary;
     }
 
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-
-    if (tag.IsPrivate() ||
-        IsBinaryTag(key))
+    if (IsBinaryTag(element.getTag()))
     {
       bool ok;
 
-      switch (key.getEVR())
+      switch (element.getTag().getEVR())
       {
         case EVR_OW:
           if (decoded->size() % sizeof(Uint16) != 0)
@@ -1619,7 +1481,7 @@
     
     try
     {
-      switch (key.getEVR())
+      switch (element.getTag().getEVR())
       {
         // http://support.dcmtk.org/docs/dcvr_8h-source.html
 
@@ -1628,7 +1490,6 @@
          **/
 
         case EVR_OB:  // other byte
-        case EVR_OF:  // other float
         case EVR_OW:  // other word
         case EVR_AT:  // attribute tag
           throw OrthancException(ErrorCode_NotImplemented);
@@ -1683,6 +1544,9 @@
         }
 
         case EVR_UL:  // unsigned long
+#if DCMTK_VERSION_NUMBER >= 362
+        case EVR_OL:  // other long (requires byte-swapping)
+#endif
         {
           ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good();
           break;
@@ -1695,12 +1559,16 @@
         }
 
         case EVR_FL:  // float single-precision
+        case EVR_OF:  // other float (requires byte swapping)
         {
           ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
           break;
         }
 
         case EVR_FD:  // float double-precision
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_OD:  // other double (requires byte-swapping)
+#endif
         {
           ok = element.putFloat64(boost::lexical_cast<double>(*decoded)).good();
           break;
@@ -1750,6 +1618,7 @@
 
     if (!ok)
     {
+      DicomTag tag(element.getTag().getGroup(), element.getTag().getElement());
       throw OrthancException(ErrorCode_BadFileFormat,
                              "While creating a DICOM instance, tag (" + tag.Format() +
                              ") has out-of-range value: \"" + (*decoded) + "\"");
@@ -1760,20 +1629,21 @@
   DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag,
                                         const Json::Value& value,
                                         bool decodeDataUriScheme,
-                                        Encoding dicomEncoding)
+                                        Encoding dicomEncoding,
+                                        const std::string& privateCreator)
   {
     std::auto_ptr<DcmElement> element;
 
     switch (value.type())
     {
       case Json::stringValue:
-        element.reset(CreateElementForTag(tag));
-        FillElementWithString(*element, tag, value.asString(), decodeDataUriScheme, dicomEncoding);
+        element.reset(CreateElementForTag(tag, privateCreator));
+        FillElementWithString(*element, value.asString(), decodeDataUriScheme, dicomEncoding);
         break;
 
       case Json::nullValue:
-        element.reset(CreateElementForTag(tag));
-        FillElementWithString(*element, tag, "", decodeDataUriScheme, dicomEncoding);
+        element.reset(CreateElementForTag(tag, privateCreator));
+        FillElementWithString(*element, "", decodeDataUriScheme, dicomEncoding);
         break;
 
       case Json::arrayValue:
@@ -1798,7 +1668,7 @@
               Json::Value::Members members = value[i].getMemberNames();
               for (Json::Value::ArrayIndex j = 0; j < members.size(); j++)
               {
-                item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding));
+                item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding, privateCreator));
               }
               break;
             }
@@ -1907,7 +1777,8 @@
   DcmDataset* FromDcmtkBridge::FromJson(const Json::Value& json,  // Encoded using UTF-8
                                         bool generateIdentifiers,
                                         bool decodeDataUriScheme,
-                                        Encoding defaultEncoding)
+                                        Encoding defaultEncoding,
+                                        const std::string& privateCreator)
   {
     std::auto_ptr<DcmDataset> result(new DcmDataset);
     Encoding encoding = ExtractEncoding(json, defaultEncoding);
@@ -1945,7 +1816,7 @@
 
       if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
       {
-        std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding));
+        std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
         const DcmTagKey& tag = element->getTag();
 
         result->findAndDeleteElement(tag);
@@ -2268,13 +2139,6 @@
      **/
 
     if (evr == EVR_OB ||  // other byte
-        evr == EVR_OF ||  // other float
-#if DCMTK_VERSION_NUMBER >= 361
-        evr == EVR_OD ||  // other double
-#endif
-#if DCMTK_VERSION_NUMBER >= 362
-        evr == EVR_OL ||  // other long
-#endif
         evr == EVR_OW ||  // other word
         evr == EVR_UN)    // unknown value representation
     {
@@ -2464,6 +2328,9 @@
         }
 
         case EVR_UL:  // unsigned long
+#if DCMTK_VERSION_NUMBER >= 362
+        case EVR_OL:
+#endif
         {
           DcmUnsignedLong& content = dynamic_cast<DcmUnsignedLong&>(element);
 
@@ -2504,6 +2371,7 @@
         }
 
         case EVR_FL:  // float single-precision
+        case EVR_OF:
         {
           DcmFloatingPointSingle& content = dynamic_cast<DcmFloatingPointSingle&>(element);
 
@@ -2524,6 +2392,9 @@
         }
 
         case EVR_FD:  // float double-precision
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_OD:
+#endif
         {
           DcmFloatingPointDouble& content = dynamic_cast<DcmFloatingPointDouble&>(element);
 
--- a/Core/DicomParsing/FromDcmtkBridge.h	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomParsing/FromDcmtkBridge.h	Wed Feb 26 10:39:55 2020 +0100
@@ -209,10 +209,10 @@
 
     static ValueRepresentation LookupValueRepresentation(const DicomTag& tag);
 
-    static DcmElement* CreateElementForTag(const DicomTag& tag);
+    static DcmElement* CreateElementForTag(const DicomTag& tag,
+                                           const std::string& privateCreator);
     
     static void FillElementWithString(DcmElement& element,
-                                      const DicomTag& tag,
                                       const std::string& utf8alue,  // Encoded using UTF-8
                                       bool decodeDataUriScheme,
                                       Encoding dicomEncoding);
@@ -220,7 +220,8 @@
     static DcmElement* FromJson(const DicomTag& tag,
                                 const Json::Value& element,  // Encoded using UTF-8
                                 bool decodeDataUriScheme,
-                                Encoding dicomEncoding);
+                                Encoding dicomEncoding,
+                                const std::string& privateCreator);
 
     static DcmPixelSequence* GetPixelSequence(DcmDataset& dataset);
 
@@ -230,7 +231,8 @@
     static DcmDataset* FromJson(const Json::Value& json,  // Encoded using UTF-8
                                 bool generateIdentifiers,
                                 bool decodeDataUriScheme,
-                                Encoding defaultEncoding);
+                                Encoding defaultEncoding,
+                                const std::string& privateCreator);
 
     static DcmFileFormat* LoadFromMemoryBuffer(const void* buffer,
                                                size_t size);
--- a/Core/DicomParsing/ITagVisitor.h	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomParsing/ITagVisitor.h	Wed Feb 26 10:39:55 2020 +0100
@@ -71,7 +71,7 @@
                                ValueRepresentation vr,
                                const std::vector<int64_t>& values) = 0;
 
-    // FL, FD
+    // FL, FD, OD, OF
     virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
                               const std::vector<size_t>& parentIndexes,
                               const DicomTag& tag,
@@ -84,7 +84,7 @@
                                  const DicomTag& tag,
                                  const std::vector<DicomTag>& values) = 0;
 
-    // OB, OD, OF, OL, OW, UN
+    // OB, OL, OW, UN
     virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
                              const std::vector<size_t>& parentIndexes,
                              const DicomTag& tag,
--- a/Core/DicomParsing/ParsedDicomFile.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomParsing/ParsedDicomFile.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -619,7 +619,8 @@
 
   void ParsedDicomFile::Insert(const DicomTag& tag,
                                const Json::Value& value,
-                               bool decodeDataUriScheme)
+                               bool decodeDataUriScheme,
+                               const std::string& privateCreator)
   {
     if (tag.GetElement() == 0x0000)
     {
@@ -648,11 +649,38 @@
 
     bool hasCodeExtensions;
     Encoding encoding = DetectEncoding(hasCodeExtensions);
-    std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding));
+    std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
     InsertInternal(*pimpl_->file_->getDataset(), element.release());
   }
 
 
+  void ParsedDicomFile::ReplacePlainString(const DicomTag& tag,
+                                           const std::string& utf8Value)
+  {
+    if (tag.IsPrivate())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot apply this function to private tags: " + tag.Format());
+    }
+    else
+    {
+      Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent,
+              "" /* not a private tag, so no private creator */);
+    }
+  }
+
+
+  void ParsedDicomFile::SetIfAbsent(const DicomTag& tag,
+                                    const std::string& utf8Value)
+  {
+    std::string currentValue;
+    if (!GetTagValue(currentValue, tag))
+    {
+      ReplacePlainString(tag, utf8Value);
+    }
+  }
+
+
   static bool CanReplaceProceed(DcmDataset& dicom,
                                 const DcmTagKey& tag,
                                 DicomReplaceMode mode)
@@ -742,7 +770,8 @@
   void ParsedDicomFile::Replace(const DicomTag& tag,
                                 const std::string& utf8Value,
                                 bool decodeDataUriScheme,
-                                DicomReplaceMode mode)
+                                DicomReplaceMode mode,
+                                const std::string& privateCreator)
   {
     if (tag.GetElement() == 0x0000)
     {
@@ -769,13 +798,13 @@
         }
       }
 
-      std::auto_ptr<DcmElement> element(FromDcmtkBridge::CreateElementForTag(tag));
+      std::auto_ptr<DcmElement> element(FromDcmtkBridge::CreateElementForTag(tag, privateCreator));
 
       if (!utf8Value.empty())
       {
         bool hasCodeExtensions;
         Encoding encoding = DetectEncoding(hasCodeExtensions);
-        FromDcmtkBridge::FillElementWithString(*element, tag, utf8Value, decodeDataUriScheme, encoding);
+        FromDcmtkBridge::FillElementWithString(*element, utf8Value, decodeDataUriScheme, encoding);
       }
 
       InsertInternal(dicom, element.release());
@@ -787,7 +816,8 @@
   void ParsedDicomFile::Replace(const DicomTag& tag,
                                 const Json::Value& value,
                                 bool decodeDataUriScheme,
-                                DicomReplaceMode mode)
+                                DicomReplaceMode mode,
+                                const std::string& privateCreator)
   {
     if (tag.GetElement() == 0x0000)
     {
@@ -817,7 +847,7 @@
 
       bool hasCodeExtensions;
       Encoding encoding = DetectEncoding(hasCodeExtensions);
-      InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding));
+      InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
 
       if (tag == DICOM_TAG_SOP_CLASS_UID ||
           tag == DICOM_TAG_SOP_INSTANCE_UID)
@@ -1483,7 +1513,8 @@
 
 
   ParsedDicomFile* ParsedDicomFile::CreateFromJson(const Json::Value& json,
-                                                   DicomFromJsonFlags flags)
+                                                   DicomFromJsonFlags flags,
+                                                   const std::string& privateCreator)
   {
     const bool generateIdentifiers = (flags & DicomFromJsonFlags_GenerateIdentifiers) ? true : false;
     const bool decodeDataUriScheme = (flags & DicomFromJsonFlags_DecodeDataUriScheme) ? true : false;
@@ -1512,7 +1543,7 @@
       }
       else if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
       {
-        result->Replace(tag, value, decodeDataUriScheme, DicomReplaceMode_InsertIfAbsent);
+        result->Replace(tag, value, decodeDataUriScheme, DicomReplaceMode_InsertIfAbsent, privateCreator);
       }
     }
 
--- a/Core/DicomParsing/ParsedDicomFile.h	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/DicomParsing/ParsedDicomFile.h	Wed Feb 26 10:39:55 2020 +0100
@@ -138,32 +138,27 @@
     void Replace(const DicomTag& tag,
                  const std::string& utf8Value,
                  bool decodeDataUriScheme,
-                 DicomReplaceMode mode);
+                 DicomReplaceMode mode,
+                 const std::string& privateCreator /* used only for private tags */);
 
     void Replace(const DicomTag& tag,
                  const Json::Value& value,  // Assumed to be encoded with UTF-8
                  bool decodeDataUriScheme,
-                 DicomReplaceMode mode);
+                 DicomReplaceMode mode,
+                 const std::string& privateCreator /* used only for private tags */);
 
     void Insert(const DicomTag& tag,
                 const Json::Value& value,   // Assumed to be encoded with UTF-8
-                bool decodeDataUriScheme);
+                bool decodeDataUriScheme,
+                const std::string& privateCreator /* used only for private tags */);
 
+    // Cannot be applied to private tags
     void ReplacePlainString(const DicomTag& tag,
-                            const std::string& utf8Value)
-    {
-      Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent);
-    }
+                            const std::string& utf8Value);
 
+    // Cannot be applied to private tags
     void SetIfAbsent(const DicomTag& tag,
-                     const std::string& utf8Value)
-    {
-      std::string currentValue;
-      if (!GetTagValue(currentValue, tag))
-      {
-        ReplacePlainString(tag, utf8Value);
-      }
-    }
+                     const std::string& utf8Value);
 
     void RemovePrivateTags()
     {
@@ -234,7 +229,8 @@
     unsigned int GetFramesCount() const;
 
     static ParsedDicomFile* CreateFromJson(const Json::Value& value,
-                                           DicomFromJsonFlags flags);
+                                           DicomFromJsonFlags flags,
+                                           const std::string& privateCreator);
 
     void ChangeEncoding(Encoding target);
 
--- a/Core/Images/ImageProcessing.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/Images/ImageProcessing.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -41,11 +41,11 @@
 
 #ifdef __EMSCRIPTEN__
 /* 
-Avoid this error:
------------------
-.../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits<long long>::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion]
-.../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround<float>' requested here
-.../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal<unsigned char, true>' requested here
+   Avoid this error:
+   -----------------
+   .../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits<long long>::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion]
+   .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround<float>' requested here
+   .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal<unsigned char, true>' requested here
 */
 #pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion"
 #endif 
@@ -385,28 +385,47 @@
   }
 
 
-  template <typename PixelType,
-            bool UseRound>
-  static void ShiftScaleInternal(ImageAccessor& image,
-                                 float offset,
-                                 float scaling,
-                                 const PixelType LowestValue = std::numeric_limits<PixelType>::min())
+  // Computes "a * x + b" at each pixel => Note that this is not the
+  // same convention as in "ShiftScale()"
+  template <typename TargetType,
+            typename SourceType,
+            bool UseRound,
+            bool Invert>
+  static void ShiftScaleInternal(ImageAccessor& target,
+                                 const ImageAccessor& source,
+                                 float a,
+                                 float b,
+                                 const TargetType LowestValue)
+  // This function can be applied inplace (source == target)
   {
-    const PixelType minPixelValue = LowestValue;
-    const PixelType maxPixelValue = std::numeric_limits<PixelType>::max();
+    if (source.GetWidth() != target.GetWidth() ||
+        source.GetHeight() != target.GetHeight())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageSize);
+    }
+
+    if (&source == &target &&
+        source.GetFormat() != target.GetFormat())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat);
+    }
+    
+    const TargetType minPixelValue = LowestValue;
+    const TargetType maxPixelValue = std::numeric_limits<TargetType>::max();
     const float minFloatValue = static_cast<float>(LowestValue);
     const float maxFloatValue = static_cast<float>(maxPixelValue);
 
-    const unsigned int height = image.GetHeight();
-    const unsigned int width = image.GetWidth();
+    const unsigned int height = target.GetHeight();
+    const unsigned int width = target.GetWidth();
     
     for (unsigned int y = 0; y < height; y++)
     {
-      PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
+      TargetType* p = reinterpret_cast<TargetType*>(target.GetRow(y));
+      const SourceType* q = reinterpret_cast<const SourceType*>(source.GetRow(y));
 
-      for (unsigned int x = 0; x < width; x++, p++)
+      for (unsigned int x = 0; x < width; x++, p++, q++)
       {
-        float v = (static_cast<float>(*p) + offset) * scaling;
+        float v = a * static_cast<float>(*q) + b;
 
         if (v >= maxFloatValue)
         {
@@ -419,11 +438,16 @@
         else if (UseRound)
         {
           // The "round" operation is very costly
-          *p = static_cast<PixelType>(boost::math::iround(v));
+          *p = static_cast<TargetType>(boost::math::iround(v));
         }
         else
         {
-          *p = static_cast<PixelType>(v);
+          *p = static_cast<TargetType>(std::floor(v));
+        }
+
+        if (Invert)
+        {
+          *p = maxPixelValue - *p;
         }
       }
     }
@@ -498,57 +522,39 @@
                                      float rescaleIntercept,
                                      bool invert)
   {
+    assert(sizeof(SourceType) == source.GetBytesPerPixel() &&
+           sizeof(TargetType) == target.GetBytesPerPixel());
+    
     // WARNING - "::min()" should be replaced by "::lowest()" if
     // dealing with float or double (which is not the case so far)
     assert(sizeof(TargetType) <= 2);  // Safeguard to remember about "float/double"
     const TargetType minTargetValue = std::numeric_limits<TargetType>::min();
     const TargetType maxTargetValue = std::numeric_limits<TargetType>::max();
-    const float minFloatValue = static_cast<float>(minTargetValue);
     const float maxFloatValue = static_cast<float>(maxTargetValue);
-
+    
     const float windowIntercept = windowCenter - windowWidth / 2.0f;
     const float windowSlope = (maxFloatValue + 1.0f) / windowWidth;
 
-    const unsigned int width = source.GetWidth();
-    const unsigned int height = source.GetHeight();
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y));
-      const SourceType* s = reinterpret_cast<const SourceType*>(source.GetConstRow(y));
+    const float a = rescaleSlope * windowSlope;
+    const float b = (rescaleIntercept - windowIntercept) * windowSlope;
 
-      for (unsigned int x = 0; x < width; x++, t++, s++)
-      {
-        float rescaledValue = *s * rescaleSlope + rescaleIntercept;
-        float v = (rescaledValue - windowIntercept) * windowSlope;
-        if (v <= minFloatValue)
-        {
-          v = minFloatValue;
-        }
-        else if (v >= maxFloatValue)
-        {
-          v = maxFloatValue;
-        }
-
-        TargetType vv = static_cast<TargetType>(v);
-
-        if (invert)
-        {
-          vv = maxTargetValue - vv;
-        }
-
-        *t = static_cast<TargetType>(vv);
-      }
+    if (invert)
+    {
+      ShiftScaleInternal<TargetType, SourceType, false, true>(target, source, a, b, minTargetValue);
+    }
+    else
+    {
+      ShiftScaleInternal<TargetType, SourceType, false, false>(target, source, a, b, minTargetValue);
     }
   }
 
-  void ImageProcessing::ApplyWindowing(ImageAccessor& target,
-                                       const ImageAccessor& source,
-                                       float windowCenter,
-                                       float windowWidth,
-                                       float rescaleSlope,
-                                       float rescaleIntercept,
-                                       bool invert)
+  void ImageProcessing::ApplyWindowing_Deprecated(ImageAccessor& target,
+                                                  const ImageAccessor& source,
+                                                  float windowCenter,
+                                                  float windowWidth,
+                                                  float rescaleSlope,
+                                                  float rescaleIntercept,
+                                                  bool invert)
   {
     if (target.GetWidth() != source.GetWidth() ||
         target.GetHeight() != source.GetHeight())
@@ -562,42 +568,42 @@
       {
         switch (target.GetFormat())
         {
-        case Orthanc::PixelFormat_Grayscale8:
-          ApplyWindowingInternal<uint8_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-          break;
-        case Orthanc::PixelFormat_Grayscale16:
-          ApplyWindowingInternal<uint16_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-          break;
-        default:
-          throw OrthancException(ErrorCode_NotImplemented);
+          case Orthanc::PixelFormat_Grayscale8:
+            ApplyWindowingInternal<uint8_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          case Orthanc::PixelFormat_Grayscale16:
+            ApplyWindowingInternal<uint16_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
         }
       };break;
       case Orthanc::PixelFormat_Grayscale8:
       {
         switch (target.GetFormat())
         {
-        case Orthanc::PixelFormat_Grayscale8:
-          ApplyWindowingInternal<uint8_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-          break;
-        case Orthanc::PixelFormat_Grayscale16:
-          ApplyWindowingInternal<uint16_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-          break;
-        default:
-          throw OrthancException(ErrorCode_NotImplemented);
+          case Orthanc::PixelFormat_Grayscale8:
+            ApplyWindowingInternal<uint8_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          case Orthanc::PixelFormat_Grayscale16:
+            ApplyWindowingInternal<uint16_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
         }
       };break;
       case Orthanc::PixelFormat_Grayscale16:
       {
         switch (target.GetFormat())
         {
-        case Orthanc::PixelFormat_Grayscale8:
-          ApplyWindowingInternal<uint8_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-          break;
-        case Orthanc::PixelFormat_Grayscale16:
-          ApplyWindowingInternal<uint16_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-          break;
-        default:
-          throw OrthancException(ErrorCode_NotImplemented);
+          case Orthanc::PixelFormat_Grayscale8:
+            ApplyWindowingInternal<uint8_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          case Orthanc::PixelFormat_Grayscale16:
+            ApplyWindowingInternal<uint16_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
         }
       };break;
       default:
@@ -1181,7 +1187,7 @@
         ShiftRightInternal<uint16_t>(image, shift);
         break;
       }
-    default:
+      default:
         throw OrthancException(ErrorCode_NotImplemented);
     }
   }
@@ -1210,7 +1216,7 @@
         ShiftLeftInternal<uint16_t>(image, shift);
         break;
       }
-    default:
+      default:
         throw OrthancException(ErrorCode_NotImplemented);
     }
   }
@@ -1366,49 +1372,55 @@
                                    float scaling,
                                    bool useRound)
   {
+    // Rewrite "(x + offset) * scaling" as "a * x + b"
+
+    const float a = scaling;
+    const float b = offset * scaling;
+    
     switch (image.GetFormat())
     {
       case PixelFormat_Grayscale8:
         if (useRound)
         {
-          ShiftScaleInternal<uint8_t, true>(image, offset, scaling);
+          ShiftScaleInternal<uint8_t, uint8_t, true, false>(image, image, a, b, std::numeric_limits<uint8_t>::min());
         }
         else
         {
-          ShiftScaleInternal<uint8_t, false>(image, offset, scaling);
+          ShiftScaleInternal<uint8_t, uint8_t, false, false>(image, image, a, b, std::numeric_limits<uint8_t>::min());
         }
         return;
 
       case PixelFormat_Grayscale16:
         if (useRound)
         {
-          ShiftScaleInternal<uint16_t, true>(image, offset, scaling);
+          ShiftScaleInternal<uint16_t, uint16_t, true, false>(image, image, a, b, std::numeric_limits<uint16_t>::min());
         }
         else
         {
-          ShiftScaleInternal<uint16_t, false>(image, offset, scaling);
+          ShiftScaleInternal<uint16_t, uint16_t, false, false>(image, image, a, b, std::numeric_limits<uint16_t>::min());
         }
         return;
 
       case PixelFormat_SignedGrayscale16:
         if (useRound)
         {
-          ShiftScaleInternal<int16_t, true>(image, offset, scaling);
+          ShiftScaleInternal<int16_t, int16_t, true, false>(image, image, a, b, std::numeric_limits<int16_t>::min());
         }
         else
         {
-          ShiftScaleInternal<int16_t, false>(image, offset, scaling);
+          ShiftScaleInternal<int16_t, int16_t, false, false>(image, image, a, b, std::numeric_limits<int16_t>::min());
         }
         return;
 
       case PixelFormat_Float32:
+        // "::min()" must be replaced by "::lowest()" or "-::max()" if dealing with float or double.
         if (useRound)
         {
-          ShiftScaleInternal<float, true>(image, offset, scaling, -std::numeric_limits<float>::max());
+          ShiftScaleInternal<float, float, true, false>(image, image, a, b, -std::numeric_limits<float>::max());
         }
         else
         {
-          ShiftScaleInternal<float, false>(image, offset, scaling, -std::numeric_limits<float>::max());
+          ShiftScaleInternal<float, float, false, false>(image, image, a, b, -std::numeric_limits<float>::max());
         }
         return;
 
@@ -1418,6 +1430,46 @@
   }
 
 
+  void ImageProcessing::ShiftScale(ImageAccessor& target,
+                                   const ImageAccessor& source,
+                                   float offset,
+                                   float scaling,
+                                   bool useRound)
+  {
+    // Rewrite "(x + offset) * scaling" as "a * x + b"
+
+    const float a = scaling;
+    const float b = offset * scaling;
+    
+    switch (target.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+
+        switch (source.GetFormat())
+        {
+          case PixelFormat_Float32:
+            if (useRound)
+            {
+              ShiftScaleInternal<uint8_t, float, true, false>(
+                target, source, a, b, std::numeric_limits<uint8_t>::min());
+            }
+            else
+            {
+              ShiftScaleInternal<uint8_t, float, false, false>(
+                target, source, a, b, std::numeric_limits<uint8_t>::min());
+            }
+            return;
+
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+        
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
   void ImageProcessing::Invert(ImageAccessor& image, int64_t maxValue)
   {
     const unsigned int width = image.GetWidth();
@@ -2216,7 +2268,7 @@
 
         // Deal with the right border
         for (unsigned int x = static_cast<unsigned int>(
-          horizontalAnchor + width - horizontal.size() + 1); x < width; x++)
+               horizontalAnchor + width - horizontal.size() + 1); x < width; x++)
         {
           for (unsigned int c = 0; c < ChannelsCount; c++, p++)
           {
--- a/Core/Images/ImageProcessing.h	Thu Feb 20 20:36:47 2020 +0100
+++ b/Core/Images/ImageProcessing.h	Wed Feb 26 10:39:55 2020 +0100
@@ -82,13 +82,13 @@
     void Convert(ImageAccessor& target,
                  const ImageAccessor& source);
 
-    void ApplyWindowing(ImageAccessor& target,
-                        const ImageAccessor& source,
-                        float windowCenter,
-                        float windowWidth,
-                        float rescaleSlope,
-                        float rescaleIntercept,
-                        bool invert);
+    void ApplyWindowing_Deprecated(ImageAccessor& target,
+                                   const ImageAccessor& source,
+                                   float windowCenter,
+                                   float windowWidth,
+                                   float rescaleSlope,
+                                   float rescaleIntercept,
+                                   bool invert);
 
     void Set(ImageAccessor& image,
              int64_t value);
@@ -127,12 +127,18 @@
                           float factor,
                           bool useRound);
 
-    // "useRound" is expensive
+    // Computes "(x + offset) * scaling" inplace. "useRound" is expensive.
     void ShiftScale(ImageAccessor& image,
                     float offset,
                     float scaling,
                     bool useRound);
 
+    void ShiftScale(ImageAccessor& target,
+                    const ImageAccessor& source,
+                    float offset,
+                    float scaling,
+                    bool useRound);
+
     void Invert(ImageAccessor& image);
 
     void Invert(ImageAccessor& image, int64_t maxValue);
--- a/NEWS	Thu Feb 20 20:36:47 2020 +0100
+++ b/NEWS	Wed Feb 26 10:39:55 2020 +0100
@@ -12,9 +12,13 @@
 * API version has been upgraded to 5
 * added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually
   retrieve its version number)
-* /changes: Allow the "limit" argument to be greater than 100
-* /instances/{id}/preview: Now takes the windowing into account
-* /tools/log-level: Possibility to access and change the log level without restarting Orthanc
+* "/changes": Allow the "limit" argument to be greater than 100
+* "/instances/{id}/preview": Now takes the windowing into account
+* "/tools/log-level": Possibility to access and change the log level without restarting Orthanc
+* added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes
+  to render frames, taking windowing and resizing into account
+* "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files
+* ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags
 
 Plugins
 -------
@@ -40,6 +44,7 @@
 * More strict C-FIND SCP wrt. the DICOM standard: Forbid wildcard
   matching on some VRs, ignore main tags below the queried level
 * Fix issue #65 (Logging improvements)
+* Fix issue #140 (Modifying private tags with REST API changes VR from LO to UN)
 * Fix issue #156 (Chunked Dicom-web transfer uses 100% CPU)
 * Fix issue #165 (Boundary parameter in multipart Content-Type is too long)
 * Fix issue #166 (CMake find_boost version is now broken with newer boost/cmake)
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -394,7 +394,8 @@
             content.append(item);
           }
 
-          dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent);
+          dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent,
+                        "" /* no private creator */);
         }
       }
 
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -267,7 +267,8 @@
 
   static void InjectTags(ParsedDicomFile& dicom,
                          const Json::Value& tags,
-                         bool decodeBinaryTags)
+                         bool decodeBinaryTags,
+                         const std::string& privateCreator)
   {
     if (tags.type() != Json::objectValue)
     {
@@ -305,7 +306,7 @@
         }
         else
         {
-          dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent);
+          dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent, privateCreator);
         }
       }
     }
@@ -315,7 +316,8 @@
   static void CreateSeries(RestApiPostCall& call,
                            ParsedDicomFile& base /* in */,
                            const Json::Value& content,
-                           bool decodeBinaryTags)
+                           bool decodeBinaryTags,
+                           const std::string& privateCreator)
   {
     assert(content.isArray());
     assert(content.size() > 0);
@@ -348,7 +350,7 @@
 
           if (content[i].isMember("Tags"))
           {
-            InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags);
+            InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags, privateCreator);
           }
         }
 
@@ -538,6 +540,20 @@
       decodeBinaryTags = v.asBool();
     }
 
+
+    // New argument in Orthanc 1.6.0
+    std::string privateCreator;
+    if (request.isMember("PrivateCreator"))
+    {
+      const Json::Value& v = request["PrivateCreator"];
+      if (v.type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      privateCreator = v.asString();
+    }
+
     
     // Inject time-related information
     std::string date, time;
@@ -565,7 +581,7 @@
     }
 
 
-    InjectTags(dicom, request["Tags"], decodeBinaryTags);
+    InjectTags(dicom, request["Tags"], decodeBinaryTags, privateCreator);
 
 
     // Inject the content (either an image, or a PDF file)
@@ -583,7 +599,7 @@
         if (content.size() > 0)
         {
           // Let's create a series instead of a single instance
-          CreateSeries(call, dicom, content, decodeBinaryTags);
+          CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator);
           return;
         }
       }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -34,11 +34,14 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
+#include "../../Core/Compression/GzipCompressor.h"
 #include "../../Core/Logging.h"
 #include "../../Core/MetricsRegistry.h"
 #include "../../Core/SerializationToolbox.h"
 #include "../ServerContext.h"
 
+#include <boost/algorithm/string/predicate.hpp>
+
 namespace Orthanc
 {
   static void SetupResourceAnswer(Json::Value& result,
@@ -118,13 +121,22 @@
                              "Received an empty DICOM file");
     }
 
-    // TODO Remove unneccessary memcpy
-    std::string postData;
-    call.BodyToString(postData);
+    std::string dicom;
+
+    if (boost::iequals(call.GetHttpHeader("content-encoding", ""), "gzip"))
+    {
+      GzipCompressor compressor;
+      compressor.Uncompress(dicom, call.GetBodyData(), call.GetBodySize());
+    }
+    else
+    {
+      // TODO Remove unneccessary memcpy
+      call.BodyToString(dicom);
+    }
 
     DicomInstanceToStore toStore;
     toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
-    toStore.SetBuffer(postData);
+    toStore.SetBuffer(dicom);
 
     std::string publicId;
     StoreStatus status = context.Store(publicId, toStore);
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -1277,7 +1277,8 @@
         MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
       std::auto_ptr<ParsedDicomFile> query
-        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0)));
+        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0),
+                                         "" /* no private creator */));
 
       DicomFindAnswers answers(true);
 
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -35,10 +35,12 @@
 #include "OrthancRestApi.h"
 
 #include "../../Core/Compression/GzipCompressor.h"
+#include "../../Core/DicomFormat/DicomImageInformation.h"
 #include "../../Core/DicomParsing/DicomWebJsonVisitor.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
 #include "../../Core/HttpServer/HttpContentNegociation.h"
+#include "../../Core/Images/Image.h"
 #include "../../Core/Images/ImageProcessing.h"
 #include "../../Core/Logging.h"
 #include "../DefaultDicomImageDecoder.h"
@@ -50,6 +52,9 @@
 
 #include "../../Plugins/Engine/OrthancPlugins.h"
 
+// This "include" is mandatory for Release builds using Linux Standard Base
+#include <boost/math/special_functions/round.hpp>
+
 
 namespace Orthanc
 {
@@ -503,152 +508,445 @@
   }
 
 
-  void LookupWindowingTags(const ParsedDicomFile& dicom, float& windowCenter, float& windowWidth, float& rescaleSlope, float& rescaleIntercept, bool& invert)
+  namespace
   {
-    DicomMap dicomTags;
-    dicom.ExtractDicomSummary(dicomTags);
+    class IDecodedFrameHandler : public boost::noncopyable
+    {
+    public:
+      virtual ~IDecodedFrameHandler()
+      {
+      }
+
+      virtual void Handle(RestApiGetCall& call,
+                          std::auto_ptr<ImageAccessor>& decoded,
+                          const DicomMap& dicom) = 0;
+
+      virtual bool RequiresDicomTags() const = 0;
+
+      static void Apply(RestApiGetCall& call,
+                        IDecodedFrameHandler& handler)
+      {
+        ServerContext& context = OrthancRestApi::GetContext(call);
+
+        std::string frameId = call.GetUriComponent("frame", "0");
+
+        unsigned int frame;
+        try
+        {
+          frame = boost::lexical_cast<unsigned int>(frameId);
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          return;
+        }
+
+        DicomMap dicom;
+        std::auto_ptr<ImageAccessor> decoded;
+
+        try
+        {
+          std::string publicId = call.GetUriComponent("id", "");
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+          if (context.GetPlugins().HasCustomImageDecoder())
+          {
+            // TODO create a cache of file
+            std::string dicomContent;
+            context.ReadDicom(dicomContent, publicId);
+            decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame));
+
+            /**
+             * Note that we call "DecodeUnsafe()": We do not fallback to
+             * the builtin decoder if no installed decoder plugin is able
+             * to decode the image. This allows us to take advantage of
+             * the cache below.
+             **/
+
+            if (handler.RequiresDicomTags() &&
+                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);
+              parsed.ExtractDicomSummary(dicom);
+            }
+          }
+#endif
+
+          if (decoded.get() == NULL)
+          {
+            // Use Orthanc's built-in decoder, using the cache to speed-up
+            // things on multi-frame images
+            ServerContext::DicomCacheLocker locker(context, publicId);        
+            decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame));
+
+            if (handler.RequiresDicomTags())
+            {
+              locker.GetDicom().ExtractDicomSummary(dicom);
+            }
+          }
+        }
+        catch (OrthancException& e)
+        {
+          if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange ||
+              e.GetErrorCode() == ErrorCode_UnknownResource)
+          {
+            // The frame number is out of the range for this DICOM
+            // instance, the resource is not existent
+          }
+          else
+          {
+            std::string root = "";
+            for (size_t i = 1; i < call.GetFullUri().size(); i++)
+            {
+              root += "../";
+            }
+
+            call.GetOutput().Redirect(root + "app/images/unsupported.png");
+          }
+          return;
+        }
+
+        handler.Handle(call, decoded, dicom);
+      }
+
+
+      static void DefaultHandler(RestApiGetCall& call,
+                                 std::auto_ptr<ImageAccessor>& decoded,
+                                 ImageExtractionMode mode,
+                                 bool invert)
+      {
+        ImageToEncode image(decoded, mode, invert);
+
+        HttpContentNegociation negociation;
+        EncodePng png(image);
+        negociation.Register(MIME_PNG, png);
+
+        EncodeJpeg jpeg(image, call);
+        negociation.Register(MIME_JPEG, jpeg);
+
+        EncodePam pam(image);
+        negociation.Register(MIME_PAM, pam);
+
+        if (negociation.Apply(call.GetHttpHeaders()))
+        {
+          image.Answer(call.GetOutput());
+        }
+      }
+    };
+
+
+    class GetImageHandler : public IDecodedFrameHandler
+    {
+    private:
+      ImageExtractionMode mode_;
+
+    public:
+      GetImageHandler(ImageExtractionMode mode) :
+        mode_(mode)
+      {
+      }
+
+      virtual void Handle(RestApiGetCall& call,
+                          std::auto_ptr<ImageAccessor>& decoded,
+                          const DicomMap& dicom) ORTHANC_OVERRIDE
+      {
+        bool invert = false;
+
+        if (mode_ == ImageExtractionMode_Preview)
+        {
+          DicomImageInformation info(dicom);
+          invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
+        }
+
+        DefaultHandler(call, decoded, mode_, invert);
+      }
+
+      virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
+      {
+        return mode_ == ImageExtractionMode_Preview;
+      }
+    };
 
 
-    unsigned int bitsStored = boost::lexical_cast<unsigned int>(dicomTags.GetStringValue(Orthanc::DICOM_TAG_BITS_STORED, "8", false));
-    windowWidth = static_cast<float>(2 << (bitsStored - 1));
-    windowCenter = windowWidth / 2;
-    rescaleSlope = 1.0f;
-    rescaleIntercept = 0.0f;
-    invert = false;
+    class RenderedFrameHandler : public IDecodedFrameHandler
+    {
+    private:
+      static void GetDicomParameters(bool& invert,
+                                     float& rescaleSlope,
+                                     float& rescaleIntercept,
+                                     float& windowWidth,
+                                     float& windowCenter,
+                                     const DicomMap& dicom)
+      {
+        DicomImageInformation info(dicom);
+
+        invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
+
+        rescaleSlope = 1.0f;
+        rescaleIntercept = 0.0f;
+
+        if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) &&
+            dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT))
+        {
+          dicom.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE);
+          dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT);
+        }
+
+        windowWidth = static_cast<float>(1 << info.GetBitsStored());
+        windowCenter = windowWidth / 2.0f;
+
+        if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) &&
+            dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH))
+        {
+          dicom.ParseFirstFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER);
+          dicom.ParseFirstFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH);
+        }
+      }
+
+      static void GetUserArguments(float& windowWidth /* inout */,
+                                   float& windowCenter /* inout */,
+                                   unsigned int& argWidth,
+                                   unsigned int& argHeight,
+                                   bool& smooth,
+                                   RestApiGetCall& call)
+      {
+        static const char* ARG_WINDOW_CENTER = "window-center";
+        static const char* ARG_WINDOW_WIDTH = "window-width";
+        static const char* ARG_WIDTH = "width";
+        static const char* ARG_HEIGHT = "height";
+        static const char* ARG_SMOOTH = "smooth";
+
+        if (call.HasArgument(ARG_WINDOW_WIDTH))
+        {
+          try
+          {
+            windowWidth = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_WIDTH, ""));
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH));
+          }
+        }
 
-    if (dicomTags.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) && dicomTags.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH))
-    {
-      dicomTags.ParseFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER);
-      dicomTags.ParseFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH);
-    }
+        if (call.HasArgument(ARG_WINDOW_CENTER))
+        {
+          try
+          {
+            windowCenter = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_CENTER, ""));
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Bad value for argument: " + std::string(ARG_WINDOW_CENTER));
+          }
+        }
+
+        argWidth = 0;
+        argHeight = 0;
+
+        if (call.HasArgument(ARG_WIDTH))
+        {
+          try
+          {
+            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_WIDTH, ""));
+            if (tmp < 0)
+            {
+              throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                     "Argument cannot be negative: " + std::string(ARG_WIDTH));
+            }
+            else
+            {
+              argWidth = static_cast<unsigned int>(tmp);
+            }
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Bad value for argument: " + std::string(ARG_WIDTH));
+          }
+        }
+
+        if (call.HasArgument(ARG_HEIGHT))
+        {
+          try
+          {
+            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_HEIGHT, ""));
+            if (tmp < 0)
+            {
+              throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                     "Argument cannot be negative: " + std::string(ARG_HEIGHT));
+            }
+            else
+            {
+              argHeight = static_cast<unsigned int>(tmp);
+            }
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Bad value for argument: " + std::string(ARG_HEIGHT));
+          }
+        }
+
+        smooth = false;
 
-    if (dicomTags.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) && dicomTags.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT))
-    {
-      dicomTags.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE);
-      dicomTags.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT);
-    }
+        if (call.HasArgument(ARG_SMOOTH))
+        {
+          std::string value = call.GetArgument(ARG_SMOOTH, "");
+          if (value == "0" ||
+              value == "false")
+          {
+            smooth = false;
+          }
+          else if (value == "1" ||
+                   value == "true")
+          {
+            smooth = true;
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Argument must be Boolean: " + std::string(ARG_SMOOTH));
+          }
+        }        
+      }
+                                
+      
+    public:
+      virtual void Handle(RestApiGetCall& call,
+                          std::auto_ptr<ImageAccessor>& decoded,
+                          const DicomMap& dicom) ORTHANC_OVERRIDE
+      {
+        bool invert;
+        float rescaleSlope, rescaleIntercept, windowWidth, windowCenter;
+        GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom);
+
+        unsigned int argWidth, argHeight;
+        bool smooth;
+        GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call);
+
+        unsigned int targetWidth = decoded->GetWidth();
+        unsigned int targetHeight = decoded->GetHeight();
+
+        if (decoded->GetWidth() != 0 &&
+            decoded->GetHeight() != 0)
+        {
+          float ratio = 1;
 
-    PhotometricInterpretation photometric;
-    if (dicom.LookupPhotometricInterpretation(photometric))
-    {
-      invert = (photometric == PhotometricInterpretation_Monochrome1);
-    }
+          if (argWidth != 0 &&
+              argHeight != 0)
+          {
+            float ratioX = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
+            float ratioY = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
+            ratio = std::min(ratioX, ratioY);
+          }
+          else if (argWidth != 0)
+          {
+            ratio = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
+          }
+          else if (argHeight != 0)
+          {
+            ratio = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
+          }
+          
+          targetWidth = boost::math::iround(ratio * static_cast<float>(decoded->GetWidth()));
+          targetHeight = boost::math::iround(ratio * static_cast<float>(decoded->GetHeight()));
+        }
+        
+        if (decoded->GetFormat() == PixelFormat_RGB24)
+        {
+          if (targetWidth == decoded->GetWidth() &&
+              targetHeight == decoded->GetHeight())
+          {
+            DefaultHandler(call, decoded, ImageExtractionMode_Preview, false);
+          }
+          else
+          {
+            std::auto_ptr<ImageAccessor> resized(
+              new Image(decoded->GetFormat(), targetWidth, targetHeight, false));
+            
+            if (smooth &&
+                (targetWidth < decoded->GetWidth() ||
+                 targetHeight < decoded->GetHeight()))
+            {
+              ImageProcessing::SmoothGaussian5x5(*decoded);
+            }
+            
+            ImageProcessing::Resize(*resized, *decoded);
+            DefaultHandler(call, resized, ImageExtractionMode_Preview, false);
+          }
+        }
+        else
+        {
+          // Grayscale image: (1) convert to Float32, (2) apply
+          // windowing to get a Grayscale8, (3) possibly resize
+
+          Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false);
+          ImageProcessing::Convert(converted, *decoded);
+
+          // Avoid divisions by zero
+          if (windowWidth <= 1.0f)
+          {
+            windowWidth = 1;
+          }
+
+          if (std::abs(rescaleSlope) <= 0.1f)
+          {
+            rescaleSlope = 0.1f;
+          }
+
+          const float scaling = 255.0f * rescaleSlope / windowWidth;
+          const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope;
+
+          std::auto_ptr<ImageAccessor> rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false));
+          ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false);
+
+          if (targetWidth == decoded->GetWidth() &&
+              targetHeight == decoded->GetHeight())
+          {
+            DefaultHandler(call, rescaled, ImageExtractionMode_UInt8, invert);
+          }
+          else
+          {
+            std::auto_ptr<ImageAccessor> resized(
+              new Image(PixelFormat_Grayscale8, targetWidth, targetHeight, false));
+            
+            if (smooth &&
+                (targetWidth < decoded->GetWidth() ||
+                 targetHeight < decoded->GetHeight()))
+            {
+              ImageProcessing::SmoothGaussian5x5(*rescaled);
+            }
+            
+            ImageProcessing::Resize(*resized, *rescaled);
+            DefaultHandler(call, resized, ImageExtractionMode_UInt8, invert);
+          }
+        }
+      }
+
+      virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
+      {
+        return true;
+      }
+    };
   }
 
+
   template <enum ImageExtractionMode mode>
   static void GetImage(RestApiGetCall& call)
   {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string frameId = call.GetUriComponent("frame", "0");
-
-    unsigned int frame;
-    try
-    {
-      frame = boost::lexical_cast<unsigned int>(frameId);
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      return;
-    }
-
-    bool invert = false;
-    float windowCenter = 128.0f;
-    float windowWidth = 256.0f;
-    float rescaleSlope = 1.0f;
-    float rescaleIntercept = 0.0f;
-
-    std::auto_ptr<ImageAccessor> decoded;
-
-    try
-    {
-      std::string publicId = call.GetUriComponent("id", "");
+    GetImageHandler handler(mode);
+    IDecodedFrameHandler::Apply(call, handler);
+  }
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-      if (context.GetPlugins().HasCustomImageDecoder())
-      {
-        // TODO create a cache of file
-        std::string dicomContent;
-        context.ReadDicom(dicomContent, publicId);
-        decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame));
-
-        /**
-         * Note that we call "DecodeUnsafe()": We do not fallback to
-         * the builtin decoder if no installed decoder plugin is able
-         * 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);
-          
-          LookupWindowingTags(dicomContent, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-        }
-      }
-#endif
 
-      if (decoded.get() == NULL)
-      {
-        // Use Orthanc's built-in decoder, using the cache to speed-up
-        // things on multi-frame images
-        ServerContext::DicomCacheLocker locker(context, publicId);        
-        decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame));
-        LookupWindowingTags(locker.GetDicom(), windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-
-        if (mode != ImageExtractionMode_Preview)
-        {
-          invert = false;
-        }
-      }
-    }
-    catch (OrthancException& e)
-    {
-      if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange || e.GetErrorCode() == ErrorCode_UnknownResource)
-      {
-        // The frame number is out of the range for this DICOM
-        // instance, the resource is not existent
-      }
-      else
-      {
-        std::string root = "";
-        for (size_t i = 1; i < call.GetFullUri().size(); i++)
-        {
-          root += "../";
-        }
-
-        call.GetOutput().Redirect(root + "app/images/unsupported.png");
-      }
-      return;
-    }
-
-    if (mode == ImageExtractionMode_Preview
-        && (decoded->GetFormat() == Orthanc::PixelFormat_Grayscale8 || decoded->GetFormat() == Orthanc::PixelFormat_Grayscale16))
-    {
-      ImageProcessing::ApplyWindowing(*decoded, *decoded, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-      invert = false; // don't invert it later on when encoding it, it has been inverted in the ApplyWindowing function
-    }
-
-    ImageToEncode image(decoded, mode, invert);
-
-    HttpContentNegociation negociation;
-    EncodePng png(image);
-    negociation.Register(MIME_PNG, png);
-
-    EncodeJpeg jpeg(image, call);
-    negociation.Register(MIME_JPEG, jpeg);
-
-    EncodePam pam(image);
-    negociation.Register(MIME_PAM, pam);
-
-    if (negociation.Apply(call.GetHttpHeaders()))
-    {
-      image.Answer(call.GetOutput());
-    }
+  static void GetRenderedFrame(RestApiGetCall& call)
+  {
+    RenderedFrameHandler handler;
+    IDecodedFrameHandler::Apply(call, handler);
   }
 
 
@@ -1802,6 +2100,7 @@
     Register("/instances/{id}/frames", ListFrames);
 
     Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>);
+    Register("/instances/{id}/frames/{frame}/rendered", GetRenderedFrame);
     Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
     Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
     Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>);
@@ -1810,6 +2109,7 @@
     Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame<true>);
     Register("/instances/{id}/pdf", ExtractPdf);
     Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>);
+    Register("/instances/{id}/rendered", GetRenderedFrame);
     Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
     Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
     Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>);
--- a/OrthancServer/Search/HierarchicalMatcher.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/OrthancServer/Search/HierarchicalMatcher.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -268,7 +268,13 @@
       if (source.findAndGetElement(tag, element).good() &&
           element != NULL)
       {
-        std::auto_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it));
+        if (it->IsPrivate())
+        {
+          throw OrthancException(ErrorCode_NotImplemented,
+                                 "Not applicable to private tags: " + it->Format());
+        }
+        
+        std::auto_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, "" /* no private creator */));
         cloned->copyFrom(*element);
         target->insert(cloned.release());
       }
--- a/Plugins/Engine/OrthancPlugins.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/Plugins/Engine/OrthancPlugins.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -931,7 +931,8 @@
           Json::Value target;
           call.ExecuteToJson(target, true);
           
-          filtered_.reset(ParsedDicomFile::CreateFromJson(target, DicomFromJsonFlags_None));
+          filtered_.reset(ParsedDicomFile::CreateFromJson(target, DicomFromJsonFlags_None,
+                                                          "" /* no private creator */));
           currentQuery_ = filtered_.get();
         }
       }
@@ -3049,7 +3050,8 @@
 
     {
       std::auto_ptr<ParsedDicomFile> file
-        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(p.flags)));
+        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(p.flags),
+                                         "" /* TODO - private creator */));
 
       if (p.pixelData)
       {
--- a/Resources/CMake/CivetwebConfiguration.cmake	Thu Feb 20 20:36:47 2020 +0100
+++ b/Resources/CMake/CivetwebConfiguration.cmake	Wed Feb 26 10:39:55 2020 +0100
@@ -30,6 +30,12 @@
     ${CIVETWEB_SOURCES_DIR}/src/civetweb.c
     )
 
+  # New in Orthanc 1.6.0: Enable support of compression in civetweb
+  set_source_files_properties(
+    ${CIVETWEB_SOURCES}
+    PROPERTIES COMPILE_DEFINITIONS
+    "USE_ZLIB=1")
+  
   if (ENABLE_SSL)
     add_definitions(
       -DNO_SSL_DL=1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/FromDcmtkBridge.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -0,0 +1,168 @@
+  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag)
+  {
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+
+    if (tag.IsPrivate())
+    {
+      // This raises BitBucket issue 140 (Modifying private tags with
+      // REST API changes VR from LO to UN)
+      // https://bitbucket.org/sjodogne/orthanc/issues/140
+      LOG(WARNING) << "You are using DCMTK < 3.6.1: All the private tags "
+        "are considered as having a binary value representation";
+      return new DcmOtherByteOtherWord(key);
+    }
+    else if (IsBinaryTag(key))
+    {
+      return new DcmOtherByteOtherWord(key);
+    }
+
+    switch (key.getEVR())
+    {
+      // http://support.dcmtk.org/docs/dcvr_8h-source.html
+
+      /**
+       * Binary types, handled above
+       **/
+    
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_OD:
+#endif            
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EVR_OL:
+#endif            
+
+      case EVR_OB:  // other byte
+      case EVR_OF:  // other float
+      case EVR_OW:  // other word
+      case EVR_UN:  // unknown value representation
+      case EVR_ox:  // OB or OW depending on context
+        throw OrthancException(ErrorCode_InternalError);
+
+
+      /**
+       * String types.
+       * http://support.dcmtk.org/docs/classDcmByteString.html
+       **/
+      
+      case EVR_AS:  // age string
+        return new DcmAgeString(key);
+
+      case EVR_AE:  // application entity title
+        return new DcmApplicationEntity(key);
+
+      case EVR_CS:  // code string
+        return new DcmCodeString(key);        
+
+      case EVR_DA:  // date string
+        return new DcmDate(key);
+        
+      case EVR_DT:  // date time string
+        return new DcmDateTime(key);
+
+      case EVR_DS:  // decimal string
+        return new DcmDecimalString(key);
+
+      case EVR_IS:  // integer string
+        return new DcmIntegerString(key);
+
+      case EVR_TM:  // time string
+        return new DcmTime(key);
+
+      case EVR_UI:  // unique identifier
+        return new DcmUniqueIdentifier(key);
+
+      case EVR_ST:  // short text
+        return new DcmShortText(key);
+
+      case EVR_LO:  // long string
+        return new DcmLongString(key);
+
+      case EVR_LT:  // long text
+        return new DcmLongText(key);
+
+      case EVR_UT:  // unlimited text
+        return new DcmUnlimitedText(key);
+
+      case EVR_SH:  // short string
+        return new DcmShortString(key);
+
+      case EVR_PN:  // person name
+        return new DcmPersonName(key);
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UC:  // unlimited characters
+        return new DcmUnlimitedCharacters(key);
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UR:  // URI/URL
+        return new DcmUniversalResourceIdentifierOrLocator(key);
+#endif
+          
+        
+      /**
+       * Numerical types
+       **/ 
+      
+      case EVR_SL:  // signed long
+        return new DcmSignedLong(key);
+
+      case EVR_SS:  // signed short
+        return new DcmSignedShort(key);
+
+      case EVR_UL:  // unsigned long
+        return new DcmUnsignedLong(key);
+
+      case EVR_US:  // unsigned short
+        return new DcmUnsignedShort(key);
+
+      case EVR_FL:  // float single-precision
+        return new DcmFloatingPointSingle(key);
+
+      case EVR_FD:  // float double-precision
+        return new DcmFloatingPointDouble(key);
+
+
+      /**
+       * Sequence types, should never occur at this point.
+       **/
+
+      case EVR_SQ:  // sequence of items
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+
+
+      /**
+       * TODO
+       **/
+
+      case EVR_AT:  // attribute tag
+        throw OrthancException(ErrorCode_NotImplemented);
+
+
+      /**
+       * Internal to DCMTK.
+       **/ 
+
+      case EVR_xs:  // SS or US depending on context
+      case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
+      case EVR_na:  // na="not applicable", for data which has no VR
+      case EVR_up:  // up="unsigned pointer", used internally for DICOMDIR suppor
+      case EVR_item:  // used internally for items
+      case EVR_metainfo:  // used internally for meta info datasets
+      case EVR_dataset:  // used internally for datasets
+      case EVR_fileFormat:  // used internally for DICOM files
+      case EVR_dicomDir:  // used internally for DICOMDIR objects
+      case EVR_dirRecord:  // used internally for DICOMDIR records
+      case EVR_pixelSQ:  // used internally for pixel sequences in a compressed image
+      case EVR_pixelItem:  // used internally for pixel items in a compressed image
+      case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+      case EVR_PixelData:  // used internally for uncompressed pixeld data
+      case EVR_OverlayData:  // used internally for overlay data
+      case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+      default:
+        break;
+    }
+
+    throw OrthancException(ErrorCode_InternalError);
+  }
--- a/UnitTestsSources/DicomMapTests.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/UnitTestsSources/DicomMapTests.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -725,9 +725,9 @@
   dicom.ReplacePlainString(DicomTag(0x0008, 0x0070), "LO");
   dicom.ReplacePlainString(DicomTag(0x0010, 0x4000), "LT");
   dicom.ReplacePlainString(DicomTag(0x0028, 0x2000), "OB");
-  dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "OD");
-  dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "OF");
-  dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46");
+  dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "3.14159");  // OD (other double)
+  dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "2.71828");  // OF (other float)
+  dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46");  // OL (other long)
   ASSERT_THROW(dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "O"), OrthancException);
   dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "OWOW");
   dicom.ReplacePlainString(DicomTag(0x0010, 0x0010), "PN");
@@ -784,16 +784,17 @@
 
 #if DCMTK_VERSION_NUMBER >= 361
   ASSERT_EQ("OD", visitor.GetResult() ["7FE00009"]["vr"].asString());
+  ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast<float>(visitor.GetResult() ["7FE00009"]["Value"][0].asString()));
 #else
   ASSERT_EQ("UN", visitor.GetResult() ["7FE00009"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["7FE00009"]["InlineBinary"].asString());
+  ASSERT_EQ(8u, s.size()); // Because of padding
+  ASSERT_EQ(0, s[7]);
+  ASSERT_EQ("3.14159", s.substr(0, 7));
 #endif
 
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["7FE00009"]["InlineBinary"].asString());
-  ASSERT_EQ("OD", s);
-
   ASSERT_EQ("OF", visitor.GetResult() ["00640009"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00640009"]["InlineBinary"].asString());
-  ASSERT_EQ("OF", s);
+  ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast<float>(visitor.GetResult() ["00640009"]["Value"][0].asString()));
 
 #if DCMTK_VERSION_NUMBER < 361
   ASSERT_EQ("UN", visitor.GetResult() ["00660040"]["vr"].asString());
@@ -802,10 +803,9 @@
 #elif DCMTK_VERSION_NUMBER == 361
   ASSERT_EQ("UL", visitor.GetResult() ["00660040"]["vr"].asString());
   ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt());
-#elif DCMTK_VERSION_NUMBER > 361
+#else
   ASSERT_EQ("OL", visitor.GetResult() ["00660040"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00660040"]["InlineBinary"].asString());
-  ASSERT_EQ("46", s);
+  ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt());
 #endif
 
   ASSERT_EQ("OW", visitor.GetResult() ["00281201"]["vr"].asString());
@@ -893,8 +893,18 @@
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0070), false));  ASSERT_EQ("LO", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x4000), false));  ASSERT_EQ("LT", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x2000), true));   ASSERT_EQ("OB", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7fe0, 0x0009), true));   ASSERT_EQ("OD", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0064, 0x0009), true));   ASSERT_EQ("OF", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7fe0, 0x0009), true));
+
+#if DCMTK_VERSION_NUMBER >= 361
+    ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast<float>(s));
+#else
+    ASSERT_EQ(8u, s.size()); // Because of padding
+    ASSERT_EQ(0, s[7]);
+    ASSERT_EQ("3.14159", s.substr(0, 7));
+#endif
+
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0064, 0x0009), true));
+    ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast<float>(s));
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x1201), true));   ASSERT_EQ("OWOW", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x0010), false));  ASSERT_EQ("PN", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0050), false));  ASSERT_EQ("SH", s);
@@ -902,20 +912,23 @@
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x9219), false));  ASSERT_EQ("-16", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0081), false));  ASSERT_EQ("ST", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0013), false));  ASSERT_EQ("TM", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), false));  ASSERT_EQ("UC", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0016), false));  ASSERT_EQ("UI", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1161), false));  ASSERT_EQ("128", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x4342, 0x1234), true));   ASSERT_EQ("UN", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), false));  ASSERT_EQ("UR", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), false));  ASSERT_EQ("17", s);
     ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0031), false));  ASSERT_EQ("UT", s);
 
-#if DCMTK_VERSION_NUMBER == 361
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), false));
+#if DCMTK_VERSION_NUMBER >= 361
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), false));  ASSERT_EQ("46", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), false));  ASSERT_EQ("UC", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), false));  ASSERT_EQ("UR", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), false));  ASSERT_EQ("17", s);
 #else
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), true));
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), true));  ASSERT_EQ("46", s);  // OL
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), true));  ASSERT_EQ("UC", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), true));  ASSERT_EQ("UR", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), true));  ASSERT_EQ("17", s);  // US (but tag unknown to DCMTK 3.6.0)
 #endif
-    ASSERT_EQ("46", s);
+    
   }
 }
 
--- a/UnitTestsSources/FromDcmtkTests.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -108,7 +108,7 @@
 {
   ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName"));
 
-  const DicomTag privateTag(0x0045, 0x0010);
+  const DicomTag privateTag(0x0045, 0x1010);
   const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020"));
   ASSERT_TRUE(privateTag.IsPrivate());
   ASSERT_TRUE(privateTag2.IsPrivate());
@@ -119,19 +119,19 @@
   ParsedDicomFile o(true);
   o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
   ASSERT_FALSE(o.GetTagValue(s, privateTag));
-  o.Insert(privateTag, "private tag", false);
+  o.Insert(privateTag, "private tag", false, "OrthancCreator");
   ASSERT_TRUE(o.GetTagValue(s, privateTag));
   ASSERT_STREQ("private tag", s.c_str());
 
   ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent), OrthancException);
+  ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException);
   ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent);
+  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator");
   ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent);
+  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
   ASSERT_TRUE(o.GetTagValue(s, privateTag2));
   ASSERT_STREQ("hello", s.c_str());
-  o.ReplacePlainString(privateTag2, "hello world");
+  o.Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
   ASSERT_TRUE(o.GetTagValue(s, privateTag2));
   ASSERT_STREQ("hello world", s.c_str());
 
@@ -290,7 +290,7 @@
       f.SetEncoding(testEncodings[i]);
 
       std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
-      f.Insert(DICOM_TAG_PATIENT_NAME, s, false);
+      f.Insert(DICOM_TAG_PATIENT_NAME, s, false, "");
       f.SaveToMemoryBuffer(dicom);
     }
 
@@ -407,7 +407,7 @@
     {
       Json::Value a;
       a = "Hello";
-      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8));
+      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, ""));
 
       Json::Value b;
       std::set<DicomTag> ignoreTagLength;
@@ -439,20 +439,20 @@
       Json::Value a;
       a = "Hello";
       // Cannot assign a string to a sequence
-      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8)), OrthancException);
+      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8, "")), OrthancException);
     }
 
     {
       Json::Value a = Json::arrayValue;
       a.append("Hello");
       // Cannot assign an array to a string
-      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8)), OrthancException);
+      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, "")), OrthancException);
     }
 
     {
       Json::Value a;
       a = "data:application/octet-stream;base64,SGVsbG8=";  // echo -n "Hello" | base64
-      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8));
+      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8, ""));
 
       Json::Value b;
       std::set<DicomTag> ignoreTagLength;
@@ -464,7 +464,7 @@
     {
       Json::Value a = Json::arrayValue;
       CreateSampleJson(a);
-      element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8));
+      element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8, ""));
 
       {
         Json::Value b;
@@ -506,8 +506,8 @@
 {
   ParsedDicomFile f(true);
 
-  f.Insert(DICOM_TAG_PATIENT_NAME, "World", false);
-  ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false), OrthancException);  // Already existing tag
+  f.Insert(DICOM_TAG_PATIENT_NAME, "World", false, "");
+  ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false, ""), OrthancException);  // Already existing tag
   f.ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, "Toto");  // (*)
   f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata");  // (**)
 
@@ -515,16 +515,16 @@
   ASSERT_FALSE(f.LookupTransferSyntax(s));
 
   ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"),
-                         false, DicomReplaceMode_ThrowIfAbsent), OrthancException);
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_IgnoreIfAbsent);
+                         false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_IgnoreIfAbsent, "");
   ASSERT_FALSE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_InsertIfAbsent);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_InsertIfAbsent, "");
   ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
   ASSERT_EQ(s, "Accession");
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession2"), false, DicomReplaceMode_IgnoreIfAbsent);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession2"), false, DicomReplaceMode_IgnoreIfAbsent, "");
   ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
   ASSERT_EQ(s, "Accession2");
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent, "");
   ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
   ASSERT_EQ(s, "Accession3");
 
@@ -552,20 +552,20 @@
 
   ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
   f.Remove(REFERENCED_STUDY_SEQUENCE);  // No effect
-  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true);
+  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
   ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-  ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true), OrthancException);
+  ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""), OrthancException);
   f.Remove(REFERENCED_STUDY_SEQUENCE);
   ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true);
+  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
   ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
 
   ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent), OrthancException);
+  ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
   ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent);
+  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent, "");
   ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent);
+  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent, "");
   ASSERT_TRUE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
 
   {
@@ -580,8 +580,8 @@
   }
 
   a = "data:application/octet-stream;base64,VGF0YQ==";   // echo -n "Tata" | base64 
-  f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false, DicomReplaceMode_InsertIfAbsent);  // (*)
-  f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true, DicomReplaceMode_InsertIfAbsent);  // (**)
+  f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false, DicomReplaceMode_InsertIfAbsent, "");  // (*)
+  f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true, DicomReplaceMode_InsertIfAbsent, "");  // (**)
 
   std::string s;
   ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID));
@@ -614,7 +614,7 @@
       }
 
       Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
-      f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent);
+      f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, "");
 
       Json::Value v;
       f.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
@@ -626,13 +626,13 @@
 
 TEST(ParsedDicomFile, ToJsonFlags1)
 {
-  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_PersonName, "MyPrivateTag", 1, 1, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag", 1, 1, "OrthancCreator");
   FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag", 1, 1, "");
 
   ParsedDicomFile f(true);
-  f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false);  // Even group => public tag
-  f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false);  // Even group => public, unknown tag
-  f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false);  // Odd group => private tag
+  f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false, "");  // Even group => public tag
+  f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false, "");  // Even group => public, unknown tag
+  f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false, "OrthancCreator");  // Odd group => private tag
 
   Json::Value v;
   f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
@@ -644,7 +644,7 @@
   ASSERT_EQ(Json::stringValue, v["7050,1000"].type());
   ASSERT_EQ("Some public tag", v["7050,1000"].asString());
 
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
   ASSERT_EQ(Json::objectValue, v.type());
   ASSERT_EQ(7u, v.getMemberNames().size());
   ASSERT_FALSE(v.isMember("7052,1000"));
@@ -653,7 +653,14 @@
   ASSERT_EQ("Some public tag", v["7050,1000"].asString());
   ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
 
-  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePrivateTags, 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7052,1000"));
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_FALSE(v.isMember("7053,1000"));
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary), 0);
   ASSERT_EQ(Json::objectValue, v.type());
   ASSERT_EQ(7u, v.getMemberNames().size());
   ASSERT_FALSE(v.isMember("7052,1000"));
@@ -666,7 +673,7 @@
   ASSERT_EQ("application/octet-stream", mime);
   ASSERT_EQ("Some private tag", content);
 
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
   ASSERT_EQ(Json::objectValue, v.type());
   ASSERT_EQ(7u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7050,1000"));
@@ -675,7 +682,7 @@
   ASSERT_EQ("Some public tag", v["7050,1000"].asString());
   ASSERT_EQ(Json::nullValue, v["7052,1000"].type());
 
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags), 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary), 0);
   ASSERT_EQ(Json::objectValue, v.type());
   ASSERT_EQ(7u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7050,1000"));
@@ -687,7 +694,7 @@
   ASSERT_EQ("application/octet-stream", mime);
   ASSERT_EQ("Some unknown tag", content);
 
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
   ASSERT_EQ(Json::objectValue, v.type());
   ASSERT_EQ(8u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7050,1000"));
@@ -702,7 +709,7 @@
 TEST(ParsedDicomFile, ToJsonFlags2)
 {
   ParsedDicomFile f(true);
-  f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false);
+  f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false, "");
 
   Json::Value v;
   f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
@@ -811,7 +818,7 @@
 
   {
     std::auto_ptr<ParsedDicomFile> dicom
-      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers)));
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers), ""));
 
     Json::Value vv;
     dicom->DatasetToJson(vv, DicomToJsonFormat_Human, toJsonFlags, 0);
@@ -827,7 +834,7 @@
 
   {
     std::auto_ptr<ParsedDicomFile> dicom
-      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers)));
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers), ""));
 
     Json::Value vv;
     dicom->DatasetToJson(vv, DicomToJsonFormat_Human, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData), 0);
@@ -841,7 +848,7 @@
 
   {
     std::auto_ptr<ParsedDicomFile> dicom
-      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_DecodeDataUriScheme)));
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_DecodeDataUriScheme), ""));
 
     Json::Value vv;
     dicom->DatasetToJson(vv, DicomToJsonFormat_Short, toJsonFlags, 0);
--- a/UnitTestsSources/ImageProcessingTests.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/UnitTestsSources/ImageProcessingTests.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -285,7 +285,7 @@
 static void SetGrayscale16Pixel(ImageAccessor& image,
                                 unsigned int x,
                                 unsigned int y,
-                                uint8_t value)
+                                uint16_t value)
 {
   ImageTraits<PixelFormat_Grayscale16>::SetPixel(image, value, x, y);
 }
@@ -301,6 +301,25 @@
   return p == value;
 }
 
+static void SetSignedGrayscale16Pixel(ImageAccessor& image,
+                                unsigned int x,
+                                unsigned int y,
+                                int16_t value)
+{
+  ImageTraits<PixelFormat_SignedGrayscale16>::SetPixel(image, value, x, y);
+}
+
+static bool TestSignedGrayscale16Pixel(const ImageAccessor& image,
+                                       unsigned int x,
+                                       unsigned int y,
+                                       int16_t value)
+{
+  PixelTraits<PixelFormat_SignedGrayscale16>::PixelType p;
+  ImageTraits<PixelFormat_SignedGrayscale16>::GetPixel(p, image, x, y);
+  if (p != value) printf("%d %d\n", p, value);
+  return p == value;
+}
+
 static void SetRGB24Pixel(ImageAccessor& image,
                           unsigned int x,
                           unsigned int y,
@@ -807,7 +826,7 @@
 
     {
       Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
 
       ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));
       ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0));
@@ -819,7 +838,7 @@
 
     {
       Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true);
 
       ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255));
       ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255));
@@ -831,7 +850,7 @@
 
     {
       Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing(target, image, 5000.0f, 10000.0f, 1000.0f, 0.0f, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, false);
 
       ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));
       ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0));
@@ -843,7 +862,7 @@
 
     {
       Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing(target, image, 5000.0f, 10000.0f, 1000.0f, 0.0f, true);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, true);
 
       ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255));
       ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255));
@@ -855,7 +874,7 @@
 
     {
       Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing(target, image, 50.0f, 100.0f, 10.0f, 30.0f, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 50.0f, 100.1f, 10.0f, 30.0f, false);
 
       ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));  // (-5 * 10) + 30 => pixel value = -20 => 0
       ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 256*30/100));  // ((0 * 10) + 30 => pixel value = 30 => 30%
@@ -881,7 +900,7 @@
 
     {
       Image target(PixelFormat_Grayscale16, 6, 1, false);
-      ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
 
       ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
       ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 0));
@@ -905,7 +924,7 @@
 
     {
       Image target(PixelFormat_Grayscale16, 5, 1, false);
-      ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
 
       ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
       ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10));
@@ -928,7 +947,7 @@
 
     {
       Image target(PixelFormat_Grayscale16, 5, 1, false);
-      ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
 
       ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
       ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10));
@@ -938,3 +957,57 @@
     }
   }
 }
+
+
+TEST(ImageProcessing, ShiftScaleGrayscale8)
+{
+  Image image(PixelFormat_Grayscale8, 5, 1, false);
+  SetGrayscale8Pixel(image, 0, 0, 0);
+  SetGrayscale8Pixel(image, 1, 0, 2);
+  SetGrayscale8Pixel(image, 2, 0, 5);
+  SetGrayscale8Pixel(image, 3, 0, 10);
+  SetGrayscale8Pixel(image, 4, 0, 255);
+
+  ImageProcessing::ShiftScale(image, -1.1, 1.5, true);
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0));
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 1));
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 6));
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 13));
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 255));
+}
+
+
+TEST(ImageProcessing, ShiftScaleGrayscale16)
+{
+  Image image(PixelFormat_Grayscale16, 5, 1, false);
+  SetGrayscale16Pixel(image, 0, 0, 0);
+  SetGrayscale16Pixel(image, 1, 0, 2);
+  SetGrayscale16Pixel(image, 2, 0, 5);
+  SetGrayscale16Pixel(image, 3, 0, 10);
+  SetGrayscale16Pixel(image, 4, 0, 255);
+
+  ImageProcessing::ShiftScale(image, -1.1, 1.5, true);
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 0, 0, 0));
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 1, 0, 1));
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 2, 0, 6));
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 3, 0, 13));
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 4, 0, 381));
+}
+
+
+TEST(ImageProcessing, ShiftScaleSignedGrayscale16)
+{
+  Image image(PixelFormat_SignedGrayscale16, 5, 1, false);
+  SetSignedGrayscale16Pixel(image, 0, 0, 0);
+  SetSignedGrayscale16Pixel(image, 1, 0, 2);
+  SetSignedGrayscale16Pixel(image, 2, 0, 5);
+  SetSignedGrayscale16Pixel(image, 3, 0, 10);
+  SetSignedGrayscale16Pixel(image, 4, 0, 255);
+
+  ImageProcessing::ShiftScale(image, -17.1, 11.5, true);
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, -197));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, -174));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, -139));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, -82));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 2736));
+}
--- a/UnitTestsSources/MultiThreadingTests.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -1143,9 +1143,9 @@
   Json::Value s;
 
   ParsedDicomFile source(true);
-  source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false);
-  source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false);
-  source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false);
+  source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false, "");
+  source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false, "");
+  source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false, "");
 
   std::auto_ptr<ParsedDicomFile> modified(source.Clone(true));
 
@@ -1310,7 +1310,7 @@
       // Create a sample DICOM file
       ParsedDicomFile dicom(true);
       dicom.Replace(DICOM_TAG_PATIENT_NAME, std::string("JODOGNE"),
-                    false, DicomReplaceMode_InsertIfAbsent);
+                    false, DicomReplaceMode_InsertIfAbsent, "");
 
       DicomInstanceToStore toStore;
       toStore.SetParsedDicomFile(dicom);
--- a/UnitTestsSources/VersionsTests.cpp	Thu Feb 20 20:36:47 2020 +0100
+++ b/UnitTestsSources/VersionsTests.cpp	Wed Feb 26 10:39:55 2020 +0100
@@ -113,6 +113,14 @@
 }
 
 
+#if ORTHANC_ENABLE_CIVETWEB == 1
+TEST(Version, CivetwebCompression)
+{
+  ASSERT_TRUE(mg_check_feature(MG_FEATURES_COMPRESSION));
+}
+#endif
+
+
 #if ORTHANC_STATIC == 1
 
 TEST(Versions, ZlibStatic)