changeset 3248:c2798f7daf8b

Merged in jku11/orthanc (pull request #7) Support for DICOM storage SOP classes BreastProjectionXRayImageStorageForProcessing and BreastProjectionXRayImageStorageForPresentation.
author Sébastien Jodogne <s.jodogne@gmail.com>
date Sat, 16 Feb 2019 10:29:26 +0000
parents ae68bf751187 (diff) fcc2b99feaaf (current diff)
children bce0e4c9ca45
files
diffstat 54 files changed, 2111 insertions(+), 701 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Feb 13 13:01:21 2019 +0000
+++ b/.hgignore	Sat Feb 16 10:29:26 2019 +0000
@@ -4,3 +4,4 @@
 *.cpp.orig
 *.h.orig
 .vs/
+*~
--- a/CMakeLists.txt	Wed Feb 13 13:01:21 2019 +0000
+++ b/CMakeLists.txt	Sat Feb 16 10:29:26 2019 +0000
@@ -338,6 +338,29 @@
 
 
 #####################################################################
+## Build a static library to share code between the plugins
+#####################################################################
+
+if (ENABLE_PLUGINS AND
+    (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS))
+  add_library(ThirdPartyPlugins STATIC
+    ${BOOST_SOURCES}
+    ${JSONCPP_SOURCES}
+    ${LIBICONV_SOURCES}
+    ${LIBICU_SOURCES}
+    Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+    )
+
+  # Add the "-fPIC" option as this static library must be embedded
+  # inside shared libraries (important on UNIX)
+  set_property(
+    TARGET ThirdPartyPlugins
+    PROPERTY POSITION_INDEPENDENT_CODE ON
+    )
+endif()
+
+
+#####################################################################
 ## Build the "ServeFolders" plugin
 #####################################################################
 
@@ -359,14 +382,12 @@
   endif()  
 
   add_library(ServeFolders SHARED 
-    ${BOOST_SOURCES}
-    ${JSONCPP_SOURCES}
-    ${LIBICONV_SOURCES}
     Plugins/Samples/ServeFolders/Plugin.cpp
-    Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
     ${SERVE_FOLDERS_RESOURCES}
     )
 
+  target_link_libraries(ServeFolders ThirdPartyPlugins)
+
   set_target_properties(
     ServeFolders PROPERTIES 
     VERSION ${ORTHANC_VERSION} 
@@ -404,14 +425,12 @@
   endif()
 
   add_library(ModalityWorklists SHARED 
-    ${BOOST_SOURCES}
-    ${JSONCPP_SOURCES}
-    ${LIBICONV_SOURCES}
-    Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
     Plugins/Samples/ModalityWorklists/Plugin.cpp
     ${MODALITY_WORKLISTS_RESOURCES}
     )
 
+  target_link_libraries(ModalityWorklists ThirdPartyPlugins)
+
   set_target_properties(
     ModalityWorklists PROPERTIES 
     VERSION ${ORTHANC_VERSION} 
--- a/Core/DicomParsing/DicomDirWriter.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/DicomParsing/DicomDirWriter.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -161,6 +161,7 @@
     static bool GetUtf8TagValue(std::string& result,
                                 DcmItem& source,
                                 Encoding encoding,
+                                bool hasCodeExtensions,
                                 const DcmTagKey& key)
     {
       DcmElement* element = NULL;
@@ -174,7 +175,7 @@
         {
           if (s != NULL)
           {
-            result = Toolbox::ConvertToUtf8(s, encoding);
+            result = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
           }
           
           return true;
@@ -202,6 +203,7 @@
     static bool CopyString(DcmDirectoryRecord& target,
                            DcmDataset& source,
                            Encoding encoding,
+                           bool hasCodeExtensions,
                            const DcmTagKey& key,
                            bool optional,
                            bool copyEmpty)
@@ -214,7 +216,7 @@
       }
 
       std::string value;
-      bool found = GetUtf8TagValue(value, source, encoding, key);
+      bool found = GetUtf8TagValue(value, source, encoding, hasCodeExtensions, key);
 
       if (!found)
       {
@@ -231,33 +233,37 @@
     static void CopyStringType1(DcmDirectoryRecord& target,
                                 DcmDataset& source,
                                 Encoding encoding,
+                                bool hasCodeExtensions,
                                 const DcmTagKey& key)
     {
-      CopyString(target, source, encoding, key, false, false);
+      CopyString(target, source, encoding, hasCodeExtensions, key, false, false);
     }
 
     static void CopyStringType1C(DcmDirectoryRecord& target,
                                  DcmDataset& source,
                                  Encoding encoding,
+                                 bool hasCodeExtensions,
                                  const DcmTagKey& key)
     {
-      CopyString(target, source, encoding, key, true, false);
+      CopyString(target, source, encoding, hasCodeExtensions, key, true, false);
     }
 
     static void CopyStringType2(DcmDirectoryRecord& target,
                                 DcmDataset& source,
                                 Encoding encoding,
+                                bool hasCodeExtensions,
                                 const DcmTagKey& key)
     {
-      CopyString(target, source, encoding, key, false, true);
+      CopyString(target, source, encoding, hasCodeExtensions, key, false, true);
     }
 
     static void CopyStringType3(DcmDirectoryRecord& target,
                                 DcmDataset& source,
                                 Encoding encoding,
+                                bool hasCodeExtensions,
                                 const DcmTagKey& key)
     {
-      CopyString(target, source, encoding, key, true, true);
+      CopyString(target, source, encoding, hasCodeExtensions, key, true, true);
     }
 
 
@@ -298,17 +304,19 @@
 
     void FillPatient(DcmDirectoryRecord& record,
                      DcmDataset& dicom,
-                     Encoding encoding)
+                     Encoding encoding,
+                     bool hasCodeExtensions)
     {
       // cf. "DicomDirInterface::buildPatientRecord()"
 
-      CopyStringType1C(record, dicom, encoding, DCM_PatientID);
-      CopyStringType2(record, dicom, encoding, DCM_PatientName);
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_PatientID);
+      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_PatientName);
     }
 
     void FillStudy(DcmDirectoryRecord& record,
                    DcmDataset& dicom,
-                   Encoding encoding)
+                   Encoding encoding,
+                   bool hasCodeExtensions)
     {
       // cf. "DicomDirInterface::buildStudyRecord()"
 
@@ -316,19 +324,19 @@
       SystemToolbox::GetNowDicom(nowDate, nowTime, utc_);
 
       std::string studyDate;
-      if (!GetUtf8TagValue(studyDate, dicom, encoding, DCM_StudyDate) &&
-          !GetUtf8TagValue(studyDate, dicom, encoding, DCM_SeriesDate) &&
-          !GetUtf8TagValue(studyDate, dicom, encoding, DCM_AcquisitionDate) &&
-          !GetUtf8TagValue(studyDate, dicom, encoding, DCM_ContentDate))
+      if (!GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_StudyDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_SeriesDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_AcquisitionDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_ContentDate))
       {
         studyDate = nowDate;
       }
           
       std::string studyTime;
-      if (!GetUtf8TagValue(studyTime, dicom, encoding, DCM_StudyTime) &&
-          !GetUtf8TagValue(studyTime, dicom, encoding, DCM_SeriesTime) &&
-          !GetUtf8TagValue(studyTime, dicom, encoding, DCM_AcquisitionTime) &&
-          !GetUtf8TagValue(studyTime, dicom, encoding, DCM_ContentTime))
+      if (!GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_StudyTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_SeriesTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_AcquisitionTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_ContentTime))
       {
         studyTime = nowTime;
       }
@@ -336,52 +344,54 @@
       /* copy attribute values from dataset to study record */
       SetTagValue(record, DCM_StudyDate, studyDate);
       SetTagValue(record, DCM_StudyTime, studyTime);
-      CopyStringType2(record, dicom, encoding, DCM_StudyDescription);
-      CopyStringType1(record, dicom, encoding, DCM_StudyInstanceUID);
+      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_StudyDescription);
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
       /* use type 1C instead of 1 in order to avoid unwanted overwriting */
-      CopyStringType1C(record, dicom, encoding, DCM_StudyID);
-      CopyStringType2(record, dicom, encoding, DCM_AccessionNumber);
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_StudyID);
+      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_AccessionNumber);
     }
 
     void FillSeries(DcmDirectoryRecord& record,
                     DcmDataset& dicom,
-                    Encoding encoding)
+                    Encoding encoding,
+                    bool hasCodeExtensions)
     {
       // cf. "DicomDirInterface::buildSeriesRecord()"
 
       /* copy attribute values from dataset to series record */
-      CopyStringType1(record, dicom, encoding, DCM_Modality);
-      CopyStringType1(record, dicom, encoding, DCM_SeriesInstanceUID);
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_Modality);
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
       /* use type 1C instead of 1 in order to avoid unwanted overwriting */
-      CopyStringType1C(record, dicom, encoding, DCM_SeriesNumber);
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_SeriesNumber);
 
       // Add extended (non-standard) type 3 tags, those are not generated by DCMTK
       // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part02/sect_7.3.html
       // https://groups.google.com/d/msg/orthanc-users/Y7LOvZMDeoc/9cp3kDgxAwAJ
       if (extendedSopClass_)
       {
-        CopyStringType3(record, dicom, encoding, DCM_SeriesDescription);
+        CopyStringType3(record, dicom, encoding, hasCodeExtensions, DCM_SeriesDescription);
       }
     }
 
     void FillInstance(DcmDirectoryRecord& record,
                       DcmDataset& dicom,
                       Encoding encoding,
+                      bool hasCodeExtensions,
                       DcmMetaInfo& metaInfo,
                       const char* path)
     {
       // cf. "DicomDirInterface::buildImageRecord()"
 
       /* copy attribute values from dataset to image record */
-      CopyStringType1(record, dicom, encoding, DCM_InstanceNumber);
-      //CopyElementType1C(record, dicom, encoding, DCM_ImageType);
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_InstanceNumber);
+      //CopyElementType1C(record, dicom, encoding, hasCodeExtensions, DCM_ImageType);
 
       // REMOVED since 0.9.7: copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
 
       std::string sopClassUid, sopInstanceUid, transferSyntaxUid;
-      if (!GetUtf8TagValue(sopClassUid, dicom, encoding, DCM_SOPClassUID) ||
-          !GetUtf8TagValue(sopInstanceUid, dicom, encoding, DCM_SOPInstanceUID) ||
-          !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, DCM_TransferSyntaxUID))
+      if (!GetUtf8TagValue(sopClassUid, dicom, encoding, hasCodeExtensions, DCM_SOPClassUID) ||
+          !GetUtf8TagValue(sopInstanceUid, dicom, encoding, hasCodeExtensions, DCM_SOPInstanceUID) ||
+          !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, hasCodeExtensions, DCM_TransferSyntaxUID))
       {
         throw OrthancException(ErrorCode_BadFileFormat);
       }
@@ -401,7 +411,9 @@
                         const char* path)
     {
       DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
-      Encoding encoding = dicom.GetEncoding();
+
+      bool hasCodeExtensions;
+      Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
 
       bool found;
       std::string id;
@@ -410,7 +422,7 @@
       switch (level)
       {
         case ResourceType_Patient:
-          if (!GetUtf8TagValue(id, dataset, encoding, DCM_PatientID))
+          if (!GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_PatientID))
           {
             // Be tolerant about missing patient ID. Fixes issue #124
             // (GET /studies/ID/media fails for certain dicom file).
@@ -422,17 +434,17 @@
           break;
 
         case ResourceType_Study:
-          found = GetUtf8TagValue(id, dataset, encoding, DCM_StudyInstanceUID);
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
           type = ERT_Study;
           break;
 
         case ResourceType_Series:
-          found = GetUtf8TagValue(id, dataset, encoding, DCM_SeriesInstanceUID);
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
           type = ERT_Series;
           break;
 
         case ResourceType_Instance:
-          found = GetUtf8TagValue(id, dataset, encoding, DCM_SOPInstanceUID);
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_SOPInstanceUID);
           type = ERT_Image;
           break;
 
@@ -459,26 +471,26 @@
       switch (level)
       {
         case ResourceType_Patient:
-          FillPatient(*record, dataset, encoding);
+          FillPatient(*record, dataset, encoding, hasCodeExtensions);
           break;
 
         case ResourceType_Study:
-          FillStudy(*record, dataset, encoding);
+          FillStudy(*record, dataset, encoding, hasCodeExtensions);
           break;
 
         case ResourceType_Series:
-          FillSeries(*record, dataset, encoding);
+          FillSeries(*record, dataset, encoding, hasCodeExtensions);
           break;
 
         case ResourceType_Instance:
-          FillInstance(*record, dataset, encoding, *dicom.GetDcmtkObject().getMetaInfo(), path);
+          FillInstance(*record, dataset, encoding, hasCodeExtensions, *dicom.GetDcmtkObject().getMetaInfo(), path);
           break;
 
         default:
           throw OrthancException(ErrorCode_InternalError);
       }
 
-      CopyStringType1C(*record, dataset, encoding, DCM_SpecificCharacterSet);
+      CopyStringType1C(*record, dataset, encoding, hasCodeExtensions, DCM_SpecificCharacterSet);
 
       target = record.get();
       GetRoot().insertSub(record.release());
--- a/Core/DicomParsing/DicomWebJsonVisitor.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/DicomParsing/DicomWebJsonVisitor.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -43,9 +43,12 @@
 
 
 static const char* const KEY_ALPHABETIC = "Alphabetic";
+static const char* const KEY_IDEOGRAPHIC = "Ideographic";
+static const char* const KEY_PHONETIC = "Phonetic";
 static const char* const KEY_BULK_DATA_URI = "BulkDataURI";
 static const char* const KEY_INLINE_BINARY = "InlineBinary";
 static const char* const KEY_SQ = "SQ";
+static const char* const KEY_TAG = "tag";
 static const char* const KEY_VALUE = "Value";
 static const char* const KEY_VR = "vr";
 
@@ -53,9 +56,42 @@
 namespace Orthanc
 {
 #if ORTHANC_ENABLE_PUGIXML == 1
+  static void DecomposeXmlPersonName(pugi::xml_node& target,
+                                     const std::string& source)
+  {
+    std::vector<std::string> tokens;
+    Toolbox::TokenizeString(tokens, source, '^');
+
+    if (tokens.size() >= 1)
+    {
+      target.append_child("FamilyName").text() = tokens[0].c_str();
+    }
+            
+    if (tokens.size() >= 2)
+    {
+      target.append_child("GivenName").text() = tokens[1].c_str();
+    }
+            
+    if (tokens.size() >= 3)
+    {
+      target.append_child("MiddleName").text() = tokens[2].c_str();
+    }
+            
+    if (tokens.size() >= 4)
+    {
+      target.append_child("NamePrefix").text() = tokens[3].c_str();
+    }
+            
+    if (tokens.size() >= 5)
+    {
+      target.append_child("NameSuffix").text() = tokens[4].c_str();
+    }
+  }
+  
   static void ExploreXmlDataset(pugi::xml_node& target,
                                 const Json::Value& source)
   {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
     assert(source.type() == Json::objectValue);
 
     Json::Value::Members members = source.getMemberNames();
@@ -65,15 +101,15 @@
       const Json::Value& content = source[members[i]];
 
       assert(content.type() == Json::objectValue &&
-             content.isMember("vr") &&
-             content["vr"].type() == Json::stringValue);
-      const std::string vr = content["vr"].asString();
+             content.isMember(KEY_VR) &&
+             content[KEY_VR].type() == Json::stringValue);
+      const std::string vr = content[KEY_VR].asString();
 
       const std::string keyword = FromDcmtkBridge::GetTagName(tag, "");
     
       pugi::xml_node node = target.append_child("DicomAttribute");
-      node.append_attribute("tag").set_value(members[i].c_str());
-      node.append_attribute("vr").set_value(vr.c_str());
+      node.append_attribute(KEY_TAG).set_value(members[i].c_str());
+      node.append_attribute(KEY_VR).set_value(vr.c_str());
 
       if (keyword != std::string(DcmTag_ERROR_TagName))
       {
@@ -99,40 +135,38 @@
           }
           if (vr == "PN")
           {
-            if (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) &&
-                content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue)
+            bool hasAlphabetic = (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) &&
+                                  content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue);
+
+            bool hasIdeographic = (content[KEY_VALUE][j].isMember(KEY_IDEOGRAPHIC) &&
+                                   content[KEY_VALUE][j][KEY_IDEOGRAPHIC].type() == Json::stringValue);
+
+            bool hasPhonetic = (content[KEY_VALUE][j].isMember(KEY_PHONETIC) &&
+                                content[KEY_VALUE][j][KEY_PHONETIC].type() == Json::stringValue);
+
+            if (hasAlphabetic ||
+                hasIdeographic ||
+                hasPhonetic)
             {
-              std::vector<std::string> tokens;
-              Toolbox::TokenizeString(tokens, content[KEY_VALUE][j][KEY_ALPHABETIC].asString(), '^');
-
               pugi::xml_node child = node.append_child("PersonName");
               child.append_attribute("number").set_value(number.c_str());
-            
-              pugi::xml_node name = child.append_child(KEY_ALPHABETIC);
-            
-              if (tokens.size() >= 1)
+
+              if (hasAlphabetic)
               {
-                name.append_child("FamilyName").text() = tokens[0].c_str();
-              }
-            
-              if (tokens.size() >= 2)
-              {
-                name.append_child("GivenName").text() = tokens[1].c_str();
+                pugi::xml_node name = child.append_child(KEY_ALPHABETIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_ALPHABETIC].asString());
               }
-            
-              if (tokens.size() >= 3)
+
+              if (hasIdeographic)
               {
-                name.append_child("MiddleName").text() = tokens[2].c_str();
+                pugi::xml_node name = child.append_child(KEY_IDEOGRAPHIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_IDEOGRAPHIC].asString());
               }
-            
-              if (tokens.size() >= 4)
+
+              if (hasPhonetic)
               {
-                name.append_child("NamePrefix").text() = tokens[3].c_str();
-              }
-            
-              if (tokens.size() >= 5)
-              {
-                name.append_child("NameSuffix").text() = tokens[4].c_str();
+                pugi::xml_node name = child.append_child(KEY_PHONETIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_PHONETIC].asString());
               }
             }
           }
@@ -517,8 +551,25 @@
                   Json::Value value = Json::objectValue;
                   if (!tokens[i].empty())
                   {
-                    value[KEY_ALPHABETIC] = tokens[i];
+                    std::vector<std::string> components;
+                    Toolbox::TokenizeString(components, tokens[i], '=');
+
+                    if (components.size() >= 1)
+                    {
+                      value[KEY_ALPHABETIC] = components[0];
+                    }
+
+                    if (components.size() >= 2)
+                    {
+                      value[KEY_IDEOGRAPHIC] = components[1];
+                    }
+
+                    if (components.size() >= 3)
+                    {
+                      value[KEY_PHONETIC] = components[2];
+                    }
                   }
+                  
                   node[KEY_VALUE].append(value);
                   break;
                 }
--- a/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/DicomParsing/FromDcmtkBridge.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -414,37 +414,49 @@
   }
 
 
-  Encoding FromDcmtkBridge::DetectEncoding(DcmItem& dataset,
+  Encoding FromDcmtkBridge::DetectEncoding(bool& hasCodeExtensions,
+                                           DcmItem& dataset,
                                            Encoding defaultEncoding)
   {
-    Encoding encoding = defaultEncoding;
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
 
     OFString tmp;
-    if (dataset.findAndGetOFString(DCM_SpecificCharacterSet, tmp).good())
+    if (dataset.findAndGetOFStringArray(DCM_SpecificCharacterSet, tmp).good())
     {
-      std::string characterSet = Toolbox::StripSpaces(std::string(tmp.c_str()));
-
-      if (characterSet.empty())
+      std::vector<std::string> tokens;
+      Toolbox::TokenizeString(tokens, std::string(tmp.c_str()), '\\');
+
+      hasCodeExtensions = (tokens.size() > 1);
+
+      for (size_t i = 0; i < tokens.size(); i++)
       {
-        // Empty specific character set tag: Use the default encoding
-      }
-      else if (GetDicomEncoding(encoding, characterSet.c_str()))
-      {
-        // The specific character set is supported by the Orthanc core
-      }
-      else
-      {
-        LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
-                     << ", fallback to ASCII (remove all special characters)";
-        encoding = Encoding_Ascii;
+        std::string characterSet = Toolbox::StripSpaces(tokens[i]);
+
+        if (!characterSet.empty())
+        {
+          Encoding encoding;
+          
+          if (GetDicomEncoding(encoding, characterSet.c_str()))
+          {
+            // The specific character set is supported by the Orthanc core
+            return encoding;
+          }
+          else
+          {
+            LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
+                         << ", fallback to ASCII (remove all special characters)";
+            return Encoding_Ascii;
+          }
+        }
       }
     }
     else
     {
-      // No specific character set tag: Use the default encoding
+      hasCodeExtensions = false;
     }
-
-    return encoding;
+    
+    // No specific character set tag: Use the default encoding
+    return defaultEncoding;
   }
 
 
@@ -454,8 +466,9 @@
                                             Encoding defaultEncoding)
   {
     std::set<DicomTag> ignoreTagLength;
-    
-    Encoding encoding = DetectEncoding(dataset, defaultEncoding);
+
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
 
     target.Clear();
     for (unsigned long i = 0; i < dataset.card(); i++)
@@ -466,7 +479,7 @@
         target.SetValue(element->getTag().getGTag(),
                         element->getTag().getETag(),
                         ConvertLeafElement(*element, DicomToJsonFlags_Default,
-                                           maxStringLength, encoding, ignoreTagLength));
+                                           maxStringLength, encoding, hasCodeExtensions, ignoreTagLength));
       }
     }
   }
@@ -488,6 +501,7 @@
                                                   DicomToJsonFlags flags,
                                                   unsigned int maxStringLength,
                                                   Encoding encoding,
+                                                  bool hasCodeExtensions,
                                                   const std::set<DicomTag>& ignoreTagLength)
   {
     if (!element.isLeaf())
@@ -507,7 +521,7 @@
       else
       {
         std::string s(c);
-        std::string utf8 = Toolbox::ConvertToUtf8(s, encoding);
+        std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
 
         if (maxStringLength != 0 &&
             utf8.size() > maxStringLength &&
@@ -855,6 +869,7 @@
                                       DicomToJsonFlags flags,
                                       unsigned int maxStringLength,
                                       Encoding encoding,
+                                      bool hasCodeExtensions,
                                       const std::set<DicomTag>& ignoreTagLength)
   {
     if (parent.type() == Json::nullValue)
@@ -869,7 +884,7 @@
     {
       // The "0" below lets "LeafValueToJson()" take care of "TooLong" values
       std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
-                                  (element, flags, 0, encoding, ignoreTagLength));
+                                  (element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength));
 
       if (ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end())
       {
@@ -894,7 +909,7 @@
       {
         DcmItem* child = sequence.getItem(i);
         Json::Value& v = target.append(Json::objectValue);
-        DatasetToJson(v, *child, format, flags, maxStringLength, encoding, ignoreTagLength);
+        DatasetToJson(v, *child, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength);
       }
     }
   }
@@ -906,6 +921,7 @@
                                       DicomToJsonFlags flags,
                                       unsigned int maxStringLength,
                                       Encoding encoding,
+                                      bool hasCodeExtensions,
                                       const std::set<DicomTag>& ignoreTagLength)
   {
     assert(parent.type() == Json::objectValue);
@@ -952,7 +968,7 @@
       }
 
       FromDcmtkBridge::ElementToJson(parent, *element, format, flags,
-                                     maxStringLength, encoding, ignoreTagLength);
+                                     maxStringLength, encoding, hasCodeExtensions, ignoreTagLength);
     }
   }
 
@@ -965,10 +981,11 @@
                                            Encoding defaultEncoding,
                                            const std::set<DicomTag>& ignoreTagLength)
   {
-    Encoding encoding = DetectEncoding(dataset, defaultEncoding);
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
 
     target = Json::objectValue;
-    DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, ignoreTagLength);
+    DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, ignoreTagLength);
   }
 
 
@@ -980,7 +997,7 @@
   {
     std::set<DicomTag> ignoreTagLength;
     target = Json::objectValue;
-    DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, ignoreTagLength);
+    DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, false, ignoreTagLength);
   }
 
 
@@ -2033,6 +2050,7 @@
 
   void FromDcmtkBridge::ChangeStringEncoding(DcmItem& dataset,
                                              Encoding source,
+                                             bool hasSourceCodeExtensions,
                                              Encoding target)
   {
     // Recursive exploration of a dataset to change the encoding of
@@ -2055,7 +2073,7 @@
               element->getString(c).good() && 
               c != NULL)
           {
-            std::string a = Toolbox::ConvertToUtf8(c, source);
+            std::string a = Toolbox::ConvertToUtf8(c, source, hasSourceCodeExtensions);
             std::string b = Toolbox::ConvertFromUtf8(a, target);
             element->putString(b.c_str());
           }
@@ -2069,7 +2087,7 @@
 
           for (unsigned long j = 0; j < sequence.card(); j++)
           {
-            ChangeStringEncoding(*sequence.getItem(j), source, target);
+            ChangeStringEncoding(*sequence.getItem(j), source, hasSourceCodeExtensions, target);
           }
         }
       }
@@ -2192,13 +2210,15 @@
                                     ITagVisitor& visitor,
                                     const std::vector<DicomTag>& parentTags,
                                     const std::vector<size_t>& parentIndexes,
-                                    Encoding encoding);
+                                    Encoding encoding,
+                                    bool hasCodeExtensions);
  
   static void ApplyVisitorToDataset(DcmItem& dataset,
                                     ITagVisitor& visitor,
                                     const std::vector<DicomTag>& parentTags,
                                     const std::vector<size_t>& parentIndexes,
-                                    Encoding encoding)
+                                    Encoding encoding,
+                                    bool hasCodeExtensions)
   {
     assert(parentTags.size() == parentIndexes.size());
 
@@ -2211,7 +2231,7 @@
       }
       else
       {
-        ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding);
+        ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions);
       }      
     }
   }
@@ -2222,7 +2242,8 @@
                                  const std::vector<DicomTag>& parentTags,
                                  const std::vector<size_t>& parentIndexes,
                                  const DicomTag& tag,
-                                 Encoding encoding)
+                                 Encoding encoding,
+                                 bool hasCodeExtensions)
   {
     // TODO - Merge this function, that is more recent, with ConvertLeafElement()
 
@@ -2299,7 +2320,7 @@
       if (c != NULL)  // This case corresponds to the empty string
       {
         std::string s(c);
-        utf8 = Toolbox::ConvertToUtf8(s, encoding);
+        utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
       }
 
       std::string newValue;
@@ -2380,7 +2401,7 @@
               std::string s(reinterpret_cast<const char*>(data), l);
               ITagVisitor::Action action = visitor.VisitString
                 (ignored, parentTags, parentIndexes, tag, vr,
-                 Toolbox::ConvertToUtf8(s, encoding));
+                 Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
 
               if (action != ITagVisitor::Action_None)
               {
@@ -2608,7 +2629,8 @@
                                     ITagVisitor& visitor,
                                     const std::vector<DicomTag>& parentTags,
                                     const std::vector<size_t>& parentIndexes,
-                                    Encoding encoding)
+                                    Encoding encoding,
+                                    bool hasCodeExtensions)
   {
     assert(parentTags.size() == parentIndexes.size());
 
@@ -2616,7 +2638,7 @@
 
     if (element.isLeaf())
     {
-      ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding);
+      ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions);
     }
     else
     {
@@ -2640,7 +2662,7 @@
         {
           indexes.back() = static_cast<size_t>(i);
           DcmItem* child = sequence.getItem(i);
-          ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding);
+          ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding, hasCodeExtensions);
         }
       }
     }
@@ -2653,7 +2675,8 @@
   {
     std::vector<DicomTag> parentTags;
     std::vector<size_t> parentIndexes;
-    Encoding encoding = DetectEncoding(dataset, defaultEncoding);
-    ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding);
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+    ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions);
   }
 }
--- a/Core/DicomParsing/FromDcmtkBridge.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/DicomParsing/FromDcmtkBridge.h	Sat Feb 16 10:29:26 2019 +0000
@@ -92,6 +92,7 @@
                               DicomToJsonFlags flags,
                               unsigned int maxStringLength,
                               Encoding encoding,
+                              bool hasCodeExtensions,
                               const std::set<DicomTag>& ignoreTagLength);
 
     static void ElementToJson(Json::Value& parent,
@@ -100,6 +101,7 @@
                               DicomToJsonFlags flags,
                               unsigned int maxStringLength,
                               Encoding dicomEncoding,
+                              bool hasCodeExtensions,
                               const std::set<DicomTag>& ignoreTagLength);
 
     static void ExtractDicomAsJson(Json::Value& target, 
@@ -112,6 +114,7 @@
 
     static void ChangeStringEncoding(DcmItem& dataset,
                                      Encoding source,
+                                     bool hasSourceCodeExtensions,
                                      Encoding target);
 
   public:
@@ -124,8 +127,17 @@
                                       unsigned int maxMultiplicity,
                                       const std::string& privateCreator);
 
+    static Encoding DetectEncoding(bool& hasCodeExtensions,
+                                   DcmItem& dataset,
+                                   Encoding defaultEncoding);
+
     static Encoding DetectEncoding(DcmItem& dataset,
-                                   Encoding defaultEncoding);
+                                   Encoding defaultEncoding)
+    {
+      // Compatibility wrapper for Orthanc <= 1.5.4
+      bool hasCodeExtensions;  // ignored
+      return DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+    }
 
     static DicomTag Convert(const DcmTag& tag);
 
@@ -137,6 +149,7 @@
                                           DicomToJsonFlags flags,
                                           unsigned int maxStringLength,
                                           Encoding encoding,
+                                          bool hasCodeExtensions,
                                           const std::set<DicomTag>& ignoreTagLength);
 
     static void ExtractHeaderAsJson(Json::Value& target, 
--- a/Core/DicomParsing/ParsedDicomFile.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/DicomParsing/ParsedDicomFile.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -645,7 +645,10 @@
     }
 
     InvalidateCache();
-    std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, GetEncoding()));
+
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions);
+    std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding));
     InsertInternal(*pimpl_->file_->getDataset(), element.release());
   }
 
@@ -706,8 +709,9 @@
     }
     else
     {
-      Encoding encoding = GetEncoding();
-      if (GetEncoding() != Encoding_Utf8)
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      if (encoding != Encoding_Utf8)
       {
         binary = Toolbox::ConvertFromUtf8(utf8Value, encoding);
         decoded = &binary;
@@ -766,7 +770,10 @@
       }
 
       std::auto_ptr<DcmElement> element(FromDcmtkBridge::CreateElementForTag(tag));
-      FromDcmtkBridge::FillElementWithString(*element, tag, utf8Value, decodeDataUriScheme, GetEncoding());
+
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      FromDcmtkBridge::FillElementWithString(*element, tag, utf8Value, decodeDataUriScheme, encoding);
 
       InsertInternal(dicom, element.release());
       UpdateStorageUid(tag, utf8Value, false);
@@ -805,7 +812,9 @@
         }
       }
 
-      InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, GetEncoding()));
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding));
 
       if (tag == DICOM_TAG_SOP_CLASS_UID ||
           tag == DICOM_TAG_SOP_INSTANCE_UID)
@@ -875,10 +884,13 @@
         return false;
       }
 
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      
       std::set<DicomTag> tmp;
       std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
                                   (*element, DicomToJsonFlags_Default, 
-                                   0, GetEncoding(), tmp));
+                                   0, encoding, hasCodeExtensions, tmp));
       
       if (v.get() == NULL ||
           v->IsNull())
@@ -1294,9 +1306,10 @@
   }
 
   
-  Encoding ParsedDicomFile::GetEncoding() const
+  Encoding ParsedDicomFile::DetectEncoding(bool& hasCodeExtensions) const
   {
-    return FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset(),
+    return FromDcmtkBridge::DetectEncoding(hasCodeExtensions,
+                                           *pimpl_->file_->getDataset(),
                                            GetDefaultDicomEncoding());
   }
 
@@ -1532,12 +1545,13 @@
 
   void ParsedDicomFile::ChangeEncoding(Encoding target)
   {
-    Encoding source = GetEncoding();
+    bool hasCodeExtensions;
+    Encoding source = DetectEncoding(hasCodeExtensions);
 
     if (source != target)  // Avoid unnecessary conversion
     {
       ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target));
-      FromDcmtkBridge::ChangeStringEncoding(*pimpl_->file_->getDataset(), source, target);
+      FromDcmtkBridge::ChangeStringEncoding(*pimpl_->file_->getDataset(), source, hasCodeExtensions, target);
     }
   }
 
--- a/Core/DicomParsing/ParsedDicomFile.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/DicomParsing/ParsedDicomFile.h	Sat Feb 16 10:29:26 2019 +0000
@@ -186,7 +186,7 @@
     void EmbedImage(MimeType mime,
                     const std::string& content);
 
-    Encoding GetEncoding() const;
+    Encoding DetectEncoding(bool& hasCodeExtensions) const;
 
     // WARNING: This function only sets the encoding, it will not
     // convert the encoding of the tags. Use "ChangeEncoding()" if need be.
--- a/Core/Enumerations.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/Enumerations.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -647,6 +647,15 @@
       case Encoding_Chinese:
         return "Chinese";
 
+      case Encoding_Korean:
+        return "Korean";
+
+      case Encoding_JapaneseKanji:
+        return "JapaneseKanji";
+
+      case Encoding_SimplifiedChinese:
+        return "SimplifiedChinese";
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -1202,6 +1211,21 @@
       return Encoding_Chinese;
     }
 
+    if (s == "KOREAN")
+    {
+      return Encoding_Korean;
+    }
+
+    if (s == "JAPANESEKANJI")
+    {
+      return Encoding_JapaneseKanji;
+    }
+
+    if (s == "SIMPLIFIEDCHINESE")
+    {
+      return Encoding_SimplifiedChinese;
+    }
+
     throw OrthancException(ErrorCode_ParameterOutOfRange);
   }
 
@@ -1836,11 +1860,13 @@
     {
       encoding = Encoding_Hebrew;
     }
-    else if (s == "ISO_IR 166" || s == "ISO 2022 IR 166")
+    else if (s == "ISO_IR 166" ||
+             s == "ISO 2022 IR 166")
     {
       encoding = Encoding_Thai;
     }
-    else if (s == "ISO_IR 13" || s == "ISO 2022 IR 13")
+    else if (s == "ISO_IR 13" ||
+             s == "ISO 2022 IR 13")
     {
       encoding = Encoding_Japanese;
     }
@@ -1855,18 +1881,22 @@
        **/
       encoding = Encoding_Chinese;
     }
+    else if (s == "ISO 2022 IR 149")
+    {
+      encoding = Encoding_Korean;
+    }
+    else if (s == "ISO 2022 IR 87")
+    {
+      encoding = Encoding_JapaneseKanji;
+    }
+    else if (s == "ISO 2022 IR 58")
+    {
+      encoding = Encoding_SimplifiedChinese;
+    }
     /*
-      else if (s == "ISO 2022 IR 149")
-      {
-      TODO
-      }
       else if (s == "ISO 2022 IR 159")
       {
-      TODO
-      }
-      else if (s == "ISO 2022 IR 87")
-      {
-      TODO
+      TODO - Supplementary Kanji set
       }
     */
     else
@@ -2013,6 +2043,15 @@
       case Encoding_Thai:
         return "ISO_IR 166";
 
+      case Encoding_Korean:
+        return "ISO 2022 IR 149";
+
+      case Encoding_JapaneseKanji:
+        return "ISO 2022 IR 87";
+
+      case Encoding_SimplifiedChinese:
+        return "ISO 2022 IR 58";
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
--- a/Core/Enumerations.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/Enumerations.h	Sat Feb 16 10:29:26 2019 +0000
@@ -442,10 +442,11 @@
     Encoding_Hebrew,
     Encoding_Thai,                          // TIS 620-2533
     Encoding_Japanese,                      // JIS X 0201 (Shift JIS): Katakana
-    Encoding_Chinese                        // GB18030 - Chinese simplified
-    //Encoding_JapaneseKanji,               // Multibyte - JIS X 0208: Kanji
+    Encoding_Chinese,                       // GB18030 - Chinese simplified
+    Encoding_JapaneseKanji,                 // Multibyte - JIS X 0208: Kanji
     //Encoding_JapaneseSupplementaryKanji,  // Multibyte - JIS X 0212: Supplementary Kanji set
-    //Encoding_Korean,                      // Multibyte - KS X 1001: Hangul and Hanja
+    Encoding_Korean,                        // Multibyte - KS X 1001: Hangul and Hanja
+    Encoding_SimplifiedChinese              // ISO 2022 IR 58
   };
 
 
--- a/Core/Images/ImageProcessing.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/Images/ImageProcessing.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -20,7 +20,7 @@
  * you do not wish to do so, delete this exception statement from your
  * version. If you delete this exception statement from all source files
  * in the program, then also delete it here.
- * 
+ *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
@@ -147,8 +147,8 @@
       {
         // Y = 0.2126 R + 0.7152 G + 0.0722 B
         int32_t v = (2126 * static_cast<int32_t>(s[0]) +
-                     7152 * static_cast<int32_t>(s[1]) +
-                     0722 * static_cast<int32_t>(s[2])) / 10000;
+            7152 * static_cast<int32_t>(s[1]) +
+            0722 * static_cast<int32_t>(s[2])) / 10000;
         
         if (static_cast<int32_t>(v) < static_cast<int32_t>(minValue))
         {
@@ -168,7 +168,7 @@
 
 
   static void MemsetZeroInternal(ImageAccessor& image)
-  {      
+  {
     const unsigned int height = image.GetHeight();
     const size_t lineSize = image.GetBytesPerPixel() * image.GetWidth();
     const size_t pitch = image.GetPitch();
@@ -535,8 +535,8 @@
         for (unsigned int x = 0; x < width; x++, q++)
         {
           *q = static_cast<uint8_t>((2126 * static_cast<uint32_t>(p[0]) +
-                                     7152 * static_cast<uint32_t>(p[1]) +
-                                     0722 * static_cast<uint32_t>(p[2])) / 10000);
+                                    7152 * static_cast<uint32_t>(p[1]) +
+              0722 * static_cast<uint32_t>(p[2])) / 10000);
           p += 4;
         }
       }
@@ -554,8 +554,8 @@
         for (unsigned int x = 0; x < width; x++, q++)
         {
           *q = static_cast<uint8_t>((2126 * static_cast<uint32_t>(p[2]) +
-                                     7152 * static_cast<uint32_t>(p[1]) +
-                                     0722 * static_cast<uint32_t>(p[0])) / 10000);
+                                    7152 * static_cast<uint32_t>(p[1]) +
+              0722 * static_cast<uint32_t>(p[0])) / 10000);
           p += 4;
         }
       }
@@ -788,33 +788,33 @@
   {
     switch (image.GetFormat())
     {
-      case PixelFormat_Grayscale8:
-        SetInternal<uint8_t>(image, value);
-        return;
+    case PixelFormat_Grayscale8:
+      SetInternal<uint8_t>(image, value);
+      return;
 
-      case PixelFormat_Grayscale16:
-        SetInternal<uint16_t>(image, value);
-        return;
+    case PixelFormat_Grayscale16:
+      SetInternal<uint16_t>(image, value);
+      return;
 
-      case PixelFormat_Grayscale32:
-        SetInternal<uint32_t>(image, value);
-        return;
+    case PixelFormat_Grayscale32:
+      SetInternal<uint32_t>(image, value);
+      return;
 
-      case PixelFormat_Grayscale64:
-        SetInternal<uint64_t>(image, value);
-        return;
+    case PixelFormat_Grayscale64:
+      SetInternal<uint64_t>(image, value);
+      return;
 
-      case PixelFormat_SignedGrayscale16:
-        SetInternal<int16_t>(image, value);
-        return;
+    case PixelFormat_SignedGrayscale16:
+      SetInternal<int16_t>(image, value);
+      return;
 
-      case PixelFormat_Float32:
-        assert(sizeof(float) == 4);
-        SetInternal<float>(image, value);
-        return;
+    case PixelFormat_Float32:
+      assert(sizeof(float) == 4);
+      SetInternal<float>(image, value);
+      return;
 
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
     }
   }
 
@@ -830,32 +830,32 @@
 
     switch (image.GetFormat())
     {
-      case PixelFormat_RGBA32:
-        p[0] = red;
-        p[1] = green;
-        p[2] = blue;
-        p[3] = alpha;
-        size = 4;
-        break;
+    case PixelFormat_RGBA32:
+      p[0] = red;
+      p[1] = green;
+      p[2] = blue;
+      p[3] = alpha;
+      size = 4;
+      break;
 
-      case PixelFormat_BGRA32:
-        p[0] = blue;
-        p[1] = green;
-        p[2] = red;
-        p[3] = alpha;
-        size = 4;
-        break;
+    case PixelFormat_BGRA32:
+      p[0] = blue;
+      p[1] = green;
+      p[2] = red;
+      p[3] = alpha;
+      size = 4;
+      break;
 
-      case PixelFormat_RGB24:
-        p[0] = red;
-        p[1] = green;
-        p[2] = blue;
-        size = 3;
-        break;
+    case PixelFormat_RGB24:
+      p[0] = red;
+      p[1] = green;
+      p[2] = blue;
+      size = 3;
+      break;
 
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }    
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
 
     const unsigned int width = image.GetWidth();
     const unsigned int height = image.GetHeight();
@@ -898,44 +898,44 @@
   {
     switch (image.GetFormat())
     {
-      case PixelFormat_Grayscale8:
-      {
-        uint8_t a, b;
-        GetMinMaxValueInternal<uint8_t>(a, b, image);
-        minValue = a;
-        maxValue = b;
-        break;
-      }
+    case PixelFormat_Grayscale8:
+    {
+      uint8_t a, b;
+      GetMinMaxValueInternal<uint8_t>(a, b, image);
+      minValue = a;
+      maxValue = b;
+      break;
+    }
 
-      case PixelFormat_Grayscale16:
-      {
-        uint16_t a, b;
-        GetMinMaxValueInternal<uint16_t>(a, b, image);
-        minValue = a;
-        maxValue = b;
-        break;
-      }
+    case PixelFormat_Grayscale16:
+    {
+      uint16_t a, b;
+      GetMinMaxValueInternal<uint16_t>(a, b, image);
+      minValue = a;
+      maxValue = b;
+      break;
+    }
 
-      case PixelFormat_Grayscale32:
-      {
-        uint32_t a, b;
-        GetMinMaxValueInternal<uint32_t>(a, b, image);
-        minValue = a;
-        maxValue = b;
-        break;
-      }
+    case PixelFormat_Grayscale32:
+    {
+      uint32_t a, b;
+      GetMinMaxValueInternal<uint32_t>(a, b, image);
+      minValue = a;
+      maxValue = b;
+      break;
+    }
 
-      case PixelFormat_SignedGrayscale16:
-      {
-        int16_t a, b;
-        GetMinMaxValueInternal<int16_t>(a, b, image);
-        minValue = a;
-        maxValue = b;
-        break;
-      }
+    case PixelFormat_SignedGrayscale16:
+    {
+      int16_t a, b;
+      GetMinMaxValueInternal<int16_t>(a, b, image);
+      minValue = a;
+      maxValue = b;
+      break;
+    }
 
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
     }
   }
 
@@ -946,18 +946,18 @@
   {
     switch (image.GetFormat())
     {
-      case PixelFormat_Float32:
-      {
-        assert(sizeof(float) == 4);
-        float a, b;
-        GetMinMaxValueInternal<float>(a, b, image);
-        minValue = a;
-        maxValue = b;
-        break;
-      }
+    case PixelFormat_Float32:
+    {
+      assert(sizeof(float) == 4);
+      float a, b;
+      GetMinMaxValueInternal<float>(a, b, image);
+      minValue = a;
+      maxValue = b;
+      break;
+    }
 
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
     }
   }
 
@@ -968,20 +968,20 @@
   {
     switch (image.GetFormat())
     {
-      case PixelFormat_Grayscale8:
-        AddConstantInternal<uint8_t>(image, value);
-        return;
+    case PixelFormat_Grayscale8:
+      AddConstantInternal<uint8_t>(image, value);
+      return;
 
-      case PixelFormat_Grayscale16:
-        AddConstantInternal<uint16_t>(image, value);
-        return;
+    case PixelFormat_Grayscale16:
+      AddConstantInternal<uint16_t>(image, value);
+      return;
 
-      case PixelFormat_SignedGrayscale16:
-        AddConstantInternal<int16_t>(image, value);
-        return;
+    case PixelFormat_SignedGrayscale16:
+      AddConstantInternal<int16_t>(image, value);
+      return;
 
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
     }
   }
 
@@ -992,41 +992,41 @@
   {
     switch (image.GetFormat())
     {
-      case PixelFormat_Grayscale8:
-        if (useRound)
-        {
-          MultiplyConstantInternal<uint8_t, true>(image, factor);
-        }
-        else
-        {
-          MultiplyConstantInternal<uint8_t, false>(image, factor);
-        }
-        return;
+    case PixelFormat_Grayscale8:
+      if (useRound)
+      {
+        MultiplyConstantInternal<uint8_t, true>(image, factor);
+      }
+      else
+      {
+        MultiplyConstantInternal<uint8_t, false>(image, factor);
+      }
+      return;
 
-      case PixelFormat_Grayscale16:
-        if (useRound)
-        {
-          MultiplyConstantInternal<uint16_t, true>(image, factor);
-        }
-        else
-        {
-          MultiplyConstantInternal<uint16_t, false>(image, factor);
-        }
-        return;
+    case PixelFormat_Grayscale16:
+      if (useRound)
+      {
+        MultiplyConstantInternal<uint16_t, true>(image, factor);
+      }
+      else
+      {
+        MultiplyConstantInternal<uint16_t, false>(image, factor);
+      }
+      return;
 
-      case PixelFormat_SignedGrayscale16:
-        if (useRound)
-        {
-          MultiplyConstantInternal<int16_t, true>(image, factor);
-        }
-        else
-        {
-          MultiplyConstantInternal<int16_t, false>(image, factor);
-        }
-        return;
+    case PixelFormat_SignedGrayscale16:
+      if (useRound)
+      {
+        MultiplyConstantInternal<int16_t, true>(image, factor);
+      }
+      else
+      {
+        MultiplyConstantInternal<int16_t, false>(image, factor);
+      }
+      return;
 
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
     }
   }
 
@@ -1038,70 +1038,100 @@
   {
     switch (image.GetFormat())
     {
-      case PixelFormat_Grayscale8:
-        if (useRound)
-        {
-          ShiftScaleInternal<uint8_t, true>(image, offset, scaling);
-        }
-        else
-        {
-          ShiftScaleInternal<uint8_t, false>(image, offset, scaling);
-        }
-        return;
+    case PixelFormat_Grayscale8:
+      if (useRound)
+      {
+        ShiftScaleInternal<uint8_t, true>(image, offset, scaling);
+      }
+      else
+      {
+        ShiftScaleInternal<uint8_t, false>(image, offset, scaling);
+      }
+      return;
 
-      case PixelFormat_Grayscale16:
-        if (useRound)
-        {
-          ShiftScaleInternal<uint16_t, true>(image, offset, scaling);
-        }
-        else
-        {
-          ShiftScaleInternal<uint16_t, false>(image, offset, scaling);
-        }
-        return;
+    case PixelFormat_Grayscale16:
+      if (useRound)
+      {
+        ShiftScaleInternal<uint16_t, true>(image, offset, scaling);
+      }
+      else
+      {
+        ShiftScaleInternal<uint16_t, false>(image, offset, scaling);
+      }
+      return;
 
-      case PixelFormat_SignedGrayscale16:
-        if (useRound)
-        {
-          ShiftScaleInternal<int16_t, true>(image, offset, scaling);
-        }
-        else
-        {
-          ShiftScaleInternal<int16_t, false>(image, offset, scaling);
-        }
-        return;
+    case PixelFormat_SignedGrayscale16:
+      if (useRound)
+      {
+        ShiftScaleInternal<int16_t, true>(image, offset, scaling);
+      }
+      else
+      {
+        ShiftScaleInternal<int16_t, false>(image, offset, scaling);
+      }
+      return;
 
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
     }
   }
 
 
-  void ImageProcessing::Invert(ImageAccessor& image)
+  void ImageProcessing::Invert(ImageAccessor& image, int64_t maxValue)
   {
     const unsigned int width = image.GetWidth();
     const unsigned int height = image.GetHeight();
-    
+
     switch (image.GetFormat())
     {
-      case PixelFormat_Grayscale8:
-      {
-        for (unsigned int y = 0; y < height; y++)
-        {
-          uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+    case PixelFormat_Grayscale16:
+    {
+      uint16_t maxValueUint16 = (uint16_t)(std::min(maxValue, static_cast<int64_t>(std::numeric_limits<uint16_t>::max())));
 
-          for (unsigned int x = 0; x < width; x++, p++)
-          {
-            *p = 255 - (*p);
-          }
+      for (unsigned int y = 0; y < height; y++)
+      {
+        uint16_t* p = reinterpret_cast<uint16_t*>(image.GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++, p++)
+        {
+          *p = maxValueUint16 - (*p);
         }
-        
-        return;
       }
 
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }   
+      return;
+    }
+    case PixelFormat_Grayscale8:
+    {
+      uint8_t maxValueUint8 = (uint8_t)(std::min(maxValue, static_cast<int64_t>(std::numeric_limits<uint8_t>::max())));
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++, p++)
+        {
+          *p = maxValueUint8 - (*p);
+        }
+      }
+
+      return;
+    }
+
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+  }
+
+  void ImageProcessing::Invert(ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+    case PixelFormat_Grayscale8:
+      return Invert(image, 255);
+    default:
+      throw OrthancException(ErrorCode_NotImplemented); // you should use the Invert(image, maxValue) overload
+    }
   }
 
 
@@ -1113,7 +1143,7 @@
     {
     private:
       typedef typename PixelTraits<Format>::PixelType  PixelType;
-    
+
       Orthanc::ImageAccessor&  image_;
       PixelType                value_;
 
@@ -1144,7 +1174,7 @@
             y = y + yi;
             d = d - 2 * dx;
           }
-      
+
           d = d + 2*dy;
         }
       }
@@ -1157,13 +1187,13 @@
         int dx = x1 - x0;
         int dy = y1 - y0;
         int xi = 1;
-    
+
         if (dx < 0)
         {
           xi = -1;
           dx = -dx;
         }
-    
+
         int d = 2 * dx - dy;
         int x = x0;
 
@@ -1176,7 +1206,7 @@
             x = x + xi;
             d = d - 2 * dy;
           }
-      
+
           d = d + 2 * dx;
         }
       }
@@ -1216,7 +1246,7 @@
       {
         // This is an implementation of Bresenham's line algorithm
         // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#All_cases
-    
+
         if (abs(y1 - y0) < abs(x1 - x0))
         {
           if (x0 > x1)
@@ -1252,30 +1282,30 @@
                                         int64_t value)
   {
     switch (image.GetFormat())
-    {       
-      case Orthanc::PixelFormat_Grayscale8:
-      {
-        BresenhamPixelWriter<Orthanc::PixelFormat_Grayscale8> writer(image, value);
-        writer.DrawSegment(x0, y0, x1, y1);
-        break;
-      }
+    {
+    case Orthanc::PixelFormat_Grayscale8:
+    {
+      BresenhamPixelWriter<Orthanc::PixelFormat_Grayscale8> writer(image, value);
+      writer.DrawSegment(x0, y0, x1, y1);
+      break;
+    }
 
-      case Orthanc::PixelFormat_Grayscale16:
-      {
-        BresenhamPixelWriter<Orthanc::PixelFormat_Grayscale16> writer(image, value);
-        writer.DrawSegment(x0, y0, x1, y1);
-        break;
-      }
+    case Orthanc::PixelFormat_Grayscale16:
+    {
+      BresenhamPixelWriter<Orthanc::PixelFormat_Grayscale16> writer(image, value);
+      writer.DrawSegment(x0, y0, x1, y1);
+      break;
+    }
 
-      case Orthanc::PixelFormat_SignedGrayscale16:
-      {
-        BresenhamPixelWriter<Orthanc::PixelFormat_SignedGrayscale16> writer(image, value);
-        writer.DrawSegment(x0, y0, x1, y1);
-        break;
-      }
-        
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    case Orthanc::PixelFormat_SignedGrayscale16:
+    {
+      BresenhamPixelWriter<Orthanc::PixelFormat_SignedGrayscale16> writer(image, value);
+      writer.DrawSegment(x0, y0, x1, y1);
+      break;
+    }
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
     }
   }
 
@@ -1292,46 +1322,46 @@
   {
     switch (image.GetFormat())
     {
-      case Orthanc::PixelFormat_BGRA32:
-      {
-        PixelTraits<Orthanc::PixelFormat_BGRA32>::PixelType pixel;
-        pixel.red_ = red;
-        pixel.green_ = green;
-        pixel.blue_ = blue;
-        pixel.alpha_ = alpha;
+    case Orthanc::PixelFormat_BGRA32:
+    {
+      PixelTraits<Orthanc::PixelFormat_BGRA32>::PixelType pixel;
+      pixel.red_ = red;
+      pixel.green_ = green;
+      pixel.blue_ = blue;
+      pixel.alpha_ = alpha;
+
+      BresenhamPixelWriter<Orthanc::PixelFormat_BGRA32> writer(image, pixel);
+      writer.DrawSegment(x0, y0, x1, y1);
+      break;
+    }
 
-        BresenhamPixelWriter<Orthanc::PixelFormat_BGRA32> writer(image, pixel);
-        writer.DrawSegment(x0, y0, x1, y1);
-        break;
-      }
-        
-      case Orthanc::PixelFormat_RGBA32:
-      {
-        PixelTraits<Orthanc::PixelFormat_RGBA32>::PixelType pixel;
-        pixel.red_ = red;
-        pixel.green_ = green;
-        pixel.blue_ = blue;
-        pixel.alpha_ = alpha;
+    case Orthanc::PixelFormat_RGBA32:
+    {
+      PixelTraits<Orthanc::PixelFormat_RGBA32>::PixelType pixel;
+      pixel.red_ = red;
+      pixel.green_ = green;
+      pixel.blue_ = blue;
+      pixel.alpha_ = alpha;
+
+      BresenhamPixelWriter<Orthanc::PixelFormat_RGBA32> writer(image, pixel);
+      writer.DrawSegment(x0, y0, x1, y1);
+      break;
+    }
 
-        BresenhamPixelWriter<Orthanc::PixelFormat_RGBA32> writer(image, pixel);
-        writer.DrawSegment(x0, y0, x1, y1);
-        break;
-      }
-        
-      case Orthanc::PixelFormat_RGB24:
-      {
-        PixelTraits<Orthanc::PixelFormat_RGB24>::PixelType pixel;
-        pixel.red_ = red;
-        pixel.green_ = green;
-        pixel.blue_ = blue;
+    case Orthanc::PixelFormat_RGB24:
+    {
+      PixelTraits<Orthanc::PixelFormat_RGB24>::PixelType pixel;
+      pixel.red_ = red;
+      pixel.green_ = green;
+      pixel.blue_ = blue;
 
-        BresenhamPixelWriter<Orthanc::PixelFormat_RGB24> writer(image, pixel);
-        writer.DrawSegment(x0, y0, x1, y1);
-        break;
-      }
-        
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      BresenhamPixelWriter<Orthanc::PixelFormat_RGB24> writer(image, pixel);
+      writer.DrawSegment(x0, y0, x1, y1);
+      break;
+    }
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
     }
   }
 }
--- a/Core/Images/ImageProcessing.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/Images/ImageProcessing.h	Sat Feb 16 10:29:26 2019 +0000
@@ -83,6 +83,8 @@
 
     void Invert(ImageAccessor& image);
 
+    void Invert(ImageAccessor& image, int64_t maxValue);
+
     void DrawLineSegment(ImageAccessor& image,
                          int x0,
                          int y0,
--- a/Core/JobsEngine/JobStatus.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/JobsEngine/JobStatus.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -49,10 +49,12 @@
 
   
   JobStatus::JobStatus(ErrorCode code,
+                       const std::string& details,
                        IJob& job) :
     errorCode_(code),
     progress_(job.GetProgress()),
-    publicContent_(Json::objectValue)
+    publicContent_(Json::objectValue),
+    details_(details)
   {
     if (progress_ < 0)
     {
--- a/Core/JobsEngine/JobStatus.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/JobsEngine/JobStatus.h	Sat Feb 16 10:29:26 2019 +0000
@@ -46,11 +46,13 @@
     Json::Value    publicContent_;
     Json::Value    serialized_;
     bool           hasSerialized_;
+    std::string    details_;
 
   public:
     JobStatus();
 
     JobStatus(ErrorCode code,
+              const std::string& details,
               IJob& job);
 
     ErrorCode GetErrorCode() const
@@ -84,5 +86,10 @@
     {
       return hasSerialized_;
     }
+
+    const std::string& GetDetails() const
+    {
+      return details_;
+    }
   };
 }
--- a/Core/JobsEngine/JobStepResult.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/JobsEngine/JobStepResult.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -46,14 +46,28 @@
   }
 
 
-  JobStepResult JobStepResult::Failure(const ErrorCode& error)
+  JobStepResult JobStepResult::Failure(const ErrorCode& error,
+                                       const char* details)
   {
     JobStepResult result(JobStepCode_Failure);
     result.error_ = error;
+
+    if (details != NULL)
+    {
+      result.failureDetails_ = details;
+    }
+    
     return result;
   }
 
 
+  JobStepResult JobStepResult::Failure(const OrthancException& exception)
+  {
+    return Failure(exception.GetErrorCode(),
+                   exception.HasDetails() ? exception.GetDetails() : NULL);
+  }
+  
+
   unsigned int JobStepResult::GetRetryTimeout() const
   {
     if (code_ == JobStepCode_Retry)
@@ -78,4 +92,17 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
   }
+
+
+  const std::string& JobStepResult::GetFailureDetails() const
+  {
+    if (code_ == JobStepCode_Failure)
+    {
+      return failureDetails_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
 }
--- a/Core/JobsEngine/JobStepResult.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/JobsEngine/JobStepResult.h	Sat Feb 16 10:29:26 2019 +0000
@@ -37,12 +37,15 @@
 
 namespace Orthanc
 {
+  class OrthancException;
+  
   class JobStepResult
   {
   private:
     JobStepCode   code_;
     unsigned int  timeout_;
     ErrorCode     error_;
+    std::string   failureDetails_;
     
     explicit JobStepResult(JobStepCode code) :
       code_(code),
@@ -71,7 +74,10 @@
 
     static JobStepResult Retry(unsigned int timeout);
 
-    static JobStepResult Failure(const ErrorCode& error);
+    static JobStepResult Failure(const ErrorCode& error,
+                                 const char* details);
+
+    static JobStepResult Failure(const OrthancException& exception);
 
     JobStepCode GetCode() const
     {
@@ -81,5 +87,7 @@
     unsigned int GetRetryTimeout() const;
 
     ErrorCode GetFailureCode() const;
+
+    const std::string& GetFailureDetails() const;
   };
 }
--- a/Core/JobsEngine/JobsEngine.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/JobsEngine/JobsEngine.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -75,39 +75,39 @@
     }
     catch (OrthancException& e)
     {
-      result = JobStepResult::Failure(e.GetErrorCode());
+      result = JobStepResult::Failure(e);
     }
     catch (boost::bad_lexical_cast&)
     {
-      result = JobStepResult::Failure(ErrorCode_BadFileFormat);
+      result = JobStepResult::Failure(ErrorCode_BadFileFormat, NULL);
     }
     catch (...)
     {
-      result = JobStepResult::Failure(ErrorCode_InternalError);
+      result = JobStepResult::Failure(ErrorCode_InternalError, NULL);
     }
 
     switch (result.GetCode())
     {
       case JobStepCode_Success:
         running.GetJob().Stop(JobStopReason_Success);
-        running.UpdateStatus(ErrorCode_Success);
+        running.UpdateStatus(ErrorCode_Success, "");
         running.MarkSuccess();
         return false;
 
       case JobStepCode_Failure:
         running.GetJob().Stop(JobStopReason_Failure);
-        running.UpdateStatus(result.GetFailureCode());
+        running.UpdateStatus(result.GetFailureCode(), result.GetFailureDetails());
         running.MarkFailure();
         return false;
 
       case JobStepCode_Retry:
         running.GetJob().Stop(JobStopReason_Retry);
-        running.UpdateStatus(ErrorCode_Success);
+        running.UpdateStatus(ErrorCode_Success, "");
         running.MarkRetry(result.GetRetryTimeout());
         return false;
 
       case JobStepCode_Continue:
-        running.UpdateStatus(ErrorCode_Success);
+        running.UpdateStatus(ErrorCode_Success, "");
         return true;
             
       default:
--- a/Core/JobsEngine/JobsRegistry.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/JobsEngine/JobsRegistry.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -110,7 +110,7 @@
       job->GetJobType(jobType_);
       job->Start();
 
-      lastStatus_ = JobStatus(ErrorCode_Success, *job_);
+      lastStatus_ = JobStatus(ErrorCode_Success, "", *job_);
     }
 
     const std::string& GetId() const
@@ -318,7 +318,7 @@
       job_->GetJobType(jobType_);
       job_->Start();
 
-      lastStatus_ = JobStatus(ErrorCode_Success, *job_);
+      lastStatus_ = JobStatus(ErrorCode_Success, "", *job_);
     }
   };
 
@@ -739,7 +739,7 @@
   }
 
 
-  bool JobsRegistry::SubmitAndWait(Json::Value& successContent,
+  void JobsRegistry::SubmitAndWait(Json::Value& successContent,
                                    IJob* job,        // Takes ownership
                                    int priority)
   {
@@ -764,7 +764,25 @@
         else if (state == JobState_Failure)
         {
           // Failure
-          break;
+          JobsIndex::const_iterator it = jobsIndex_.find(id);
+          if (it != jobsIndex_.end())  // Should always be true, already tested in GetStateInternal()
+          {
+            ErrorCode code = it->second->GetLastStatus().GetErrorCode();
+            const std::string& details = it->second->GetLastStatus().GetDetails();
+
+            if (details.empty())
+            {
+              throw OrthancException(code);
+            }
+            else
+            {
+              throw OrthancException(code, details);
+            }
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
         }
         else if (state == JobState_Success)
         {
@@ -781,7 +799,7 @@
             successContent = status.GetPublicContent();
           }
           
-          break;
+          return;
         }
         else
         {
@@ -790,8 +808,6 @@
         }
       }
     }
-
-    return (state == JobState_Success);
   }
 
 
@@ -1320,7 +1336,8 @@
   }
       
 
-  void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code)
+  void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code,
+                                              const std::string& details)
   {
     if (!IsValid())
     {
@@ -1328,7 +1345,7 @@
     }
     else
     {
-      JobStatus status(code, *job_);
+      JobStatus status(code, details, *job_);
           
       boost::mutex::scoped_lock lock(registry_.mutex_);
       registry_.CheckInvariants();
--- a/Core/JobsEngine/JobsRegistry.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/JobsEngine/JobsRegistry.h	Sat Feb 16 10:29:26 2019 +0000
@@ -175,7 +175,7 @@
     void Submit(IJob* job,        // Takes ownership
                 int priority);
 
-    bool SubmitAndWait(Json::Value& successContent,
+    void SubmitAndWait(Json::Value& successContent,
                        IJob* job,        // Takes ownership
                        int priority);
     
@@ -248,7 +248,8 @@
 
       void MarkRetry(unsigned int timeout);
 
-      void UpdateStatus(ErrorCode code);
+      void UpdateStatus(ErrorCode code,
+                        const std::string& details);
     };
   };
 }
--- a/Core/JobsEngine/SetOfCommandsJob.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/JobsEngine/SetOfCommandsJob.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -174,7 +174,7 @@
         // Error
         if (!permissive_)
         {
-          return JobStepResult::Failure(ErrorCode_InternalError);
+          return JobStepResult::Failure(ErrorCode_InternalError, NULL);
         }
       }
     }
@@ -186,7 +186,7 @@
       }
       else
       {
-        return JobStepResult::Failure(e.GetErrorCode());
+        return JobStepResult::Failure(e);
       }
     }
 
--- a/Core/Toolbox.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/Toolbox.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -464,55 +464,55 @@
 
       case Encoding_Latin1:
         return "ISO-8859-1";
-        break;
 
       case Encoding_Latin2:
         return "ISO-8859-2";
-        break;
 
       case Encoding_Latin3:
         return "ISO-8859-3";
-        break;
 
       case Encoding_Latin4:
         return "ISO-8859-4";
-        break;
 
       case Encoding_Latin5:
         return "ISO-8859-9";
-        break;
 
       case Encoding_Cyrillic:
         return "ISO-8859-5";
-        break;
 
       case Encoding_Windows1251:
         return "WINDOWS-1251";
-        break;
 
       case Encoding_Arabic:
         return "ISO-8859-6";
-        break;
 
       case Encoding_Greek:
         return "ISO-8859-7";
-        break;
 
       case Encoding_Hebrew:
         return "ISO-8859-8";
-        break;
         
       case Encoding_Japanese:
         return "SHIFT-JIS";
-        break;
 
       case Encoding_Chinese:
         return "GB18030";
-        break;
 
       case Encoding_Thai:
+#if BOOST_LOCALE_WITH_ICU == 1
+        return "tis620.2533";
+#else
         return "TIS620.2533-0";
-        break;
+#endif
+
+      case Encoding_Korean:
+        return "ISO-IR-149";
+
+      case Encoding_JapaneseKanji:
+        return "JIS";
+
+      case Encoding_SimplifiedChinese:
+        return "GB2312";
 
       default:
         throw OrthancException(ErrorCode_NotImplemented);
@@ -522,32 +522,52 @@
 
 
 #if ORTHANC_ENABLE_LOCALE == 1
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
   std::string Toolbox::ConvertToUtf8(const std::string& source,
-                                     Encoding sourceEncoding)
+                                     Encoding sourceEncoding,
+                                     bool hasCodeExtensions)
   {
     // The "::skip" flag makes boost skip invalid UTF-8
     // characters. This can occur in badly-encoded DICOM files.
     
     try
     {
-      if (sourceEncoding == Encoding_Utf8)
-      {
-        // Already in UTF-8: No conversion is required
-        return boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
-      }
-      else if (sourceEncoding == Encoding_Ascii)
+      if (sourceEncoding == Encoding_Ascii)
       {
         return ConvertToAscii(source);
       }
-      else
+      else 
       {
-        const char* encoding = GetBoostLocaleEncoding(sourceEncoding);
-        return boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip);
+        std::string s;
+        
+        if (sourceEncoding == Encoding_Utf8)
+        {
+          // Already in UTF-8: No conversion is required, but we ensure
+          // the output is correctly encoded
+          s = boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
+        }
+        else
+        {
+          const char* encoding = GetBoostLocaleEncoding(sourceEncoding);
+          s = boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip);
+        }
+
+        if (hasCodeExtensions)
+        {
+          std::string t;
+          RemoveIso2022EscapeSequences(t, s);
+          return t;
+        }
+        else
+        {
+          return s;
+        }        
       }
     }
-    catch (std::runtime_error&)
+    catch (std::runtime_error& e)
     {
       // Bad input string or bad encoding
+      LOG(INFO) << e.what();
       return ConvertToAscii(source);
     }
   }
@@ -1593,6 +1613,182 @@
 
     return boost::regex_replace(source, pattern, formatter);
   }
+
+
+  namespace Iso2022
+  {
+    /**
+       Returns whether the string s contains a single-byte control message
+       at index i
+    **/
+    static inline bool IsControlMessage1(const std::string& s, size_t i)
+    {
+      if (i < s.size())
+      {
+        char c = s[i];
+        return
+          (c == '\x0f') || // Locking shift zero
+          (c == '\x0e');   // Locking shift one
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       Returns whether the string s contains a double-byte control message
+       at index i
+    **/
+    static inline size_t IsControlMessage2(const std::string& s, size_t i)
+    {
+      if (i + 1 < s.size())
+      {
+        char c1 = s[i];
+        char c2 = s[i + 1];
+        return (c1 == 0x1b) && (
+          (c2 == '\x6e') || // Locking shift two
+          (c2 == '\x6f') || // Locking shift three
+          (c2 == '\x4e') || // Single shift two (alt)
+          (c2 == '\x4f') || // Single shift three (alt)
+          (c2 == '\x7c') || // Locking shift three right
+          (c2 == '\x7d') || // Locking shift two right
+          (c2 == '\x7e')    // Locking shift one right
+          );
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       Returns whether the string s contains a triple-byte control message
+       at index i
+    **/
+    static inline size_t IsControlMessage3(const std::string& s, size_t i)
+    {
+      if (i + 2 < s.size())
+      {
+        char c1 = s[i];
+        char c2 = s[i + 1];
+        char c3 = s[i + 2];
+        return ((c1 == '\x8e' && c2 == 0x1b && c3 == '\x4e') ||
+                (c1 == '\x8f' && c2 == 0x1b && c3 == '\x4f'));
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       This function returns true if the index i in the supplied string s:
+       - is valid
+       - contains the c character
+       This function returns false otherwise.
+    **/
+    static inline bool TestCharValue(
+      const std::string& s, size_t i, char c)
+    {
+      if (i < s.size())
+        return s[i] == c;
+      else
+        return false;
+    }
+
+    /**
+       This function returns true if the index i in the supplied string s:
+       - is valid
+       - has a c character that is >= cMin and <= cMax (included)
+       This function returns false otherwise.
+    **/
+    static inline bool TestCharRange(
+      const std::string& s, size_t i, char cMin, char cMax)
+    {
+      if (i < s.size())
+        return (s[i] >= cMin) && (s[i] <= cMax);
+      else
+        return false;
+    }
+
+    /**
+       This function returns the total length in bytes of the escape sequence
+       located in string s at index i, if there is one, or 0 otherwise.
+    **/
+    static inline size_t GetEscapeSequenceLength(const std::string& s, size_t i)
+    {
+      if (TestCharValue(s, i, 0x1b))
+      {
+        size_t j = i+1;
+
+        // advance reading cursor while we are in a sequence 
+        while (TestCharRange(s, j, '\x20', '\x2f'))
+          ++j;
+
+        // check there is a valid termination byte AND we're long enough (there
+        // must be at least one byte between 0x20 and 0x2f
+        if (TestCharRange(s, j, '\x30', '\x7f') && (j - i) >= 2)
+          return j - i + 1;
+        else
+          return 0;
+      }
+      else
+        return 0;
+    }
+  }
+
+  
+
+  /**
+     This function will strip all ISO/IEC 2022 control codes and escape
+     sequences.
+     Please see https://en.wikipedia.org/wiki/ISO/IEC_2022 (as of 2019-02)
+     for a list of those.
+
+     Please note that this operation is potentially destructive, because
+     it removes the character set information from the byte stream.
+
+     However, in the case where the encoding is unique, then suppressing
+     the escape sequences allows to provide us with a clean string after
+     conversion to utf-8 with boost.
+  **/
+  void Toolbox::RemoveIso2022EscapeSequences(std::string& dest, const std::string& src)
+  {
+    // we need AT MOST the same size as the source string in the output
+    dest.clear();
+    if (dest.capacity() < src.size())
+      dest.reserve(src.size());
+
+    size_t i = 0;
+
+    // uint8_t view to the string
+    while (i < src.size())
+    {
+      size_t j = i;
+
+      // The i index will only be incremented if a message is detected
+      // in that case, the message is skipped and the index is set to the
+      // next position to read
+      if (Iso2022::IsControlMessage1(src, i))
+        i += 1;
+      else if (Iso2022::IsControlMessage2(src, i))
+        i += 2;
+      else if (Iso2022::IsControlMessage3(src, i))
+        i += 3;
+      else
+        i += Iso2022::GetEscapeSequenceLength(src, i);
+
+      // if the index was NOT incremented, this means there was no message at
+      // this location: we then may copy the character at this index and 
+      // increment the index to point to the next read position
+      if (j == i)
+      {
+        dest.push_back(src[i]);
+        i++;
+      }
+    }
+  }
 }
 
 
--- a/Core/Toolbox.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Core/Toolbox.h	Sat Feb 16 10:29:26 2019 +0000
@@ -163,7 +163,8 @@
 
 #if ORTHANC_ENABLE_LOCALE == 1
     std::string ConvertToUtf8(const std::string& source,
-                              Encoding sourceEncoding);
+                              Encoding sourceEncoding,
+                              bool hasCodeExtensions);
 
     std::string ConvertFromUtf8(const std::string& source,
                                 Encoding targetEncoding);
@@ -248,6 +249,9 @@
 
     std::string SubstituteVariables(const std::string& source,
                                     const std::map<std::string, std::string>& dictionary);
+
+    void RemoveIso2022EscapeSequences(std::string& dest,
+                                      const std::string& src);
   }
 }
 
--- a/INSTALL	Wed Feb 13 13:01:21 2019 +0000
+++ b/INSTALL	Sat Feb 16 10:29:26 2019 +0000
@@ -71,11 +71,12 @@
 
 
 
-Native Windows build with Microsoft Visual Studio
--------------------------------------------------
+Native Windows build with Microsoft Visual Studio 2008
+------------------------------------------------------
 
 # cd [...]\OrthancBuild
-# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON -DUSE_LEGACY_JSONCPP=ON -G "Visual Studio 9 2008" [...]\Orthanc
+# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON \
+  -DUSE_LEGACY_JSONCPP=ON -G "Visual Studio 9 2008" [...]\Orthanc
 
 Then open the "[...]/OrthancBuild/Orthanc.sln" with Visual Studio.
 
@@ -91,6 +92,16 @@
   Visual Studio that do not support C++11
 
 
+Orthanc as compiled above will not work properly with some Asian
+encodings (unit tests will fail). In international setups, you can
+compile Orthanc together with ICU as follows:
+
+# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON \
+  -DBOOST_LOCALE_BACKEND=icu -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON \
+  -G "Visual Studio 9 2008" [...]\Orthanc
+
+
+
 Native Windows build with Microsoft Visual Studio 2015, Ninja and QtCreator
 ---------------------------------------------------------------------------
 
@@ -105,6 +116,11 @@
 * Import build from [...]\OrthancBuild
 
 
+Instructions to include support for Asian encodings:
+
+# cmake -G Ninja -DSTATIC_BUILD=ON -DBOOST_LOCALE_BACKEND=icu [...]\Orthanc
+
+
 
 Cross-Compilation for Windows under GNU/Linux
 ---------------------------------------------
--- a/NEWS	Wed Feb 13 13:01:21 2019 +0000
+++ b/NEWS	Sat Feb 16 10:29:26 2019 +0000
@@ -1,6 +1,14 @@
 Pending changes in the mainline
 ===============================
 
+* Separation of ideographic and phonetic characters in DICOMweb JSON and XML
+* Support of the following multi-byte specific character sets:
+  - Japanese Kanji (ISO 2022 IR 87)
+  - Korean (ISO 2022 IR 149)
+  - Simplified Chinese (ISO 2022 IR 58)
+* Basic support for character sets with code extensions (ISO 2022 escape sequences)
+* Fix issue #134 (/patient/modify gives 500, should really be 400)
+
 
 Version 1.5.4 (2019-02-08)
 ==========================
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -519,7 +519,10 @@
           else if (tag["Type"] == "String")
           {
             std::string value = tag["Value"].asString();
-            dicom.ReplacePlainString(*it, Toolbox::ConvertFromUtf8(value, dicom.GetEncoding()));
+
+            bool hasCodeExtensions;
+            Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+            dicom.ReplacePlainString(*it, Toolbox::ConvertFromUtf8(value, encoding));
           }
         }
       }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -250,17 +250,11 @@
     if (synchronous)
     {
       Json::Value successContent;
-      if (context.GetJobsEngine().GetRegistry().SubmitAndWait
-          (successContent, raii.release(), priority))
-      {
-        // Success in synchronous execution
-        output.AnswerJson(successContent);
-      }
-      else
-      {
-        // Error during synchronous execution
-        output.SignalError(HttpStatus_500_InternalServerError);
-      }
+      context.GetJobsEngine().GetRegistry().SubmitAndWait
+        (successContent, raii.release(), priority);
+
+      // Success in synchronous execution
+      output.AnswerJson(successContent);
     }
     else
     {
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -147,8 +147,9 @@
       job->SetSynchronousTarget(tmp);
     
       Json::Value publicContent;
-      if (context.GetJobsEngine().GetRegistry().SubmitAndWait
-          (publicContent, job.release(), priority))
+      context.GetJobsEngine().GetRegistry().SubmitAndWait
+        (publicContent, job.release(), priority);
+      
       {
         // The archive is now created: Prepare the sending of the ZIP file
         FilesystemHttpSender sender(tmp->GetPath(), MimeType_Zip);
@@ -157,10 +158,6 @@
         // Send the ZIP
         output.AnswerStream(sender);
       }
-      else
-      {
-        output.SignalError(HttpStatus_500_InternalServerError);
-      }
     }
     else
     {
--- a/OrthancServer/Search/DatabaseLookup.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/OrthancServer/Search/DatabaseLookup.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -95,7 +95,8 @@
 
 
   bool DatabaseLookup::IsMatch(DcmItem& item,
-                               Encoding encoding) const
+                               Encoding encoding,
+                               bool hasCodeExtensions) const
   {
     for (size_t i = 0; i < constraints_.size(); i++)
     {
@@ -118,7 +119,7 @@
       std::set<DicomTag> ignoreTagLength;
       std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
                                       (*element, DicomToJsonFlags_None, 
-                                       0, encoding, ignoreTagLength));
+                                       0, encoding, hasCodeExtensions, ignoreTagLength));
 
       // WARNING: Also modify "HierarchicalMatcher::Setup()" if modifying this code
       if (value.get() == NULL ||
--- a/OrthancServer/Search/DatabaseLookup.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/OrthancServer/Search/DatabaseLookup.h	Sat Feb 16 10:29:26 2019 +0000
@@ -74,7 +74,8 @@
     bool IsMatch(const DicomMap& value) const;
 
     bool IsMatch(DcmItem& item,
-                 Encoding encoding) const;
+                 Encoding encoding,
+                 bool hasCodeExtensions) const;
 
     void AddDicomConstraint(const DicomTag& tag,
                             const std::string& dicomQuery,
--- a/OrthancServer/Search/HierarchicalMatcher.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/OrthancServer/Search/HierarchicalMatcher.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -53,7 +53,9 @@
       caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
     }
 
-    Setup(*query.GetDcmtkObject().getDataset(), caseSensitivePN, query.GetEncoding());
+    bool hasCodeExtensions;
+    Encoding encoding = query.DetectEncoding(hasCodeExtensions);
+    Setup(*query.GetDcmtkObject().getDataset(), caseSensitivePN, encoding, hasCodeExtensions);
   }
 
 
@@ -72,7 +74,8 @@
 
   void HierarchicalMatcher::Setup(DcmItem& dataset,
                                   bool caseSensitivePN,
-                                  Encoding encoding)
+                                  Encoding encoding,
+                                  bool hasCodeExtensions)
   {
     for (unsigned long i = 0; i < dataset.card(); i++)
     {
@@ -108,7 +111,7 @@
         }
         else if (sequence.card() == 1)
         {
-          sequences_[tag] = new HierarchicalMatcher(*sequence.getItem(0), caseSensitivePN, encoding);
+          sequences_[tag] = new HierarchicalMatcher(*sequence.getItem(0), caseSensitivePN, encoding, hasCodeExtensions);
         }
         else
         {
@@ -122,7 +125,7 @@
         std::set<DicomTag> ignoreTagLength;
         std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
                                         (*element, DicomToJsonFlags_None, 
-                                         0, encoding, ignoreTagLength));
+                                         0, encoding, hasCodeExtensions, ignoreTagLength));
 
         // WARNING: Also modify "DatabaseLookup::IsMatch()" if modifying this code
         if (value.get() == NULL ||
@@ -197,15 +200,19 @@
 
   bool HierarchicalMatcher::Match(ParsedDicomFile& dicom) const
   {
+    bool hasCodeExtensions;
+    Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+    
     return MatchInternal(*dicom.GetDcmtkObject().getDataset(),
-                         dicom.GetEncoding());
+                         encoding, hasCodeExtensions);
   }
 
 
   bool HierarchicalMatcher::MatchInternal(DcmItem& item,
-                                          Encoding encoding) const
+                                          Encoding encoding,
+                                          bool hasCodeExtensions) const
   {
-    if (!flatConstraints_.IsMatch(item, encoding))
+    if (!flatConstraints_.IsMatch(item, encoding, hasCodeExtensions))
     {
       return false;
     }
@@ -228,7 +235,7 @@
 
         for (unsigned long i = 0; i < sequence->card(); i++)
         {
-          if (it->second->MatchInternal(*sequence->getItem(i), encoding))
+          if (it->second->MatchInternal(*sequence->getItem(i), encoding, hasCodeExtensions))
           {
             match = true;
             break;
@@ -247,7 +254,8 @@
 
 
   DcmDataset* HierarchicalMatcher::ExtractInternal(DcmItem& source,
-                                                   Encoding encoding) const
+                                                   Encoding encoding,
+                                                   bool hasCodeExtensions) const
   {
     std::auto_ptr<DcmDataset> target(new DcmDataset);
 
@@ -283,13 +291,13 @@
           {
             cloned->append(new DcmItem(*sequence->getItem(i)));
           }
-          else if (it->second->MatchInternal(*sequence->getItem(i), encoding))  // TODO Might be optimized
+          else if (it->second->MatchInternal(*sequence->getItem(i), encoding, hasCodeExtensions))  // TODO Might be optimized
           {
             // It is necessary to encapsulate the child dataset into a
             // "DcmItem" object before it can be included in a
             // sequence. Otherwise, "dciodvfy" reports an error "Bad
             // tag in sequence - Expecting Item or Sequence Delimiter."
-            std::auto_ptr<DcmDataset> child(it->second->ExtractInternal(*sequence->getItem(i), encoding));
+            std::auto_ptr<DcmDataset> child(it->second->ExtractInternal(*sequence->getItem(i), encoding, hasCodeExtensions));
             cloned->append(new DcmItem(*child));
           }
         }
@@ -304,11 +312,14 @@
 
   ParsedDicomFile* HierarchicalMatcher::Extract(ParsedDicomFile& dicom) const
   {
+    bool hasCodeExtensions;
+    Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+    
     std::auto_ptr<DcmDataset> dataset(ExtractInternal(*dicom.GetDcmtkObject().getDataset(),
-                                                      dicom.GetEncoding()));
+                                                      encoding, hasCodeExtensions));
 
     std::auto_ptr<ParsedDicomFile> result(new ParsedDicomFile(*dataset));
-    result->SetEncoding(dicom.GetEncoding());
+    result->SetEncoding(encoding);
 
     return result.release();
   }
--- a/OrthancServer/Search/HierarchicalMatcher.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/OrthancServer/Search/HierarchicalMatcher.h	Sat Feb 16 10:29:26 2019 +0000
@@ -51,20 +51,24 @@
 
     void Setup(DcmItem& query,
                bool caseSensitivePN,
-               Encoding encoding);
+               Encoding encoding,
+               bool hasCodeExtensions);
 
     HierarchicalMatcher(DcmItem& query,
                         bool caseSensitivePN,
-                        Encoding encoding)
+                        Encoding encoding,
+                        bool hasCodeExtensions)
     {
-      Setup(query, caseSensitivePN, encoding);
+      Setup(query, caseSensitivePN, encoding, hasCodeExtensions);
     }
 
     bool MatchInternal(DcmItem& dicom,
-                       Encoding encoding) const;
+                       Encoding encoding,
+                       bool hasCodeExtensions) const;
 
     DcmDataset* ExtractInternal(DcmItem& dicom,
-                                Encoding encoding) const;
+                                Encoding encoding,
+                                bool hasCodeExtensions) const;
 
   public:
     HierarchicalMatcher(ParsedDicomFile& query);
--- a/OrthancServer/ServerJobs/ArchiveJob.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/OrthancServer/ServerJobs/ArchiveJob.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -943,7 +943,8 @@
         synchronousTarget_.unique())
     {
       LOG(WARNING) << "A client has disconnected while creating an archive";
-      return JobStepResult::Failure(ErrorCode_NetworkProtocol);          
+      return JobStepResult::Failure(ErrorCode_NetworkProtocol,
+                                    "A client has disconnected while creating an archive");
     }
         
     if (writer_->GetStepsCount() == 0)
--- a/Plugins/Engine/PluginsJob.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Plugins/Engine/PluginsJob.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -88,7 +88,7 @@
         return JobStepResult::Success();
 
       case OrthancPluginJobStepStatus_Failure:
-        return JobStepResult::Failure(ErrorCode_Plugin);
+        return JobStepResult::Failure(ErrorCode_Plugin, NULL);
 
       case OrthancPluginJobStepStatus_Continue:
         return JobStepResult::Continue();
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Sat Feb 16 10:29:26 2019 +0000
@@ -5868,7 +5868,7 @@
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param callback The main callback.
    * @param getMoveSize Callback to read the number of C-Move suboperations.
-   * @param applyMove Callback to apply one C-Move suboperations.
+   * @param applyMove Callback to apply one C-Move suboperation.
    * @param freeMove Callback to free the C-Move driver.
    * @return 0 if success, other value if error.
    * @ingroup DicomCallbacks
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -891,7 +891,7 @@
   }
 
 
-  void OrthancImage::CheckImageAvailable()
+  void OrthancImage::CheckImageAvailable() const
   {
     if (image_ == NULL)
     {
@@ -985,42 +985,42 @@
   }
 
 
-  OrthancPluginPixelFormat OrthancImage::GetPixelFormat()
+  OrthancPluginPixelFormat OrthancImage::GetPixelFormat() const
   {
     CheckImageAvailable();
     return OrthancPluginGetImagePixelFormat(GetGlobalContext(), image_);
   }
 
 
-  unsigned int OrthancImage::GetWidth()
+  unsigned int OrthancImage::GetWidth() const
   {
     CheckImageAvailable();
     return OrthancPluginGetImageWidth(GetGlobalContext(), image_);
   }
 
 
-  unsigned int OrthancImage::GetHeight()
+  unsigned int OrthancImage::GetHeight() const
   {
     CheckImageAvailable();
     return OrthancPluginGetImageHeight(GetGlobalContext(), image_);
   }
 
 
-  unsigned int OrthancImage::GetPitch()
+  unsigned int OrthancImage::GetPitch() const
   {
     CheckImageAvailable();
     return OrthancPluginGetImagePitch(GetGlobalContext(), image_);
   }
 
 
-  const void* OrthancImage::GetBuffer()
+  const void* OrthancImage::GetBuffer() const
   {
     CheckImageAvailable();
     return OrthancPluginGetImageBuffer(GetGlobalContext(), image_);
   }
 
 
-  void OrthancImage::CompressPngImage(MemoryBuffer& target)
+  void OrthancImage::CompressPngImage(MemoryBuffer& target) const
   {
     CheckImageAvailable();
 
@@ -1033,7 +1033,7 @@
 
 
   void OrthancImage::CompressJpegImage(MemoryBuffer& target,
-                                       uint8_t quality)
+                                       uint8_t quality) const
   {
     CheckImageAvailable();
 
@@ -1045,7 +1045,7 @@
   }
 
 
-  void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output)
+  void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output) const
   {
     CheckImageAvailable();
     OrthancPluginCompressAndAnswerPngImage(GetGlobalContext(), output, GetPixelFormat(),
@@ -1054,7 +1054,7 @@
 
 
   void OrthancImage::AnswerJpegImage(OrthancPluginRestOutput* output,
-                                     uint8_t quality)
+                                     uint8_t quality) const
   {
     CheckImageAvailable();
     OrthancPluginCompressAndAnswerJpegImage(GetGlobalContext(), output, GetPixelFormat(),
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Feb 13 13:01:21 2019 +0000
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Sat Feb 16 10:29:26 2019 +0000
@@ -335,7 +335,7 @@
 
     void Clear();
 
-    void CheckImageAvailable();
+    void CheckImageAvailable() const;
 
   public:
     OrthancImage();
@@ -368,30 +368,30 @@
                           size_t size,
                           unsigned int frame);
 
-    OrthancPluginPixelFormat GetPixelFormat();
+    OrthancPluginPixelFormat GetPixelFormat() const;
 
-    unsigned int GetWidth();
+    unsigned int GetWidth() const;
 
-    unsigned int GetHeight();
+    unsigned int GetHeight() const;
 
-    unsigned int GetPitch();
+    unsigned int GetPitch() const;
     
-    const void* GetBuffer();
+    const void* GetBuffer() const;
 
     const OrthancPluginImage* GetObject() const
     {
       return image_;
     }
 
-    void CompressPngImage(MemoryBuffer& target);
+    void CompressPngImage(MemoryBuffer& target) const;
 
     void CompressJpegImage(MemoryBuffer& target,
-                           uint8_t quality);
+                           uint8_t quality) const;
 
-    void AnswerPngImage(OrthancPluginRestOutput* output);
+    void AnswerPngImage(OrthancPluginRestOutput* output) const;
 
     void AnswerJpegImage(OrthancPluginRestOutput* output,
-                         uint8_t quality);
+                         uint8_t quality) const;
   };
 
 
--- a/Resources/CMake/BoostConfiguration.cmake	Wed Feb 13 13:01:21 2019 +0000
+++ b/Resources/CMake/BoostConfiguration.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -228,6 +228,18 @@
   if (NOT ENABLE_LOCALE)
     message("boost::locale is disabled")
   else()
+    set(BOOST_ICU_SOURCES
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/boundary.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/codecvt.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/collator.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/conversion.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/date_time.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/formatter.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/icu_backend.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/numeric.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/time_zone.cpp
+      )
+
     list(APPEND BOOST_SOURCES
       ${BOOST_SOURCES_DIR}/libs/locale/src/encoding/codepage.cpp
       ${BOOST_SOURCES_DIR}/libs/locale/src/shared/generator.cpp
@@ -246,6 +258,11 @@
 
     if (CMAKE_SYSTEM_NAME STREQUAL "OpenBSD" OR
         CMAKE_SYSTEM_VERSION STREQUAL "LinuxStandardBase")
+      add_definitions(
+        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
+        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
+        )
+      
       list(APPEND BOOST_SOURCES
         ${BOOST_SOURCES_DIR}/libs/locale/src/std/codecvt.cpp
         ${BOOST_SOURCES_DIR}/libs/locale/src/std/collate.cpp
@@ -254,12 +271,16 @@
         ${BOOST_SOURCES_DIR}/libs/locale/src/std/std_backend.cpp
         )
 
-      add_definitions(
-        -DBOOST_LOCALE_WITH_ICONV=1
-        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
-        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
-        )
-      
+      if (BOOST_LOCALE_BACKEND STREQUAL "gcc" OR
+          BOOST_LOCALE_BACKEND STREQUAL "libiconv")
+        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
+      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
+        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
+      else()
+        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
+      endif()
+
     elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR
             CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR
             CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR
@@ -268,6 +289,11 @@
             CMAKE_SYSTEM_NAME STREQUAL "NaCl32" OR
             CMAKE_SYSTEM_NAME STREQUAL "NaCl64" OR
             CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # For WebAssembly or asm.js
+      add_definitions(
+        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
+        -DBOOST_LOCALE_NO_STD_BACKEND=1
+        )
+      
       list(APPEND BOOST_SOURCES
         ${BOOST_SOURCES_DIR}/libs/locale/src/posix/codecvt.cpp
         ${BOOST_SOURCES_DIR}/libs/locale/src/posix/collate.cpp
@@ -276,13 +302,25 @@
         ${BOOST_SOURCES_DIR}/libs/locale/src/posix/posix_backend.cpp
         )
 
+      if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR
+          BOOST_LOCALE_BACKEND STREQUAL "gcc" OR
+          BOOST_LOCALE_BACKEND STREQUAL "libiconv")
+        # In WebAssembly or asm.js, we rely on the version of iconv
+        # that is shipped with the stdlib
+        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
+      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
+        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
+      else()
+        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
+      endif()
+
+    elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
       add_definitions(
-        -DBOOST_LOCALE_WITH_ICONV=1
-        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
+        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
         -DBOOST_LOCALE_NO_STD_BACKEND=1
         )
-      
-    elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+
       list(APPEND BOOST_SOURCES
         ${BOOST_SOURCES_DIR}/libs/locale/src/win32/collate.cpp
         ${BOOST_SOURCES_DIR}/libs/locale/src/win32/converter.cpp
@@ -291,21 +329,22 @@
         ${BOOST_SOURCES_DIR}/libs/locale/src/win32/win_backend.cpp
         )
 
-      add_definitions(
-        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
-        -DBOOST_LOCALE_NO_STD_BACKEND=1
-        )
+      # Starting with release 0.8.2, Orthanc statically links against
+      # libiconv on Windows. Indeed, the "WCONV" library of Windows XP
+      # seems not to support properly several codepages (notably
+      # "Latin3", "Hebrew", and "Arabic"). Set "BOOST_LOCALE_BACKEND"
+      # to "wconv" to use WCONV anyway.
 
-      # Starting with release 0.8.2, Orthanc statically links against
-      # libiconv, even on Windows. Indeed, the "WCONV" library of
-      # Windows XP seems not to support properly several codepages
-      # (notably "Latin3", "Hebrew", and "Arabic"). Set
-      # "USE_BOOST_ICONV" to "OFF" to use WCONV anyway.
-
-      if (USE_BOOST_ICONV)
+      if (BOOST_LOCALE_BACKEND STREQUAL "libiconv")
         add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
+      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
+        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
+      elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv")
+        message("Using Window's wconv")
+        add_definitions(-DBOOST_LOCALE_WITH_WCONV=1)
       else()
-        add_definitions(-DBOOST_LOCALE_WITH_WCONV=1)
+        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND on Windows: ${BOOST_LOCALE_BACKEND}")
       endif()
 
     else()
--- a/Resources/CMake/Compiler.cmake	Wed Feb 13 13:01:21 2019 +0000
+++ b/Resources/CMake/Compiler.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -36,8 +36,14 @@
     string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}")
   endforeach(flag_var)
 
-  # Add /Zm256 compiler option to Visual Studio to fix PCH errors
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256")
+  if (BOOST_LOCALE_BACKEND STREQUAL "icu")
+    # If compiling icu, the heap space must be further increased:
+    # "icudt58l_dat.c(1638339): fatal error C1060: compiler is out of heap space"
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm512")
+  else()
+    # Add /Zm256 compiler option to Visual Studio to fix PCH errors
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256")
+  endif()
 
   add_definitions(
     -D_CRT_SECURE_NO_WARNINGS=1
--- a/Resources/CMake/DownloadPackage.cmake	Wed Feb 13 13:01:21 2019 +0000
+++ b/Resources/CMake/DownloadPackage.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -57,45 +57,64 @@
   if (${TAR_EXECUTABLE} MATCHES "TAR_EXECUTABLE-NOTFOUND")
     message(FATAL_ERROR "Please install the 'tar' package")
   endif()
+
+  find_program(GUNZIP_EXECUTABLE gunzip)
+  if (${GUNZIP_EXECUTABLE} MATCHES "GUNZIP_EXECUTABLE-NOTFOUND")
+    message(FATAL_ERROR "Please install the 'gzip' package")
+  endif()
 endif()
 
 
+macro(DownloadFile MD5 Url)
+  GetUrlFilename(TMP_FILENAME "${Url}")
+
+  set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}")
+  if (NOT EXISTS "${TMP_PATH}")
+    message("Downloading ${Url}")
+
+    # This fixes issue 6: "I think cmake shouldn't download the
+    # packages which are not in the system, it should stop and let
+    # user know."
+    # https://code.google.com/p/orthanc/issues/detail?id=6
+    if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
+      message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
+    endif()
+
+    if ("${MD5}" STREQUAL "no-check")
+      message(WARNING "Not checking the MD5 of: ${Url}")
+      file(DOWNLOAD "${Url}" "${TMP_PATH}"
+        SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60
+        STATUS Failure)
+    else()
+      file(DOWNLOAD "${Url}" "${TMP_PATH}"
+        SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60
+        EXPECTED_MD5 "${MD5}" STATUS Failure)
+    endif()
+
+    list(GET Failure 0 Status)
+    if (NOT Status EQUAL 0)
+      message(FATAL_ERROR "Cannot download file: ${Url}")
+    endif()
+    
+  else()
+    message("Using local copy of ${Url}")
+
+    if ("${MD5}" STREQUAL "no-check")
+      message(WARNING "Not checking the MD5 of: ${Url}")
+    else()
+      file(MD5 ${TMP_PATH} ActualMD5)
+      if (NOT "${ActualMD5}" STREQUAL "${MD5}")
+        message(FATAL_ERROR "The MD5 hash of a previously download file is invalid: ${TMP_PATH}")
+      endif()
+    endif()
+  endif()
+endmacro()
+
+
 macro(DownloadPackage MD5 Url TargetDirectory)
   if (NOT IS_DIRECTORY "${TargetDirectory}")
-    GetUrlFilename(TMP_FILENAME "${Url}")
-
-    set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}")
-    if (NOT EXISTS "${TMP_PATH}")
-      message("Downloading ${Url}")
-
-      # This fixes issue 6: "I think cmake shouldn't download the
-      # packages which are not in the system, it should stop and let
-      # user know."
-      # https://code.google.com/p/orthanc/issues/detail?id=6
-      if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
-	message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
-      endif()
-
-      if ("${MD5}" STREQUAL "no-check")
-        message(WARNING "Not checking the MD5 of: ${Url}")
-        file(DOWNLOAD "${Url}" "${TMP_PATH}" SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60)
-      else()
-        file(DOWNLOAD "${Url}" "${TMP_PATH}" SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60 EXPECTED_MD5 "${MD5}")
-      endif()
-
-    else()
-      message("Using local copy of ${Url}")
-
-      if ("${MD5}" STREQUAL "no-check")
-        message(WARNING "Not checking the MD5 of: ${Url}")
-      else()
-        file(MD5 ${TMP_PATH} ActualMD5)
-        if (NOT "${ActualMD5}" STREQUAL "${MD5}")
-          message(FATAL_ERROR "The MD5 hash of a previously download file is invalid: ${TMP_PATH}")
-        endif()
-      endif()
-    endif()
-
+    DownloadFile("${MD5}" "${Url}")
+    
     GetUrlExtension(TMP_EXTENSION "${Url}")
     #message(${TMP_EXTENSION})
     message("Uncompressing ${TMP_FILENAME}")
@@ -140,7 +159,7 @@
           OUTPUT_QUIET
           )
       else()
-        message(FATAL_ERROR "Support your platform here")
+        message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}")
       endif()
 
     else()
@@ -170,7 +189,7 @@
           RESULT_VARIABLE Failure
           )
       else()
-        message(FATAL_ERROR "Unknown package format.")
+        message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}")
       endif()
     endif()
    
@@ -183,3 +202,58 @@
     endif()
   endif()
 endmacro()
+
+
+
+macro(DownloadCompressedFile MD5 Url TargetFile)
+  message(${MD5})
+  message(${Url})
+  message(${TargetFile})
+  if (NOT EXISTS "${TargetFile}")
+    DownloadFile("${MD5}" "${Url}")
+    
+    GetUrlExtension(TMP_EXTENSION "${Url}")
+    #message(${TMP_EXTENSION})
+    message("Uncompressing ${TMP_FILENAME}")
+
+    if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+      # How to silently extract files using 7-zip
+      # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
+
+      if ("${TMP_EXTENSION}" STREQUAL "gz")
+        execute_process(
+          COMMAND ${ZIP_EXECUTABLE} e -y ${TMP_PATH}
+          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+          RESULT_VARIABLE Failure
+          OUTPUT_QUIET
+          )
+
+        if (Failure)
+          message(FATAL_ERROR "Error while running the uncompression tool")
+        endif()
+
+      else()
+        message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
+      endif()
+
+    else()
+      if ("${TMP_EXTENSION}" STREQUAL "gz")
+        execute_process(
+          COMMAND sh -c "${GUNZIP_EXECUTABLE} -c ${TMP_PATH}"
+          OUTPUT_FILE "${TargetFile}"
+          RESULT_VARIABLE Failure
+          )
+      else()
+        message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
+      endif()
+    endif()
+   
+    if (Failure)
+      message(FATAL_ERROR "Error while running the uncompression tool")
+    endif()
+
+    if (NOT EXISTS "${TargetFile}")
+      message(FATAL_ERROR "The file was not uncompressed at the proper location. Check the CMake instructions.")
+    endif()
+  endif()
+endmacro()
--- a/Resources/CMake/LibIconvConfiguration.cmake	Wed Feb 13 13:01:21 2019 +0000
+++ b/Resources/CMake/LibIconvConfiguration.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -1,98 +1,90 @@
-if (NOT ENABLE_LOCALE)
-  message("Support for locales is disabled")
-
-elseif (NOT USE_BOOST_ICONV)
-  message("Not using libiconv")
+message("Using libiconv")
 
-else()
-  message("Using libiconv")
+if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICONV)
+  set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.15)
+  set(LIBICONV_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libiconv-1.15.tar.gz")
+  set(LIBICONV_MD5 "ace8b5f2db42f7b3b3057585e80d9808")
 
-  if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICONV)
-    set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.15)
-    set(LIBICONV_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libiconv-1.15.tar.gz")
-    set(LIBICONV_MD5 "ace8b5f2db42f7b3b3057585e80d9808")
-
-    DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}")
+  DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}")
 
-    # Disable the support of libiconv that is shipped by default with
-    # the C standard library on Linux. Setting this macro redirects
-    # calls from "iconv*()" to "libiconv*()" by defining macros in the
-    # C headers of "libiconv-1.15".
-    add_definitions(-DLIBICONV_PLUG=1)
+  # Disable the support of libiconv that is shipped by default with
+  # the C standard library on Linux. Setting this macro redirects
+  # calls from "iconv*()" to "libiconv*()" by defining macros in the
+  # C headers of "libiconv-1.15".
+  add_definitions(-DLIBICONV_PLUG=1)
 
-    # https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ
-    add_definitions(
-      -DBUILDING_LIBICONV=1
-      -DIN_LIBRARY=1
-      -DLIBDIR=""
-      -DICONV_CONST=
-      )
+  # https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ
+  add_definitions(
+    -DBUILDING_LIBICONV=1
+    -DIN_LIBRARY=1
+    -DLIBDIR=""
+    -DICONV_CONST=
+    #-DENABLE_EXTRA=1
+    )
 
-    configure_file(
-      ${LIBICONV_SOURCES_DIR}/srclib/localcharset.h
-      ${LIBICONV_SOURCES_DIR}/include
-      COPYONLY)
+  configure_file(
+    ${LIBICONV_SOURCES_DIR}/srclib/localcharset.h
+    ${LIBICONV_SOURCES_DIR}/include
+    COPYONLY)
 
-    set(HAVE_VISIBILITY 0)
-    set(ICONV_CONST ${ICONV_CONST})
-    set(USE_MBSTATE_T 1)
-    set(BROKEN_WCHAR_H 0)
-    set(EILSEQ)
-    set(HAVE_WCHAR_T 1)
-    configure_file(
-      ${LIBICONV_SOURCES_DIR}/include/iconv.h.build.in
-      ${LIBICONV_SOURCES_DIR}/include/iconv.h
-      )
-    unset(HAVE_VISIBILITY)
-    unset(ICONV_CONST)
-    unset(USE_MBSTATE_T)
-    unset(BROKEN_WCHAR_H)
-    unset(EILSEQ)
-    unset(HAVE_WCHAR_T)
+  set(HAVE_VISIBILITY 0)
+  set(ICONV_CONST ${ICONV_CONST})
+  set(USE_MBSTATE_T 1)
+  set(BROKEN_WCHAR_H 0)
+  set(EILSEQ)
+  set(HAVE_WCHAR_T 1)
+  configure_file(
+    ${LIBICONV_SOURCES_DIR}/include/iconv.h.build.in
+    ${LIBICONV_SOURCES_DIR}/include/iconv.h
+    )
+  unset(HAVE_VISIBILITY)
+  unset(ICONV_CONST)
+  unset(USE_MBSTATE_T)
+  unset(BROKEN_WCHAR_H)
+  unset(EILSEQ)
+  unset(HAVE_WCHAR_T)
 
-    if (NOT EXISTS ${LIBICONV_SOURCES_DIR}/include/config.h)
-      # Create an empty "config.h" for libiconv
-      file(WRITE ${LIBICONV_SOURCES_DIR}/include/config.h "")
-    endif()
+  if (NOT EXISTS ${LIBICONV_SOURCES_DIR}/include/config.h)
+    # Create an empty "config.h" for libiconv
+    file(WRITE ${LIBICONV_SOURCES_DIR}/include/config.h "")
+  endif()
 
-    include_directories(
-      ${LIBICONV_SOURCES_DIR}/include
-      )
+  include_directories(
+    ${LIBICONV_SOURCES_DIR}/include
+    )
 
-    set(LIBICONV_SOURCES
-      ${LIBICONV_SOURCES_DIR}/lib/iconv.c  
-      ${LIBICONV_SOURCES_DIR}/lib/relocatable.c
-      ${LIBICONV_SOURCES_DIR}/libcharset/lib/localcharset.c  
-      ${LIBICONV_SOURCES_DIR}/libcharset/lib/relocatable.c
-      )
+  set(LIBICONV_SOURCES
+    ${LIBICONV_SOURCES_DIR}/lib/iconv.c  
+    ${LIBICONV_SOURCES_DIR}/lib/relocatable.c
+    ${LIBICONV_SOURCES_DIR}/libcharset/lib/localcharset.c  
+    ${LIBICONV_SOURCES_DIR}/libcharset/lib/relocatable.c
+    )
 
-    source_group(ThirdParty\\libiconv REGULAR_EXPRESSION ${LIBICONV_SOURCES_DIR}/.*)
+  source_group(ThirdParty\\libiconv REGULAR_EXPRESSION ${LIBICONV_SOURCES_DIR}/.*)
 
-    if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
-      add_definitions(-DHAVE_WORKING_O_NOFOLLOW=0)
-    else()
-      add_definitions(-DHAVE_WORKING_O_NOFOLLOW=1)
-    endif()
+  if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+    add_definitions(-DHAVE_WORKING_O_NOFOLLOW=0)
+  else()
+    add_definitions(-DHAVE_WORKING_O_NOFOLLOW=1)
+  endif()
 
-  else() 
-    CHECK_INCLUDE_FILE_CXX(iconv.h HAVE_ICONV_H)
-    if (NOT HAVE_ICONV_H)
-      message(FATAL_ERROR "Please install the libiconv-dev package")
-    endif()
+else() 
+  CHECK_INCLUDE_FILE_CXX(iconv.h HAVE_ICONV_H)
+  if (NOT HAVE_ICONV_H)
+    message(FATAL_ERROR "Please install the libiconv-dev package")
+  endif()
 
-    # Check whether the support for libiconv is bundled within the
-    # standard C library
-    CHECK_FUNCTION_EXISTS(iconv_open HAVE_ICONV_LIB)
-    if (NOT HAVE_ICONV_LIB)
-      # No builtin support for libiconv, try and find an external library.
-      # Open question: Does this make sense on any platform?
-      CHECK_LIBRARY_EXISTS(iconv iconv_open "" HAVE_ICONV_LIB_2)
-      if (NOT HAVE_ICONV_LIB_2)
-        message(FATAL_ERROR "Please install the libiconv-dev package")
-      else()
-        link_libraries(iconv)
-      endif()
+  # Check whether the support for libiconv is bundled within the
+  # standard C library
+  CHECK_FUNCTION_EXISTS(iconv_open HAVE_ICONV_LIB)
+  if (NOT HAVE_ICONV_LIB)
+    # No builtin support for libiconv, try and find an external library.
+    # Open question: Does this make sense on any platform?
+    CHECK_LIBRARY_EXISTS(iconv iconv_open "" HAVE_ICONV_LIB_2)
+    if (NOT HAVE_ICONV_LIB_2)
+      message(FATAL_ERROR "Please install the libiconv-dev package")
+    else()
+      link_libraries(iconv)
     endif()
-
   endif()
 endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/LibIcuConfiguration.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -0,0 +1,71 @@
+
+# Check out: ../ThirdParty/icu/README.txt
+
+# http://userguide.icu-project.org/packaging
+# http://userguide.icu-project.org/howtouseicu
+
+message("Using libicu")
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICU)
+  include(${CMAKE_CURRENT_LIST_DIR}/../ThirdParty/icu/Version.cmake)
+  DownloadPackage(${LIBICU_MD5} ${LIBICU_URL} "${LIBICU_SOURCES_DIR}")
+  DownloadCompressedFile(${LIBICU_DATA_MD5} ${LIBICU_DATA_URL} ${LIBICU_DATA})
+  
+  include_directories(BEFORE
+    ${LIBICU_SOURCES_DIR}/source/common
+    ${LIBICU_SOURCES_DIR}/source/i18n
+    )
+
+  set(LIBICU_SOURCES
+    ${CMAKE_BINARY_DIR}/${LIBICU_DATA}
+    )
+
+  aux_source_directory(${LIBICU_SOURCES_DIR}/source/common LIBICU_SOURCES)
+  aux_source_directory(${LIBICU_SOURCES_DIR}/source/i18n LIBICU_SOURCES)
+
+  add_definitions(
+    #-DU_COMBINED_IMPLEMENTATION
+    #-DU_DEF_ICUDATA_ENTRY_POINT=icudt63l_dat
+    #-DU_LIB_SUFFIX_C_NAME=l
+
+    #-DUCONFIG_NO_SERVICE=1
+    -DU_COMMON_IMPLEMENTATION
+    -DU_ENABLE_DYLOAD=0
+    -DU_HAVE_STD_STRING=1
+    -DU_I18N_IMPLEMENTATION
+    -DU_IO_IMPLEMENTATION
+    -DU_STATIC_IMPLEMENTATION=1
+    #-DU_CHARSET_IS_UTF8
+    -DUNISTR_FROM_STRING_EXPLICIT=
+    )
+
+  set_source_files_properties(
+    ${CMAKE_BINARY_DIR}/${LIBICU_DATA}
+    PROPERTIES COMPILE_DEFINITIONS "char16_t=uint16_t"
+    )
+
+  if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+    set_source_files_properties(
+      ${LIBICU_SOURCES_DIR}/source/common/locmap.c
+      PROPERTIES COMPILE_DEFINITIONS "LOCALE_SNAME=0x0000005c"
+      )
+  endif()
+
+  source_group(ThirdParty\\libicu REGULAR_EXPRESSION ${LIBICU_SOURCES_DIR}/.*)
+
+else() 
+  CHECK_INCLUDE_FILE_CXX(unicode/uvernum.h HAVE_ICU_H)
+  if (NOT HAVE_ICU_H)
+    message(FATAL_ERROR "Please install the libicu-dev package")
+  endif()
+
+  find_library(LIBICU_PATH_1 NAMES icuuc)
+  find_library(LIBICU_PATH_2 NAMES icui18n)
+
+  if (NOT LIBICU_PATH_1 OR 
+      NOT LIBICU_PATH_2)
+    message(FATAL_ERROR "Please install the libicu-dev package")
+  else()
+    link_libraries(icuuc icui18n)
+  endif()
+endif()
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed Feb 13 13:01:21 2019 +0000
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -94,7 +94,7 @@
 endif()
 
 if (NOT ENABLE_LOCALE)
-  unset(USE_SYSTEM_LIBICONV CACHE)
+  unset(BOOST_LOCALE_BACKEND CACHE)
   add_definitions(-DORTHANC_ENABLE_LOCALE=0)
 endif()
 
@@ -378,16 +378,25 @@
 
 
 ##
-## Locale support: libiconv
+## Locale support
 ##
 
 if (ENABLE_LOCALE)
   if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
     # In WebAssembly or asm.js, we rely on the version of iconv that
     # is shipped with the stdlib
-    unset(USE_BOOST_ICONV CACHE)
+    unset(BOOST_LOCALE_BACKEND CACHE)
   else()
-    include(${CMAKE_CURRENT_LIST_DIR}/LibIconvConfiguration.cmake)
+    if (BOOST_LOCALE_BACKEND STREQUAL "gcc")
+    elseif (BOOST_LOCALE_BACKEND STREQUAL "libiconv")
+      include(${CMAKE_CURRENT_LIST_DIR}/LibIconvConfiguration.cmake)
+    elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+      include(${CMAKE_CURRENT_LIST_DIR}/LibIcuConfiguration.cmake)
+    elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv")
+      message("Using Microsoft Window's wconv")
+    else()
+      message(FATAL_ERROR "Invalid value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
+    endif()
   endif()
   
   add_definitions(-DORTHANC_ENABLE_LOCALE=1)
@@ -610,6 +619,7 @@
   ${CURL_SOURCES}
   ${JSONCPP_SOURCES}
   ${LIBICONV_SOURCES}
+  ${LIBICU_SOURCES}
   ${LIBJPEG_SOURCES}
   ${LIBP11_SOURCES}
   ${LIBPNG_SOURCES}
--- a/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Feb 13 13:01:21 2019 +0000
+++ b/Resources/CMake/OrthancFrameworkParameters.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -43,6 +43,7 @@
 set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test")
 set(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
 set(USE_SYSTEM_LIBICONV ON CACHE BOOL "Use the system version of libiconv")
+set(USE_SYSTEM_LIBICU ON CACHE BOOL "Use the system version of libicu")
 set(USE_SYSTEM_LIBJPEG ON CACHE BOOL "Use the system version of libjpeg")
 set(USE_SYSTEM_LIBP11 OFF CACHE BOOL "Use the system version of libp11 (PKCS#11 wrapper library)")
 set(USE_SYSTEM_LIBPNG ON CACHE BOOL "Use the system version of libpng")
@@ -66,15 +67,15 @@
 # Advanced and distribution-specific parameters
 set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
 set(SYSTEM_MONGOOSE_USE_CALLBACKS ON CACHE BOOL "The system version of Mongoose uses callbacks (version >= 3.7)")
-set(USE_BOOST_ICONV ON CACHE BOOL "Use iconv instead of wconv (Windows only)")
+set(BOOST_LOCALE_BACKEND "libiconv" CACHE STRING "Back-end for locales that is used by Boost (can be \"gcc\", \"libiconv\", \"icu\", or \"wconv\" on Windows)")
 set(USE_PUGIXML ON CACHE BOOL "Use the Pugixml parser (turn off only for debug)")
-set(USE_LEGACY_JSONCPP OFF CACHE BOOL "Use the old branch 0.x.y of JsonCpp, that does not require a C++11 compiler (for old versions of Visual Studio)")
+set(USE_LEGACY_JSONCPP OFF CACHE BOOL "Use the old branch 0.x.y of JsonCpp, that does not require a C++11 compiler (for LSB and old versions of Visual Studio)")
+set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
 
 mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
 mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS)
-mark_as_advanced(USE_BOOST_ICONV)
+mark_as_advanced(BOOST_LOCALE_BACKEND)
 mark_as_advanced(USE_PUGIXML)
-mark_as_advanced(USE_LEGACY_JSONCPP)
 
 
 #####################################################################
--- a/Resources/Configuration.json	Wed Feb 13 13:01:21 2019 +0000
+++ b/Resources/Configuration.json	Sat Feb 16 10:29:26 2019 +0000
@@ -111,7 +111,8 @@
   // C-Find requests (including worklists). The allowed values are
   // "Ascii", "Utf8", "Latin1", "Latin2", "Latin3", "Latin4",
   // "Latin5", "Cyrillic", "Windows1251", "Arabic", "Greek", "Hebrew",
-  // "Thai", "Japanese", and "Chinese".
+  // "Thai", "Japanese", "Chinese", "JapaneseKanji", "Korean", and
+  // "SimplifiedChinese".
   "DefaultEncoding" : "Latin1",
 
   // The transfer syntaxes that are accepted by Orthanc C-Store SCP
--- a/Resources/LinuxStandardBaseToolchain.cmake	Wed Feb 13 13:01:21 2019 +0000
+++ b/Resources/LinuxStandardBaseToolchain.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -1,4 +1,4 @@
-# LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON
+# LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DBOOST_LOCALE_BACKEND=icu
 
 INCLUDE(CMakeForceCompiler)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/ThirdParty/icu/CMakeLists.txt	Sat Feb 16 10:29:26 2019 +0000
@@ -0,0 +1,88 @@
+cmake_minimum_required(VERSION 2.8)
+project(IcuCodeGeneration)
+
+set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
+
+if (NOT USE_LEGACY_LIBICU)
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
+endif()
+
+include(${CMAKE_SOURCE_DIR}/../../CMake/Compiler.cmake)
+include(${CMAKE_SOURCE_DIR}/../../CMake/DownloadPackage.cmake)
+include(Version.cmake)
+
+set(SOURCE_DATA
+  "${LIBICU_SOURCES_DIR}/source/data/in/${LIBICU_DATA_VERSION}${LIBICU_SUFFIX}.dat")
+
+set(ALLOW_DOWNLOADS ON)
+DownloadPackage(${LIBICU_MD5} ${LIBICU_URL} "${LIBICU_SOURCES_DIR}")
+
+include_directories(
+  ${LIBICU_SOURCES_DIR}/source/common
+  ${LIBICU_SOURCES_DIR}/source/i18n
+  ${LIBICU_SOURCES_DIR}/source/tools/toolutil/
+  )
+
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/common         LIBICU_SOURCES)
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/i18n           LIBICU_SOURCES)
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/tools/toolutil LIBICU_SOURCES)
+
+if (USE_LEGACY_LIBICU)
+  list(APPEND LIBICU_SOURCES
+    ${LIBICU_SOURCES_DIR}/source/stubdata/stubdata.c
+    )
+else()
+  list(APPEND LIBICU_SOURCES
+    ${LIBICU_SOURCES_DIR}/source/stubdata/stubdata.cpp
+    )
+  set_source_files_properties(
+    ${LIBICU_SOURCES_DIR}/source/tools/genccode/genccode.c
+    PROPERTIES COMPILE_DEFINITIONS "char16_t=uint16_t"
+    )
+endif()
+
+
+
+add_executable(IcuCodeGeneration
+  ${LIBICU_SOURCES_DIR}/source/tools/genccode/genccode.c
+  ${LIBICU_SOURCES}
+  )
+
+configure_file(${SOURCE_DATA}
+  ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat
+  COPYONLY)
+
+add_custom_command(
+  OUTPUT   ${CMAKE_BINARY_DIR}/${LIBICU_DATA}
+  COMMAND  IcuCodeGeneration ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat
+  DEPENDS  IcuCodeGeneration
+  )
+
+message(${LIBICU_DATA}.gz)
+
+
+# "--no-name" is necessary for 7-zip on Windows to behave similarly to gunzip
+add_custom_command(
+  OUTPUT   ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  COMMAND  gzip ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat.c --no-name -c > ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  DEPENDS  ${LIBICU_DATA}
+  )
+
+add_custom_target(Final ALL DEPENDS  ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz)
+
+install(
+  FILES ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  DESTINATION ${CMAKE_SOURCE_DIR}/../../../ThirdPartyDownloads
+  )
+
+add_definitions(
+  #-DU_COMBINED_IMPLEMENTATION
+  -DUCONFIG_NO_SERVICE=1
+  -DU_COMMON_IMPLEMENTATION
+  -DU_ENABLE_DYLOAD=0
+  -DU_HAVE_STD_STRING=1
+  -DU_I18N_IMPLEMENTATION
+  -DU_IO_IMPLEMENTATION
+  -DU_STATIC_IMPLEMENTATION=1
+  -DU_TOOLUTIL_IMPLEMENTATION
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/ThirdParty/icu/README.txt	Sat Feb 16 10:29:26 2019 +0000
@@ -0,0 +1,36 @@
+Generating ICU data file
+========================
+
+This folder generates the "icudtXXX_dat.c" file that contains the
+resources internal to ICU.
+
+IMPORTANT: Since ICU 59, C++11 is mandatory, making it incompatible
+with Linux Standard Base (LSB) SDK. The option
+"-DUSE_LEGACY_LIBICU=ON" will use the latest version of ICU that does
+not use C++11 (58-2).
+
+
+Usage
+-----
+
+Newest release of icu:
+
+$ cmake .. -G Ninja && ninja install
+
+Legacy version for LSB:
+
+$ cmake .. -G Ninja -DUSE_LEGACY_LIBICU=ON && ninja install
+
+Legacy version using LSB:
+
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -G Ninja \
+  -DCMAKE_TOOLCHAIN_FILE=../../../LinuxStandardBaseToolchain.cmake \
+  -DUSE_LEGACY_LIBICU=ON
+$ ninja install
+
+
+Result
+------
+
+The resulting files are placed in the "ThirdPartyDownloads" folder at
+the root of the Orthanc repository (next to the main "CMakeLists.txt").
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/ThirdParty/icu/Version.cmake	Sat Feb 16 10:29:26 2019 +0000
@@ -0,0 +1,32 @@
+# NB: Orthanc assume that the platform is of ASCII-family, not of
+# EBCDIC-family. A "e" suffix would be needed on EBCDIC. Look for
+# macro "U_ICUDATA_TYPE_LETTER" in the source code of icu for more
+# information.
+
+include(TestBigEndian)
+TEST_BIG_ENDIAN(IS_BIG_ENDIAN)
+if(IS_BIG_ENDIAN)
+  set(LIBICU_SUFFIX "b")
+else()
+  set(LIBICU_SUFFIX "l")
+endif()
+
+set(LIBICU_BASE_URL "http://orthanc.osimis.io/ThirdPartyDownloads")
+
+if (USE_LEGACY_LIBICU)
+  # This is the last version of icu that compiles with C++11
+  # support. It can be used for Linux Standard Base and Visual Studio 2008.
+  set(LIBICU_URL "${LIBICU_BASE_URL}/icu4c-58_2-src.tgz")
+  set(LIBICU_MD5 "fac212b32b7ec7ab007a12dff1f3aea1")
+  set(LIBICU_DATA_VERSION "icudt58")
+  set(LIBICU_DATA_MD5 "ce2c7791ab637898553c121633155fb6")
+else()
+  set(LIBICU_URL "${LIBICU_BASE_URL}/icu4c-63_1-src.tgz")
+  set(LIBICU_MD5 "9e40f6055294284df958200e308bce50")
+  set(LIBICU_DATA_VERSION "icudt63")
+  set(LIBICU_DATA_MD5 "92b5c73a1accd8ecf8c20c89bc6925a9")
+endif()
+
+set(LIBICU_SOURCES_DIR ${CMAKE_BINARY_DIR}/icu)
+set(LIBICU_DATA "${LIBICU_DATA_VERSION}${LIBICU_SUFFIX}_dat.c")
+set(LIBICU_DATA_URL "${LIBICU_BASE_URL}/${LIBICU_DATA}.gz")
--- a/TODO	Wed Feb 13 13:01:21 2019 +0000
+++ b/TODO	Sat Feb 16 10:29:26 2019 +0000
@@ -2,9 +2,6 @@
 === Orthanc Roadmap ===
 =======================
 
-The wishlist from Orthanc users is available on Trello:
-https://trello.com/b/gcn33tDM/orthanc-wishlist
-
 
 =======
 General
@@ -82,6 +79,15 @@
 * Support extended association:
   https://groups.google.com/d/msg/orthanc-users/xD4d3mpc6ms/srF7E2goAAAJ
 
+---------
+Encodings
+---------
+
+* Support multiple specific character sets (cf. "SCSH32" in orthanc-tests)
+  - http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
+  - Japanese test: http://dicom.nema.org/MEDICAL/dicom/2017c/output/chtml/part05/sect_H.3.2.html
+* Support Supplementary Kanji set (ISO 2022 IR 159)
+
 
 =======
 Plugins
--- a/UnitTestsSources/DicomMapTests.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/UnitTestsSources/DicomMapTests.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -425,7 +425,7 @@
   const unsigned char raw[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 };
   std::string latin1((char*) &raw[0], sizeof(raw) / sizeof(char));
 
-  std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1);
+  std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1, false);
 
   ParsedDicomFile dicom(false);
   dicom.SetEncoding(Encoding_Latin1);
--- a/UnitTestsSources/FromDcmtkTests.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -36,6 +36,7 @@
 
 #include "../Core/DicomNetworking/DicomFindAnswers.h"
 #include "../Core/DicomParsing/DicomModification.h"
+#include "../Core/DicomParsing/DicomWebJsonVisitor.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
 #include "../Core/DicomParsing/ToDcmtkBridge.h"
@@ -53,6 +54,11 @@
 
 #include <dcmtk/dcmdata/dcelem.h>
 #include <dcmtk/dcmdata/dcdeftag.h>
+#include <boost/algorithm/string/predicate.hpp>
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include <pugixml.hpp>
+#endif
 
 using namespace Orthanc;
 
@@ -217,7 +223,7 @@
   {
     std::string source(testEncodingsEncoded[i]);
     std::string expected(testEncodingsExpected[i]);
-    std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i]);
+    std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false);
     //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
     EXPECT_EQ(expected, s);
   }
@@ -260,9 +266,10 @@
   ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 166"));  ASSERT_EQ(Encoding_Thai, e);
 
   // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-4
-  ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 87"));   //ASSERT_EQ(Encoding_JapaneseKanji, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 87"));    ASSERT_EQ(Encoding_JapaneseKanji, e);
   ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 159"));  //ASSERT_EQ(Encoding_JapaneseKanjiSupplementary, e);
-  ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 149"));  //ASSERT_EQ(Encoding_Korean, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 149"));   ASSERT_EQ(Encoding_Korean, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 58"));    ASSERT_EQ(Encoding_SimplifiedChinese, e);
 
   // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-5
   ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 192"));  ASSERT_EQ(Encoding_Utf8, e);
@@ -282,7 +289,7 @@
       ParsedDicomFile f(true);
       f.SetEncoding(testEncodings[i]);
 
-      std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i]);
+      std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
       f.Insert(DICOM_TAG_PATIENT_NAME, s, false);
       f.SaveToMemoryBuffer(dicom);
     }
@@ -293,7 +300,9 @@
 
       if (testEncodings[i] != Encoding_Ascii)
       {
-        ASSERT_EQ(testEncodings[i], g.GetEncoding());
+        bool hasCodeExtensions;
+        ASSERT_EQ(testEncodings[i], g.DetectEncoding(hasCodeExtensions));
+        ASSERT_FALSE(hasCodeExtensions);
       }
 
       std::string tag;
@@ -405,16 +414,16 @@
       ignoreTagLength.insert(DICOM_TAG_PATIENT_ID);
 
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
       ASSERT_TRUE(b.isMember("0010,0010"));
       ASSERT_EQ("Hello", b["0010,0010"].asString());
 
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength);
       ASSERT_TRUE(b["0010,0010"].isNull()); // "Hello" has more than 3 characters
 
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
-                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength);
       ASSERT_TRUE(b["0010,0010"].isObject());
       ASSERT_EQ("PatientName", b["0010,0010"]["Name"].asString());
       ASSERT_EQ("TooLong", b["0010,0010"]["Type"].asString());
@@ -422,7 +431,7 @@
 
       ignoreTagLength.insert(DICOM_TAG_PATIENT_NAME);
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength);
       ASSERT_EQ("Hello", b["0010,0010"].asString());
     }
 
@@ -448,7 +457,7 @@
       Json::Value b;
       std::set<DicomTag> ignoreTagLength;
       FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, ignoreTagLength);
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
       ASSERT_EQ("Hello", b["0010,0010"].asString());
     }
 
@@ -461,7 +470,7 @@
         Json::Value b;
         std::set<DicomTag> ignoreTagLength;
         FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, ignoreTagLength);
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
         ASSERT_EQ(Json::arrayValue, b["0008,1110"].type());
         ASSERT_EQ(2u, b["0008,1110"].size());
       
@@ -480,7 +489,7 @@
         Json::Value b;
         std::set<DicomTag> ignoreTagLength;
         FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
-                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, ignoreTagLength);
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
 
         Json::Value c;
         ServerToolbox::SimplifyTags(c, b, DicomToJsonFormat_Human);
@@ -599,10 +608,12 @@
 
       if (testEncodings[i] != Encoding_Ascii)
       {
-        ASSERT_EQ(testEncodings[i], f.GetEncoding());
+        bool hasCodeExtensions;
+        ASSERT_EQ(testEncodings[i], f.DetectEncoding(hasCodeExtensions));
+        ASSERT_FALSE(hasCodeExtensions);
       }
 
-      Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i]);
+      Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
       f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent);
 
       Json::Value v;
@@ -1161,7 +1172,7 @@
         // Sanity check to test the proper behavior of "EncodingTests.py"
         std::string encoded = Toolbox::ConvertFromUtf8(testEncodingsExpected[i], testEncodings[i]);
         ASSERT_STREQ(testEncodingsEncoded[i], encoded.c_str());
-        std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i]);
+        std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i], false);
         ASSERT_STREQ(testEncodingsExpected[i], decoded.c_str());
 
         if (testEncodings[i] != Encoding_Chinese)
@@ -1169,7 +1180,7 @@
           // A specific source string is used in "EncodingTests.py" to
           // test against Chinese, it is normal that it does not correspond to UTF8
 
-          std::string encoded = Toolbox::ConvertToUtf8(Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i]);
+          std::string encoded = Toolbox::ConvertToUtf8(Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i], false);
           ASSERT_STREQ(testEncodingsExpected[i], encoded.c_str());
         }
       }
@@ -1227,7 +1238,9 @@
       std::string tag;
 
       ParsedDicomFile dicom(m, Encoding_Utf8);
-      ASSERT_EQ(Encoding_Utf8, dicom.GetEncoding());
+      bool hasCodeExtensions;
+      ASSERT_EQ(Encoding_Utf8, dicom.DetectEncoding(hasCodeExtensions));
+      ASSERT_FALSE(hasCodeExtensions);
       ASSERT_TRUE(dicom.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));
       ASSERT_EQ(tag, testEncodingsExpected[i]);
 
@@ -1240,7 +1253,8 @@
 
       dicom.ChangeEncoding(testEncodings[i]);
 
-      ASSERT_EQ(testEncodings[i], dicom.GetEncoding());
+      ASSERT_EQ(testEncodings[i], dicom.DetectEncoding(hasCodeExtensions));
+      ASSERT_FALSE(hasCodeExtensions);
       
       const char* c = NULL;
       ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, c).good());
@@ -1275,7 +1289,10 @@
     m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
 
     ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */);
-    ASSERT_EQ(Encoding_Latin3, d.GetEncoding());
+
+    bool hasCodeExtensions;
+    ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions));
+    ASSERT_FALSE(hasCodeExtensions);
   }
   
   {
@@ -1285,7 +1302,10 @@
     m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
 
     ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */);
-    ASSERT_EQ(Encoding_Japanese, d.GetEncoding());
+
+    bool hasCodeExtensions;
+    ASSERT_EQ(Encoding_Japanese, d.DetectEncoding(hasCodeExtensions));
+    ASSERT_FALSE(hasCodeExtensions);
   }
   
   {
@@ -1314,6 +1334,528 @@
     m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
 
     ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */);
-    ASSERT_EQ(Encoding_Latin3, d.GetEncoding());
+
+    bool hasCodeExtensions;
+    ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions));
+    ASSERT_FALSE(hasCodeExtensions);
   }
 }
+
+
+
+TEST(Toolbox, RemoveIso2022EscapeSequences)
+{
+  // +----------------------------------+
+  // | one-byte control messages        |
+  // +----------------------------------+
+
+  static const uint8_t iso2022_cstr_oneByteControl[] = {
+    0x0f, 0x41, 
+    0x0e, 0x42, 
+    0x8e, 0x1b, 0x4e, 0x43, 
+    0x8f, 0x1b, 0x4f, 0x44,
+    0x8e, 0x1b, 0x4a, 0x45, 
+    0x8f, 0x1b, 0x4a, 0x46,
+    0x50, 0x51, 0x52, 0x00
+  };
+  
+  static const uint8_t iso2022_cstr_oneByteControl_ref[] = {
+    0x41,
+    0x42,
+    0x43,
+    0x44,
+    0x8e, 0x1b, 0x4a, 0x45, 
+    0x8f, 0x1b, 0x4a, 0x46,
+    0x50, 0x51, 0x52, 0x00
+  };
+
+  // +----------------------------------+
+  // | two-byte control messages        |
+  // +----------------------------------+
+
+  static const uint8_t iso2022_cstr_twoByteControl[] = {
+    0x1b, 0x6e, 0x41,
+    0x1b, 0x6f, 0x42,
+    0x1b, 0x4e, 0x43,
+    0x1b, 0x4f, 0x44,
+    0x1b, 0x7e, 0x45,
+    0x1b, 0x7d, 0x46,
+    0x1b, 0x7c, 0x47, 0x00
+  };
+  
+  static const uint8_t iso2022_cstr_twoByteControl_ref[] = {
+    0x41,
+    0x42,
+    0x43,
+    0x44,
+    0x45,
+    0x46,
+    0x47, 0x00
+  };
+
+  // +----------------------------------+
+  // | various-length escape sequences  |
+  // +----------------------------------+
+
+  static const uint8_t iso2022_cstr_escapeSequence[] = {
+    0x1b, 0x40, 0x41, // 1b and 40 should not be removed (invalid esc seq)
+    0x1b, 0x50, 0x42, // ditto 
+    0x1b, 0x7f, 0x43, // ditto
+    0x1b, 0x21, 0x4a, 0x44, // this will match
+    0x1b, 0x20, 0x21, 0x2f, 0x40, 0x45, // this will match
+    0x1b, 0x20, 0x21, 0x2f, 0x2f, 0x40, 0x46, // this will match too
+    0x1b, 0x20, 0x21, 0x2f, 0x1f, 0x47, 0x48, 0x00 // this will NOT match!
+  };
+  
+  static const uint8_t iso2022_cstr_escapeSequence_ref[] = {
+    0x1b, 0x40, 0x41, // 1b and 40 should not be removed (invalid esc seq)
+    0x1b, 0x50, 0x42, // ditto 
+    0x1b, 0x7f, 0x43, // ditto
+    0x44, // this will match
+    0x45, // this will match
+    0x46, // this will match too
+    0x1b, 0x20, 0x21, 0x2f, 0x1f, 0x47, 0x48, 0x00 // this will NOT match!
+  };
+
+  
+  // +----------------------------------+
+  // | a real-world japanese sample     |
+  // +----------------------------------+
+
+  static const uint8_t iso2022_cstr_real_ir13[] = {
+    0xd4, 0xcf, 0xc0, 0xde, 0x5e, 0xc0, 0xdb, 0xb3,
+    0x3d, 0x1b, 0x24, 0x42, 0x3b, 0x33, 0x45, 0x44,
+    0x1b, 0x28, 0x4a, 0x5e, 0x1b, 0x24, 0x42, 0x42,
+    0x40, 0x4f, 0x3a, 0x1b, 0x28, 0x4a, 0x3d, 0x1b,
+    0x24, 0x42, 0x24, 0x64, 0x24, 0x5e, 0x24, 0x40,
+    0x1b, 0x28, 0x4a, 0x5e, 0x1b, 0x24, 0x42, 0x24,
+    0x3f, 0x24, 0x6d, 0x24, 0x26, 0x1b, 0x28, 0x4a, 0x00
+  };
+
+  static const uint8_t iso2022_cstr_real_ir13_ref[] = {
+    0xd4, 0xcf, 0xc0, 0xde, 0x5e, 0xc0, 0xdb, 0xb3,
+    0x3d,
+    0x3b, 0x33, 0x45, 0x44,
+    0x5e,
+    0x42,
+    0x40, 0x4f, 0x3a,
+    0x3d,
+    0x24, 0x64, 0x24, 0x5e, 0x24, 0x40,
+    0x5e,
+    0x24,
+    0x3f, 0x24, 0x6d, 0x24, 0x26, 0x00
+  };
+
+
+
+  // +----------------------------------+
+  // | the actual test                  |
+  // +----------------------------------+
+
+  std::string iso2022_str_oneByteControl(
+    reinterpret_cast<const char*>(iso2022_cstr_oneByteControl));
+  std::string iso2022_str_oneByteControl_ref(
+    reinterpret_cast<const char*>(iso2022_cstr_oneByteControl_ref));
+  std::string iso2022_str_twoByteControl(
+    reinterpret_cast<const char*>(iso2022_cstr_twoByteControl));
+  std::string iso2022_str_twoByteControl_ref(
+    reinterpret_cast<const char*>(iso2022_cstr_twoByteControl_ref));
+  std::string iso2022_str_escapeSequence(
+    reinterpret_cast<const char*>(iso2022_cstr_escapeSequence));
+  std::string iso2022_str_escapeSequence_ref(
+    reinterpret_cast<const char*>(iso2022_cstr_escapeSequence_ref));
+  std::string iso2022_str_real_ir13(
+    reinterpret_cast<const char*>(iso2022_cstr_real_ir13));
+  std::string iso2022_str_real_ir13_ref(
+    reinterpret_cast<const char*>(iso2022_cstr_real_ir13_ref));
+
+  std::string dest;
+
+  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_oneByteControl);
+  ASSERT_EQ(dest, iso2022_str_oneByteControl_ref);
+
+  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_twoByteControl);
+  ASSERT_EQ(dest, iso2022_str_twoByteControl_ref);
+
+  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_escapeSequence);
+  ASSERT_EQ(dest, iso2022_str_escapeSequence_ref);
+
+  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_real_ir13);
+  ASSERT_EQ(dest, iso2022_str_real_ir13_ref);
+}
+
+
+
+static std::string DecodeFromSpecification(const std::string& s)
+{
+  std::vector<std::string> tokens;
+  Toolbox::TokenizeString(tokens, s, ' ');
+
+  std::string result;
+  result.resize(tokens.size());
+  
+  for (size_t i = 0; i < tokens.size(); i++)
+  {
+    std::vector<std::string> components;
+    Toolbox::TokenizeString(components, tokens[i], '/');
+
+    if (components.size() != 2)
+    {
+      throw;
+    }
+
+    int a = boost::lexical_cast<int>(components[0]);
+    int b = boost::lexical_cast<int>(components[1]);
+    if (a < 0 || a > 15 ||
+        b < 0 || b > 15 ||
+        (a == 0 && b == 0))
+    {
+      throw;
+    }
+
+    result[i] = static_cast<uint8_t>(a * 16 + b);
+  }
+
+  return result;
+}
+
+
+
+TEST(Toolbox, EncodingsKorean)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_I.2.html
+
+  std::string korean = DecodeFromSpecification(
+    "04/08 06/15 06/14 06/07 05/14 04/07 06/09 06/12 06/04 06/15 06/14 06/07 03/13 "
+    "01/11 02/04 02/09 04/03 15/11 15/03 05/14 01/11 02/04 02/09 04/03 13/01 12/14 "
+    "13/04 13/07 03/13 01/11 02/04 02/09 04/03 12/08 10/11 05/14 01/11 02/04 02/09 "
+    "04/03 11/01 14/06 11/05 11/15");
+
+  // This array can be re-generated using command-line:
+  // echo -n "Hong^Gildong=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
+  static const uint8_t utf8raw[] = {
+    0x48, 0x6f, 0x6e, 0x67, 0x5e, 0x47, 0x69, 0x6c, 0x64, 0x6f, 0x6e, 0x67, 0x3d, 0xe6,
+    0xb4, 0xaa, 0x5e, 0xe5, 0x90, 0x89, 0xe6, 0xb4, 0x9e, 0x3d, 0xed, 0x99, 0x8d, 0x5e,
+    0xea, 0xb8, 0xb8, 0xeb, 0x8f, 0x99
+  };
+
+  std::string utf8(reinterpret_cast<const char*>(utf8raw), sizeof(utf8raw));
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 149");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, korean.c_str(), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_Korean, encoding);
+  ASSERT_TRUE(hasCodeExtensions);
+  
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(utf8, value);
+  
+  DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+  ASSERT_EQ(utf8.substr(0, 12), visitor.GetResult()["00100010"]["Value"][0]["Alphabetic"].asString());
+  ASSERT_EQ(utf8.substr(13, 10), visitor.GetResult()["00100010"]["Value"][0]["Ideographic"].asString());
+  ASSERT_EQ(utf8.substr(24), visitor.GetResult()["00100010"]["Value"][0]["Phonetic"].asString());
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
+  std::string xml;
+  visitor.FormatXml(xml);
+
+  pugi::xml_document doc;
+  doc.load_buffer(xml.c_str(), xml.size());
+
+  pugi::xpath_node node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
+  ASSERT_STREQ("ISO_IR 192", node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
+  ASSERT_STREQ("CS", node.node().attribute("vr").value());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
+  ASSERT_STREQ("PN", node.node().attribute("vr").value());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
+  ASSERT_STREQ("Hong", node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
+  ASSERT_STREQ("Gildong", node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
+  ASSERT_EQ(utf8.substr(13, 3), node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
+  ASSERT_EQ(utf8.substr(17, 6), node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
+  ASSERT_EQ(utf8.substr(24, 3), node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
+  ASSERT_EQ(utf8.substr(28), node.node().text().as_string());
+#endif  
+}
+
+
+
+TEST(Toolbox, EncodingsJapaneseKanji)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_H.3.html
+
+  std::string japanese = DecodeFromSpecification(
+    "05/09 06/01 06/13 06/01 06/04 06/01 05/14 05/04 06/01 07/02 06/15 07/05 03/13 "
+    "01/11 02/04 04/02 03/11 03/03 04/05 04/04 01/11 02/08 04/02 05/14 01/11 02/04 "
+    "04/02 04/02 04/00 04/15 03/10 01/11 02/08 04/02 03/13 01/11 02/04 04/02 02/04 "
+    "06/04 02/04 05/14 02/04 04/00 01/11 02/08 04/02 05/14 01/11 02/04 04/02 02/04 "
+    "03/15 02/04 06/13 02/04 02/06 01/11 02/08 04/02");
+
+  // This array can be re-generated using command-line:
+  // echo -n "Yamada^Tarou=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
+  static const uint8_t utf8raw[] = {
+    0x59, 0x61, 0x6d, 0x61, 0x64, 0x61, 0x5e, 0x54, 0x61, 0x72, 0x6f, 0x75, 0x3d, 0xe5,
+    0xb1, 0xb1, 0xe7, 0x94, 0xb0, 0x5e, 0xe5, 0xa4, 0xaa, 0xe9, 0x83, 0x8e, 0x3d, 0xe3,
+    0x82, 0x84, 0xe3, 0x81, 0xbe, 0xe3, 0x81, 0xa0, 0x5e, 0xe3, 0x81, 0x9f, 0xe3, 0x82,
+    0x8d, 0xe3, 0x81, 0x86
+  };
+
+  std::string utf8(reinterpret_cast<const char*>(utf8raw), sizeof(utf8raw));
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 87");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, japanese.c_str(), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_JapaneseKanji, encoding);
+  ASSERT_TRUE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(utf8, value);
+  
+  DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+  ASSERT_EQ(utf8.substr(0, 12), visitor.GetResult()["00100010"]["Value"][0]["Alphabetic"].asString());
+  ASSERT_EQ(utf8.substr(13, 13), visitor.GetResult()["00100010"]["Value"][0]["Ideographic"].asString());
+  ASSERT_EQ(utf8.substr(27), visitor.GetResult()["00100010"]["Value"][0]["Phonetic"].asString());
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
+  std::string xml;
+  visitor.FormatXml(xml);
+
+  pugi::xml_document doc;
+  doc.load_buffer(xml.c_str(), xml.size());
+
+  pugi::xpath_node node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
+  ASSERT_STREQ("ISO_IR 192", node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
+  ASSERT_STREQ("CS", node.node().attribute("vr").value());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
+  ASSERT_STREQ("PN", node.node().attribute("vr").value());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
+  ASSERT_STREQ("Yamada", node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
+  ASSERT_STREQ("Tarou", node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
+  ASSERT_EQ(utf8.substr(13, 6), node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
+  ASSERT_EQ(utf8.substr(20, 6), node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
+  ASSERT_EQ(utf8.substr(27, 9), node.node().text().as_string());
+
+  node = doc.select_single_node("//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
+  ASSERT_EQ(utf8.substr(37), node.node().text().as_string());
+#endif  
+}
+
+
+
+TEST(Toolbox, EncodingsChinese3)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_J.3.html
+
+  static const uint8_t chinese[] = {
+    0x57, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f,
+    0x6e, 0x67, 0x3d, 0xcd, 0xf5, 0x5e, 0xd0, 0xa1, 0xb6, 0xab, 0x3d, 0x00
+  };
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "GB18030");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_Chinese, encoding);
+  ASSERT_FALSE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+
+  std::vector<std::string> tokens;
+  Orthanc::Toolbox::TokenizeString(tokens, value, '=');
+  ASSERT_EQ(3u, tokens.size());
+  ASSERT_EQ("Wang^XiaoDong", tokens[0]);
+  ASSERT_TRUE(tokens[2].empty());
+
+  std::vector<std::string> middle;
+  Orthanc::Toolbox::TokenizeString(middle, tokens[1], '^');
+  ASSERT_EQ(2u, middle.size());
+  ASSERT_EQ(3u, middle[0].size());
+  ASSERT_EQ(6u, middle[1].size());
+
+  // CDF5 in GB18030
+  ASSERT_EQ(static_cast<char>(0xe7), middle[0][0]);
+  ASSERT_EQ(static_cast<char>(0x8e), middle[0][1]);
+  ASSERT_EQ(static_cast<char>(0x8b), middle[0][2]);
+
+  // D0A1 in GB18030
+  ASSERT_EQ(static_cast<char>(0xe5), middle[1][0]);
+  ASSERT_EQ(static_cast<char>(0xb0), middle[1][1]);
+  ASSERT_EQ(static_cast<char>(0x8f), middle[1][2]);
+
+  // B6AB in GB18030
+  ASSERT_EQ(static_cast<char>(0xe4), middle[1][3]);
+  ASSERT_EQ(static_cast<char>(0xb8), middle[1][4]);
+  ASSERT_EQ(static_cast<char>(0x9c), middle[1][5]);
+}
+
+
+TEST(Toolbox, EncodingsChinese4)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_J.4.html
+
+  static const uint8_t chinese[] = {
+    0x54, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6c, 0x69, 0x6e,
+    0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0xd6, 0xd0, 0xce,
+    0xc4, 0x2e, 0x0d, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e,
+    0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
+    0x65, 0x73, 0xd6, 0xd0, 0xce, 0xc4, 0x2c, 0x20, 0x74, 0x6f, 0x6f, 0x2e, 0x0d,
+    0x0a, 0x54, 0x68, 0x65, 0x20, 0x74, 0x68, 0x69, 0x72, 0x64, 0x20, 0x6c, 0x69,
+    0x6e, 0x65, 0x2e, 0x0d, 0x0a, 0x00
+  };
+
+  static const uint8_t patternRaw[] = {
+    0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87
+  };
+
+  const std::string pattern(reinterpret_cast<const char*>(patternRaw), sizeof(patternRaw));
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "GB18030");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientComments, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_Chinese, encoding);
+  ASSERT_FALSE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_COMMENTS));
+
+  std::vector<std::string> lines;
+  Orthanc::Toolbox::TokenizeString(lines, value, '\n');
+  ASSERT_EQ(4u, lines.size());
+  ASSERT_TRUE(boost::starts_with(lines[0], "The first line includes"));
+  ASSERT_TRUE(boost::ends_with(lines[0], ".\r"));
+  ASSERT_TRUE(lines[0].find(pattern) != std::string::npos);
+  ASSERT_TRUE(boost::starts_with(lines[1], "The second line includes"));
+  ASSERT_TRUE(boost::ends_with(lines[1], ", too.\r"));
+  ASSERT_TRUE(lines[1].find(pattern) != std::string::npos);
+  ASSERT_EQ("The third line.\r", lines[2]);
+  ASSERT_FALSE(lines[1].find(pattern) == std::string::npos);
+  ASSERT_TRUE(lines[3].empty());
+}
+
+
+TEST(Toolbox, EncodingsSimplifiedChinese2)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_K.2.html
+
+  static const uint8_t chinese[] = {
+    0x5a, 0x68, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f,
+    0x6e, 0x67, 0x3d, 0x1b, 0x24, 0x29, 0x41, 0xd5, 0xc5, 0x5e, 0x1b, 0x24,
+    0x29, 0x41, 0xd0, 0xa1, 0xb6, 0xab, 0x3d, 0x20, 0x00
+  };
+
+  // echo -n "Zhang^XiaoDong=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
+  static const uint8_t utf8[] = {
+    0x5a, 0x68, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f, 0x6e, 0x67,
+    0x3d, 0xe5, 0xbc, 0xa0, 0x5e, 0xe5, 0xb0, 0x8f, 0xe4, 0xb8, 0x9c, 0x3d
+  };
+  
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 58");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_SimplifiedChinese, encoding);
+  ASSERT_TRUE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(value, std::string(reinterpret_cast<const char*>(utf8), sizeof(utf8)));
+}
+
+
+TEST(Toolbox, EncodingsSimplifiedChinese3)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_K.2.html
+
+  static const uint8_t chinese[] = {
+    0x31, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xd2, 0xbb, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a,
+    0x32, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xb6, 0xfe, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a,
+    0x33, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xc8, 0xfd, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a, 0x00
+  };
+
+  static const uint8_t line1[] = {
+    0x31, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xb8, 0x80, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
+    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
+  };
+
+  static const uint8_t line2[] = {
+    0x32, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xba, 0x8c, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
+    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
+  };
+
+  static const uint8_t line3[] = {
+    0x33, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xb8, 0x89, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
+    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
+  };
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 58");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_SimplifiedChinese, encoding);
+  ASSERT_TRUE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+
+  std::vector<std::string> lines;
+  Toolbox::TokenizeString(lines, value, '\n');
+  ASSERT_EQ(4u, lines.size());
+  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line1), sizeof(line1)), lines[0]);
+  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line2), sizeof(line2)), lines[1]);
+  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line3), sizeof(line3)), lines[2]);
+  ASSERT_TRUE(lines[3].empty());
+}
+
--- a/UnitTestsSources/MultiThreadingTests.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -105,7 +105,7 @@
     {
       if (fails_)
       {
-        return JobStepResult::Failure(ErrorCode_ParameterOutOfRange);
+        return JobStepResult::Failure(ErrorCode_ParameterOutOfRange, NULL);
       }
       else if (count_ == steps_ - 1)
       {
@@ -724,12 +724,12 @@
   engine.Start();
 
   Json::Value content = Json::nullValue;
-  ASSERT_TRUE(engine.GetRegistry().SubmitAndWait(content, new DummyJob(), rand() % 10));
+  engine.GetRegistry().SubmitAndWait(content, new DummyJob(), rand() % 10);
   ASSERT_EQ(Json::objectValue, content.type());
   ASSERT_EQ("world", content["hello"].asString());
 
   content = Json::nullValue;
-  ASSERT_FALSE(engine.GetRegistry().SubmitAndWait(content, new DummyJob(true), rand() % 10));
+  ASSERT_THROW(engine.GetRegistry().SubmitAndWait(content, new DummyJob(true), rand() % 10), OrthancException);
   ASSERT_EQ(Json::nullValue, content.type());
 
   engine.Stop();
--- a/UnitTestsSources/UnitTestsMain.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/UnitTestsSources/UnitTestsMain.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -450,7 +450,7 @@
   ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s));
 
   // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C"
-  std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1);
+  std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false);
   ASSERT_EQ(15u, utf8.size());
   ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[0]));
   ASSERT_EQ(0xa0, static_cast<unsigned char>(utf8[1]));
@@ -477,8 +477,8 @@
 
   std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char));
 
-  ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1), Encoding_Latin1));
-  ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8));
+  ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false), Encoding_Latin1));
+  ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false));
 }
 
 
@@ -690,6 +690,9 @@
   ASSERT_EQ(Encoding_Japanese, StringToEncoding(EnumerationToString(Encoding_Japanese)));
   ASSERT_EQ(Encoding_Chinese, StringToEncoding(EnumerationToString(Encoding_Chinese)));
   ASSERT_EQ(Encoding_Thai, StringToEncoding(EnumerationToString(Encoding_Thai)));
+  ASSERT_EQ(Encoding_Korean, StringToEncoding(EnumerationToString(Encoding_Korean)));
+  ASSERT_EQ(Encoding_JapaneseKanji, StringToEncoding(EnumerationToString(Encoding_JapaneseKanji)));
+  ASSERT_EQ(Encoding_SimplifiedChinese, StringToEncoding(EnumerationToString(Encoding_SimplifiedChinese)));
 
   ASSERT_EQ(ResourceType_Patient, StringToResourceType(EnumerationToString(ResourceType_Patient)));
   ASSERT_EQ(ResourceType_Study, StringToResourceType(EnumerationToString(ResourceType_Study)));
--- a/UnitTestsSources/VersionsTests.cpp	Wed Feb 13 13:01:21 2019 +0000
+++ b/UnitTestsSources/VersionsTests.cpp	Sat Feb 16 10:29:26 2019 +0000
@@ -44,7 +44,10 @@
 #include <sqlite3.h>
 #include <lua.h>
 #include <jpeglib.h>
-#include <iconv.h>
+
+#if BUILDING_LIBICONV == 1
+#  include <iconv.h>
+#endif
 
 #if ORTHANC_ENABLE_SSL == 1
 #  include <openssl/opensslv.h>
@@ -148,12 +151,15 @@
   ASSERT_STREQ("Lua 5.3.5", LUA_RELEASE);
 }
 
+
+#if BUILDING_LIBICONV == 1
 TEST(Version, LibIconvStatic)
 {
   static const int major = 1;
   static const int minor = 15;  
   ASSERT_EQ((major << 8) + minor, _LIBICONV_VERSION);
 }
+#endif
 
 
 #if ORTHANC_ENABLE_SSL == 1