changeset 4696:dd6274412ff4

new configuration option "ExternalDictionaries" to load external DICOM dictionaries
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 17 Jun 2021 15:47:21 +0200
parents 651f069c7f77
children 569d9ef165b1
files NEWS OrthancFramework/Sources/DicomFormat/DicomTag.cpp OrthancFramework/Sources/DicomFormat/DicomTag.h OrthancFramework/Sources/DicomParsing/DicomModification.cpp OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h OrthancFramework/Sources/OrthancFramework.cpp OrthancFramework/UnitTestsSources/DicomMapTests.cpp OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp OrthancServer/Resources/Configuration.json OrthancServer/Sources/OrthancInitialization.cpp
diffstat 11 files changed, 219 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Jun 17 14:00:34 2021 +0200
+++ b/NEWS	Thu Jun 17 15:47:21 2021 +0200
@@ -5,7 +5,9 @@
 -------
 
 * Orthanc now anonymizes according to Basic Profile of PS 3.15-2021b Table E.1-1
-* New configuration option "SynchronousZipStream" to disable streaming of ZIP
+* New configuration options:
+  - "ExternalDictionaries" to load external DICOM dictionaries (useful for DICONDE)
+  - "SynchronousZipStream" to disable streaming of ZIP
 
 REST API
 --------
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Thu Jun 17 15:47:21 2021 +0200
@@ -183,61 +183,6 @@
   }
 
 
-  const char* DicomTag::GetMainTagsName() const
-  {
-    if (*this == DICOM_TAG_ACCESSION_NUMBER)
-      return "AccessionNumber";
-
-    if (*this == DICOM_TAG_SOP_INSTANCE_UID)
-      return "SOPInstanceUID";
-
-    if (*this == DICOM_TAG_PATIENT_ID)
-      return "PatientID";
-
-    if (*this == DICOM_TAG_SERIES_INSTANCE_UID)
-      return "SeriesInstanceUID";
-
-    if (*this == DICOM_TAG_STUDY_INSTANCE_UID)
-      return "StudyInstanceUID"; 
-
-    if (*this == DICOM_TAG_PIXEL_DATA)
-      return "PixelData";
-
-    if (*this == DICOM_TAG_IMAGE_INDEX)
-      return "ImageIndex";
-
-    if (*this == DICOM_TAG_INSTANCE_NUMBER)
-      return "InstanceNumber";
-
-    if (*this == DICOM_TAG_NUMBER_OF_SLICES)
-      return "NumberOfSlices";
-
-    if (*this == DICOM_TAG_NUMBER_OF_FRAMES)
-      return "NumberOfFrames";
-
-    if (*this == DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)
-      return "CardiacNumberOfImages";
-
-    if (*this == DICOM_TAG_IMAGES_IN_ACQUISITION)
-      return "ImagesInAcquisition";
-
-    if (*this == DICOM_TAG_PATIENT_NAME)
-      return "PatientName";
-
-    if (*this == DICOM_TAG_IMAGE_POSITION_PATIENT)
-      return "ImagePositionPatient";
-
-    if (*this == DICOM_TAG_IMAGE_ORIENTATION_PATIENT)
-      return "ImageOrientationPatient";
-
-    // New in Orthanc 1.6.0, as tagged as "RETIRED_" since DCMTK 3.6.4
-    if (*this == DICOM_TAG_OTHER_PATIENT_IDS)
-      return "OtherPatientIDs";
-
-    return "";
-  }
-
-
   void DicomTag::AddTagsForModule(std::set<DicomTag>& target,
                                   DicomModule module)
   {
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Thu Jun 17 15:47:21 2021 +0200
@@ -49,8 +49,6 @@
 
     bool IsPrivate() const;
 
-    const char* GetMainTagsName() const;
-
     bool operator< (const DicomTag& other) const;
 
     bool operator<= (const DicomTag& other) const;
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Thu Jun 17 15:47:21 2021 +0200
@@ -804,14 +804,16 @@
       ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(*it);
       if (*it == DICOM_TAG_PATIENT_ID)
       {
-        if (vr != ValueRepresentation_LongString)
+        if (vr != ValueRepresentation_LongString &&
+            vr != ValueRepresentation_NotSupported /* if no dictionary loaded */)
         {
           throw OrthancException(ErrorCode_InternalError);
         }
       }
       else if (*it == DICOM_TAG_PATIENT_NAME)
       {
-        if (vr != ValueRepresentation_PersonName)
+        if (vr != ValueRepresentation_PersonName &&
+            vr != ValueRepresentation_NotSupported /* if no dictionary loaded */)
         {
           throw OrthancException(ErrorCode_InternalError);
         }
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Thu Jun 17 15:47:21 2021 +0200
@@ -117,6 +117,9 @@
 #endif
 
 
+static bool hasExternalDictionaries_ = false;
+
+
 namespace Orthanc
 {
   static bool IsBinaryTag(const DcmTag& key)
@@ -226,22 +229,22 @@
     }
 
     
-#define DCMTK_TO_CTYPE_CONVERTER(converter, cType, dcmtkType, getter, toStringFunction)  \
+#define DCMTK_TO_CTYPE_CONVERTER(converter, cType, dcmtkType, getter, toStringFunction) \
                                                                         \
     struct converter                                                    \
     {                                                                   \
       typedef cType CType;                                              \
                                                                         \
       ORTHANC_FORCE_INLINE                                              \
-      static bool Apply(CType& result,                                  \
-                        DcmElement& element,                            \
-                        size_t i)                                       \
+        static bool Apply(CType& result,                                \
+                          DcmElement& element,                          \
+                          size_t i)                                     \
       {                                                                 \
         return dynamic_cast<dcmtkType&>(element).getter(result, i).good(); \
       }                                                                 \
                                                                         \
       ORTHANC_FORCE_INLINE                                              \
-      static std::string ToString(CType value)                          \
+        static std::string ToString(CType value)                        \
       {                                                                 \
         return toStringFunction(value);                                 \
       }                                                                 \
@@ -285,15 +288,15 @@
 
   void FromDcmtkBridge::InitializeDictionary(bool loadPrivateDictionary)
   {
-    LOG(INFO) << "Using DCTMK version: " << DCMTK_VERSION_NUMBER;
+    CLOG(INFO, DICOM) << "Using DCTMK version: " << DCMTK_VERSION_NUMBER;
     
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
     {
       DictionaryLocker locker;
 
       locker->clear();
 
-#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
-      LOG(INFO) << "Loading the embedded dictionaries";
+      CLOG(INFO, DICOM) << "Loading the embedded dictionaries";
       /**
        * Do not load DICONDE dictionary, it breaks the other tags. The
        * command "strace storescu 2>&1 |grep dic" shows that DICONDE
@@ -305,15 +308,16 @@
 
       if (loadPrivateDictionary)
       {
-        LOG(INFO) << "Loading the embedded dictionary of private tags";
+        CLOG(INFO, DICOM) << "Loading the embedded dictionary of private tags";
         LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_PRIVATE);
       }
       else
       {
-        LOG(INFO) << "The dictionary of private tags has not been loaded";
+        CLOG(INFO, DICOM) << "The dictionary of private tags has not been loaded";
       }
-
+    }
 #else
+    {
       std::vector<std::string> dictionaries;
       
       const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE);
@@ -331,21 +335,17 @@
       {
         boost::filesystem::path base = DCMTK_DICTIONARY_DIR;
         dictionaries.push_back((base / "dicom.dic").string());
-        dictionaries.push_back((base / "private.dic").string());
-      }
-
-      for (size_t i = 0; i < dictionaries.size(); i++)
-      {
-        LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\"";
-        
-        if (!locker->loadDictionary(dictionaries[i].c_str()))
+
+        if (loadPrivateDictionary)
         {
-          throw OrthancException(ErrorCode_InexistentFile);
+          dictionaries.push_back((base / "private.dic").string());
         }
       }
 
+      LoadExternalDictionaries(dictionaries);
+      hasExternalDictionaries_ = false;  // Fix the side-effect of "LoadExternalDictionaries()"
+    }
 #endif
-    }
 
     /* make sure data dictionary is loaded */
     if (!dcmDataDict.isDictionaryLoaded())
@@ -367,6 +367,27 @@
   }
 
 
+  void FromDcmtkBridge::LoadExternalDictionaries(const std::vector<std::string>& dictionaries)
+  {
+    DictionaryLocker locker;
+
+    CLOG(INFO, DICOM) << "Clearing the DICOM dictionary";
+    locker->clear();
+
+    for (size_t i = 0; i < dictionaries.size(); i++)
+    {
+      LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\"";
+        
+      if (!locker->loadDictionary(dictionaries[i].c_str()))
+      {
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
+    }    
+
+    hasExternalDictionaries_ = true;
+  }
+
+
   void FromDcmtkBridge::RegisterDictionaryTag(const DicomTag& tag,
                                               ValueRepresentation vr,
                                               const std::string& name,
@@ -392,10 +413,10 @@
     
     DcmEVR evr = ToDcmtkBridge::Convert(vr);
 
-    LOG(INFO) << "Registering tag in dictionary: (" << tag.Format() << ") "
-              << (DcmVR(evr).getValidVRName()) << " " 
-              << name << " (multiplicity: " << minMultiplicity << "-" 
-              << (arbitrary ? "n" : boost::lexical_cast<std::string>(maxMultiplicity)) << ")";
+    CLOG(INFO, DICOM) << "Registering tag in dictionary: (" << tag.Format() << ") "
+                      << (DcmVR(evr).getValidVRName()) << " " 
+                      << name << " (multiplicity: " << minMultiplicity << "-" 
+                      << (arbitrary ? "n" : boost::lexical_cast<std::string>(maxMultiplicity)) << ")";
 
     std::unique_ptr<DcmDictEntry>  entry;
     if (privateCreator.empty())
@@ -983,7 +1004,7 @@
     {
       // The "0" below lets "LeafValueToJson()" take care of "TooLong" values
       std::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
-                                  (element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength));
+                                    (element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength));
 
       if (ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end())
       {
@@ -1113,18 +1134,69 @@
   }
 
 
-
   static std::string GetTagNameInternal(DcmTag& tag)
   {
+    if (!hasExternalDictionaries_)
     {
-      // Some patches for important tags because of different DICOM
-      // dictionaries between DCMTK versions
+      /**
+       * Some patches for important tags because of different DICOM
+       * dictionaries between DCMTK versions. Since Orthanc 1.9.4, we
+       * don't apply these patches if external dictionaries are
+       * loaded, notably for compatibility with DICONDE. In Orthanc <=
+       * 1.9.3, this was done by method "DicomTag::GetMainTagsName()".
+       **/
+      
       DicomTag tmp(tag.getGroup(), tag.getElement());
-      std::string n = tmp.GetMainTagsName();
-      if (n.size() != 0)
-      {
-        return n;
-      }
+
+      if (tmp == DICOM_TAG_ACCESSION_NUMBER)
+        return "AccessionNumber";
+
+      if (tmp == DICOM_TAG_SOP_INSTANCE_UID)
+        return "SOPInstanceUID";
+
+      if (tmp == DICOM_TAG_PATIENT_ID)
+        return "PatientID";
+
+      if (tmp == DICOM_TAG_SERIES_INSTANCE_UID)
+        return "SeriesInstanceUID";
+
+      if (tmp == DICOM_TAG_STUDY_INSTANCE_UID)
+        return "StudyInstanceUID"; 
+
+      if (tmp == DICOM_TAG_PIXEL_DATA)
+        return "PixelData";
+
+      if (tmp == DICOM_TAG_IMAGE_INDEX)
+        return "ImageIndex";
+
+      if (tmp == DICOM_TAG_INSTANCE_NUMBER)
+        return "InstanceNumber";
+
+      if (tmp == DICOM_TAG_NUMBER_OF_SLICES)
+        return "NumberOfSlices";
+
+      if (tmp == DICOM_TAG_NUMBER_OF_FRAMES)
+        return "NumberOfFrames";
+
+      if (tmp == DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)
+        return "CardiacNumberOfImages";
+
+      if (tmp == DICOM_TAG_IMAGES_IN_ACQUISITION)
+        return "ImagesInAcquisition";
+
+      if (tmp == DICOM_TAG_PATIENT_NAME)
+        return "PatientName";
+
+      if (tmp == DICOM_TAG_IMAGE_POSITION_PATIENT)
+        return "ImagePositionPatient";
+
+      if (tmp == DICOM_TAG_IMAGE_ORIENTATION_PATIENT)
+        return "ImageOrientationPatient";
+
+      // New in Orthanc 1.6.0, as tagged as "RETIRED_" since DCMTK 3.6.4
+      if (tmp == DICOM_TAG_OTHER_PATIENT_IDS)
+        return "OtherPatientIDs";
+
       // End of patches
     }
 
@@ -1217,7 +1289,7 @@
     }
     else
     {
-      LOG(INFO) << "Unknown DICOM tag: \"" << name << "\"";
+      CLOG(INFO, DICOM) << "Unknown DICOM tag: \"" << name << "\"";
       throw OrthancException(ErrorCode_UnknownDicomTag);
     }
 #endif
@@ -1441,14 +1513,14 @@
 
         if (known)
         {
-          LOG(INFO) << "Transcoded an image from transfer syntax "
-                    << GetTransferSyntaxUid(sourceSyntax) << " to "
-                    << GetTransferSyntaxUid(syntax);
+          CLOG(INFO, DICOM) << "Transcoded an image from transfer syntax "
+                            << GetTransferSyntaxUid(sourceSyntax) << " to "
+                            << GetTransferSyntaxUid(syntax);
         }
         else
         {
-          LOG(INFO) << "Transcoded an image from unknown transfer syntax to "
-                    << GetTransferSyntaxUid(syntax);
+          CLOG(INFO, DICOM) << "Transcoded an image from unknown transfer syntax to "
+                            << GetTransferSyntaxUid(syntax);
         }
         
         return true;
@@ -1507,16 +1579,16 @@
         return ValueRepresentation_OtherByte;
 
 #if DCMTK_VERSION_NUMBER >= 361
-        case EVR_OD:
-          return ValueRepresentation_OtherDouble;
+      case EVR_OD:
+        return ValueRepresentation_OtherDouble;
 #endif
 
       case EVR_OF:
         return ValueRepresentation_OtherFloat;
 
 #if DCMTK_VERSION_NUMBER >= 362
-        case EVR_OL:
-          return ValueRepresentation_OtherLong;
+      case EVR_OL:
+        return ValueRepresentation_OtherLong;
 #endif
 
       case EVR_OW:
@@ -1695,9 +1767,9 @@
           throw OrthancException(ErrorCode_ParameterOutOfRange);
 
 
-        /**
-         * String types.
-         **/
+          /**
+           * String types.
+           **/
       
         case EVR_DS:  // decimal string
         case EVR_IS:  // integer string
@@ -2189,7 +2261,7 @@
   void FromDcmtkBridge::InitializeCodecs()
   {
 #if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
-    LOG(INFO) << "Registering JPEG Lossless codecs in DCMTK";
+    CLOG(INFO, DICOM) << "Registering JPEG Lossless codecs in DCMTK";
     DJLSDecoderRegistration::registerCodecs();
 # if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
     DJLSEncoderRegistration::registerCodecs();
@@ -2197,14 +2269,14 @@
 #endif
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    LOG(INFO) << "Registering JPEG codecs in DCMTK";
+    CLOG(INFO, DICOM) << "Registering JPEG codecs in DCMTK";
     DJDecoderRegistration::registerCodecs(); 
 # if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
     DJEncoderRegistration::registerCodecs();
 # endif
 #endif
 
-    LOG(INFO) << "Registering RLE codecs in DCMTK";
+    CLOG(INFO, DICOM) << "Registering RLE codecs in DCMTK";
     DcmRLEDecoderRegistration::registerCodecs(); 
 #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
     DcmRLEEncoderRegistration::registerCodecs();
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Thu Jun 17 15:47:21 2021 +0200
@@ -103,8 +103,22 @@
                                      Encoding target);
 
   public:
+    /**
+     * Initialize DCMTK to use the default DICOM dictionaries (either
+     * embedded into the binaries for official releases, or using the
+     * environment variable "DCM_DICT_ENVIRONMENT_VARIABLE", or using
+     * the system-wide path to the DCMTK library for developers)
+     **/
     static void InitializeDictionary(bool loadPrivateDictionary);
 
+    /**
+     * Replace the default DICOM dictionaries by the manually-provided
+     * external dictionaries. This is needed to use DICONDE for
+     * instance. Pay attention to the fact that the current dictionary
+     * will be reinitialized (all its tags are cleared).
+     **/
+    static void LoadExternalDictionaries(const std::vector<std::string>& dictionaries);
+
     static void RegisterDictionaryTag(const DicomTag& tag,
                                       ValueRepresentation vr,
                                       const std::string& name,
--- a/OrthancFramework/Sources/OrthancFramework.cpp	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancFramework/Sources/OrthancFramework.cpp	Thu Jun 17 15:47:21 2021 +0200
@@ -79,7 +79,7 @@
 #endif
 
 #if ORTHANC_ENABLE_DCMTK == 1
-    FromDcmtkBridge::InitializeDictionary(true);
+    FromDcmtkBridge::InitializeDictionary(loadPrivateDictionary);
     FromDcmtkBridge::InitializeCodecs();
 #endif
 
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Thu Jun 17 15:47:21 2021 +0200
@@ -644,7 +644,7 @@
       ASSERT_EQ(1u, b.GetSize());
       ASSERT_EQ("TEST", b.GetStringValue(*it, "", false));
 
-      std::string main = it->GetMainTagsName();
+      std::string main = FromDcmtkBridge::GetTagName(*it, "");
       if (!main.empty())
       {
         ASSERT_EQ(main, name);
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Thu Jun 17 15:47:21 2021 +0200
@@ -700,7 +700,7 @@
 {
   FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7057, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag2", 1, 1, "ORTHANC");
   FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7059, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag3", 1, 1, "");
-  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag2", 1, 1, "");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1002), ValueRepresentation_PersonName, "Declared public tag2", 1, 1, "");
 
   Json::Value v;
   const std::string sopClassUid = "1.2.840.10008.5.1.4.1.1.1";  // CR Image Storage:
@@ -713,7 +713,7 @@
     v["SOPClassUID"] = sopClassUid;
     v["SpecificCharacterSet"] = "ISO_IR 148";    // This is latin-5
     v["PatientName"] = "Sébastien";
-    v["7050-1000"] = "Some public tag";  // Even group => public tag
+    v["7050-1002"] = "Some public tag";  // Even group => public tag
     v["7052-1000"] = "Some unknown tag";  // Even group => public, unknown tag
     v["7057-1000"] = "Some private tag";  // Odd group => private tag
     v["7059-1000"] = "Some private tag2";  // Odd group => private tag, with an odd length to test padding
@@ -784,7 +784,7 @@
     ASSERT_EQ(2u, vv["0040,0100"].size());
     ASSERT_EQ("MR", vv["0040,0100"][0]["0008,0060"].asString());
     ASSERT_EQ("CT", vv["0040,0100"][1]["0008,0060"].asString());
-    ASSERT_EQ("Some public tag", vv["7050,1000"].asString());
+    ASSERT_EQ("Some public tag", vv["7050,1002"].asString());
     ASSERT_EQ("Some unknown tag", vv["7052,1000"].asString());
     ASSERT_EQ("Some private tag", vv["7057,1000"].asString());
     ASSERT_EQ("Some private tag2", vv["7059,1000"].asString());
--- a/OrthancServer/Resources/Configuration.json	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancServer/Resources/Configuration.json	Thu Jun 17 15:47:21 2021 +0200
@@ -625,6 +625,18 @@
     // "2001,5f" : [ "SQ", "StackSequence", 1, 1, "Philips Imaging DD 001" ]
   },
 
+  // Load a set of external DICOM dictionaries in order to replace the
+  // default dictionaries. This option must contain a set of files in
+  // the DCMTK format. The order of the dictionaries *is*
+  // important. This option can be used to turn Orhanc into a DICONDE
+  // server. (new in Orthanc 1.9.4)
+  /**
+     "ExternalDictionaries" : [
+     "/usr/share/libdcmtk12/dicom.dic",
+     "/usr/share/libdcmtk12/diconde.dic"
+     ]
+  **/
+
   // Whether to run DICOM C-MOVE operations synchronously. If set to
   // "false" (asynchronous mode), each incoming C-MOVE request results
   // in the creation of a new background job. Up to Orthanc 1.3.2, the
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Thu Jun 17 14:00:34 2021 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Thu Jun 17 15:47:21 2021 +0200
@@ -53,6 +53,7 @@
 #include "../../OrthancFramework/Sources/HttpClient.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/OrthancException.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
 
 #include "Database/SQLiteDatabaseWrapper.h"
 #include "OrthancConfiguration.h"
@@ -62,14 +63,19 @@
 #include <dcmtk/dcmnet/diutil.h>  // For DCM_dcmnetLogger
 
 
+static const char* const STORAGE_DIRECTORY = "StorageDirectory";
+static const char* const ORTHANC_STORAGE = "OrthancStorage";
+
 
 namespace Orthanc
 {
   static void RegisterUserMetadata(const Json::Value& config)
   {
-    if (config.isMember("UserMetadata"))
+    static const char* const USER_METADATA = "UserMetadata";
+    
+    if (config.isMember(USER_METADATA))
     {
-      const Json::Value& parameter = config["UserMetadata"];
+      const Json::Value& parameter = config[USER_METADATA];
 
       Json::Value::Members members = parameter.getMemberNames();
       for (size_t i = 0; i < members.size(); i++)
@@ -103,9 +109,11 @@
 
   static void RegisterUserContentType(const Json::Value& config)
   {
-    if (config.isMember("UserContentType"))
+    static const char* const USER_CONTENT_TYPE = "UserContentType";
+    
+    if (config.isMember(USER_CONTENT_TYPE))
     {
-      const Json::Value& parameter = config["UserContentType"];
+      const Json::Value& parameter = config[USER_CONTENT_TYPE];
 
       Json::Value::Members members = parameter.getMemberNames();
       for (size_t i = 0; i < members.size(); i++)
@@ -150,20 +158,36 @@
   }
 
 
+  static void LoadExternalDictionaries(const Json::Value& configuration)
+  {
+    static const char* const EXTERNAL_DICTIONARIES = "ExternalDictionaries";
+    
+    if (configuration.type() == Json::objectValue &&
+        configuration.isMember(EXTERNAL_DICTIONARIES))
+    {
+      std::vector<std::string> dictionaries;
+      SerializationToolbox::ReadArrayOfStrings(dictionaries, configuration, EXTERNAL_DICTIONARIES);
+      FromDcmtkBridge::LoadExternalDictionaries(dictionaries);
+    }
+  }
+
+
   static void LoadCustomDictionary(const Json::Value& configuration)
   {
+    static const char* const DICTIONARY = "Dictionary";
+    
     if (configuration.type() != Json::objectValue ||
-        !configuration.isMember("Dictionary") ||
-        configuration["Dictionary"].type() != Json::objectValue)
+        !configuration.isMember(DICTIONARY) ||
+        configuration[DICTIONARY].type() != Json::objectValue)
     {
       return;
     }
 
-    Json::Value::Members tags(configuration["Dictionary"].getMemberNames());
+    Json::Value::Members tags(configuration[DICTIONARY].getMemberNames());
 
     for (Json::Value::ArrayIndex i = 0; i < tags.size(); i++)
     {
-      const Json::Value& content = configuration["Dictionary"][tags[i]];
+      const Json::Value& content = configuration[DICTIONARY][tags[i]];
       if (content.type() != Json::arrayValue ||
           content.size() < 2 ||
           content.size() > 5 ||
@@ -190,9 +214,13 @@
 
   static void ConfigurePkcs11(const Json::Value& config)
   {
+    static const char* const MODULE = "Module";
+    static const char* const VERBOSE = "Verbose";
+    static const char* const PIN = "Pin";
+    
     if (config.type() != Json::objectValue ||
-        !config.isMember("Module") ||
-        config["Module"].type() != Json::stringValue)
+        !config.isMember(MODULE) ||
+        config[MODULE].type() != Json::stringValue)
     {
       throw OrthancException(ErrorCode_BadFileFormat,
                              "No path to the PKCS#11 module (DLL or .so) is provided "
@@ -200,11 +228,11 @@
     }
 
     std::string pin;
-    if (config.isMember("Pin"))
+    if (config.isMember(PIN))
     {
-      if (config["Pin"].type() == Json::stringValue)
+      if (config[PIN].type() == Json::stringValue)
       {
-        pin = config["Pin"].asString();
+        pin = config[PIN].asString();
       }
       else
       {
@@ -214,11 +242,11 @@
     }
 
     bool verbose = false;
-    if (config.isMember("Verbose"))
+    if (config.isMember(VERBOSE))
     {
-      if (config["Verbose"].type() == Json::booleanValue)
+      if (config[VERBOSE].type() == Json::booleanValue)
       {
-        verbose = config["Verbose"].asBool();
+        verbose = config[VERBOSE].asBool();
       }
       else
       {
@@ -227,17 +255,18 @@
       }
     }
 
-    HttpClient::InitializePkcs11(config["Module"].asString(), pin, verbose);
+    HttpClient::InitializePkcs11(config[MODULE].asString(), pin, verbose);
   }
 
 
 
   void OrthancInitialize(const char* configurationFile)
   {
-    static const char* LOCALE = "Locale";
-    static const char* PKCS11 = "Pkcs11";
-    static const char* DEFAULT_ENCODING = "DefaultEncoding";
-    static const char* MALLOC_ARENA_MAX = "MallocArenaMax";
+    static const char* const LOCALE = "Locale";
+    static const char* const PKCS11 = "Pkcs11";
+    static const char* const DEFAULT_ENCODING = "DefaultEncoding";
+    static const char* const MALLOC_ARENA_MAX = "MallocArenaMax";
+    static const char* const LOAD_PRIVATE_DICTIONARY = "LoadPrivateDictionary";
     
     OrthancConfiguration::WriterLock lock;
 
@@ -254,7 +283,7 @@
         locale = lock.GetConfiguration().GetStringParameter(LOCALE, "");
       }
       
-      bool loadPrivate = lock.GetConfiguration().GetBooleanParameter("LoadPrivateDictionary", true);
+      bool loadPrivate = lock.GetConfiguration().GetBooleanParameter(LOAD_PRIVATE_DICTIONARY, true);
       Orthanc::InitializeFramework(locale, loadPrivate);
     }
 
@@ -278,6 +307,7 @@
     RegisterUserMetadata(lock.GetJson());
     RegisterUserContentType(lock.GetJson());
 
+    LoadExternalDictionaries(lock.GetJson());  // New in Orthanc 1.9.4
     LoadCustomDictionary(lock.GetJson());
 
     lock.GetConfiguration().RegisterFont(ServerResources::FONT_UBUNTU_MONO_BOLD_16);
@@ -319,7 +349,7 @@
     OrthancConfiguration::ReaderLock lock;
 
     std::string storageDirectoryStr = 
-      lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage");
+      lock.GetConfiguration().GetStringParameter(STORAGE_DIRECTORY, ORTHANC_STORAGE);
 
     // Open the database
     boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(
@@ -413,10 +443,13 @@
 
   static IStorageArea* CreateFilesystemStorage()
   {
+    static const char* const SYNC_STORAGE_AREA = "SyncStorageArea";
+    static const char* const STORE_DICOM = "StoreDicom";
+    
     OrthancConfiguration::ReaderLock lock;
 
     std::string storageDirectoryStr = 
-      lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage");
+      lock.GetConfiguration().GetStringParameter(STORAGE_DIRECTORY, ORTHANC_STORAGE);
 
     boost::filesystem::path storageDirectory = 
       lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr);
@@ -424,9 +457,9 @@
     LOG(WARNING) << "Storage directory: " << storageDirectory;
 
     // New in Orthanc 1.7.4
-    bool fsyncOnWrite = lock.GetConfiguration().GetBooleanParameter("SyncStorageArea", true);
+    bool fsyncOnWrite = lock.GetConfiguration().GetBooleanParameter(SYNC_STORAGE_AREA, true);
 
-    if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true))
+    if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM, true))
     {
       return new FilesystemStorage(storageDirectory.string(), fsyncOnWrite);
     }