changeset 6016:295f51db7976

merge
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 24 Feb 2025 08:03:02 +0100
parents 78b1934b4ce3 (current diff) 97cfdcdf47e3 (diff)
children 90ecf7d849cd
files NEWS
diffstat 18 files changed, 281 insertions(+), 49 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Feb 24 08:01:43 2025 +0100
+++ b/NEWS	Mon Feb 24 08:03:02 2025 +0100
@@ -8,6 +8,9 @@
 * GET /studies/../archive and sibbling routes now all accept a 'filename' GET argument.
 * POST /studies/../archive and sibbling routes now all accept a 'Filename' query argument.
 * GET /instances/../file and sibbling ../attachments/../data routes now all accept a 'filename' GET argument.
+* All routes accepting a "transcode" url argument or a "Transcode" field in the payload now also
+  accepts a "lossy-quality" url argument or a "LossyQuality" field to define the compression quality factor.
+  If not specified, the "DicomLossyTranscodingQuality" configuration is taken into account.
 
 
 Maintenance
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -51,7 +51,7 @@
 namespace Orthanc
 {
   DcmtkTranscoder::DcmtkTranscoder(unsigned int maxConcurrentExecutions) :
-    lossyQuality_(90),
+  defaultLossyQuality_(90),
     maxConcurrentExecutionsSemaphore_(maxConcurrentExecutions)
   {
   }
@@ -64,26 +64,26 @@
   }
 
   
-  void DcmtkTranscoder::SetLossyQuality(unsigned int quality)
+  void DcmtkTranscoder::SetDefaultLossyQuality(unsigned int quality)
   {
     if (quality == 0 ||
         quality > 100)
     {
       throw OrthancException(
         ErrorCode_ParameterOutOfRange,
-        "The quality for lossy transcoding must be an integer between 1 and 100, received: " +
+        "The default quality for lossy transcoding must be an integer between 1 and 100, received: " +
         boost::lexical_cast<std::string>(quality));
     }
     else
     {
-      LOG(INFO) << "Quality for lossy transcoding using DCMTK is set to: " << quality;
-      lossyQuality_ = quality;
+      LOG(INFO) << "Default quality for lossy transcoding using DCMTK is set to: " << quality;
+      defaultLossyQuality_ = quality;
     }
   }
 
-  unsigned int DcmtkTranscoder::GetLossyQuality() const
+  unsigned int DcmtkTranscoder::GetDefaultLossyQuality() const
   {
-    return lossyQuality_;
+    return defaultLossyQuality_;
   }
 
   bool TryTranscode(std::vector<std::string>& failureReasons, /* out */
@@ -109,7 +109,8 @@
                                          std::string& failureReason /* out */,
                                          DcmFileFormat& dicom, /* in/out */
                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                         bool allowNewSopInstanceUid) 
+                                         bool allowNewSopInstanceUid,
+                                         unsigned int lossyQuality) 
   {
     std::vector<std::string> failureReasons;
 
@@ -169,7 +170,7 @@
       else
       {
         // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-        DJ_RPLossy parameters(lossyQuality_);
+        DJ_RPLossy parameters(lossyQuality);
           
         if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
         {
@@ -195,7 +196,7 @@
       else
       {
         // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-        DJ_RPLossy parameters(lossyQuality_);
+        DJ_RPLossy parameters(lossyQuality);
         if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
         {
           selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
@@ -312,11 +313,20 @@
     return false;
   }
 
+  bool DcmtkTranscoder::Transcode(DicomImage& target,
+                                  DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                  const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                  bool allowNewSopInstanceUid)
+  {
+    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, defaultLossyQuality_);
+  }
+
 
   bool DcmtkTranscoder::Transcode(DicomImage& target,
                                   DicomImage& source /* in, "GetParsed()" possibly modified */,
                                   const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                  bool allowNewSopInstanceUid)
+                                  bool allowNewSopInstanceUid,
+                                  unsigned int lossyQuality)
   {
     Semaphore::Locker lock(maxConcurrentExecutionsSemaphore_); // limit the number of concurrent executions
 
@@ -363,7 +373,7 @@
       return true;
     }
     else if (InplaceTranscode(targetSyntax, failureReason, source.GetParsed(),
-                              allowedSyntaxes, allowNewSopInstanceUid))
+                              allowedSyntaxes, allowNewSopInstanceUid, lossyQuality))
     {   
       // Sanity check
       DicomTransferSyntax targetSyntax2;
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Mon Feb 24 08:03:02 2025 +0100
@@ -41,21 +41,22 @@
   class ORTHANC_PUBLIC DcmtkTranscoder : public IDicomTranscoder
   {
   private:
-    unsigned int  lossyQuality_;
+    unsigned int  defaultLossyQuality_;
     Semaphore maxConcurrentExecutionsSemaphore_;
 
     bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
                           std::string& failureReason /* out */,
                           DcmFileFormat& dicom,
                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                          bool allowNewSopInstanceUid);
+                          bool allowNewSopInstanceUid,
+                          unsigned int lossyQuality);
     
   public:
     explicit DcmtkTranscoder(unsigned int maxConcurrentExecutions);
 
-    void SetLossyQuality(unsigned int quality);
+    void SetDefaultLossyQuality(unsigned int quality);
 
-    unsigned int GetLossyQuality() const;
+    unsigned int GetDefaultLossyQuality() const;
     
     static bool IsSupported(DicomTransferSyntax syntax);
 
@@ -63,5 +64,11 @@
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQuality) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Mon Feb 24 08:03:02 2025 +0100
@@ -116,6 +116,12 @@
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) = 0;
 
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQuality) = 0;
+
     static std::string GetSopInstanceUid(DcmFileFormat& dicom);
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -56,6 +56,14 @@
 #endif
   }
     
+  bool MemoryBufferTranscoder::Transcode(DicomImage& target,
+                                         DicomImage& source,
+                                         const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                         bool allowNewSopInstanceUid,
+                                         unsigned int lossyQualityNotUsed)
+  {
+    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
+  }
 
   bool MemoryBufferTranscoder::Transcode(DicomImage& target,
                                          DicomImage& source,
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Mon Feb 24 08:03:02 2025 +0100
@@ -43,5 +43,11 @@
                            DicomImage& source,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+
+    virtual bool Transcode(DicomImage& target /* out */,
+                           DicomImage& source,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQualityNotUsed) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -69,4 +69,31 @@
                              name + "\", found: " + found->second);
     }
   }
+
+  uint32_t RestApiGetCall::GetUnsignedInteger32Argument(const std::string& name,
+                                                        uint32_t defaultValue) const
+  {
+    HttpToolbox::Arguments::const_iterator found = getArguments_.find(name);
+
+    uint32_t value;
+    
+    if (found == getArguments_.end())
+    {
+      return defaultValue;
+    }
+    else if (found->second.empty())
+    {
+      return true;
+    }
+    else if (SerializationToolbox::ParseUnsignedInteger32(value, found->second))
+    {
+      return value;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Expected a Unsigned Int for GET argument \"" +
+                             name + "\", found: " + found->second);
+    }
+  }
+
 }
--- a/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Mon Feb 24 08:03:02 2025 +0100
@@ -65,7 +65,10 @@
 
     bool GetBooleanArgument(const std::string& name,
                             bool defaultValue) const;
-    
+
+    uint32_t GetUnsignedInteger32Argument(const std::string& name,      
+                                          uint32_t defaultValue) const;
+  
     virtual bool ParseJsonRequest(Json::Value& result) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Resources/Configuration.json	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Resources/Configuration.json	Mon Feb 24 08:03:02 2025 +0100
@@ -920,8 +920,10 @@
   // that have a compressed transfer syntax (new in Orthanc 1.8.2).
   "IngestTranscodingOfCompressed" : true,
   
-  // The compression level that is used when transcoding to one of the
-  // lossy/JPEG transfer syntaxes (integer between 1 and 100).
+  // The default compression level that is used when transcoding to one
+  // of the lossy/JPEG transfer syntaxes (integer between 1 and 100).
+  // This value is currently only used by the default built-in DCMTK
+  // transcoder and is not provided to transcoding plugins.
   "DicomLossyTranscodingQuality" : 90,
 
   // Whether "fsync()" is called after each write to the storage area
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -46,6 +46,7 @@
 static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier";
 static const char* const WARNINGS = "Warnings";
 static const char* const JOBS_ENGINE_THREADS_COUNT = "JobsEngineThreadsCount";
+static const char* const DICOM_LOSSY_TRANSCODING_QUALITY = "DicomLossyTranscodingQuality";
 
 namespace Orthanc
 {
@@ -700,6 +701,11 @@
     }
   }
 
+  unsigned int OrthancConfiguration::GetDicomLossyTranscodingQuality() const
+  {
+    return GetUnsignedIntegerParameter(DICOM_LOSSY_TRANSCODING_QUALITY, 90);
+  }
+
 
   bool OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const
   {
--- a/OrthancServer/Sources/OrthancConfiguration.h	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Mon Feb 24 08:03:02 2025 +0100
@@ -197,6 +197,8 @@
 
     void GetListOfOrthancPeers(std::set<std::string>& target) const;
 
+    unsigned int GetDicomLossyTranscodingQuality() const;
+    
     // Returns "true" iff. at least one user is registered
     bool SetupRegisteredUsers(HttpServer& httpServer) const;
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -58,6 +58,8 @@
 static const char* const SERIES = "Series";
 static const char* const TAGS = "Tags";
 static const char* const TRANSCODE = "Transcode";
+static const char* const LOSSY_QUALITY = "LossyQuality";
+
 
 
 namespace Orthanc
@@ -88,6 +90,10 @@
       .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String,
                        "Transcode the DICOM instances to the provided DICOM transfer syntax: "
                        "https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+      .SetRequestField(LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                        "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                        "as an integer between 1 and 100.  If not provided, the value is defined "
+                        "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
       .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
                        "Allow the modification of tags related to DICOM identifiers, at the risk of "
                        "breaking the DICOM model of the real world", false)
@@ -116,6 +122,10 @@
       .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String,
                        "Transcode the DICOM instances to the provided DICOM transfer syntax: "
                        "https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+      .SetRequestField(LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                        "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                        "as an integer between 1 and 100.  If not provided, the value is defined "
+                        "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
       .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
                        "Allow the modification of tags related to DICOM identifiers, at the risk of "
                        "breaking the DICOM model of the real world", false)
@@ -196,7 +206,8 @@
   static void AnonymizeOrModifyInstance(DicomModification& modification,
                                         RestApiPostCall& call,
                                         bool transcode,
-                                        DicomTransferSyntax targetSyntax)
+                                        DicomTransferSyntax targetSyntax,
+                                        unsigned int lossyQuality)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
     std::string id = call.GetUriComponent("id", "");
@@ -220,7 +231,7 @@
       std::set<DicomTransferSyntax> s;
       s.insert(targetSyntax);
       
-      if (context.Transcode(transcoded, source, s, true))
+      if (context.Transcode(transcoded, source, s, true, lossyQuality))
       {      
         call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
                                       transcoded.GetBufferSize(), MimeType_Dicom);
@@ -259,6 +270,20 @@
     }
   }
 
+  static unsigned int GetLossyQuality(const Json::Value& request)
+  {
+    unsigned int lossyQuality;
+    {
+      OrthancConfiguration::ReaderLock lock;
+      lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+    }
+
+    if (request.isMember(LOSSY_QUALITY)) 
+    {
+      lossyQuality = SerializationToolbox::ReadUnsignedInteger(request, LOSSY_QUALITY);
+    }
+    return lossyQuality;
+}
 
   static void ModifyInstance(RestApiPostCall& call)
   {
@@ -286,11 +311,11 @@
     if (request.isMember(TRANSCODE))
     {
       std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
-      
+
       DicomTransferSyntax syntax;
       if (LookupTransferSyntax(syntax, s))
       {
-        AnonymizeOrModifyInstance(modification, call, true, syntax);
+        AnonymizeOrModifyInstance(modification, call, true, syntax, GetLossyQuality(request));
       }
       else
       {
@@ -300,7 +325,7 @@
     else
     {
       AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
-                                DicomTransferSyntax_LittleEndianImplicit /* unused */);
+                                DicomTransferSyntax_LittleEndianImplicit /* unused */, 0 /* unused */);
     }
   }
 
@@ -326,8 +351,25 @@
     Json::Value request;
     ParseAnonymizationRequest(request, modification, call);
 
-    AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
-                              DicomTransferSyntax_LittleEndianImplicit /* unused */);
+    if (request.isMember(TRANSCODE))
+    {
+      std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
+
+      DicomTransferSyntax syntax;
+      if (LookupTransferSyntax(syntax, s))
+      {
+        AnonymizeOrModifyInstance(modification, call, true, syntax, GetLossyQuality(request));
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown transfer syntax: " + s);
+      }
+    }
+    else
+    {
+      AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
+                                DicomTransferSyntax_LittleEndianImplicit /* unused */, 0 /* unused */);
+    }
   }
 
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -42,9 +42,11 @@
   static const char* const KEY_RESOURCES = "Resources";
   static const char* const KEY_EXTENDED = "Extended";
   static const char* const KEY_TRANSCODE = "Transcode";
+  static const char* const KEY_LOSSY_QUALITY = "LossyQuality";
   static const char* const KEY_FILENAME = "Filename";
   
   static const char* const GET_TRANSCODE = "transcode";
+  static const char* const GET_LOSSY_QUALITY = "lossy-quality";
   static const char* const GET_FILENAME = "filename";
   static const char* const GET_RESOURCES = "resources";
 
@@ -118,6 +120,7 @@
                                bool& extended,               /* out */
                                bool& transcode,              /* out */
                                DicomTransferSyntax& syntax,  /* out */
+                               unsigned int& lossyQuality,   /* out */
                                int& priority,                /* out */
                                unsigned int& loaderThreads,  /* out */
                                std::string& filename,        /* out */
@@ -145,6 +148,16 @@
     {
       transcode = true;
       syntax = Orthanc::GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE));
+      
+      {
+        OrthancConfiguration::ReaderLock lock;
+        lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+      }
+
+      if (body.isMember(KEY_LOSSY_QUALITY)) 
+      {
+        lossyQuality = SerializationToolbox::ReadUnsignedInteger(body, KEY_LOSSY_QUALITY);
+      }
     }
     else
     {
@@ -526,6 +539,10 @@
       .SetRequestField(KEY_TRANSCODE, RestApiCallDocumentation::Type_String,
                        "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(KEY_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                        "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                        "as an integer between 1 and 100.  If not provided, the value is defined "
+                        "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
       .SetRequestField(KEY_FILENAME, RestApiCallDocumentation::Type_String,
                         "Filename to set in the \"Content-Disposition\" HTTP header "
                         "(including file extension)", false)
@@ -578,7 +595,9 @@
       int priority;
       unsigned int loaderThreads;
       std::string filename;
-      GetJobParameters(synchronous, extended, transcode, transferSyntax,
+      unsigned int lossyQuality;
+
+      GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
                        priority, loaderThreads, filename, body, DEFAULT_IS_EXTENDED, "Archive.zip");
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, ResourceType_Patient));
@@ -587,6 +606,7 @@
       if (transcode)
       {
         job->SetTranscode(transferSyntax);
+        job->SetLossyQuality(lossyQuality);
       }
       
       job->SetLoaderThreads(loaderThreads);
@@ -599,7 +619,18 @@
                              "Expected a list of resources to archive in the body");
     }
   }
-  
+
+  static unsigned int GetLossyQuality(RestApiGetCall& call)
+  {
+    unsigned int lossyQuality;
+
+    OrthancConfiguration::ReaderLock lock;
+    lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+    lossyQuality = call.GetUnsignedInteger32Argument(GET_LOSSY_QUALITY, lossyQuality);
+    
+    return lossyQuality;
+  }
+
 
   template <bool IS_MEDIA,
             bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
@@ -619,6 +650,10 @@
         .SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String,
                             "If present, the DICOM files will be transcoded to the provided "
                             "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+        .SetHttpGetArgument(GET_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                            "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                            "as an integer between 1 and 100.  If not provided, the value is defined "
+                            "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
         .SetHttpGetArgument(GET_RESOURCES, RestApiCallDocumentation::Type_String,
                             "A comma separated list of Orthanc resource identifiers to include in the " + m + ".", true);
       return;
@@ -627,11 +662,13 @@
     ServerContext& context = OrthancRestApi::GetContext(call);
     bool transcode = false;
     DicomTransferSyntax transferSyntax = DicomTransferSyntax_LittleEndianImplicit;  // Initialize variable to avoid warnings
+    unsigned int lossyQuality;
 
     if (call.HasArgument(GET_TRANSCODE))
     {
       transcode = true;
       transferSyntax = GetTransferSyntax(call.GetArgument(GET_TRANSCODE, ""));
+      lossyQuality = GetLossyQuality(call);
     }
     
     if (!call.HasArgument(GET_RESOURCES))
@@ -645,6 +682,7 @@
     if (transcode)
     {
       job->SetTranscode(transferSyntax);
+      job->SetLossyQuality(lossyQuality);
     }
 
     const std::string filename = call.GetArgument(GET_FILENAME, "Archive.zip");  // New in Orthanc 1.12.7
@@ -677,6 +715,10 @@
         .SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String,
                             "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)
+        .SetHttpGetArgument(GET_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                            "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                            "as an integer between 1 and 100.  If not provided, the value is defined "
+                            "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
         .AddAnswerType(MimeType_Zip, "ZIP file containing the archive");
       if (IS_MEDIA)
       {
@@ -708,6 +750,7 @@
     if (call.HasArgument(GET_TRANSCODE))
     {
       job->SetTranscode(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
+      job->SetLossyQuality(GetLossyQuality(call));
     }
 
     {
@@ -752,7 +795,8 @@
       int priority;
       unsigned int loaderThreads;
       std::string filename;
-      GetJobParameters(synchronous, extended, transcode, transferSyntax,
+      unsigned int lossyQuality;
+      GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
                        priority, loaderThreads, filename, body, false /* by default, not extented */, id + ".zip");
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, LEVEL));
@@ -761,6 +805,7 @@
       if (transcode)
       {
         job->SetTranscode(transferSyntax);
+        job->SetLossyQuality(lossyQuality);
       }
 
       job->SetLoaderThreads(loaderThreads);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -338,6 +338,7 @@
   static void GetInstanceFile(RestApiGetCall& call)
   {
     static const char* const GET_TRANSCODE = "transcode";
+    static const char* const GET_LOSSY_QUALITY = "lossy-quality";
     static const char* const GET_FILENAME = "filename";
 
     if (call.IsDocumentation())
@@ -351,6 +352,10 @@
         .SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String,
                             "If present, the DICOM file will be transcoded to the provided "
                             "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
+        .SetHttpGetArgument(GET_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
+                            "If transcoding to a lossy transfer syntax, this entry defines the quality "
+                            "as an integer between 1 and 100.  If not provided, the value is defined "
+                            "by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
         .SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String,
                               "Filename to set in the \"Content-Disposition\" HTTP header "
                               "(including file extension)", false)
@@ -406,12 +411,34 @@
 
     if (call.HasArgument(GET_TRANSCODE))
     {
+      unsigned int lossyQuality;
+      unsigned int defaultLossyQuality;
+      {
+        OrthancConfiguration::ReaderLock lock;
+        defaultLossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+      }
+      lossyQuality = call.GetUnsignedInteger32Argument(GET_LOSSY_QUALITY, defaultLossyQuality);
+
       std::string source;
       std::string attachmentId;
       std::string transcoded;
       context.ReadDicom(source, attachmentId, publicId);
 
-      if (context.TranscodeWithCache(transcoded, source, publicId, attachmentId, GetTransferSyntax(call.GetArgument(GET_TRANSCODE, ""))))
+      if (lossyQuality != defaultLossyQuality) // we can't use the cache if the lossy quality is not the default one
+      {
+        IDicomTranscoder::DicomImage targetImage;
+        IDicomTranscoder::DicomImage sourceImage;
+        sourceImage.SetExternalBuffer(source);
+        std::set<DicomTransferSyntax> allowedSyntaxes;
+        allowedSyntaxes.insert(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
+
+        if (context.Transcode(targetImage, sourceImage, allowedSyntaxes, true, lossyQuality))
+        {
+          call.GetOutput().SetContentFilename(filename.c_str());
+          call.GetOutput().AnswerBuffer(targetImage.GetBufferData(), targetImage.GetBufferSize(), MimeType_Dicom);
+        }
+      }
+      else if (context.TranscodeWithCache(transcoded, source, publicId, attachmentId, GetTransferSyntax(call.GetArgument(GET_TRANSCODE, ""))))
       {
         call.GetOutput().SetContentFilename(filename.c_str());
         call.GetOutput().AnswerBuffer(transcoded, MimeType_Dicom);
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -393,8 +393,6 @@
   {
     try
     {
-      unsigned int lossyQuality;
-
       {
         OrthancConfiguration::ReaderLock lock;
 
@@ -425,7 +423,6 @@
         // New options in Orthanc 1.7.0
         transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true);
         builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After"));
-        lossyQuality = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomLossyTranscodingQuality", 90);
 
         std::string s;
         if (lock.GetConfiguration().LookupStringParameter(s, "IngestTranscoding"))
@@ -502,6 +499,8 @@
         SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses);
 
         defaultDicomRetrieveMethod_ = StringToRetrieveMethod(lock.GetConfiguration().GetStringParameter("DicomDefaultRetrieveMethod", "C-MOVE"));
+
+        dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetDefaultLossyQuality(lock.GetConfiguration().GetDicomLossyTranscodingQuality());
       }
 
       jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
@@ -516,8 +515,6 @@
 #else
       LOG(INFO) << "Your platform does not support malloc_trim(), not starting the memory trimming thread";
 #endif
-      
-      dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetLossyQuality(lossyQuality);
     }
     catch (OrthancException&)
     {
@@ -1959,15 +1956,31 @@
     return true;
   }
 
-
   bool ServerContext::Transcode(DicomImage& target,
                                 DicomImage& source /* in, "GetParsed()" possibly modified */,
                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
                                 bool allowNewSopInstanceUid)
   {
+    unsigned int lossyQuality;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
+    }
+
+    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality);
+  }
+
+
+  bool ServerContext::Transcode(DicomImage& target,
+                                DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                bool allowNewSopInstanceUid,
+                                unsigned int lossyQuality)
+  {
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
     {
-      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
+      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality))
       {
         return true;
       }
@@ -1977,7 +1990,7 @@
     if (HasPlugins() &&
         GetPlugins().HasCustomTranscoder())
     {
-      if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
+      if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))  // TODO: pass lossyQuality to plugins -> needs a new plugin interface
       {
         return true;
       }
@@ -1991,7 +2004,7 @@
 
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
     {
-      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
+      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality);
     }
     else
     {
--- a/OrthancServer/Sources/ServerContext.h	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Mon Feb 24 08:03:02 2025 +0100
@@ -577,6 +577,12 @@
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
                            bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+    
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid,
+                           unsigned int lossyQuality) ORTHANC_OVERRIDE;
 
     virtual bool TranscodeWithCache(std::string& target,
                                     const std::string& source,
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Mon Feb 24 08:03:02 2025 +0100
@@ -88,11 +88,13 @@
     ServerContext&                        context_;
     bool                                  transcode_;
     DicomTransferSyntax                   transferSyntax_;
+    unsigned int                          lossyQuality_;
   public:
-    explicit InstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax)
+    explicit InstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality)
     : context_(context),
       transcode_(transcode),
-      transferSyntax_(transferSyntax)
+      transferSyntax_(transferSyntax),
+      lossyQuality_(lossyQuality)
     {
     }
 
@@ -114,7 +116,7 @@
         IDicomTranscoder::DicomImage source, transcoded;
         source.SetExternalBuffer(sourceBuffer);
 
-        if (context_.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
+        if (context_.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */, lossyQuality_))
         {
           transcodedBuffer.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()), transcoded.GetBufferSize());
           return true;
@@ -139,8 +141,8 @@
   class ArchiveJob::SynchronousInstanceLoader : public ArchiveJob::InstanceLoader
   {
   public:
-    explicit SynchronousInstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax)
-    : InstanceLoader(context, transcode, transferSyntax)
+    explicit SynchronousInstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality)
+    : InstanceLoader(context, transcode, transferSyntax, lossyQuality)
     {
     }
 
@@ -192,8 +194,8 @@
 
 
   public:
-    ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax)
-    : InstanceLoader(context, transcode, transferSyntax),
+    ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality)
+    : InstanceLoader(context, transcode, transferSyntax, lossyQuality),
       availableInstancesSemaphore_(0),
       bufferedInstancesSemaphore_(3*threadCount)
     {
@@ -1250,6 +1252,7 @@
     archiveSize_(0),
     transcode_(false),
     transferSyntax_(DicomTransferSyntax_LittleEndianImplicit),
+    lossyQuality_(100),
     loaderThreads_(0)
   {
   }
@@ -1349,7 +1352,20 @@
     }
   }
 
-  
+
+  void ArchiveJob::SetLossyQuality(unsigned int lossyQuality)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      lossyQuality_ = lossyQuality;
+    }
+  }
+
+
   void ArchiveJob::SetLoaderThreads(unsigned int loaderThreads)
   {
     if (writer_.get() != NULL)   // Already started
@@ -1375,11 +1391,11 @@
     if (loaderThreads_ == 0)
     {
       // default behaviour before loaderThreads was introducted in 1.10.0
-      instanceLoader_.reset(new SynchronousInstanceLoader(context_, transcode_, transferSyntax_));
+      instanceLoader_.reset(new SynchronousInstanceLoader(context_, transcode_, transferSyntax_, lossyQuality_));
     }
     else
     {
-      instanceLoader_.reset(new ThreadedInstanceLoader(context_, loaderThreads_, transcode_, transferSyntax_));
+      instanceLoader_.reset(new ThreadedInstanceLoader(context_, loaderThreads_, transcode_, transferSyntax_, lossyQuality_));
     }
 
     if (writer_.get() != NULL)
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Mon Feb 24 08:01:43 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Mon Feb 24 08:03:02 2025 +0100
@@ -69,6 +69,7 @@
     // New in Orthanc 1.7.0
     bool                 transcode_;
     DicomTransferSyntax  transferSyntax_;
+    unsigned int         lossyQuality_;
 
     // New in Orthanc 1.10.0
     unsigned int         loaderThreads_;
@@ -105,6 +106,8 @@
 
     void SetTranscode(DicomTransferSyntax transferSyntax);
 
+    void SetLossyQuality(unsigned int lossyQuality);
+
     void SetLoaderThreads(unsigned int loaderThreads);
 
     virtual void Reset() ORTHANC_OVERRIDE;