changeset 5744:9d6167ddcb35 find-refactoring-clean

merged default -> find-refactoring-clean
author Alain Mazy <am@orthanc.team>
date Thu, 29 Aug 2024 15:41:42 +0200
parents e63538a6d9de (current diff) 8bb3f2fca242 (diff)
children bbeefd4567dc
files NEWS OrthancServer/Sources/Database/FindResponse.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h
diffstat 21 files changed, 249 insertions(+), 127 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Jul 19 14:03:09 2024 +0200
+++ b/NEWS	Thu Aug 29 15:41:42 2024 +0200
@@ -21,6 +21,14 @@
   for a long period.
 * Fix C-Find queries not returning computed tags like ModalitiesInStudy, NumberOfStudyRelatedSeries, ...
   in very specific use-cases.
+* Fix extremely rare error when 2 threads are trying to create the same folder in the File Storage 
+  at the same time.
+* Upgraded dependencies for static builds:
+  - curl 8.9.0
+* Added a new fallback when trying to decode a frame: transcode the file using the plugin
+  before decoding the frame.  This solves some issues with JP2K Lossy compression:
+  https://discourse.orthanc-server.org/t/decoding-displaying-jpeg2000-lossy-images/5117
+* Added a new warning that can be disabled in the configuration: W003_DecoderFailure
 
 
 
--- a/OrthancFramework/Resources/CMake/Compiler.cmake	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancFramework/Resources/CMake/Compiler.cmake	Thu Aug 29 15:41:42 2024 +0200
@@ -232,6 +232,10 @@
   endif()
 
 elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+
+  # fix this error that appears with recent compilers on MacOS: boost/mpl/aux_/integral_wrapper.hpp:73:31: error: integer value -1 is outside the valid range of values [0, 3] for this enumeration type [-Wenum-constexpr-conversion]
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-enum-constexpr-conversion")
+
   add_definitions(
     -D_XOPEN_SOURCE=1
     )
--- a/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Thu Aug 29 15:41:42 2024 +0200
@@ -21,9 +21,9 @@
 
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_CURL)
-  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-8.5.0)
-  SET(CURL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/curl-8.5.0.tar.gz")
-  SET(CURL_MD5 "0bc69288b20ae165ff4b7d6d7bbe70d2")
+  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-8.9.0)
+  SET(CURL_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/curl-8.9.0.tar.gz")
+  SET(CURL_MD5 "f9bca5d4d5bac1f04e6c5eb4d0418618")
 
   if (IS_DIRECTORY "${CURL_SOURCES_DIR}")
     set(FirstRun OFF)
@@ -36,7 +36,7 @@
   if (FirstRun)
     execute_process(
       COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-      ${CMAKE_CURRENT_LIST_DIR}/../Patches/curl-8.5.0.patch
+      ${CMAKE_CURRENT_LIST_DIR}/../Patches/curl-8.9.0.patch
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
       )
--- a/OrthancFramework/Resources/Patches/curl-8.5.0.patch	Fri Jul 19 14:03:09 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-diff -urEb curl-8.5.0.orig/CMake/Macros.cmake curl-8.5.0/CMake/Macros.cmake
---- curl-8.5.0.orig/CMake/Macros.cmake	2024-01-24 17:21:21.387965189 +0100
-+++ curl-8.5.0/CMake/Macros.cmake	2024-01-24 17:21:48.523719072 +0100
-@@ -48,7 +48,7 @@
-     message(STATUS "Performing Curl Test ${CURL_TEST}")
-     try_compile(${CURL_TEST}
-       ${CMAKE_BINARY_DIR}
--      ${CMAKE_CURRENT_SOURCE_DIR}/CMake/CurlTests.c
-+      ${CURL_SOURCES_DIR}/CMake/CurlTests.c
-       CMAKE_FLAGS -DCOMPILE_DEFINITIONS:STRING=${MACRO_CHECK_FUNCTION_DEFINITIONS}
-       "${CURL_TEST_ADD_LIBRARIES}"
-       OUTPUT_VARIABLE OUTPUT)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/curl-8.9.0.patch	Thu Aug 29 15:41:42 2024 +0200
@@ -0,0 +1,12 @@
+diff -urEb curl-8.9.0.orig/CMake/Macros.cmake curl-8.9.0/CMake/Macros.cmake
+--- curl-8.9.0.orig/CMake/Macros.cmake	2024-07-26 18:47:52.920588300 +0200
++++ curl-8.9.0/CMake/Macros.cmake	2024-07-26 18:48:08.345522100 +0200
+@@ -48,7 +48,7 @@
+     message(STATUS "Performing Test ${CURL_TEST}")
+     try_compile(${CURL_TEST}
+       ${CMAKE_BINARY_DIR}
+-      ${CMAKE_CURRENT_SOURCE_DIR}/CMake/CurlTests.c
++      ${CURL_SOURCES_DIR}/CMake/CurlTests.c
+       CMAKE_FLAGS -DCOMPILE_DEFINITIONS:STRING=${MACRO_CHECK_FUNCTION_DEFINITIONS}
+       "${CURL_TEST_ADD_LIBRARIES}"
+       OUTPUT_VARIABLE OUTPUT)
--- a/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch	Thu Aug 29 15:41:42 2024 +0200
@@ -2,7 +2,7 @@
 --- protobuf-3.5.1.orig/src/google/protobuf/stubs/io_win32.cc	2023-03-26 20:13:45.095021011 +0200
 +++ protobuf-3.5.1/src/google/protobuf/stubs/io_win32.cc	2023-03-26 20:19:19.932920102 +0200
 @@ -91,7 +91,12 @@
- 
+
  template <typename char_type>
  bool null_or_empty(const char_type* s) {
 -  return s == nullptr || *s == 0;
@@ -13,5 +13,18 @@
 +   **/
 +  return s == NULL || *s == 0;
  }
- 
+
  // Returns true if the path starts with a drive letter, e.g. "c:".
+diff -urEb protobuf-3.5.1.orig/src/google/protobuf/stubs/hash.h protobuf-3.5.1/src/google/protobuf/stubs/hash.h
+--- protobuf-3.5.1.orig/src/google/protobuf/stubs/hash.h	2023-03-26 20:13:45.095021011 +0200
++++ protobuf-3.5.1/src/google/protobuf/stubs/hash.h	2023-03-26 20:19:19.932920102 +0200
+@@ -1,3 +1,9 @@
++#if _MSC_VER >= 1930       // Since Visual Studio 2022
++#define _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS
++#include <unordered_map>
++#include <hash_map>
++#endif
++
+ // Protocol Buffers - Google's data interchange format
+ // Copyright 2008 Google Inc.  All rights reserved.
+ // https://developers.google.com/protocol-buffers/
\ No newline at end of file
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -949,12 +949,12 @@
     {
       throw OrthancException(ErrorCode_NotImplemented,
                              "The built-in DCMTK decoder cannot decode some DICOM instance "
-                             "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s)));
+                             "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s)), false /* don't log here*/);
     }
     else
     {
       throw OrthancException(ErrorCode_NotImplemented,
-                             "The built-in DCMTK decoder cannot decode some DICOM instance");
+                             "The built-in DCMTK decoder cannot decode some DICOM instance", false /* don't log here*/);
     }
   }
 
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -24,6 +24,7 @@
 
 #include "../PrecompiledHeaders.h"
 #include "FilesystemStorage.h"
+#include <boost/thread.hpp>
 
 // http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system
 // http://stackoverflow.com/questions/446358/storing-a-large-number-of-images
@@ -135,26 +136,54 @@
     {
       // Extremely unlikely case: This Uuid has already been created
       // in the past.
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError, "This file UUID already exists");
     }
 
-    if (boost::filesystem::exists(path.parent_path()))
+    // In very unlikely cases, a thread could be deleting a
+    // directory while another thread needs it -> introduce 3 retries at 1 ms interval
+    int retryCount = 0;
+    const int maxRetryCount = 3;
+    
+    while (retryCount < maxRetryCount)
     {
-      if (!boost::filesystem::is_directory(path.parent_path()))
+      retryCount++;
+      if (retryCount > 1)
+      {
+        boost::this_thread::sleep(boost::posix_time::milliseconds(2 * retryCount + (rand() % 10)));
+        LOG(INFO) << "Retrying to create attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+                  << "\" type";
+      }
+
+      try 
+      {
+        boost::filesystem::create_directories(path.parent_path());  // the function ensures that the directory exists or throws
+      }
+      catch (boost::filesystem::filesystem_error& er)
       {
-        throw OrthancException(ErrorCode_DirectoryOverFile);
+        if (er.code() == boost::system::errc::file_exists  // the last element of the parent_path is a file
+          || er.code() == boost::system::errc::not_a_directory) // one of the element of the parent_path is not a directory 
+        {
+          throw OrthancException(ErrorCode_DirectoryOverFile, "One of the element of the path is a file");  // no need to retry this error
+        }
+
+        // ignore other errors and retry
+      }
+
+      try 
+      {
+        SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_);
+        
+        LOG(INFO) << "Created attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, size) << ")";
+        return;
+      }
+      catch (OrthancException& ex)
+      {
+        if (retryCount >= maxRetryCount)
+        {
+          throw ex;
+        }
       }
     }
-    else
-    {
-      if (!boost::filesystem::create_directories(path.parent_path()))
-      {
-        throw OrthancException(ErrorCode_FileStorageCannotWrite);
-      }
-    }
-
-    SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_);
-    LOG(INFO) << "Created attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, size) << ")";
   }
 
 
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -37,6 +37,7 @@
 #include "../Sources/Logging.h"
 #include "../Sources/OrthancException.h"
 #include "../Sources/Toolbox.h"
+#include "../Sources/SystemToolbox.h"
 
 #include <ctype.h>
 
@@ -88,6 +89,48 @@
   ASSERT_EQ(s.GetSize(uid), data.size());
 }
 
+TEST(FilesystemStorage, FileWithSameNameAsTopDirectory)
+{
+  FilesystemStorage s("UnitTestsStorageTop");
+  s.Clear();
+
+  std::vector<uint8_t> data;
+  StringToVector(data, Toolbox::GenerateUuid());
+
+  SystemToolbox::WriteFile("toto", "UnitTestsStorageTop/12");
+  ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException);
+  s.Clear();
+}
+
+TEST(FilesystemStorage, FileWithSameNameAsChildDirectory)
+{
+  FilesystemStorage s("UnitTestsStorageChild");
+  s.Clear();
+
+  std::vector<uint8_t> data;
+  StringToVector(data, Toolbox::GenerateUuid());
+
+  SystemToolbox::MakeDirectory("UnitTestsStorageChild/12");
+  SystemToolbox::WriteFile("toto", "UnitTestsStorageChild/12/34");
+  ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException);
+  s.Clear();
+}
+
+TEST(FilesystemStorage, FileAlreadyExists)
+{
+  FilesystemStorage s("UnitTestsStorageFileAlreadyExists");
+  s.Clear();
+
+  std::vector<uint8_t> data;
+  StringToVector(data, Toolbox::GenerateUuid());
+
+  SystemToolbox::MakeDirectory("UnitTestsStorageFileAlreadyExists/12/34");
+  SystemToolbox::WriteFile("toto", "UnitTestsStorageFileAlreadyExists/12/34/12345678-1234-1234-1234-1234567890ab");
+  ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException);
+  s.Clear();
+}
+
+
 TEST(FilesystemStorage, EndToEnd)
 {
   FilesystemStorage s("UnitTestsStorage");
--- a/OrthancServer/OrthancExplorer/explorer.js	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.js	Thu Aug 29 15:41:42 2024 +0200
@@ -715,7 +715,7 @@
     $('.' + liClass).remove();
     for (var key in attachments) {
       if (attachments[key] >= 1024) {
-        target.append('<li data-icon="gear" class="' + liClass + '"><a href="#" id="' + attachments[key] + '">Download ' + key + '</a></li>')
+        target.append('<li data-icon="gear" class="' + liClass + '"><a href="#" id="' + attachments[key] + '">Download attachment "' + key + '"</a></li>')
       }
     }
     target.listview('refresh');
--- a/OrthancServer/Resources/Configuration.json	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Resources/Configuration.json	Thu Aug 29 15:41:42 2024 +0200
@@ -205,6 +205,7 @@
   // Set the timeout (in seconds) after which the DICOM associations
   // are closed by the Orthanc SCP (server) if no further DIMSE
   // command is received from the SCU (client).
+  // A value of 0 means "no timeout".
   "DicomScpTimeout" : 30,
 
 
@@ -491,6 +492,7 @@
   // The timeout (in seconds) after which the DICOM associations are
   // considered as closed by the Orthanc SCU (client) if the remote
   // DICOM SCP (server) does not answer.
+  // A value of 0 means "no timeout".
   "DicomScuTimeout" : 10,
 
   // During a C-STORE SCU request initiated by Orthanc, if the remote
@@ -984,7 +986,11 @@
     // your response might be incomplete/inconsistent.
     // You should call patients|studies|series|instances/../reconstruct to rebuild
     // the DB.  You may also check for the "Housekeeper" plugin
-    "W002_InconsistentDicomTagsInDb": true
+    "W002_InconsistentDicomTagsInDb": true,
+
+    // Display a warning message when Orthanc and its plugins are unable
+    // to decode a frame (new in Orthanc 1.12.5).
+    "W003_DecoderFailure": true
   }
 
 }
--- a/OrthancServer/Sources/Database/FindResponse.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -180,7 +180,7 @@
     {
       std::set<std::string> s;
       s.insert(value);
-      metadataValues_[metadata] = new std::set(s);
+      metadataValues_[metadata] = new std::set<std::string>(s);
     }
     else
     {
@@ -216,7 +216,7 @@
     {
       std::set<std::string> s;
       s.insert(value);
-      mainDicomTagValues_[tag] = new std::set(s);
+      mainDicomTagValues_[tag] = new std::set<std::string>(s);
     }
     else
     {
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -1157,6 +1157,10 @@
         {
           warning = Warnings_002_InconsistentDicomTagsInDb;
         }
+        else if (name == "W003_DecoderFailure")
+        {
+          warning = Warnings_003_DecoderFailure;
+        }
         else
         {
           throw OrthancException(ErrorCode_BadFileFormat, name + " is not recognized as a valid warning name");
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -472,7 +472,7 @@
                        "If `true`, run the job in asynchronous mode, which means that the REST API call will immediately "
                        "return, reporting the identifier of a job. Prefer this flavor wherever possible.", false)
       .SetRequestField(KEY_PRIORITY, RestApiCallDocumentation::Type_Number,
-                       "In asynchronous mode, the priority of the job. The lower the value, the higher the priority.", false)
+                       "In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false)
       .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "In asynchronous mode, identifier of the job")
       .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "In asynchronous mode, path to access the job in the REST API");
   }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -509,7 +509,7 @@
                        "If present, the DICOM files in the archive will be transcoded to the provided "
                        "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
       .SetRequestField("Priority", RestApiCallDocumentation::Type_Number,
-                       "In asynchronous mode, the priority of the job. The lower the value, the higher the priority.", false)
+                       "In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false)
       .AddAnswerType(MimeType_Zip, "In synchronous mode, the ZIP file containing the archive")
       .AddAnswerType(MimeType_Json, "In asynchronous mode, information about the job that has been submitted to "
                      "generate the archive: https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -65,7 +65,7 @@
       call.GetDocumentation()
         .SetTag("Tracking changes")
         .SetSummary("List changes")
-        .SetDescription("Whenever Orthanc receives a new DICOM instance, this event is recorded in the so-called _Changes Log_. This enables remote scripts to react to the arrival of new DICOM resources. A typical application is auto-routing, where an external script waits for a new DICOM instance to arrive into Orthanc, then forward this instance to another modality.")
+        .SetDescription("Whenever Orthanc receives a new DICOM instance, this event is recorded in the so-called _Changes Log_. This enables remote scripts to react to the arrival of new DICOM resources. A typical application is auto-routing, where an external script waits for a new DICOM instance to arrive into Orthanc, then forward this instance to another modality. Please note that, when resources are deleted, their corresponding change entries are also removed from the Changes Log, which helps ensuring that this log does not grow indefinitely.")
         .SetHttpGetArgument("limit", RestApiCallDocumentation::Type_Number, "Limit the number of results", false)
         .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index", false)
         .AddAnswerType(MimeType_Json, "The list of changes")
--- a/OrthancServer/Sources/ServerContext.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -1341,18 +1341,17 @@
       // Throttle to avoid loading several large DICOM files simultaneously
       largeDicomLocker_.reset(new Semaphore::Locker(context.largeDicomThrottler_));
       
-      std::string content;
-      context_.ReadDicom(content, instancePublicId);
+      context_.ReadDicom(buffer_, instancePublicId_);
 
       // Release the throttle if loading "small" DICOM files (under
       // 50MB, which is an arbitrary value)
-      if (content.size() < 50 * 1024 * 1024)
+      if (buffer_.size() < 50 * 1024 * 1024)
       {
         largeDicomLocker_.reset(NULL);
       }
       
-      dicom_.reset(new ParsedDicomFile(content));
-      dicomSize_ = content.size();
+      dicom_.reset(new ParsedDicomFile(buffer_));
+      dicomSize_ = buffer_.size();
     }
 
     assert(accessor_.get() != NULL ||
@@ -1388,6 +1387,18 @@
     }
   }
 
+  const std::string& ServerContext::DicomCacheLocker::GetBuffer()
+  {
+    if (buffer_.size() > 0)
+    {
+      return buffer_;
+    }
+    else
+    {
+      context_.ReadDicom(buffer_, instancePublicId_);
+      return buffer_;
+    }
+  }
   
   void ServerContext::SetStoreMD5ForAttachments(bool storeMD5)
   {
@@ -1657,27 +1668,50 @@
   }
 
 
+
+
+
   ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId,
                                                  unsigned int frameIndex)
   {
+    ServerContext::DicomCacheLocker locker(*this, publicId);
+    std::unique_ptr<ImageAccessor> decoded(DecodeDicomFrame(locker.GetDicom(), locker.GetBuffer().c_str(), locker.GetBuffer().size(), frameIndex));
+
+    if (decoded.get() == NULL)
+    {
+      OrthancConfiguration::ReaderLock configLock;
+      if (configLock.GetConfiguration().IsWarningEnabled(Warnings_003_DecoderFailure))
+      {
+        LOG(WARNING) << "W003: Unable to decode frame " << frameIndex << " from instance " << publicId;
+      }
+      return NULL;
+    }
+
+    return decoded.release();
+  }
+
+
+  ImageAccessor* ServerContext::DecodeDicomFrame(const ParsedDicomFile& parsedDicom,
+                                                 const void* buffer,  // actually the buffer that is the source of the ParsedDicomFile
+                                                 size_t size,
+                                                 unsigned int frameIndex)
+  {
+    std::unique_ptr<ImageAccessor> decoded;
+
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
     {
-      // Use Orthanc's built-in decoder, using the cache to speed-up
-      // things on multi-frame images
+      // Use Orthanc's built-in decoder
 
-      std::unique_ptr<ImageAccessor> decoded;
       try
       {
-        ServerContext::DicomCacheLocker locker(*this, publicId);
-        decoded.reset(locker.GetDicom().DecodeFrame(frameIndex));
+        decoded.reset(parsedDicom.DecodeFrame(frameIndex));
+        if (decoded.get() != NULL)
+        {
+          return decoded.release();
+        }
       }
       catch (OrthancException& e)
-      {
-      }
-      
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
+      { // ignore, we'll try other alternatives
       }
     }
 
@@ -1685,14 +1719,9 @@
     if (HasPlugins() &&
         GetPlugins().HasCustomImageDecoder())
     {
-      // TODO: Store the raw buffer in the DicomCacheLocker
-      std::string dicomContent;
-      ReadDicom(dicomContent, publicId);
-      
-      std::unique_ptr<ImageAccessor> decoded;
       try
       {
-        decoded.reset(GetPlugins().Decode(dicomContent.c_str(), dicomContent.size(), frameIndex));
+        decoded.reset(GetPlugins().Decode(buffer, size, frameIndex));
       }
       catch (OrthancException& e)
       {
@@ -1712,69 +1741,50 @@
 
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
     {
-      ServerContext::DicomCacheLocker locker(*this, publicId);        
-      return locker.GetDicom().DecodeFrame(frameIndex);
+      try
+      { 
+        decoded.reset(parsedDicom.DecodeFrame(frameIndex));
+        if (decoded.get() != NULL)
+        {
+          return decoded.release();
+        }
+      }
+      catch (OrthancException& e)
+      {
+        LOG(INFO) << e.GetDetails();
+      }
     }
-    else
+
+    if (HasPlugins() && GetPlugins().HasCustomTranscoder())
     {
-      return NULL;  // Built-in decoder is disabled
+      LOG(INFO) << "The plugins and built-in image decoders failed to decode a frame, "
+                << "trying to transcode the file to LittleEndianExplicit using the plugins.";
+      DicomImage explicitTemporaryImage;
+      DicomImage source;
+      std::set<DicomTransferSyntax> allowedSyntaxes;
+
+      source.SetExternalBuffer(buffer, size);
+      allowedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
+
+      if (Transcode(explicitTemporaryImage, source, allowedSyntaxes, true))
+      {
+        std::unique_ptr<ParsedDicomFile> file(explicitTemporaryImage.ReleaseAsParsedDicomFile());
+        return file->DecodeFrame(frameIndex);
+      }
     }
+
+    return NULL;
   }
 
 
   ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom,
                                                  unsigned int frameIndex)
   {
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
-    {
-      std::unique_ptr<ImageAccessor> decoded;
-      try
-      {
-        decoded.reset(dicom.DecodeFrame(frameIndex));
-      }
-      catch (OrthancException& e)
-      {
-      }
-        
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
-      }
-    }
+    return DecodeDicomFrame(dicom.GetParsedDicomFile(),
+                            dicom.GetBufferData(),
+                            dicom.GetBufferSize(),
+                            frameIndex);
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins() &&
-        GetPlugins().HasCustomImageDecoder())
-    {
-      std::unique_ptr<ImageAccessor> decoded;
-      try
-      {
-        decoded.reset(GetPlugins().Decode(dicom.GetBufferData(), dicom.GetBufferSize(), frameIndex));
-      }
-      catch (OrthancException& e)
-      {
-      }
-    
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
-      }
-      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-      {
-        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
-                  << "fallback to the built-in DCMTK decoder";
-      }
-    }
-#endif
-
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-    {
-      return dicom.DecodeFrame(frameIndex);
-    }
-    else
-    {
-      return NULL;
-    }
   }
 
 
@@ -1782,8 +1792,8 @@
                                                  size_t size,
                                                  unsigned int frameIndex)
   {
-    std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(dicom, size));
-    return DecodeDicomFrame(*instance, frameIndex);
+    std::unique_ptr<ParsedDicomFile> instance(new ParsedDicomFile(dicom, size));
+    return DecodeDicomFrame(*instance, dicom, size, frameIndex);
   }
   
 
--- a/OrthancServer/Sources/ServerContext.h	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Thu Aug 29 15:41:42 2024 +0200
@@ -286,6 +286,7 @@
       std::unique_ptr<ParsedDicomFile>             dicom_;
       size_t                                       dicomSize_;
       std::unique_ptr<Semaphore::Locker>           largeDicomLocker_;
+      std::string                                  buffer_;
 
     public:
       DicomCacheLocker(ServerContext& context,
@@ -294,6 +295,8 @@
       ~DicomCacheLocker();
 
       ParsedDicomFile& GetDicom() const;
+
+      const std::string& GetBuffer();
     };
 
     ServerContext(IDatabaseWrapper& database,
@@ -524,7 +527,12 @@
     ImageAccessor* DecodeDicomFrame(const void* dicom,
                                     size_t size,
                                     unsigned int frameIndex);
-    
+
+    ImageAccessor* DecodeDicomFrame(const ParsedDicomFile& parsedDicom,
+                                    const void* buffer,  // actually the buffer that is the source of the ParsedDicomFile
+                                    size_t size,
+                                    unsigned int frameIndex);
+
     void StoreWithTranscoding(std::string& sopClassUid,
                               std::string& sopInstanceUid,
                               DicomStoreUserConnection& connection,
--- a/OrthancServer/Sources/ServerEnumerations.h	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Thu Aug 29 15:41:42 2024 +0200
@@ -206,6 +206,7 @@
     Warnings_None,
     Warnings_001_TagsBeingReadFromStorage,
     Warnings_002_InconsistentDicomTagsInDb,
+    Warnings_003_DecoderFailure,              // new in Orthanc 1.12.5
   };
 
 
--- a/OrthancServer/UnitTestsSources/VersionsTests.cpp	Fri Jul 19 14:03:09 2024 +0200
+++ b/OrthancServer/UnitTestsSources/VersionsTests.cpp	Thu Aug 29 15:41:42 2024 +0200
@@ -120,7 +120,7 @@
 TEST(Versions, CurlStatic)
 {
   curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
-  ASSERT_STREQ("8.5.0", v->version);
+  ASSERT_STREQ("8.9.0", v->version);
 }
 
 TEST(Versions, PngStatic)
--- a/TODO	Fri Jul 19 14:03:09 2024 +0200
+++ b/TODO	Thu Aug 29 15:41:42 2024 +0200
@@ -29,11 +29,6 @@
 * (1) Accept extra DICOM tags dictionaries in the DCMTK format '.dic' (easier to use than declare
   them in the Orthanc configuration file).  Even the standard dictionaries could be 
   overriden by these custom dictionaries.
-* Provide more flexibility wrt Dicom TLS ciphers and TLS version:
-  https://groups.google.com/g/orthanc-users/c/X4IhmXCSr7I/m/EirawAFcBwAJ
-  Can maybe be achieved by adding a configuration to select the TLS Security Profile:
-  https://github.com/DCMTK/dcmtk/blob/master/dcmtls/include/dcmtk/dcmtls/tlsciphr.h#L83 
-  (e.g: TSP_Profile_BCP195_ND instead of TSP_Profile_BCP195)
 * Add configurations to enable/disable warnings:
   - Modifying an instance while keeping its original SOPInstanceUID: This should be avoided!
   - Modifying a study while keeping its original StudyInstanceUID: This should be avoided!
@@ -41,6 +36,7 @@
   https://discourse.orthanc-server.org/t/performance-issue-when-adding-a-lot-of-jobs-in-the-queue/3915/2
   Note: that might also be the right time to have a central jobs registry when working
   with multiple Orthanc instances on the same DB.
+  Note: the json serialization of a job "content" can be very large -> compress it before saving it to DB ?
 * Right now, some Stable events never occurs (e.g. when Orthanc is restarted before the event is triggered).
   Since these events are used to e.g. generate dicom-web cache (or update it !), we should try
   to make sure these events always happen.
@@ -71,7 +67,7 @@
 * Write a getting started guide (step by step) for each platform to replace
   https://orthanc.uclouvain.be/book/users/cookbook.html :
   - Ubuntu/Debian
-  - Windows
+  - Windows (done)
   - OSX
   - Docker on Linux
   Each step by step guide should contain:
@@ -153,7 +149,7 @@
   https://groups.google.com/g/orthanc-users/c/hsZ1jng5rIg/m/8xZL2C1VBgAJ
 * add an "AutoDeleteIfSuccessful": false option when creating jobs 
   https://discourse.orthanc-server.org/t/job-history-combined-with-auto-forwarding/3729/10
-* Allow skiping automatic conversion of color-space in transcoding/decoding.
+* Allow skipping automatic conversion of color-space in transcoding/decoding.
   The patch that was initialy provided was breaking the IngestTranscoding.
   This might require a DCMTK decoding plugin ?
   https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533/9