changeset 4055:9214e3a7b0a2 framework

moving FromDcmtkTests.cpp from OrthancServer to OrthancFramework
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 11 Jun 2020 12:52:09 +0200
parents 9c37896a4457
children 55727d85f419
files CMakeLists.txt OrthancFramework/Sources/Toolbox.cpp OrthancFramework/Sources/Toolbox.h OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Sources/OrthancFindRequestHandler.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerToolbox.cpp OrthancServer/Sources/ServerToolbox.h OrthancServer/UnitTestsSources/FromDcmtkTests.cpp OrthancServer/UnitTestsSources/UnitTestsMain.cpp
diffstat 12 files changed, 2137 insertions(+), 2108 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Jun 11 12:24:38 2020 +0200
+++ b/CMakeLists.txt	Thu Jun 11 12:52:09 2020 +0200
@@ -115,6 +115,7 @@
 
 set(ORTHANC_FRAMEWORK_UNIT_TESTS
   OrthancFramework/UnitTestsSources/FrameworkTests.cpp
+  OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp
   OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp
   OrthancFramework/UnitTestsSources/ImageTests.cpp
   OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp
@@ -131,7 +132,6 @@
   OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp
   OrthancServer/UnitTestsSources/DicomMapTests.cpp
   OrthancServer/UnitTestsSources/FileStorageTests.cpp
-  OrthancServer/UnitTestsSources/FromDcmtkTests.cpp
   OrthancServer/UnitTestsSources/LuaTests.cpp
   OrthancServer/UnitTestsSources/MemoryCacheTests.cpp
   OrthancServer/UnitTestsSources/MultiThreadingTests.cpp
--- a/OrthancFramework/Sources/Toolbox.cpp	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Thu Jun 11 12:52:09 2020 +0200
@@ -2215,6 +2215,70 @@
 
     return "2.25." + LargeHexadecimalToDecimal(hex);
   }
+
+
+  void Toolbox::SimplifyDicomAsJson(Json::Value& target,
+                                    const Json::Value& source,
+                                    DicomToJsonFormat format)
+  {
+    if (!source.isObject())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    target = Json::objectValue;
+    Json::Value::Members members = source.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const Json::Value& v = source[members[i]];
+      const std::string& type = v["Type"].asString();
+
+      std::string name;
+      switch (format)
+      {
+        case DicomToJsonFormat_Human:
+          name = v["Name"].asString();
+          break;
+
+        case DicomToJsonFormat_Short:
+          name = members[i];
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      if (type == "String")
+      {
+        target[name] = v["Value"].asString();
+      }
+      else if (type == "TooLong" ||
+               type == "Null")
+      {
+        target[name] = Json::nullValue;
+      }
+      else if (type == "Sequence")
+      {
+        const Json::Value& array = v["Value"];
+        assert(array.isArray());
+
+        Json::Value children = Json::arrayValue;
+        for (Json::Value::ArrayIndex i = 0; i < array.size(); i++)
+        {
+          Json::Value c;
+          SimplifyDicomAsJson(c, array[i], format);
+          children.append(c);
+        }
+
+        target[name] = children;
+      }
+      else
+      {
+        assert(0);
+      }
+    }
+  }
 }
 
 
--- a/OrthancFramework/Sources/Toolbox.h	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Thu Jun 11 12:52:09 2020 +0200
@@ -267,6 +267,10 @@
 
     // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html
     static std::string GenerateDicomPrivateUniqueIdentifier();
+
+    static void SimplifyDicomAsJson(Json::Value& target,
+                                    const Json::Value& source,
+                                    DicomToJsonFormat format);
   };
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Thu Jun 11 12:52:09 2020 +0200
@@ -0,0 +1,1885 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "gtest/gtest.h"
+
+#include "../Sources/Compatibility.h"
+#include "../Sources/DicomNetworking/DicomFindAnswers.h"
+#include "../Sources/DicomParsing/DicomModification.h"
+#include "../Sources/DicomParsing/DicomWebJsonVisitor.h"
+#include "../Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../Sources/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../Sources/DicomParsing/ToDcmtkBridge.h"
+#include "../Sources/Endianness.h"
+#include "../Sources/Images/Image.h"
+#include "../Sources/Images/ImageBuffer.h"
+#include "../Sources/Images/ImageProcessing.h"
+#include "../Sources/Images/PngReader.h"
+#include "../Sources/Images/PngWriter.h"
+#include "../Sources/OrthancException.h"
+#include "../Sources/SystemToolbox.h"
+#include "../Resources/CodeGeneration/EncodingTests.h"
+
+#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;
+
+TEST(DicomFormat, Tag)
+{
+  ASSERT_EQ("PatientName", FromDcmtkBridge::GetTagName(DicomTag(0x0010, 0x0010), ""));
+
+  DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription");
+  ASSERT_EQ(0x0008, t.GetGroup());
+  ASSERT_EQ(0x103E, t.GetElement());
+
+  t = FromDcmtkBridge::ParseTag("0020-e040");
+  ASSERT_EQ(0x0020, t.GetGroup());
+  ASSERT_EQ(0xe040, t.GetElement());
+
+  // Test ==() and !=() operators
+  ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020));
+  ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020));
+}
+
+
+TEST(DicomModification, Basic)
+{
+  DicomModification m;
+  m.SetupAnonymization(DicomVersion_2008);
+  //m.SetLevel(DicomRootLevel_Study);
+  //m.ReplacePlainString(DICOM_TAG_PATIENT_ID, "coucou");
+  //m.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
+
+  ParsedDicomFile o(true);
+  o.SaveToFile("UnitTestsResults/anon.dcm");
+
+  for (int i = 0; i < 10; i++)
+  {
+    char b[1024];
+    sprintf(b, "UnitTestsResults/anon%06d.dcm", i);
+    std::unique_ptr<ParsedDicomFile> f(o.Clone(false));
+    if (i > 4)
+      o.ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
+    m.Apply(*f);
+    f->SaveToFile(b);
+  }
+}
+
+
+TEST(DicomModification, Anonymization)
+{
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName"));
+
+  const DicomTag privateTag(0x0045, 0x1010);
+  const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020"));
+  ASSERT_TRUE(privateTag.IsPrivate());
+  ASSERT_TRUE(privateTag2.IsPrivate());
+  ASSERT_EQ(0x0031, privateTag2.GetGroup());
+  ASSERT_EQ(0x1020, privateTag2.GetElement());
+
+  std::string s;
+  ParsedDicomFile o(true);
+  o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
+  ASSERT_FALSE(o.GetTagValue(s, privateTag));
+  o.Insert(privateTag, "private tag", false, "OrthancCreator");
+  ASSERT_TRUE(o.GetTagValue(s, privateTag));
+  ASSERT_STREQ("private tag", s.c_str());
+
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException);
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator");
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
+  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
+  ASSERT_STREQ("hello", s.c_str());
+  o.Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
+  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
+  ASSERT_STREQ("hello world", s.c_str());
+
+  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_FALSE(Toolbox::IsUuid(s));
+
+  DicomModification m;
+  m.SetupAnonymization(DicomVersion_2008);
+  m.Keep(privateTag);
+
+  m.Apply(o);
+
+  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(Toolbox::IsUuid(s));
+  ASSERT_TRUE(o.GetTagValue(s, privateTag));
+  ASSERT_STREQ("private tag", s.c_str());
+  
+  m.SetupAnonymization(DicomVersion_2008);
+  m.Apply(o);
+  ASSERT_FALSE(o.GetTagValue(s, privateTag));
+}
+
+
+#include <dcmtk/dcmdata/dcuid.h>
+
+TEST(DicomModification, Png)
+{
+  // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image)
+  std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+  std::string m, cc;
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(m, cc, s));
+
+  ASSERT_EQ("image/png", m);
+
+  PngReader reader;
+  reader.ReadFromMemory(cc);
+
+  ASSERT_EQ(5u, reader.GetHeight());
+  ASSERT_EQ(5u, reader.GetWidth());
+  ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat());
+
+  ParsedDicomFile o(true);
+  o.EmbedContent(s);
+  o.SaveToFile("UnitTestsResults/png1.dcm");
+
+  // Red dot, without alpha channel
+  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII=";
+  o.EmbedContent(s);
+  o.SaveToFile("UnitTestsResults/png2.dcm");
+
+  // Check box in Graylevel8
+  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII=";
+  o.EmbedContent(s);
+  //o.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing);
+  o.SaveToFile("UnitTestsResults/png3.dcm");
+
+
+  {
+    // Gradient in Graylevel16
+
+    ImageBuffer img;
+    img.SetWidth(256);
+    img.SetHeight(256);
+    img.SetFormat(PixelFormat_Grayscale16);
+
+    ImageAccessor accessor;
+    img.GetWriteableAccessor(accessor);
+    
+    uint16_t v = 0;
+    for (unsigned int y = 0; y < img.GetHeight(); y++)
+    {
+      uint16_t *p = reinterpret_cast<uint16_t*>(accessor.GetRow(y));
+      for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++)
+      {
+        *p = v;
+      }
+    }
+
+    o.EmbedImage(accessor);
+    o.SaveToFile("UnitTestsResults/png4.dcm");
+  }
+}
+
+
+TEST(FromDcmtkBridge, Encodings1)
+{
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    std::string source(testEncodingsEncoded[i]);
+    std::string expected(testEncodingsExpected[i]);
+    std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false);
+    //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
+    EXPECT_EQ(expected, s);
+  }
+}
+
+
+TEST(FromDcmtkBridge, Enumerations)
+{
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
+  Encoding e;
+
+  ASSERT_FALSE(GetDicomEncoding(e, ""));
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 6"));  ASSERT_EQ(Encoding_Ascii, e);
+
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-2
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 100"));  ASSERT_EQ(Encoding_Latin1, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 101"));  ASSERT_EQ(Encoding_Latin2, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 109"));  ASSERT_EQ(Encoding_Latin3, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 126"));  ASSERT_EQ(Encoding_Greek, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 138"));  ASSERT_EQ(Encoding_Hebrew, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 148"));  ASSERT_EQ(Encoding_Latin5, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 13"));   ASSERT_EQ(Encoding_Japanese, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 166"));  ASSERT_EQ(Encoding_Thai, e);
+
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-3
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 6"));    ASSERT_EQ(Encoding_Ascii, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 100"));  ASSERT_EQ(Encoding_Latin1, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 101"));  ASSERT_EQ(Encoding_Latin2, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 109"));  ASSERT_EQ(Encoding_Latin3, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 126"));  ASSERT_EQ(Encoding_Greek, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 138"));  ASSERT_EQ(Encoding_Hebrew, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 148"));  ASSERT_EQ(Encoding_Latin5, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 13"));   ASSERT_EQ(Encoding_Japanese, e);
+  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_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_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);
+  ASSERT_TRUE(GetDicomEncoding(e, "GB18030"));     ASSERT_EQ(Encoding_Chinese, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "GBK"));         ASSERT_EQ(Encoding_Chinese, e);
+}
+
+
+TEST(FromDcmtkBridge, Encodings3)
+{
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
+    std::string dicom;
+
+    {
+      ParsedDicomFile f(true);
+      f.SetEncoding(testEncodings[i]);
+
+      std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
+      f.Insert(DICOM_TAG_PATIENT_NAME, s, false, "");
+      f.SaveToMemoryBuffer(dicom);
+    }
+
+    if (testEncodings[i] != Encoding_Windows1251)
+    {
+      ParsedDicomFile g(dicom);
+
+      if (testEncodings[i] != Encoding_Ascii)
+      {
+        bool hasCodeExtensions;
+        ASSERT_EQ(testEncodings[i], g.DetectEncoding(hasCodeExtensions));
+        ASSERT_FALSE(hasCodeExtensions);
+      }
+
+      std::string tag;
+      ASSERT_TRUE(g.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));
+      ASSERT_EQ(std::string(testEncodingsExpected[i]), tag);
+    }
+  }
+}
+
+
+TEST(FromDcmtkBridge, ValueRepresentation)
+{
+  ASSERT_EQ(ValueRepresentation_PersonName, 
+            FromDcmtkBridge::LookupValueRepresentation(DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(ValueRepresentation_Date, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0020) /* StudyDate */));
+  ASSERT_EQ(ValueRepresentation_Time, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0030) /* StudyTime */));
+  ASSERT_EQ(ValueRepresentation_DateTime, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x002a) /* AcquisitionDateTime */));
+  ASSERT_EQ(ValueRepresentation_NotSupported, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0001, 0x0001) /* some private tag */));
+}
+
+
+
+static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110);
+static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120);
+
+static void CreateSampleJson(Json::Value& a)
+{
+  {
+    Json::Value b = Json::objectValue;
+    b["PatientName"] = "Hello";
+    b["PatientID"] = "World";
+    b["StudyDescription"] = "Toto";
+    a.append(b);
+  }
+
+  {
+    Json::Value b = Json::objectValue;
+    b["PatientName"] = "data:application/octet-stream;base64,SGVsbG8y";  // echo -n "Hello2" | base64
+    b["PatientID"] = "World2";
+    a.append(b);
+  }
+}
+
+
+
+TEST(ParsedDicomFile, InsertReplaceStrings)
+{
+  ParsedDicomFile f(true);
+
+  f.Insert(DICOM_TAG_PATIENT_NAME, "World", false, "");
+  ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false, ""), OrthancException);  // Already existing tag
+  f.ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, "Toto");  // (*)
+  f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata");  // (**)
+
+  std::string s;
+  ASSERT_TRUE(f.LookupTransferSyntax(s));
+  // The default transfer syntax depends on the OS endianness
+  ASSERT_TRUE(s == GetTransferSyntaxUid(DicomTransferSyntax_LittleEndianExplicit) ||
+              s == GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit));
+
+  ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"),
+                         false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_IgnoreIfAbsent, "");
+  ASSERT_FALSE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_InsertIfAbsent, "");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_EQ(s, "Accession");
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession2"), false, DicomReplaceMode_IgnoreIfAbsent, "");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_EQ(s, "Accession2");
+  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent, "");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_EQ(s, "Accession3");
+
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(s, "World");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID));
+  ASSERT_EQ(s, "Toto");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));  // Implicitly modified by (*)
+  ASSERT_EQ(s, "Toto");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID));
+  ASSERT_EQ(s, "Tata");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID));  // Implicitly modified by (**)
+  ASSERT_EQ(s, "Tata");
+}
+
+
+
+
+TEST(ParsedDicomFile, InsertReplaceJson)
+{
+  ParsedDicomFile f(true);
+
+  Json::Value a;
+  CreateSampleJson(a);
+
+  ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
+  f.Remove(REFERENCED_STUDY_SEQUENCE);  // No effect
+  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
+  ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
+  ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""), OrthancException);
+  f.Remove(REFERENCED_STUDY_SEQUENCE);
+  ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
+  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
+  ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
+
+  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
+  ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
+  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
+  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent, "");
+  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
+  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent, "");
+  ASSERT_TRUE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
+
+  {
+    Json::Value b;
+    f.DatasetToJson(b, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0);
+
+    Json::Value c;
+    Toolbox::SimplifyDicomAsJson(c, b, DicomToJsonFormat_Human);
+
+    ASSERT_EQ(0, c["ReferencedPatientSequence"].compare(a));
+    ASSERT_NE(0, c["ReferencedStudySequence"].compare(a));  // Because Data URI Scheme decoding was enabled
+  }
+
+  a = "data:application/octet-stream;base64,VGF0YQ==";   // echo -n "Tata" | base64 
+  f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false, DicomReplaceMode_InsertIfAbsent, "");  // (*)
+  f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true, DicomReplaceMode_InsertIfAbsent, "");  // (**)
+
+  std::string s;
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID));
+  ASSERT_EQ(s, a.asString());
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));  // Implicitly modified by (*)
+  ASSERT_EQ(s, a.asString());
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID));
+  ASSERT_EQ(s, "Tata");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID));  // Implicitly modified by (**)
+  ASSERT_EQ(s, "Tata");
+}
+
+
+TEST(ParsedDicomFile, JsonEncoding)
+{
+  ParsedDicomFile f(true);
+
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    if (testEncodings[i] != Encoding_Windows1251)
+    {
+      //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
+      f.SetEncoding(testEncodings[i]);
+
+      if (testEncodings[i] != Encoding_Ascii)
+      {
+        bool hasCodeExtensions;
+        ASSERT_EQ(testEncodings[i], f.DetectEncoding(hasCodeExtensions));
+        ASSERT_FALSE(hasCodeExtensions);
+      }
+
+      Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
+      f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, "");
+
+      Json::Value v;
+      f.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+      ASSERT_EQ(v["PatientName"].asString(), std::string(testEncodingsExpected[i]));
+    }
+  }
+}
+
+
+TEST(ParsedDicomFile, ToJsonFlags1)
+{
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag", 1, 1, "OrthancCreator");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag", 1, 1, "");
+
+  ParsedDicomFile f(true);
+  f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false, "");  // Even group => public tag
+  f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false, "");  // Even group => public, unknown tag
+  f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false, "OrthancCreator");  // Odd group => private tag
+
+  Json::Value v;
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7052,1000"));
+  ASSERT_FALSE(v.isMember("7053,1000"));
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_EQ(Json::stringValue, v["7050,1000"].type());
+  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(7u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7052,1000"));
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_TRUE(v.isMember("7053,1000"));
+  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
+  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7052,1000"));
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_FALSE(v.isMember("7053,1000"));
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(7u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7052,1000"));
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_TRUE(v.isMember("7053,1000"));
+  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
+  std::string mime, content;
+  ASSERT_EQ(Json::stringValue, v["7053,1000"].type());
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7053,1000"].asString()));
+  ASSERT_EQ("application/octet-stream", mime);
+  ASSERT_EQ("Some private tag", content);
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(7u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_TRUE(v.isMember("7052,1000"));
+  ASSERT_FALSE(v.isMember("7053,1000"));
+  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
+  ASSERT_EQ(Json::nullValue, v["7052,1000"].type());
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(7u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_TRUE(v.isMember("7052,1000"));
+  ASSERT_FALSE(v.isMember("7053,1000"));
+  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
+  ASSERT_EQ(Json::stringValue, v["7052,1000"].type());
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7052,1000"].asString()));
+  ASSERT_EQ("application/octet-stream", mime);
+  ASSERT_EQ("Some unknown tag", content);
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(8u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_TRUE(v.isMember("7052,1000"));
+  ASSERT_TRUE(v.isMember("7053,1000"));
+  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
+  ASSERT_EQ(Json::nullValue, v["7052,1000"].type());
+  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
+}
+
+
+TEST(ParsedDicomFile, ToJsonFlags2)
+{
+  ParsedDicomFile f(true);
+
+  {
+    // "ParsedDicomFile" uses Little Endian => 'B' (least significant
+    // byte) will be stored first in the memory buffer and in the
+    // file, then 'A'. Hence the expected "BA" value below.
+    Uint16 v[] = { 'A' * 256 + 'B', 0 };
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint16Array(DCM_PixelData, v, 2).good());
+  }
+
+  Json::Value v;
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(5u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7fe0,0010"));  
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7fe0,0010"));  
+  ASSERT_EQ(Json::nullValue, v["7fe0,0010"].type());  
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToAscii), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7fe0,0010"));  
+  ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());  
+  ASSERT_EQ("BA", v["7fe0,0010"].asString().substr(0, 2));
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7fe0,0010"));  
+  ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());
+  std::string mime, content;
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString()));
+  ASSERT_EQ("application/octet-stream", mime);
+  ASSERT_EQ("BA", content.substr(0, 2));
+}
+
+
+TEST(DicomFindAnswers, Basic)
+{
+  DicomFindAnswers a(false);
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_ID, "hello", false);
+    a.Add(m);
+  }
+
+  {
+    ParsedDicomFile d(true);
+    d.ReplacePlainString(DICOM_TAG_PATIENT_ID, "my");
+    a.Add(d);
+  }
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_ID, "world", false);
+    a.Add(m);
+  }
+
+  Json::Value j;
+  a.ToJson(j, true);
+  ASSERT_EQ(3u, j.size());
+
+  //std::cout << j;
+}
+
+
+TEST(ParsedDicomFile, FromJson)
+{
+  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, "");
+
+  Json::Value v;
+  const std::string sopClassUid = "1.2.840.10008.5.1.4.1.1.1";  // CR Image Storage:
+
+  // Test the private creator
+  ASSERT_EQ(DcmTag_ERROR_TagName, FromDcmtkBridge::GetTagName(DicomTag(0x7057, 0x1000), "NOPE"));
+  ASSERT_EQ("MyPrivateTag2", FromDcmtkBridge::GetTagName(DicomTag(0x7057, 0x1000), "ORTHANC"));
+
+  {
+    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["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
+  
+    std::string s;
+    Toolbox::EncodeDataUriScheme(s, "application/octet-stream", "Sebastien");
+    v["StudyDescription"] = s;
+
+    v["PixelData"] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";  // A red dot of 5x5 pixels
+    v["0040,0100"] = Json::arrayValue;  // ScheduledProcedureStepSequence
+
+    Json::Value vv;
+    vv["Modality"] = "MR";
+    v["0040,0100"].append(vv);
+
+    vv["Modality"] = "CT";
+    v["0040,0100"].append(vv);
+  }
+
+  const DicomToJsonFlags toJsonFlags = static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeBinary |
+                                                                     DicomToJsonFlags_IncludePixelData | 
+                                                                     DicomToJsonFlags_IncludePrivateTags | 
+                                                                     DicomToJsonFlags_IncludeUnknownTags | 
+                                                                     DicomToJsonFlags_ConvertBinaryToAscii);
+
+
+  {
+    std::unique_ptr<ParsedDicomFile> dicom
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers), ""));
+
+    Json::Value vv;
+    dicom->DatasetToJson(vv, DicomToJsonFormat_Human, toJsonFlags, 0);
+
+    ASSERT_EQ(vv["SOPClassUID"].asString(), sopClassUid);
+    ASSERT_EQ(vv["MediaStorageSOPClassUID"].asString(), sopClassUid);
+    ASSERT_TRUE(vv.isMember("SOPInstanceUID"));
+    ASSERT_TRUE(vv.isMember("SeriesInstanceUID"));
+    ASSERT_TRUE(vv.isMember("StudyInstanceUID"));
+    ASSERT_TRUE(vv.isMember("PatientID"));
+  }
+
+
+  {
+    std::unique_ptr<ParsedDicomFile> dicom
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers), ""));
+
+    Json::Value vv;
+    dicom->DatasetToJson(vv, DicomToJsonFormat_Human, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData), 0);
+
+    std::string mime, content;
+    ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, vv["PixelData"].asString()));
+    ASSERT_EQ("application/octet-stream", mime);
+    ASSERT_EQ(5u * 5u * 3u /* the red dot is 5x5 pixels in RGB24 */ + 1 /* for padding */, content.size());
+  }
+
+
+  {
+    std::unique_ptr<ParsedDicomFile> dicom
+      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_DecodeDataUriScheme), ""));
+
+    Json::Value vv;
+    dicom->DatasetToJson(vv, DicomToJsonFormat_Short, toJsonFlags, 0);
+
+    ASSERT_FALSE(vv.isMember("SOPInstanceUID"));
+    ASSERT_FALSE(vv.isMember("SeriesInstanceUID"));
+    ASSERT_FALSE(vv.isMember("StudyInstanceUID"));
+    ASSERT_FALSE(vv.isMember("PatientID"));
+    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 unknown tag", vv["7052,1000"].asString());
+    ASSERT_EQ("Some private tag", vv["7057,1000"].asString());
+    ASSERT_EQ("Some private tag2", vv["7059,1000"].asString());
+    ASSERT_EQ("Sébastien", vv["0010,0010"].asString());
+    ASSERT_EQ("Sebastien", vv["0008,1030"].asString());
+    ASSERT_EQ("ISO_IR 148", vv["0008,0005"].asString());
+    ASSERT_EQ("5", vv[DICOM_TAG_ROWS.Format()].asString());
+    ASSERT_EQ("5", vv[DICOM_TAG_COLUMNS.Format()].asString());
+    ASSERT_TRUE(vv[DICOM_TAG_PIXEL_DATA.Format()].asString().empty());
+  }
+}
+
+
+
+TEST(TestImages, PatternGrayscale8)
+{
+  static const char* PATH = "UnitTestsResults/PatternGrayscale8.dcm";
+
+  Orthanc::Image image(Orthanc::PixelFormat_Grayscale8, 256, 256, false);
+
+  for (int y = 0; y < 256; y++)
+  {
+    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+    for (int x = 0; x < 256; x++, p++)
+    {
+      *p = y;
+    }
+  }
+
+  Orthanc::ImageAccessor r;
+
+  image.GetRegion(r, 32, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 0);
+  
+  image.GetRegion(r, 160, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 255); 
+
+  {
+    ParsedDicomFile f(true);
+    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
+    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
+    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
+    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "Grayscale8");
+    f.EmbedImage(image);
+
+    f.SaveToFile(PATH);
+  }
+
+  {
+    std::string s;
+    Orthanc::SystemToolbox::ReadFile(s, PATH);
+    Orthanc::ParsedDicomFile f(s);
+    
+    std::unique_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
+    ASSERT_EQ(256u, decoded->GetWidth());
+    ASSERT_EQ(256u, decoded->GetHeight());
+    ASSERT_EQ(Orthanc::PixelFormat_Grayscale8, decoded->GetFormat());
+
+    for (int y = 0; y < 256; y++)
+    {
+      const void* a = image.GetConstRow(y);
+      const void* b = decoded->GetConstRow(y);
+      ASSERT_EQ(0, memcmp(a, b, 256));
+    }
+  }
+}
+
+
+TEST(TestImages, PatternRGB)
+{
+  static const char* PATH = "UnitTestsResults/PatternRGB24.dcm";
+
+  Orthanc::Image image(Orthanc::PixelFormat_RGB24, 384, 256, false);
+
+  for (int y = 0; y < 256; y++)
+  {
+    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+    for (int x = 0; x < 128; x++, p += 3)
+    {
+      p[0] = y;
+      p[1] = 0;
+      p[2] = 0;
+    }
+    for (int x = 128; x < 128 * 2; x++, p += 3)
+    {
+      p[0] = 0;
+      p[1] = 255 - y;
+      p[2] = 0;
+    }
+    for (int x = 128 * 2; x < 128 * 3; x++, p += 3)
+    {
+      p[0] = 0;
+      p[1] = 0;
+      p[2] = y;
+    }
+  }
+
+  {
+    ParsedDicomFile f(true);
+    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
+    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
+    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
+    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "RGB24");
+    f.EmbedImage(image);
+
+    f.SaveToFile(PATH);
+  }
+
+  {
+    std::string s;
+    Orthanc::SystemToolbox::ReadFile(s, PATH);
+    Orthanc::ParsedDicomFile f(s);
+    
+    std::unique_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
+    ASSERT_EQ(384u, decoded->GetWidth());
+    ASSERT_EQ(256u, decoded->GetHeight());
+    ASSERT_EQ(Orthanc::PixelFormat_RGB24, decoded->GetFormat());
+
+    for (int y = 0; y < 256; y++)
+    {
+      const void* a = image.GetConstRow(y);
+      const void* b = decoded->GetConstRow(y);
+      ASSERT_EQ(0, memcmp(a, b, 3 * 384));
+    }
+  }
+}
+
+
+TEST(TestImages, PatternUint16)
+{
+  static const char* PATH = "UnitTestsResults/PatternGrayscale16.dcm";
+
+  Orthanc::Image image(Orthanc::PixelFormat_Grayscale16, 256, 256, false);
+
+  uint16_t v = 0;
+  for (int y = 0; y < 256; y++)
+  {
+    uint16_t *p = reinterpret_cast<uint16_t*>(image.GetRow(y));
+    for (int x = 0; x < 256; x++, v++, p++)
+    {
+      *p = htole16(v);   // Orthanc uses Little-Endian transfer syntax to encode images
+    }
+  }
+
+  Orthanc::ImageAccessor r;
+  
+  image.GetRegion(r, 32, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 0);
+  
+  image.GetRegion(r, 160, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 65535); 
+
+  {
+    ParsedDicomFile f(true);
+    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
+    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
+    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
+    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "Grayscale16");
+    f.EmbedImage(image);
+
+    f.SaveToFile(PATH);
+  }
+
+  {
+    std::string s;
+    Orthanc::SystemToolbox::ReadFile(s, PATH);
+    Orthanc::ParsedDicomFile f(s);
+    
+    std::unique_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
+    ASSERT_EQ(256u, decoded->GetWidth());
+    ASSERT_EQ(256u, decoded->GetHeight());
+    ASSERT_EQ(Orthanc::PixelFormat_Grayscale16, decoded->GetFormat());
+
+    for (int y = 0; y < 256; y++)
+    {
+      const void* a = image.GetConstRow(y);
+      const void* b = decoded->GetConstRow(y);
+      ASSERT_EQ(0, memcmp(a, b, 512));
+    }
+  }
+}
+
+
+TEST(TestImages, PatternInt16)
+{
+  static const char* PATH = "UnitTestsResults/PatternSignedGrayscale16.dcm";
+
+  Orthanc::Image image(Orthanc::PixelFormat_SignedGrayscale16, 256, 256, false);
+
+  int16_t v = -32768;
+  for (int y = 0; y < 256; y++)
+  {
+    int16_t *p = reinterpret_cast<int16_t*>(image.GetRow(y));
+    for (int x = 0; x < 256; x++, v++, p++)
+    {
+      *p = htole16(v);   // Orthanc uses Little-Endian transfer syntax to encode images
+    }
+  }
+
+  Orthanc::ImageAccessor r;
+  image.GetRegion(r, 32, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, -32768);
+  
+  image.GetRegion(r, 160, 32, 64, 192);
+  Orthanc::ImageProcessing::Set(r, 32767); 
+
+  {
+    ParsedDicomFile f(true);
+    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
+    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
+    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
+    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
+    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "SignedGrayscale16");
+    f.EmbedImage(image);
+
+    f.SaveToFile(PATH);
+  }
+
+  {
+    std::string s;
+    Orthanc::SystemToolbox::ReadFile(s, PATH);
+    Orthanc::ParsedDicomFile f(s);
+    
+    std::unique_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
+    ASSERT_EQ(256u, decoded->GetWidth());
+    ASSERT_EQ(256u, decoded->GetHeight());
+    ASSERT_EQ(Orthanc::PixelFormat_SignedGrayscale16, decoded->GetFormat());
+
+    for (int y = 0; y < 256; y++)
+    {
+      const void* a = image.GetConstRow(y);
+      const void* b = decoded->GetConstRow(y);
+      ASSERT_EQ(0, memcmp(a, b, 512));
+    }
+  }
+}
+
+
+
+static void CheckEncoding(const ParsedDicomFile& dicom,
+                          Encoding expected)
+{
+  const char* value = NULL;
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_SpecificCharacterSet, value).good());
+
+  Encoding encoding;
+  ASSERT_TRUE(GetDicomEncoding(encoding, value));
+  ASSERT_EQ(expected, encoding);
+}
+
+
+TEST(ParsedDicomFile, DicomMapEncodings1)
+{
+  SetDefaultDicomEncoding(Encoding_Ascii);
+  ASSERT_EQ(Encoding_Ascii, GetDefaultDicomEncoding());
+
+  {
+    DicomMap m;
+    ParsedDicomFile dicom(m, GetDefaultDicomEncoding(), false);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Ascii);
+  }
+
+  {
+    DicomMap m;
+    ParsedDicomFile dicom(m, Encoding_Latin4, false);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Latin4);
+  }
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 148", false);
+    ParsedDicomFile dicom(m, GetDefaultDicomEncoding(), false);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Latin5);
+  }
+
+  {
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 148", false);
+    ParsedDicomFile dicom(m, Encoding_Latin1, false);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Latin5);
+  }
+}
+
+
+TEST(ParsedDicomFile, DicomMapEncodings2)
+{
+  const char* utf8 = NULL;
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    if (testEncodings[i] == Encoding_Utf8)
+    {
+      utf8 = testEncodingsEncoded[i];
+      break;
+    }
+  }  
+
+  ASSERT_TRUE(utf8 != NULL);
+
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    // 1251 codepage is not supported by the core DICOM standard, ignore it
+    if (testEncodings[i] != Encoding_Windows1251) 
+    {
+      {
+        // 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], false);
+        ASSERT_STREQ(testEncodingsExpected[i], decoded.c_str());
+
+        if (testEncodings[i] != Encoding_Chinese)
+        {
+          // 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], false);
+          ASSERT_STREQ(testEncodingsExpected[i], encoded.c_str());
+        }
+      }
+
+
+      Json::Value v;
+
+      {
+        DicomMap m;
+        m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
+
+        ParsedDicomFile dicom(m, testEncodings[i], false);
+    
+        const char* encoded = NULL;
+        ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, encoded).good());
+        ASSERT_STREQ(testEncodingsEncoded[i], encoded);
+
+        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+
+        Encoding encoding;
+        ASSERT_TRUE(GetDicomEncoding(encoding, v["SpecificCharacterSet"].asCString()));
+        ASSERT_EQ(encoding, testEncodings[i]);
+        ASSERT_STREQ(testEncodingsExpected[i], v["PatientName"].asCString());
+      }
+
+
+      {
+        DicomMap m;
+        m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(testEncodings[i]), false);
+        m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
+
+        ParsedDicomFile dicom(m, testEncodings[i], false);
+
+        Json::Value v2;
+        dicom.DatasetToJson(v2, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+        
+        ASSERT_EQ(v2["PatientName"].asString(), v["PatientName"].asString());
+        ASSERT_EQ(v2["SpecificCharacterSet"].asString(), v["SpecificCharacterSet"].asString());
+      }
+    }
+  }
+}
+
+
+TEST(ParsedDicomFile, ChangeEncoding)
+{
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    // 1251 codepage is not supported by the core DICOM standard, ignore it
+    if (testEncodings[i] != Encoding_Windows1251) 
+    {
+      DicomMap m;
+      m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
+
+      std::string tag;
+
+      ParsedDicomFile dicom(m, Encoding_Utf8, false);
+      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]);
+
+      {
+        Json::Value v;
+        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+        ASSERT_STREQ(v["SpecificCharacterSet"].asCString(), "ISO_IR 192");
+        ASSERT_STREQ(v["PatientName"].asCString(), testEncodingsExpected[i]);
+      }
+
+      dicom.ChangeEncoding(testEncodings[i]);
+
+      ASSERT_EQ(testEncodings[i], dicom.DetectEncoding(hasCodeExtensions));
+      ASSERT_FALSE(hasCodeExtensions);
+      
+      const char* c = NULL;
+      ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, c).good());
+      EXPECT_STREQ(c, testEncodingsEncoded[i]);
+      
+      ASSERT_TRUE(dicom.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));  // Decodes to UTF-8
+      EXPECT_EQ(tag, testEncodingsExpected[i]);
+
+      {
+        Json::Value v;
+        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+        ASSERT_STREQ(v["SpecificCharacterSet"].asCString(), GetDicomSpecificCharacterSet(testEncodings[i]));
+        ASSERT_STREQ(v["PatientName"].asCString(), testEncodingsExpected[i]);
+      }
+    }
+  }
+}
+
+
+TEST(Toolbox, CaseWithAccents)
+{
+  ASSERT_EQ(toUpperResult, Toolbox::ToUpperCaseWithAccents(toUpperSource));
+}
+
+
+
+TEST(ParsedDicomFile, InvalidCharacterSets)
+{
+  {
+    // No encoding provided, fallback to default encoding
+    DicomMap m;
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false);
+
+    bool hasCodeExtensions;
+    ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions));
+    ASSERT_FALSE(hasCodeExtensions);
+  }
+  
+  {
+    // Valid encoding, "ISO_IR 13" is Japanese
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 13", false);
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false);
+
+    bool hasCodeExtensions;
+    ASSERT_EQ(Encoding_Japanese, d.DetectEncoding(hasCodeExtensions));
+    ASSERT_FALSE(hasCodeExtensions);
+  }
+  
+  {
+    // Invalid value for an encoding ("nope" is not in the DICOM standard)
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "nope", false);
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ASSERT_THROW(ParsedDicomFile d(m, Encoding_Latin3, false),
+                 OrthancException);
+  }
+  
+  {
+    // Invalid encoding, as provided as a binary string
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 13", true);
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ASSERT_THROW(ParsedDicomFile d(m, Encoding_Latin3, false),
+                 OrthancException);
+  }
+  
+  {
+    // Encoding provided as an empty string, fallback to default encoding
+    // In Orthanc <= 1.3.1, this test was throwing an exception
+    DicomMap m;
+    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "", false);
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+
+    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false);
+
+    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;
+}
+
+
+
+// Compatibility wrapper
+static pugi::xpath_node SelectNode(const pugi::xml_document& doc,
+                                   const char* xpath)
+{
+#if PUGIXML_VERSION <= 140
+  return doc.select_single_node(xpath);  // Deprecated in pugixml 1.5
+#else
+  return doc.select_node(xpath);
+#endif
+}
+
+
+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 = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
+  ASSERT_STREQ("ISO 2022 IR 149", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
+  ASSERT_STREQ("CS", node.node().attribute("vr").value());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
+  ASSERT_STREQ("PN", node.node().attribute("vr").value());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
+  ASSERT_STREQ("Hong", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
+  ASSERT_STREQ("Gildong", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
+  ASSERT_EQ(utf8.substr(13, 3), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
+  ASSERT_EQ(utf8.substr(17, 6), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
+  ASSERT_EQ(utf8.substr(24, 3), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
+  ASSERT_EQ(utf8.substr(28), node.node().text().as_string());
+#endif
+  
+  {
+    DicomMap m;
+    m.FromDicomWeb(visitor.GetResult());
+    ASSERT_EQ(2u, m.GetSize());
+
+    std::string s;
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false));
+    ASSERT_EQ("ISO 2022 IR 149", s);
+
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
+    std::vector<std::string> v;
+    Toolbox::TokenizeString(v, s, '=');
+    ASSERT_EQ(3u, v.size());
+    ASSERT_EQ("Hong^Gildong", v[0]);
+    ASSERT_EQ(utf8, s);
+  }
+}
+
+
+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 = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
+  ASSERT_STREQ("ISO 2022 IR 87", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
+  ASSERT_STREQ("CS", node.node().attribute("vr").value());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
+  ASSERT_STREQ("PN", node.node().attribute("vr").value());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
+  ASSERT_STREQ("Yamada", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
+  ASSERT_STREQ("Tarou", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
+  ASSERT_EQ(utf8.substr(13, 6), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
+  ASSERT_EQ(utf8.substr(20, 6), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
+  ASSERT_EQ(utf8.substr(27, 9), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
+  ASSERT_EQ(utf8.substr(37), node.node().text().as_string());
+#endif  
+  
+  {
+    DicomMap m;
+    m.FromDicomWeb(visitor.GetResult());
+    ASSERT_EQ(2u, m.GetSize());
+
+    std::string s;
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false));
+    ASSERT_EQ("ISO 2022 IR 87", s);
+
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
+    std::vector<std::string> v;
+    Toolbox::TokenizeString(v, s, '=');
+    ASSERT_EQ(3u, v.size());
+    ASSERT_EQ("Yamada^Tarou", v[0]);
+    ASSERT_EQ(utf8, s);
+  }
+}
+
+
+
+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());
+}
+
+
+
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+
+#include "../Sources/DicomNetworking/DicomStoreUserConnection.h"
+#include "../Sources/DicomParsing/DcmtkTranscoder.h"
+
+TEST(Toto, DISABLED_Transcode3)
+{
+  DicomAssociationParameters p;
+  p.SetRemotePort(2000);
+
+  DicomStoreUserConnection scu(p);
+  scu.SetCommonClassesProposed(false);
+  scu.SetRetiredBigEndianProposed(true);
+
+  DcmtkTranscoder transcoder;
+
+  for (int j = 0; j < 2; j++)
+    for (int i = 0; i <= DicomTransferSyntax_XML; i++)
+    {
+      DicomTransferSyntax a = (DicomTransferSyntax) i;
+
+      std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
+                          std::string(GetTransferSyntaxUid(a)) + ".dcm");
+      if (Orthanc::SystemToolbox::IsRegularFile(path))
+      {
+        printf("\n======= %s\n", GetTransferSyntaxUid(a));
+
+        std::string source;
+        Orthanc::SystemToolbox::ReadFile(source, path);
+
+        std::string c, i;
+        try
+        {
+          scu.Transcode(c, i, transcoder, source.c_str(), source.size(), false, "", 0);
+        }
+        catch (OrthancException& e)
+        {
+          if (e.GetErrorCode() == ErrorCode_NotImplemented)
+          {
+            LOG(ERROR) << "cannot transcode " << GetTransferSyntaxUid(a);
+          }
+          else
+          {
+            throw e;
+          }
+        }
+      }
+    }
+}
+
+
+TEST(Toto, DISABLED_Transcode4)
+{
+  std::string source;
+  Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm");
+
+  std::unique_ptr<DcmFileFormat> toto(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size()));
+  const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(*toto);
+  
+  DicomTransferSyntax sourceSyntax;
+  ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto));
+
+  DcmtkTranscoder transcoder;
+
+  for (int i = 0; i <= DicomTransferSyntax_XML; i++)
+  {
+    DicomTransferSyntax a = (DicomTransferSyntax) i;
+    
+    std::set<DicomTransferSyntax> s;
+    s.insert(a);
+
+    std::string t;
+
+    IDicomTranscoder::DicomImage source, target;
+    source.AcquireParsed(dynamic_cast<DcmFileFormat*>(toto->clone()));
+
+    if (!transcoder.Transcode(target, source, s, true))
+    {
+      printf("**************** CANNOT: [%s] => [%s]\n",
+             GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a));
+    }
+    else
+    {
+      DicomTransferSyntax targetSyntax;
+      ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, target.GetParsed()));
+      
+      ASSERT_EQ(targetSyntax, a);
+      bool lossy = (a == DicomTransferSyntax_JPEGProcess1 ||
+                    a == DicomTransferSyntax_JPEGProcess2_4 ||
+                    a == DicomTransferSyntax_JPEGLSLossy);
+      
+      printf("SIZE: %lu\n", t.size());
+      if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed()))
+      {
+        ASSERT_FALSE(lossy);
+      }
+      else
+      {
+        ASSERT_TRUE(lossy);
+      }
+    }
+  }
+}
+
+#endif
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Thu Jun 11 12:52:09 2020 +0200
@@ -2670,7 +2670,7 @@
         else
         {
           Json::Value simplified;
-          ServerToolbox::SimplifyTags(simplified, instance.GetJson(), DicomToJsonFormat_Human);
+          Toolbox::SimplifyDicomAsJson(simplified, instance.GetJson(), DicomToJsonFormat_Human);
           s = writer.write(simplified);
         }
 
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Thu Jun 11 12:52:09 2020 +0200
@@ -393,7 +393,7 @@
           for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++)
           {
             Json::Value item;
-            ServerToolbox::SimplifyTags(item, source["Value"][i], DicomToJsonFormat_Short);
+            Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short);
             content.append(item);
           }
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Jun 11 12:52:09 2020 +0200
@@ -74,7 +74,7 @@
     if (mode != DicomToJsonFormat_Full)
     {
       Json::Value simplified;
-      ServerToolbox::SimplifyTags(simplified, dicom, mode);
+      Toolbox::SimplifyDicomAsJson(simplified, dicom, mode);
       call.GetOutput().AnswerJson(simplified);
     }
     else
@@ -1905,7 +1905,7 @@
       if (format != DicomToJsonFormat_Full)
       {
         Json::Value simplified;
-        ServerToolbox::SimplifyTags(simplified, full, format);
+        Toolbox::SimplifyDicomAsJson(simplified, full, format);
         result[*it] = simplified;
       }
       else
--- a/OrthancServer/Sources/ServerContext.cpp	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Jun 11 12:52:09 2020 +0200
@@ -414,7 +414,7 @@
       resultPublicId = dicom.GetHasher().HashInstance();
 
       Json::Value simplifiedTags;
-      ServerToolbox::SimplifyTags(simplifiedTags, dicom.GetJson(), DicomToJsonFormat_Human);
+      Toolbox::SimplifyDicomAsJson(simplifiedTags, dicom.GetJson(), DicomToJsonFormat_Human);
 
       // Test if the instance must be filtered out
       bool accepted = true;
--- a/OrthancServer/Sources/ServerToolbox.cpp	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Thu Jun 11 12:52:09 2020 +0200
@@ -163,70 +163,6 @@
 
   namespace ServerToolbox
   {
-    void SimplifyTags(Json::Value& target,
-                      const Json::Value& source,
-                      DicomToJsonFormat format)
-    {
-      if (!source.isObject())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-
-      target = Json::objectValue;
-      Json::Value::Members members = source.getMemberNames();
-
-      for (size_t i = 0; i < members.size(); i++)
-      {
-        const Json::Value& v = source[members[i]];
-        const std::string& type = v["Type"].asString();
-
-        std::string name;
-        switch (format)
-        {
-          case DicomToJsonFormat_Human:
-            name = v["Name"].asString();
-            break;
-
-          case DicomToJsonFormat_Short:
-            name = members[i];
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-
-        if (type == "String")
-        {
-          target[name] = v["Value"].asString();
-        }
-        else if (type == "TooLong" ||
-                 type == "Null")
-        {
-          target[name] = Json::nullValue;
-        }
-        else if (type == "Sequence")
-        {
-          const Json::Value& array = v["Value"];
-          assert(array.isArray());
-
-          Json::Value children = Json::arrayValue;
-          for (Json::Value::ArrayIndex i = 0; i < array.size(); i++)
-          {
-            Json::Value c;
-            SimplifyTags(c, array[i], format);
-            children.append(c);
-          }
-
-          target[name] = children;
-        }
-        else
-        {
-          assert(0);
-        }
-      }
-    }
-
-
     bool FindOneChildInstance(int64_t& result,
                               IDatabaseWrapper& database,
                               int64_t resource,
--- a/OrthancServer/Sources/ServerToolbox.h	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancServer/Sources/ServerToolbox.h	Thu Jun 11 12:52:09 2020 +0200
@@ -47,10 +47,6 @@
 
   namespace ServerToolbox
   {
-    void SimplifyTags(Json::Value& target,
-                      const Json::Value& source,
-                      DicomToJsonFormat format);
-
     bool FindOneChildInstance(int64_t& result,
                               IDatabaseWrapper& database,
                               int64_t resource,
--- a/OrthancServer/UnitTestsSources/FromDcmtkTests.cpp	Thu Jun 11 12:24:38 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2034 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../../OrthancFramework/Sources/Compatibility.h"
-#include "../../OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h"
-#include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h"
-#include "../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h"
-#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
-#include "../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h"
-#include "../../OrthancFramework/Sources/Endianness.h"
-#include "../../OrthancFramework/Sources/Images/Image.h"
-#include "../../OrthancFramework/Sources/Images/ImageBuffer.h"
-#include "../../OrthancFramework/Sources/Images/ImageProcessing.h"
-#include "../../OrthancFramework/Sources/Images/PngReader.h"
-#include "../../OrthancFramework/Sources/Images/PngWriter.h"
-#include "../../OrthancFramework/Sources/OrthancException.h"
-#include "../../OrthancFramework/Sources/SystemToolbox.h"
-#include "../Sources/ServerToolbox.h"
-#include "../Plugins/Engine/PluginsEnumerations.h"
-#include "../../OrthancFramework/Resources/CodeGeneration/EncodingTests.h"
-
-#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;
-
-TEST(DicomFormat, Tag)
-{
-  ASSERT_EQ("PatientName", FromDcmtkBridge::GetTagName(DicomTag(0x0010, 0x0010), ""));
-
-  DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription");
-  ASSERT_EQ(0x0008, t.GetGroup());
-  ASSERT_EQ(0x103E, t.GetElement());
-
-  t = FromDcmtkBridge::ParseTag("0020-e040");
-  ASSERT_EQ(0x0020, t.GetGroup());
-  ASSERT_EQ(0xe040, t.GetElement());
-
-  // Test ==() and !=() operators
-  ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020));
-  ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020));
-}
-
-
-TEST(DicomModification, Basic)
-{
-  DicomModification m;
-  m.SetupAnonymization(DicomVersion_2008);
-  //m.SetLevel(DicomRootLevel_Study);
-  //m.ReplacePlainString(DICOM_TAG_PATIENT_ID, "coucou");
-  //m.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
-
-  ParsedDicomFile o(true);
-  o.SaveToFile("UnitTestsResults/anon.dcm");
-
-  for (int i = 0; i < 10; i++)
-  {
-    char b[1024];
-    sprintf(b, "UnitTestsResults/anon%06d.dcm", i);
-    std::unique_ptr<ParsedDicomFile> f(o.Clone(false));
-    if (i > 4)
-      o.ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
-    m.Apply(*f);
-    f->SaveToFile(b);
-  }
-}
-
-
-TEST(DicomModification, Anonymization)
-{
-  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName"));
-
-  const DicomTag privateTag(0x0045, 0x1010);
-  const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020"));
-  ASSERT_TRUE(privateTag.IsPrivate());
-  ASSERT_TRUE(privateTag2.IsPrivate());
-  ASSERT_EQ(0x0031, privateTag2.GetGroup());
-  ASSERT_EQ(0x1020, privateTag2.GetElement());
-
-  std::string s;
-  ParsedDicomFile o(true);
-  o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
-  ASSERT_FALSE(o.GetTagValue(s, privateTag));
-  o.Insert(privateTag, "private tag", false, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag));
-  ASSERT_STREQ("private tag", s.c_str());
-
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException);
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator");
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
-  ASSERT_STREQ("hello", s.c_str());
-  o.Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
-  ASSERT_STREQ("hello world", s.c_str());
-
-  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
-  ASSERT_FALSE(Toolbox::IsUuid(s));
-
-  DicomModification m;
-  m.SetupAnonymization(DicomVersion_2008);
-  m.Keep(privateTag);
-
-  m.Apply(o);
-
-  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
-  ASSERT_TRUE(Toolbox::IsUuid(s));
-  ASSERT_TRUE(o.GetTagValue(s, privateTag));
-  ASSERT_STREQ("private tag", s.c_str());
-  
-  m.SetupAnonymization(DicomVersion_2008);
-  m.Apply(o);
-  ASSERT_FALSE(o.GetTagValue(s, privateTag));
-}
-
-
-#include <dcmtk/dcmdata/dcuid.h>
-
-TEST(DicomModification, Png)
-{
-  // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image)
-  std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
-
-  std::string m, cc;
-  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(m, cc, s));
-
-  ASSERT_EQ("image/png", m);
-
-  PngReader reader;
-  reader.ReadFromMemory(cc);
-
-  ASSERT_EQ(5u, reader.GetHeight());
-  ASSERT_EQ(5u, reader.GetWidth());
-  ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat());
-
-  ParsedDicomFile o(true);
-  o.EmbedContent(s);
-  o.SaveToFile("UnitTestsResults/png1.dcm");
-
-  // Red dot, without alpha channel
-  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII=";
-  o.EmbedContent(s);
-  o.SaveToFile("UnitTestsResults/png2.dcm");
-
-  // Check box in Graylevel8
-  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII=";
-  o.EmbedContent(s);
-  //o.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing);
-  o.SaveToFile("UnitTestsResults/png3.dcm");
-
-
-  {
-    // Gradient in Graylevel16
-
-    ImageBuffer img;
-    img.SetWidth(256);
-    img.SetHeight(256);
-    img.SetFormat(PixelFormat_Grayscale16);
-
-    ImageAccessor accessor;
-    img.GetWriteableAccessor(accessor);
-    
-    uint16_t v = 0;
-    for (unsigned int y = 0; y < img.GetHeight(); y++)
-    {
-      uint16_t *p = reinterpret_cast<uint16_t*>(accessor.GetRow(y));
-      for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++)
-      {
-        *p = v;
-      }
-    }
-
-    o.EmbedImage(accessor);
-    o.SaveToFile("UnitTestsResults/png4.dcm");
-  }
-}
-
-
-TEST(FromDcmtkBridge, Encodings1)
-{
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    std::string source(testEncodingsEncoded[i]);
-    std::string expected(testEncodingsExpected[i]);
-    std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false);
-    //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
-    EXPECT_EQ(expected, s);
-  }
-}
-
-
-TEST(FromDcmtkBridge, Enumerations)
-{
-  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
-  Encoding e;
-
-  ASSERT_FALSE(GetDicomEncoding(e, ""));
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 6"));  ASSERT_EQ(Encoding_Ascii, e);
-
-  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-2
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 100"));  ASSERT_EQ(Encoding_Latin1, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 101"));  ASSERT_EQ(Encoding_Latin2, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 109"));  ASSERT_EQ(Encoding_Latin3, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 126"));  ASSERT_EQ(Encoding_Greek, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 138"));  ASSERT_EQ(Encoding_Hebrew, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 148"));  ASSERT_EQ(Encoding_Latin5, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 13"));   ASSERT_EQ(Encoding_Japanese, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 166"));  ASSERT_EQ(Encoding_Thai, e);
-
-  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-3
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 6"));    ASSERT_EQ(Encoding_Ascii, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 100"));  ASSERT_EQ(Encoding_Latin1, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 101"));  ASSERT_EQ(Encoding_Latin2, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 109"));  ASSERT_EQ(Encoding_Latin3, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 126"));  ASSERT_EQ(Encoding_Greek, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 138"));  ASSERT_EQ(Encoding_Hebrew, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 148"));  ASSERT_EQ(Encoding_Latin5, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 13"));   ASSERT_EQ(Encoding_Japanese, e);
-  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_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_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);
-  ASSERT_TRUE(GetDicomEncoding(e, "GB18030"));     ASSERT_EQ(Encoding_Chinese, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "GBK"));         ASSERT_EQ(Encoding_Chinese, e);
-}
-
-
-TEST(FromDcmtkBridge, Encodings3)
-{
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
-    std::string dicom;
-
-    {
-      ParsedDicomFile f(true);
-      f.SetEncoding(testEncodings[i]);
-
-      std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
-      f.Insert(DICOM_TAG_PATIENT_NAME, s, false, "");
-      f.SaveToMemoryBuffer(dicom);
-    }
-
-    if (testEncodings[i] != Encoding_Windows1251)
-    {
-      ParsedDicomFile g(dicom);
-
-      if (testEncodings[i] != Encoding_Ascii)
-      {
-        bool hasCodeExtensions;
-        ASSERT_EQ(testEncodings[i], g.DetectEncoding(hasCodeExtensions));
-        ASSERT_FALSE(hasCodeExtensions);
-      }
-
-      std::string tag;
-      ASSERT_TRUE(g.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));
-      ASSERT_EQ(std::string(testEncodingsExpected[i]), tag);
-    }
-  }
-}
-
-
-TEST(FromDcmtkBridge, ValueRepresentation)
-{
-  ASSERT_EQ(ValueRepresentation_PersonName, 
-            FromDcmtkBridge::LookupValueRepresentation(DICOM_TAG_PATIENT_NAME));
-  ASSERT_EQ(ValueRepresentation_Date, 
-            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0020) /* StudyDate */));
-  ASSERT_EQ(ValueRepresentation_Time, 
-            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0030) /* StudyTime */));
-  ASSERT_EQ(ValueRepresentation_DateTime, 
-            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x002a) /* AcquisitionDateTime */));
-  ASSERT_EQ(ValueRepresentation_NotSupported, 
-            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0001, 0x0001) /* some private tag */));
-}
-
-
-TEST(FromDcmtkBridge, ValueRepresentationConversions)
-{
-#if ORTHANC_ENABLE_PLUGINS == 1
-  ASSERT_EQ(1, ValueRepresentation_ApplicationEntity);
-  ASSERT_EQ(1, OrthancPluginValueRepresentation_AE);
-
-  for (int i = ValueRepresentation_ApplicationEntity;
-       i <= ValueRepresentation_NotSupported; i++)
-  {
-    ValueRepresentation vr = static_cast<ValueRepresentation>(i);
-
-    if (vr == ValueRepresentation_NotSupported)
-    {
-      ASSERT_THROW(ToDcmtkBridge::Convert(vr), OrthancException);
-      ASSERT_THROW(Plugins::Convert(vr), OrthancException);
-    }
-    else if (vr == ValueRepresentation_OtherDouble || 
-             vr == ValueRepresentation_OtherLong ||
-             vr == ValueRepresentation_UniversalResource ||
-             vr == ValueRepresentation_UnlimitedCharacters)
-    {
-      // These VR are not supported as of DCMTK 3.6.0
-      ASSERT_THROW(ToDcmtkBridge::Convert(vr), OrthancException);
-      ASSERT_EQ(OrthancPluginValueRepresentation_UN, Plugins::Convert(vr));
-    }
-    else
-    {
-      ASSERT_EQ(vr, FromDcmtkBridge::Convert(ToDcmtkBridge::Convert(vr)));
-
-      OrthancPluginValueRepresentation plugins = Plugins::Convert(vr);
-      ASSERT_EQ(vr, Plugins::Convert(plugins));
-    }
-  }
-
-  for (int i = OrthancPluginValueRepresentation_AE;
-       i <= OrthancPluginValueRepresentation_UT; i++)
-  {
-    OrthancPluginValueRepresentation plugins = static_cast<OrthancPluginValueRepresentation>(i);
-    ValueRepresentation orthanc = Plugins::Convert(plugins);
-    ASSERT_EQ(plugins, Plugins::Convert(orthanc));
-  }
-#endif
-}
-
-
-
-static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110);
-static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120);
-
-static void CreateSampleJson(Json::Value& a)
-{
-  {
-    Json::Value b = Json::objectValue;
-    b["PatientName"] = "Hello";
-    b["PatientID"] = "World";
-    b["StudyDescription"] = "Toto";
-    a.append(b);
-  }
-
-  {
-    Json::Value b = Json::objectValue;
-    b["PatientName"] = "data:application/octet-stream;base64,SGVsbG8y";  // echo -n "Hello2" | base64
-    b["PatientID"] = "World2";
-    a.append(b);
-  }
-}
-
-
-namespace Orthanc
-{
-  // Namespace for the "FRIEND_TEST()" directive in "FromDcmtkBridge" to apply:
-  // https://github.com/google/googletest/blob/master/googletest/docs/AdvancedGuide.md#private-class-members
-  TEST(FromDcmtkBridge, FromJson)
-  {
-    std::unique_ptr<DcmElement> element;
-
-    {
-      Json::Value a;
-      a = "Hello";
-      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, ""));
-
-      Json::Value b;
-      std::set<DicomTag> ignoreTagLength;
-      ignoreTagLength.insert(DICOM_TAG_PATIENT_ID);
-
-      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     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, 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, false, ignoreTagLength);
-      ASSERT_TRUE(b["0010,0010"].isObject());
-      ASSERT_EQ("PatientName", b["0010,0010"]["Name"].asString());
-      ASSERT_EQ("TooLong", b["0010,0010"]["Type"].asString());
-      ASSERT_TRUE(b["0010,0010"]["Value"].isNull());
-
-      ignoreTagLength.insert(DICOM_TAG_PATIENT_NAME);
-      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength);
-      ASSERT_EQ("Hello", b["0010,0010"].asString());
-    }
-
-    {
-      Json::Value a;
-      a = "Hello";
-      // Cannot assign a string to a sequence
-      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8, "")), OrthancException);
-    }
-
-    {
-      Json::Value a = Json::arrayValue;
-      a.append("Hello");
-      // Cannot assign an array to a string
-      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, "")), OrthancException);
-    }
-
-    {
-      Json::Value a;
-      a = "data:application/octet-stream;base64,SGVsbG8=";  // echo -n "Hello" | base64
-      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8, ""));
-
-      Json::Value b;
-      std::set<DicomTag> ignoreTagLength;
-      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
-      ASSERT_EQ("Hello", b["0010,0010"].asString());
-    }
-
-    {
-      Json::Value a = Json::arrayValue;
-      CreateSampleJson(a);
-      element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8, ""));
-
-      {
-        Json::Value b;
-        std::set<DicomTag> ignoreTagLength;
-        FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
-        ASSERT_EQ(Json::arrayValue, b["0008,1110"].type());
-        ASSERT_EQ(2u, b["0008,1110"].size());
-      
-        Json::Value::ArrayIndex i = (b["0008,1110"][0]["0010,0010"].asString() == "Hello") ? 0 : 1;
-
-        ASSERT_EQ(3u, b["0008,1110"][i].size());
-        ASSERT_EQ(2u, b["0008,1110"][1 - i].size());
-        ASSERT_EQ(b["0008,1110"][i]["0010,0010"].asString(), "Hello");
-        ASSERT_EQ(b["0008,1110"][i]["0010,0020"].asString(), "World");
-        ASSERT_EQ(b["0008,1110"][i]["0008,1030"].asString(), "Toto");
-        ASSERT_EQ(b["0008,1110"][1 - i]["0010,0010"].asString(), "Hello2");
-        ASSERT_EQ(b["0008,1110"][1 - i]["0010,0020"].asString(), "World2");
-      }
-
-      {
-        Json::Value b;
-        std::set<DicomTag> ignoreTagLength;
-        FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
-                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
-
-        Json::Value c;
-        ServerToolbox::SimplifyTags(c, b, DicomToJsonFormat_Human);
-
-        a[1]["PatientName"] = "Hello2";  // To remove the Data URI Scheme encoding
-        ASSERT_EQ(0, c["ReferencedStudySequence"].compare(a));
-      }
-    }
-  }
-}
-
-
-TEST(ParsedDicomFile, InsertReplaceStrings)
-{
-  ParsedDicomFile f(true);
-
-  f.Insert(DICOM_TAG_PATIENT_NAME, "World", false, "");
-  ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false, ""), OrthancException);  // Already existing tag
-  f.ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, "Toto");  // (*)
-  f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata");  // (**)
-
-  std::string s;
-  ASSERT_TRUE(f.LookupTransferSyntax(s));
-  // The default transfer syntax depends on the OS endianness
-  ASSERT_TRUE(s == GetTransferSyntaxUid(DicomTransferSyntax_LittleEndianExplicit) ||
-              s == GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit));
-
-  ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"),
-                         false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_IgnoreIfAbsent, "");
-  ASSERT_FALSE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_InsertIfAbsent, "");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_EQ(s, "Accession");
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession2"), false, DicomReplaceMode_IgnoreIfAbsent, "");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_EQ(s, "Accession2");
-  f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent, "");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_EQ(s, "Accession3");
-
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
-  ASSERT_EQ(s, "World");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID));
-  ASSERT_EQ(s, "Toto");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));  // Implicitly modified by (*)
-  ASSERT_EQ(s, "Toto");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID));
-  ASSERT_EQ(s, "Tata");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID));  // Implicitly modified by (**)
-  ASSERT_EQ(s, "Tata");
-}
-
-
-
-
-TEST(ParsedDicomFile, InsertReplaceJson)
-{
-  ParsedDicomFile f(true);
-
-  Json::Value a;
-  CreateSampleJson(a);
-
-  ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-  f.Remove(REFERENCED_STUDY_SEQUENCE);  // No effect
-  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
-  ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-  ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""), OrthancException);
-  f.Remove(REFERENCED_STUDY_SEQUENCE);
-  ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
-  ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-
-  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
-  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent, "");
-  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent, "");
-  ASSERT_TRUE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-
-  {
-    Json::Value b;
-    f.DatasetToJson(b, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0);
-
-    Json::Value c;
-    ServerToolbox::SimplifyTags(c, b, DicomToJsonFormat_Human);
-
-    ASSERT_EQ(0, c["ReferencedPatientSequence"].compare(a));
-    ASSERT_NE(0, c["ReferencedStudySequence"].compare(a));  // Because Data URI Scheme decoding was enabled
-  }
-
-  a = "data:application/octet-stream;base64,VGF0YQ==";   // echo -n "Tata" | base64 
-  f.Replace(DICOM_TAG_SOP_INSTANCE_UID, a, false, DicomReplaceMode_InsertIfAbsent, "");  // (*)
-  f.Replace(DICOM_TAG_SOP_CLASS_UID, a, true, DicomReplaceMode_InsertIfAbsent, "");  // (**)
-
-  std::string s;
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID));
-  ASSERT_EQ(s, a.asString());
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));  // Implicitly modified by (*)
-  ASSERT_EQ(s, a.asString());
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID));
-  ASSERT_EQ(s, "Tata");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID));  // Implicitly modified by (**)
-  ASSERT_EQ(s, "Tata");
-}
-
-
-TEST(ParsedDicomFile, JsonEncoding)
-{
-  ParsedDicomFile f(true);
-
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    if (testEncodings[i] != Encoding_Windows1251)
-    {
-      //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
-      f.SetEncoding(testEncodings[i]);
-
-      if (testEncodings[i] != Encoding_Ascii)
-      {
-        bool hasCodeExtensions;
-        ASSERT_EQ(testEncodings[i], f.DetectEncoding(hasCodeExtensions));
-        ASSERT_FALSE(hasCodeExtensions);
-      }
-
-      Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
-      f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, "");
-
-      Json::Value v;
-      f.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
-      ASSERT_EQ(v["PatientName"].asString(), std::string(testEncodingsExpected[i]));
-    }
-  }
-}
-
-
-TEST(ParsedDicomFile, ToJsonFlags1)
-{
-  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag", 1, 1, "OrthancCreator");
-  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag", 1, 1, "");
-
-  ParsedDicomFile f(true);
-  f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false, "");  // Even group => public tag
-  f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false, "");  // Even group => public, unknown tag
-  f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false, "OrthancCreator");  // Odd group => private tag
-
-  Json::Value v;
-  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_FALSE(v.isMember("7052,1000"));
-  ASSERT_FALSE(v.isMember("7053,1000"));
-  ASSERT_TRUE(v.isMember("7050,1000"));
-  ASSERT_EQ(Json::stringValue, v["7050,1000"].type());
-  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(7u, v.getMemberNames().size());
-  ASSERT_FALSE(v.isMember("7052,1000"));
-  ASSERT_TRUE(v.isMember("7050,1000"));
-  ASSERT_TRUE(v.isMember("7053,1000"));
-  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_FALSE(v.isMember("7052,1000"));
-  ASSERT_TRUE(v.isMember("7050,1000"));
-  ASSERT_FALSE(v.isMember("7053,1000"));
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(7u, v.getMemberNames().size());
-  ASSERT_FALSE(v.isMember("7052,1000"));
-  ASSERT_TRUE(v.isMember("7050,1000"));
-  ASSERT_TRUE(v.isMember("7053,1000"));
-  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-  std::string mime, content;
-  ASSERT_EQ(Json::stringValue, v["7053,1000"].type());
-  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7053,1000"].asString()));
-  ASSERT_EQ("application/octet-stream", mime);
-  ASSERT_EQ("Some private tag", content);
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(7u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7050,1000"));
-  ASSERT_TRUE(v.isMember("7052,1000"));
-  ASSERT_FALSE(v.isMember("7053,1000"));
-  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-  ASSERT_EQ(Json::nullValue, v["7052,1000"].type());
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(7u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7050,1000"));
-  ASSERT_TRUE(v.isMember("7052,1000"));
-  ASSERT_FALSE(v.isMember("7053,1000"));
-  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-  ASSERT_EQ(Json::stringValue, v["7052,1000"].type());
-  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7052,1000"].asString()));
-  ASSERT_EQ("application/octet-stream", mime);
-  ASSERT_EQ("Some unknown tag", content);
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(8u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7050,1000"));
-  ASSERT_TRUE(v.isMember("7052,1000"));
-  ASSERT_TRUE(v.isMember("7053,1000"));
-  ASSERT_EQ("Some public tag", v["7050,1000"].asString());
-  ASSERT_EQ(Json::nullValue, v["7052,1000"].type());
-  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
-}
-
-
-TEST(ParsedDicomFile, ToJsonFlags2)
-{
-  ParsedDicomFile f(true);
-
-  {
-    // "ParsedDicomFile" uses Little Endian => 'B' (least significant
-    // byte) will be stored first in the memory buffer and in the
-    // file, then 'A'. Hence the expected "BA" value below.
-    Uint16 v[] = { 'A' * 256 + 'B', 0 };
-    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint16Array(DCM_PixelData, v, 2).good());
-  }
-
-  Json::Value v;
-  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(5u, v.getMemberNames().size());
-  ASSERT_FALSE(v.isMember("7fe0,0010"));  
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7fe0,0010"));  
-  ASSERT_EQ(Json::nullValue, v["7fe0,0010"].type());  
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToAscii), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7fe0,0010"));  
-  ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());  
-  ASSERT_EQ("BA", v["7fe0,0010"].asString().substr(0, 2));
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7fe0,0010"));  
-  ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());
-  std::string mime, content;
-  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString()));
-  ASSERT_EQ("application/octet-stream", mime);
-  ASSERT_EQ("BA", content.substr(0, 2));
-}
-
-
-TEST(DicomFindAnswers, Basic)
-{
-  DicomFindAnswers a(false);
-
-  {
-    DicomMap m;
-    m.SetValue(DICOM_TAG_PATIENT_ID, "hello", false);
-    a.Add(m);
-  }
-
-  {
-    ParsedDicomFile d(true);
-    d.ReplacePlainString(DICOM_TAG_PATIENT_ID, "my");
-    a.Add(d);
-  }
-
-  {
-    DicomMap m;
-    m.SetValue(DICOM_TAG_PATIENT_ID, "world", false);
-    a.Add(m);
-  }
-
-  Json::Value j;
-  a.ToJson(j, true);
-  ASSERT_EQ(3u, j.size());
-
-  //std::cout << j;
-}
-
-
-TEST(ParsedDicomFile, FromJson)
-{
-  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, "");
-
-  Json::Value v;
-  const std::string sopClassUid = "1.2.840.10008.5.1.4.1.1.1";  // CR Image Storage:
-
-  // Test the private creator
-  ASSERT_EQ(DcmTag_ERROR_TagName, FromDcmtkBridge::GetTagName(DicomTag(0x7057, 0x1000), "NOPE"));
-  ASSERT_EQ("MyPrivateTag2", FromDcmtkBridge::GetTagName(DicomTag(0x7057, 0x1000), "ORTHANC"));
-
-  {
-    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["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
-  
-    std::string s;
-    Toolbox::EncodeDataUriScheme(s, "application/octet-stream", "Sebastien");
-    v["StudyDescription"] = s;
-
-    v["PixelData"] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";  // A red dot of 5x5 pixels
-    v["0040,0100"] = Json::arrayValue;  // ScheduledProcedureStepSequence
-
-    Json::Value vv;
-    vv["Modality"] = "MR";
-    v["0040,0100"].append(vv);
-
-    vv["Modality"] = "CT";
-    v["0040,0100"].append(vv);
-  }
-
-  const DicomToJsonFlags toJsonFlags = static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeBinary |
-                                                                     DicomToJsonFlags_IncludePixelData | 
-                                                                     DicomToJsonFlags_IncludePrivateTags | 
-                                                                     DicomToJsonFlags_IncludeUnknownTags | 
-                                                                     DicomToJsonFlags_ConvertBinaryToAscii);
-
-
-  {
-    std::unique_ptr<ParsedDicomFile> dicom
-      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers), ""));
-
-    Json::Value vv;
-    dicom->DatasetToJson(vv, DicomToJsonFormat_Human, toJsonFlags, 0);
-
-    ASSERT_EQ(vv["SOPClassUID"].asString(), sopClassUid);
-    ASSERT_EQ(vv["MediaStorageSOPClassUID"].asString(), sopClassUid);
-    ASSERT_TRUE(vv.isMember("SOPInstanceUID"));
-    ASSERT_TRUE(vv.isMember("SeriesInstanceUID"));
-    ASSERT_TRUE(vv.isMember("StudyInstanceUID"));
-    ASSERT_TRUE(vv.isMember("PatientID"));
-  }
-
-
-  {
-    std::unique_ptr<ParsedDicomFile> dicom
-      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers), ""));
-
-    Json::Value vv;
-    dicom->DatasetToJson(vv, DicomToJsonFormat_Human, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData), 0);
-
-    std::string mime, content;
-    ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, vv["PixelData"].asString()));
-    ASSERT_EQ("application/octet-stream", mime);
-    ASSERT_EQ(5u * 5u * 3u /* the red dot is 5x5 pixels in RGB24 */ + 1 /* for padding */, content.size());
-  }
-
-
-  {
-    std::unique_ptr<ParsedDicomFile> dicom
-      (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_DecodeDataUriScheme), ""));
-
-    Json::Value vv;
-    dicom->DatasetToJson(vv, DicomToJsonFormat_Short, toJsonFlags, 0);
-
-    ASSERT_FALSE(vv.isMember("SOPInstanceUID"));
-    ASSERT_FALSE(vv.isMember("SeriesInstanceUID"));
-    ASSERT_FALSE(vv.isMember("StudyInstanceUID"));
-    ASSERT_FALSE(vv.isMember("PatientID"));
-    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 unknown tag", vv["7052,1000"].asString());
-    ASSERT_EQ("Some private tag", vv["7057,1000"].asString());
-    ASSERT_EQ("Some private tag2", vv["7059,1000"].asString());
-    ASSERT_EQ("Sébastien", vv["0010,0010"].asString());
-    ASSERT_EQ("Sebastien", vv["0008,1030"].asString());
-    ASSERT_EQ("ISO_IR 148", vv["0008,0005"].asString());
-    ASSERT_EQ("5", vv[DICOM_TAG_ROWS.Format()].asString());
-    ASSERT_EQ("5", vv[DICOM_TAG_COLUMNS.Format()].asString());
-    ASSERT_TRUE(vv[DICOM_TAG_PIXEL_DATA.Format()].asString().empty());
-  }
-}
-
-
-
-TEST(TestImages, PatternGrayscale8)
-{
-  static const char* PATH = "UnitTestsResults/PatternGrayscale8.dcm";
-
-  Orthanc::Image image(Orthanc::PixelFormat_Grayscale8, 256, 256, false);
-
-  for (int y = 0; y < 256; y++)
-  {
-    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetRow(y));
-    for (int x = 0; x < 256; x++, p++)
-    {
-      *p = y;
-    }
-  }
-
-  Orthanc::ImageAccessor r;
-
-  image.GetRegion(r, 32, 32, 64, 192);
-  Orthanc::ImageProcessing::Set(r, 0);
-  
-  image.GetRegion(r, 160, 32, 64, 192);
-  Orthanc::ImageProcessing::Set(r, 255); 
-
-  {
-    ParsedDicomFile f(true);
-    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
-    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
-    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
-    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
-    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
-    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "Grayscale8");
-    f.EmbedImage(image);
-
-    f.SaveToFile(PATH);
-  }
-
-  {
-    std::string s;
-    Orthanc::SystemToolbox::ReadFile(s, PATH);
-    Orthanc::ParsedDicomFile f(s);
-    
-    std::unique_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
-    ASSERT_EQ(256u, decoded->GetWidth());
-    ASSERT_EQ(256u, decoded->GetHeight());
-    ASSERT_EQ(Orthanc::PixelFormat_Grayscale8, decoded->GetFormat());
-
-    for (int y = 0; y < 256; y++)
-    {
-      const void* a = image.GetConstRow(y);
-      const void* b = decoded->GetConstRow(y);
-      ASSERT_EQ(0, memcmp(a, b, 256));
-    }
-  }
-}
-
-
-TEST(TestImages, PatternRGB)
-{
-  static const char* PATH = "UnitTestsResults/PatternRGB24.dcm";
-
-  Orthanc::Image image(Orthanc::PixelFormat_RGB24, 384, 256, false);
-
-  for (int y = 0; y < 256; y++)
-  {
-    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetRow(y));
-    for (int x = 0; x < 128; x++, p += 3)
-    {
-      p[0] = y;
-      p[1] = 0;
-      p[2] = 0;
-    }
-    for (int x = 128; x < 128 * 2; x++, p += 3)
-    {
-      p[0] = 0;
-      p[1] = 255 - y;
-      p[2] = 0;
-    }
-    for (int x = 128 * 2; x < 128 * 3; x++, p += 3)
-    {
-      p[0] = 0;
-      p[1] = 0;
-      p[2] = y;
-    }
-  }
-
-  {
-    ParsedDicomFile f(true);
-    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
-    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
-    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
-    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
-    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
-    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "RGB24");
-    f.EmbedImage(image);
-
-    f.SaveToFile(PATH);
-  }
-
-  {
-    std::string s;
-    Orthanc::SystemToolbox::ReadFile(s, PATH);
-    Orthanc::ParsedDicomFile f(s);
-    
-    std::unique_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
-    ASSERT_EQ(384u, decoded->GetWidth());
-    ASSERT_EQ(256u, decoded->GetHeight());
-    ASSERT_EQ(Orthanc::PixelFormat_RGB24, decoded->GetFormat());
-
-    for (int y = 0; y < 256; y++)
-    {
-      const void* a = image.GetConstRow(y);
-      const void* b = decoded->GetConstRow(y);
-      ASSERT_EQ(0, memcmp(a, b, 3 * 384));
-    }
-  }
-}
-
-
-TEST(TestImages, PatternUint16)
-{
-  static const char* PATH = "UnitTestsResults/PatternGrayscale16.dcm";
-
-  Orthanc::Image image(Orthanc::PixelFormat_Grayscale16, 256, 256, false);
-
-  uint16_t v = 0;
-  for (int y = 0; y < 256; y++)
-  {
-    uint16_t *p = reinterpret_cast<uint16_t*>(image.GetRow(y));
-    for (int x = 0; x < 256; x++, v++, p++)
-    {
-      *p = htole16(v);   // Orthanc uses Little-Endian transfer syntax to encode images
-    }
-  }
-
-  Orthanc::ImageAccessor r;
-  
-  image.GetRegion(r, 32, 32, 64, 192);
-  Orthanc::ImageProcessing::Set(r, 0);
-  
-  image.GetRegion(r, 160, 32, 64, 192);
-  Orthanc::ImageProcessing::Set(r, 65535); 
-
-  {
-    ParsedDicomFile f(true);
-    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
-    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
-    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
-    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
-    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
-    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "Grayscale16");
-    f.EmbedImage(image);
-
-    f.SaveToFile(PATH);
-  }
-
-  {
-    std::string s;
-    Orthanc::SystemToolbox::ReadFile(s, PATH);
-    Orthanc::ParsedDicomFile f(s);
-    
-    std::unique_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
-    ASSERT_EQ(256u, decoded->GetWidth());
-    ASSERT_EQ(256u, decoded->GetHeight());
-    ASSERT_EQ(Orthanc::PixelFormat_Grayscale16, decoded->GetFormat());
-
-    for (int y = 0; y < 256; y++)
-    {
-      const void* a = image.GetConstRow(y);
-      const void* b = decoded->GetConstRow(y);
-      ASSERT_EQ(0, memcmp(a, b, 512));
-    }
-  }
-}
-
-
-TEST(TestImages, PatternInt16)
-{
-  static const char* PATH = "UnitTestsResults/PatternSignedGrayscale16.dcm";
-
-  Orthanc::Image image(Orthanc::PixelFormat_SignedGrayscale16, 256, 256, false);
-
-  int16_t v = -32768;
-  for (int y = 0; y < 256; y++)
-  {
-    int16_t *p = reinterpret_cast<int16_t*>(image.GetRow(y));
-    for (int x = 0; x < 256; x++, v++, p++)
-    {
-      *p = htole16(v);   // Orthanc uses Little-Endian transfer syntax to encode images
-    }
-  }
-
-  Orthanc::ImageAccessor r;
-  image.GetRegion(r, 32, 32, 64, 192);
-  Orthanc::ImageProcessing::Set(r, -32768);
-  
-  image.GetRegion(r, 160, 32, 64, 192);
-  Orthanc::ImageProcessing::Set(r, 32767); 
-
-  {
-    ParsedDicomFile f(true);
-    f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.7");
-    f.ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.276.0.7230010.3.1.2.2831176407.321.1458901422.884998");
-    f.ReplacePlainString(DICOM_TAG_PATIENT_ID, "ORTHANC");
-    f.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Orthanc");
-    f.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, "Patterns");
-    f.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, "SignedGrayscale16");
-    f.EmbedImage(image);
-
-    f.SaveToFile(PATH);
-  }
-
-  {
-    std::string s;
-    Orthanc::SystemToolbox::ReadFile(s, PATH);
-    Orthanc::ParsedDicomFile f(s);
-    
-    std::unique_ptr<Orthanc::ImageAccessor> decoded(Orthanc::DicomImageDecoder::Decode(f, 0));
-    ASSERT_EQ(256u, decoded->GetWidth());
-    ASSERT_EQ(256u, decoded->GetHeight());
-    ASSERT_EQ(Orthanc::PixelFormat_SignedGrayscale16, decoded->GetFormat());
-
-    for (int y = 0; y < 256; y++)
-    {
-      const void* a = image.GetConstRow(y);
-      const void* b = decoded->GetConstRow(y);
-      ASSERT_EQ(0, memcmp(a, b, 512));
-    }
-  }
-}
-
-
-
-static void CheckEncoding(const ParsedDicomFile& dicom,
-                          Encoding expected)
-{
-  const char* value = NULL;
-  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_SpecificCharacterSet, value).good());
-
-  Encoding encoding;
-  ASSERT_TRUE(GetDicomEncoding(encoding, value));
-  ASSERT_EQ(expected, encoding);
-}
-
-
-TEST(ParsedDicomFile, DicomMapEncodings1)
-{
-  SetDefaultDicomEncoding(Encoding_Ascii);
-  ASSERT_EQ(Encoding_Ascii, GetDefaultDicomEncoding());
-
-  {
-    DicomMap m;
-    ParsedDicomFile dicom(m, GetDefaultDicomEncoding(), false);
-    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
-    CheckEncoding(dicom, Encoding_Ascii);
-  }
-
-  {
-    DicomMap m;
-    ParsedDicomFile dicom(m, Encoding_Latin4, false);
-    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
-    CheckEncoding(dicom, Encoding_Latin4);
-  }
-
-  {
-    DicomMap m;
-    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 148", false);
-    ParsedDicomFile dicom(m, GetDefaultDicomEncoding(), false);
-    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
-    CheckEncoding(dicom, Encoding_Latin5);
-  }
-
-  {
-    DicomMap m;
-    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 148", false);
-    ParsedDicomFile dicom(m, Encoding_Latin1, false);
-    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
-    CheckEncoding(dicom, Encoding_Latin5);
-  }
-}
-
-
-TEST(ParsedDicomFile, DicomMapEncodings2)
-{
-  const char* utf8 = NULL;
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    if (testEncodings[i] == Encoding_Utf8)
-    {
-      utf8 = testEncodingsEncoded[i];
-      break;
-    }
-  }  
-
-  ASSERT_TRUE(utf8 != NULL);
-
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    // 1251 codepage is not supported by the core DICOM standard, ignore it
-    if (testEncodings[i] != Encoding_Windows1251) 
-    {
-      {
-        // 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], false);
-        ASSERT_STREQ(testEncodingsExpected[i], decoded.c_str());
-
-        if (testEncodings[i] != Encoding_Chinese)
-        {
-          // 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], false);
-          ASSERT_STREQ(testEncodingsExpected[i], encoded.c_str());
-        }
-      }
-
-
-      Json::Value v;
-
-      {
-        DicomMap m;
-        m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
-
-        ParsedDicomFile dicom(m, testEncodings[i], false);
-    
-        const char* encoded = NULL;
-        ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, encoded).good());
-        ASSERT_STREQ(testEncodingsEncoded[i], encoded);
-
-        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
-
-        Encoding encoding;
-        ASSERT_TRUE(GetDicomEncoding(encoding, v["SpecificCharacterSet"].asCString()));
-        ASSERT_EQ(encoding, testEncodings[i]);
-        ASSERT_STREQ(testEncodingsExpected[i], v["PatientName"].asCString());
-      }
-
-
-      {
-        DicomMap m;
-        m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(testEncodings[i]), false);
-        m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
-
-        ParsedDicomFile dicom(m, testEncodings[i], false);
-
-        Json::Value v2;
-        dicom.DatasetToJson(v2, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
-        
-        ASSERT_EQ(v2["PatientName"].asString(), v["PatientName"].asString());
-        ASSERT_EQ(v2["SpecificCharacterSet"].asString(), v["SpecificCharacterSet"].asString());
-      }
-    }
-  }
-}
-
-
-TEST(ParsedDicomFile, ChangeEncoding)
-{
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    // 1251 codepage is not supported by the core DICOM standard, ignore it
-    if (testEncodings[i] != Encoding_Windows1251) 
-    {
-      DicomMap m;
-      m.SetValue(DICOM_TAG_PATIENT_NAME, testEncodingsExpected[i], false);
-
-      std::string tag;
-
-      ParsedDicomFile dicom(m, Encoding_Utf8, false);
-      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]);
-
-      {
-        Json::Value v;
-        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
-        ASSERT_STREQ(v["SpecificCharacterSet"].asCString(), "ISO_IR 192");
-        ASSERT_STREQ(v["PatientName"].asCString(), testEncodingsExpected[i]);
-      }
-
-      dicom.ChangeEncoding(testEncodings[i]);
-
-      ASSERT_EQ(testEncodings[i], dicom.DetectEncoding(hasCodeExtensions));
-      ASSERT_FALSE(hasCodeExtensions);
-      
-      const char* c = NULL;
-      ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->findAndGetString(DCM_PatientName, c).good());
-      EXPECT_STREQ(c, testEncodingsEncoded[i]);
-      
-      ASSERT_TRUE(dicom.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));  // Decodes to UTF-8
-      EXPECT_EQ(tag, testEncodingsExpected[i]);
-
-      {
-        Json::Value v;
-        dicom.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
-        ASSERT_STREQ(v["SpecificCharacterSet"].asCString(), GetDicomSpecificCharacterSet(testEncodings[i]));
-        ASSERT_STREQ(v["PatientName"].asCString(), testEncodingsExpected[i]);
-      }
-    }
-  }
-}
-
-
-TEST(Toolbox, CaseWithAccents)
-{
-  ASSERT_EQ(toUpperResult, Toolbox::ToUpperCaseWithAccents(toUpperSource));
-}
-
-
-
-TEST(ParsedDicomFile, InvalidCharacterSets)
-{
-  {
-    // No encoding provided, fallback to default encoding
-    DicomMap m;
-    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
-
-    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false);
-
-    bool hasCodeExtensions;
-    ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions));
-    ASSERT_FALSE(hasCodeExtensions);
-  }
-  
-  {
-    // Valid encoding, "ISO_IR 13" is Japanese
-    DicomMap m;
-    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 13", false);
-    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
-
-    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false);
-
-    bool hasCodeExtensions;
-    ASSERT_EQ(Encoding_Japanese, d.DetectEncoding(hasCodeExtensions));
-    ASSERT_FALSE(hasCodeExtensions);
-  }
-  
-  {
-    // Invalid value for an encoding ("nope" is not in the DICOM standard)
-    DicomMap m;
-    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "nope", false);
-    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
-
-    ASSERT_THROW(ParsedDicomFile d(m, Encoding_Latin3, false),
-                 OrthancException);
-  }
-  
-  {
-    // Invalid encoding, as provided as a binary string
-    DicomMap m;
-    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 13", true);
-    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
-
-    ASSERT_THROW(ParsedDicomFile d(m, Encoding_Latin3, false),
-                 OrthancException);
-  }
-  
-  {
-    // Encoding provided as an empty string, fallback to default encoding
-    // In Orthanc <= 1.3.1, this test was throwing an exception
-    DicomMap m;
-    m.SetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET, "", false);
-    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false);
-
-    ParsedDicomFile d(m, Encoding_Latin3 /* default encoding */, false);
-
-    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;
-}
-
-
-
-// Compatibility wrapper
-static pugi::xpath_node SelectNode(const pugi::xml_document& doc,
-                                   const char* xpath)
-{
-#if PUGIXML_VERSION <= 140
-  return doc.select_single_node(xpath);  // Deprecated in pugixml 1.5
-#else
-  return doc.select_node(xpath);
-#endif
-}
-
-
-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 = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
-  ASSERT_STREQ("ISO 2022 IR 149", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
-  ASSERT_STREQ("CS", node.node().attribute("vr").value());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
-  ASSERT_STREQ("PN", node.node().attribute("vr").value());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
-  ASSERT_STREQ("Hong", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
-  ASSERT_STREQ("Gildong", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
-  ASSERT_EQ(utf8.substr(13, 3), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
-  ASSERT_EQ(utf8.substr(17, 6), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
-  ASSERT_EQ(utf8.substr(24, 3), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
-  ASSERT_EQ(utf8.substr(28), node.node().text().as_string());
-#endif
-  
-  {
-    DicomMap m;
-    m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(2u, m.GetSize());
-
-    std::string s;
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false));
-    ASSERT_EQ("ISO 2022 IR 149", s);
-
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
-    std::vector<std::string> v;
-    Toolbox::TokenizeString(v, s, '=');
-    ASSERT_EQ(3u, v.size());
-    ASSERT_EQ("Hong^Gildong", v[0]);
-    ASSERT_EQ(utf8, s);
-  }
-}
-
-
-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 = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
-  ASSERT_STREQ("ISO 2022 IR 87", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
-  ASSERT_STREQ("CS", node.node().attribute("vr").value());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
-  ASSERT_STREQ("PN", node.node().attribute("vr").value());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
-  ASSERT_STREQ("Yamada", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
-  ASSERT_STREQ("Tarou", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
-  ASSERT_EQ(utf8.substr(13, 6), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
-  ASSERT_EQ(utf8.substr(20, 6), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
-  ASSERT_EQ(utf8.substr(27, 9), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
-  ASSERT_EQ(utf8.substr(37), node.node().text().as_string());
-#endif  
-  
-  {
-    DicomMap m;
-    m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(2u, m.GetSize());
-
-    std::string s;
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false));
-    ASSERT_EQ("ISO 2022 IR 87", s);
-
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
-    std::vector<std::string> v;
-    Toolbox::TokenizeString(v, s, '=');
-    ASSERT_EQ(3u, v.size());
-    ASSERT_EQ("Yamada^Tarou", v[0]);
-    ASSERT_EQ(utf8, s);
-  }
-}
-
-
-
-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());
-}
-
-
-
-
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-
-#include "../../OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h"
-#include "../../OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h"
-
-TEST(Toto, DISABLED_Transcode3)
-{
-  DicomAssociationParameters p;
-  p.SetRemotePort(2000);
-
-  DicomStoreUserConnection scu(p);
-  scu.SetCommonClassesProposed(false);
-  scu.SetRetiredBigEndianProposed(true);
-
-  DcmtkTranscoder transcoder;
-
-  for (int j = 0; j < 2; j++)
-    for (int i = 0; i <= DicomTransferSyntax_XML; i++)
-    {
-      DicomTransferSyntax a = (DicomTransferSyntax) i;
-
-      std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
-                          std::string(GetTransferSyntaxUid(a)) + ".dcm");
-      if (Orthanc::SystemToolbox::IsRegularFile(path))
-      {
-        printf("\n======= %s\n", GetTransferSyntaxUid(a));
-
-        std::string source;
-        Orthanc::SystemToolbox::ReadFile(source, path);
-
-        std::string c, i;
-        try
-        {
-          scu.Transcode(c, i, transcoder, source.c_str(), source.size(), false, "", 0);
-        }
-        catch (OrthancException& e)
-        {
-          if (e.GetErrorCode() == ErrorCode_NotImplemented)
-          {
-            LOG(ERROR) << "cannot transcode " << GetTransferSyntaxUid(a);
-          }
-          else
-          {
-            throw e;
-          }
-        }
-      }
-    }
-}
-
-
-TEST(Toto, DISABLED_Transcode4)
-{
-  std::string source;
-  Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm");
-
-  std::unique_ptr<DcmFileFormat> toto(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size()));
-  const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(*toto);
-  
-  DicomTransferSyntax sourceSyntax;
-  ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto));
-
-  DcmtkTranscoder transcoder;
-
-  for (int i = 0; i <= DicomTransferSyntax_XML; i++)
-  {
-    DicomTransferSyntax a = (DicomTransferSyntax) i;
-    
-    std::set<DicomTransferSyntax> s;
-    s.insert(a);
-
-    std::string t;
-
-    IDicomTranscoder::DicomImage source, target;
-    source.AcquireParsed(dynamic_cast<DcmFileFormat*>(toto->clone()));
-
-    if (!transcoder.Transcode(target, source, s, true))
-    {
-      printf("**************** CANNOT: [%s] => [%s]\n",
-             GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a));
-    }
-    else
-    {
-      DicomTransferSyntax targetSyntax;
-      ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, target.GetParsed()));
-      
-      ASSERT_EQ(targetSyntax, a);
-      bool lossy = (a == DicomTransferSyntax_JPEGProcess1 ||
-                    a == DicomTransferSyntax_JPEGProcess2_4 ||
-                    a == DicomTransferSyntax_JPEGLSLossy);
-      
-      printf("SIZE: %lu\n", t.size());
-      if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed()))
-      {
-        ASSERT_FALSE(lossy);
-      }
-      else
-      {
-        ASSERT_TRUE(lossy);
-      }
-    }
-  }
-}
-
-#endif
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Thu Jun 11 12:24:38 2020 +0200
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Thu Jun 11 12:52:09 2020 +0200
@@ -41,10 +41,14 @@
 #include "../../OrthancFramework/Sources/OrthancException.h"
 #include "../../OrthancFramework/Sources/Images/Image.h"
 #include "../../OrthancFramework/Sources/Images/PngWriter.h"
+#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h"
 
 #include "../Sources/OrthancConfiguration.h"  // For the FontRegistry
 #include "../Sources/OrthancInitialization.h"
 #include "../Sources/ServerEnumerations.h"
+#include "../Sources/ServerToolbox.h"
+#include "../Plugins/Engine/PluginsEnumerations.h"
 
 
 using namespace Orthanc;
@@ -137,6 +141,180 @@
 }
 
 
+TEST(FromDcmtkBridge, ValueRepresentationConversions)
+{
+#if ORTHANC_ENABLE_PLUGINS == 1
+  ASSERT_EQ(1, ValueRepresentation_ApplicationEntity);
+  ASSERT_EQ(1, OrthancPluginValueRepresentation_AE);
+
+  for (int i = ValueRepresentation_ApplicationEntity;
+       i <= ValueRepresentation_NotSupported; i++)
+  {
+    ValueRepresentation vr = static_cast<ValueRepresentation>(i);
+
+    if (vr == ValueRepresentation_NotSupported)
+    {
+      ASSERT_THROW(ToDcmtkBridge::Convert(vr), OrthancException);
+      ASSERT_THROW(Plugins::Convert(vr), OrthancException);
+    }
+    else if (vr == ValueRepresentation_OtherDouble || 
+             vr == ValueRepresentation_OtherLong ||
+             vr == ValueRepresentation_UniversalResource ||
+             vr == ValueRepresentation_UnlimitedCharacters)
+    {
+      // These VR are not supported as of DCMTK 3.6.0
+      ASSERT_THROW(ToDcmtkBridge::Convert(vr), OrthancException);
+      ASSERT_EQ(OrthancPluginValueRepresentation_UN, Plugins::Convert(vr));
+    }
+    else
+    {
+      ASSERT_EQ(vr, FromDcmtkBridge::Convert(ToDcmtkBridge::Convert(vr)));
+
+      OrthancPluginValueRepresentation plugins = Plugins::Convert(vr);
+      ASSERT_EQ(vr, Plugins::Convert(plugins));
+    }
+  }
+
+  for (int i = OrthancPluginValueRepresentation_AE;
+       i <= OrthancPluginValueRepresentation_UT; i++)
+  {
+    OrthancPluginValueRepresentation plugins = static_cast<OrthancPluginValueRepresentation>(i);
+    ValueRepresentation orthanc = Plugins::Convert(plugins);
+    ASSERT_EQ(plugins, Plugins::Convert(orthanc));
+  }
+#endif
+}
+
+
+
+namespace Orthanc
+{
+  // Namespace for the "FRIEND_TEST()" directive in "FromDcmtkBridge" to apply:
+  // https://github.com/google/googletest/blob/master/googletest/docs/AdvancedGuide.md#private-class-members
+
+  static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110);
+  static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120);
+
+  static void CreateSampleJson(Json::Value& a)
+  {
+    {
+      Json::Value b = Json::objectValue;
+      b["PatientName"] = "Hello";
+      b["PatientID"] = "World";
+      b["StudyDescription"] = "Toto";
+      a.append(b);
+    }
+
+    {
+      Json::Value b = Json::objectValue;
+      b["PatientName"] = "data:application/octet-stream;base64,SGVsbG8y";  // echo -n "Hello2" | base64
+      b["PatientID"] = "World2";
+      a.append(b);
+    }
+  }
+  
+  TEST(FromDcmtkBridge, FromJson)
+  {
+    std::unique_ptr<DcmElement> element;
+
+    {
+      Json::Value a;
+      a = "Hello";
+      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, ""));
+
+      Json::Value b;
+      std::set<DicomTag> ignoreTagLength;
+      ignoreTagLength.insert(DICOM_TAG_PATIENT_ID);
+
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     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, 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, false, ignoreTagLength);
+      ASSERT_TRUE(b["0010,0010"].isObject());
+      ASSERT_EQ("PatientName", b["0010,0010"]["Name"].asString());
+      ASSERT_EQ("TooLong", b["0010,0010"]["Type"].asString());
+      ASSERT_TRUE(b["0010,0010"]["Value"].isNull());
+
+      ignoreTagLength.insert(DICOM_TAG_PATIENT_NAME);
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, ignoreTagLength);
+      ASSERT_EQ("Hello", b["0010,0010"].asString());
+    }
+
+    {
+      Json::Value a;
+      a = "Hello";
+      // Cannot assign a string to a sequence
+      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, false, Encoding_Utf8, "")), OrthancException);
+    }
+
+    {
+      Json::Value a = Json::arrayValue;
+      a.append("Hello");
+      // Cannot assign an array to a string
+      ASSERT_THROW(element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, "")), OrthancException);
+    }
+
+    {
+      Json::Value a;
+      a = "data:application/octet-stream;base64,SGVsbG8=";  // echo -n "Hello" | base64
+      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, true, Encoding_Utf8, ""));
+
+      Json::Value b;
+      std::set<DicomTag> ignoreTagLength;
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
+      ASSERT_EQ("Hello", b["0010,0010"].asString());
+    }
+
+    {
+      Json::Value a = Json::arrayValue;
+      CreateSampleJson(a);
+      element.reset(FromDcmtkBridge::FromJson(REFERENCED_STUDY_SEQUENCE, a, true, Encoding_Utf8, ""));
+
+      {
+        Json::Value b;
+        std::set<DicomTag> ignoreTagLength;
+        FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
+        ASSERT_EQ(Json::arrayValue, b["0008,1110"].type());
+        ASSERT_EQ(2u, b["0008,1110"].size());
+      
+        Json::Value::ArrayIndex i = (b["0008,1110"][0]["0010,0010"].asString() == "Hello") ? 0 : 1;
+
+        ASSERT_EQ(3u, b["0008,1110"][i].size());
+        ASSERT_EQ(2u, b["0008,1110"][1 - i].size());
+        ASSERT_EQ(b["0008,1110"][i]["0010,0010"].asString(), "Hello");
+        ASSERT_EQ(b["0008,1110"][i]["0010,0020"].asString(), "World");
+        ASSERT_EQ(b["0008,1110"][i]["0008,1030"].asString(), "Toto");
+        ASSERT_EQ(b["0008,1110"][1 - i]["0010,0010"].asString(), "Hello2");
+        ASSERT_EQ(b["0008,1110"][1 - i]["0010,0020"].asString(), "World2");
+      }
+
+      {
+        Json::Value b;
+        std::set<DicomTag> ignoreTagLength;
+        FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, ignoreTagLength);
+
+        Json::Value c;
+        Toolbox::SimplifyDicomAsJson(c, b, DicomToJsonFormat_Human);
+
+        a[1]["PatientName"] = "Hello2";  // To remove the Data URI Scheme encoding
+        ASSERT_EQ(0, c["ReferencedStudySequence"].compare(a));
+      }
+    }
+  }
+}
+
+
 int main(int argc, char **argv)
 {
   Logging::Initialize();