view OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp @ 5911:bfae0fc2ea1b get-scu-test

Started to work on handling errors as warnings when trying to store instances whose SOPClassUID has not been accepted during the negotiation. Work to be finalized later
author Alain Mazy <am@orthanc.team>
date Mon, 09 Dec 2024 10:07:19 +0100
parents f622e5964cfa
children
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 **/


#include "../PrecompiledHeaders.h"
#include "DicomStoreUserConnection.h"

#include "../DicomParsing/FromDcmtkBridge.h"
#include "../DicomParsing/ParsedDicomFile.h"
#include "../Logging.h"
#include "../OrthancException.h"
#include "DicomAssociation.h"

#include <dcmtk/dcmdata/dcdeftag.h>

#include <list>


namespace Orthanc
{
  static void ProgressCallback(void * /*callbackData*/,
                               T_DIMSE_StoreProgress *progress,
                               T_DIMSE_C_StoreRQ * req)
  {
    if (req != NULL &&
        progress->state == DIMSE_StoreBegin)
    {
      OFString str;
      CLOG(TRACE, DICOM) << "Sending Store Request:" << std::endl
                         << DIMSE_dumpMessage(str, *req, DIMSE_OUTGOING);
    }
  }


  bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid,
                                                     const std::set<DicomTransferSyntax>& sourceSyntaxes,
                                                     bool hasPreferred,
                                                     DicomTransferSyntax preferred)
  {
    typedef std::list< std::list<DicomTransferSyntax> >  GroupsOfSyntaxes;

    GroupsOfSyntaxes  groups;

    // Firstly, add one group for each individual transfer syntax
    for (std::set<DicomTransferSyntax>::const_iterator
           it = sourceSyntaxes.begin(); it != sourceSyntaxes.end(); ++it)
    {
      std::list<DicomTransferSyntax> group;
      group.push_back(*it);
      groups.push_back(group);
    }

    // Secondly, add one group with the preferred transfer syntax
    if (hasPreferred &&
        sourceSyntaxes.find(preferred) == sourceSyntaxes.end())
    {
      std::list<DicomTransferSyntax> group;
      group.push_back(preferred);
      groups.push_back(group);
    }

    // Thirdly, add all the uncompressed transfer syntaxes as one single group
    if (proposeUncompressedSyntaxes_)
    {
      static const size_t N = 3;
      static const DicomTransferSyntax UNCOMPRESSED_SYNTAXES[N] = {
        DicomTransferSyntax_LittleEndianImplicit,
        DicomTransferSyntax_LittleEndianExplicit,
        DicomTransferSyntax_BigEndianExplicit
      };

      std::list<DicomTransferSyntax> group;

      for (size_t i = 0; i < N; i++)
      {
        DicomTransferSyntax syntax = UNCOMPRESSED_SYNTAXES[i];
        if (sourceSyntaxes.find(syntax) == sourceSyntaxes.end() &&
            (!hasPreferred || preferred != syntax))
        {
          group.push_back(syntax);
        }
      }

      if (!group.empty())
      {
        groups.push_back(group);
      }
    }

    // Now, propose each of these groups of transfer syntaxes
    if (association_->GetRemainingPropositions() <= groups.size())
    {
      return false;  // Not enough room
    }
    else
    {
      for (GroupsOfSyntaxes::const_iterator it = groups.begin(); it != groups.end(); ++it)
      {
        association_->ProposePresentationContext(sopClassUid, *it);

        // Remember the syntaxes that were individually proposed, in
        // order to avoid renegociation if they are seen again (**)
        if (it->size() == 1)
        {
          DicomTransferSyntax syntax = *it->begin();
          proposedOriginalClasses_.insert(std::make_pair(sopClassUid, syntax));
        }
      }

      return true;
    }
  }


  bool DicomStoreUserConnection::LookupPresentationContext(
    uint8_t& presentationContextId,
    const std::string& sopClassUid,
    DicomTransferSyntax transferSyntax)
  {
    typedef std::map<DicomTransferSyntax, uint8_t>  PresentationContexts;

    PresentationContexts pc;
    if (association_->IsOpen() &&
        association_->LookupAcceptedPresentationContext(pc, sopClassUid))
    {
      PresentationContexts::const_iterator found = pc.find(transferSyntax);
      if (found != pc.end())
      {
        presentationContextId = found->second;
        return true;
      }
    }

    return false;
  }


  DicomStoreUserConnection::DicomStoreUserConnection(
    const DicomAssociationParameters& params) :
    parameters_(params),
    association_(new DicomAssociation),
    proposeCommonClasses_(true),
    proposeUncompressedSyntaxes_(true),
    proposeRetiredBigEndian_(false)
  {
  }

  const DicomAssociationParameters &DicomStoreUserConnection::GetParameters() const
  {
    return parameters_;
  }

  void DicomStoreUserConnection::SetCommonClassesProposed(bool proposed)
  {
    proposeCommonClasses_ = proposed;
  }

  bool DicomStoreUserConnection::IsCommonClassesProposed() const
  {
    return proposeCommonClasses_;
  }

  void DicomStoreUserConnection::SetUncompressedSyntaxesProposed(bool proposed)
  {
    proposeUncompressedSyntaxes_ = proposed;
  }

  bool DicomStoreUserConnection::IsUncompressedSyntaxesProposed() const
  {
    return proposeUncompressedSyntaxes_;
  }

  void DicomStoreUserConnection::SetRetiredBigEndianProposed(bool propose)
  {
    proposeRetiredBigEndian_ = propose;
  }

  bool DicomStoreUserConnection::IsRetiredBigEndianProposed() const
  {
    return proposeRetiredBigEndian_;
  }


  void DicomStoreUserConnection::RegisterStorageClass(const std::string& sopClassUid,
                                                      DicomTransferSyntax syntax)
  {
    RegisteredClasses::iterator found = registeredClasses_.find(sopClassUid);

    if (found == registeredClasses_.end())
    {
      std::set<DicomTransferSyntax> ts;
      ts.insert(syntax);
      registeredClasses_[sopClassUid] = ts;
    }
    else
    {
      found->second.insert(syntax);
    }
  }


  void DicomStoreUserConnection::LookupParameters(std::string& sopClassUid,
                                                  std::string& sopInstanceUid,
                                                  DicomTransferSyntax& transferSyntax,
                                                  DcmFileFormat& dicom)
  {
    if (dicom.getDataset() == NULL)
    {
      throw OrthancException(ErrorCode_InternalError);
    }
    
    OFString a, b;
    if (!dicom.getDataset()->findAndGetOFString(DCM_SOPClassUID, a).good() ||
        !dicom.getDataset()->findAndGetOFString(DCM_SOPInstanceUID, b).good())
    {
      throw OrthancException(ErrorCode_NoSopClassOrInstance,
                             "Unable to determine the SOP class/instance for C-STORE with AET " +
                             parameters_.GetRemoteModality().GetApplicationEntityTitle());
    }

    sopClassUid.assign(a.c_str());
    sopInstanceUid.assign(b.c_str());

    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom))
    {
      throw OrthancException(ErrorCode_InternalError,
                             "Unknown transfer syntax from DCMTK");
    }
  }
  

  bool DicomStoreUserConnection::NegotiatePresentationContext(
    uint8_t& presentationContextId,
    const std::string& sopClassUid,
    DicomTransferSyntax transferSyntax,
    bool hasPreferred,
    DicomTransferSyntax preferred)
  {
    /**
     * Step 1: Check whether this presentation context is already
     * available in the previously negotiated assocation.
     **/

    if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
    {
      CLOG(INFO, DICOM) << "Found an accepted presentation context for SOPClassUID " << sopClassUid << " and transfer syntax " << GetTransferSyntaxUid(transferSyntax);
      return true;
    }

    // The association must be re-negotiated
    if (association_->IsOpen())
    {
      CLOG(INFO, DICOM) << "No accepted presentation context found, re-negotiating DICOM association with "
                        << parameters_.GetRemoteModality().GetApplicationEntityTitle()
                        << " for SOPClassUID " << sopClassUid << " TransferSyntax =" << GetTransferSyntaxUid(transferSyntax);

      // Check if we know that the remote modality was
      // already proposed this individual transfer syntax (**)
      if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) != proposedOriginalClasses_.end())
      {
        CLOG(INFO, DICOM) << "The remote modality has already rejected SOP class UID \""
                          << sopClassUid << "\" with transfer syntax \""
                          << GetTransferSyntaxUid(transferSyntax) << "\", but we will renegotiate anyway";
        // always renegotiating since 1.12.2 // return false;
      }
    }
    else
    {
      CLOG(INFO, DICOM) << "Negotiating DICOM association with "
                        << parameters_.GetRemoteModality().GetApplicationEntityTitle()
                        << " for SOPClassUID " << sopClassUid << " TransferSyntax =" << GetTransferSyntaxUid(transferSyntax);
    }

    association_->ClearPresentationContexts();
    proposedOriginalClasses_.clear();
    RegisterStorageClass(sopClassUid, transferSyntax);  // (*)

    
    /**
     * Step 2: Propose at least the mandatory SOP class.
     **/

    {
      RegisteredClasses::const_iterator mandatory = registeredClasses_.find(sopClassUid);

      if (mandatory == registeredClasses_.end() ||
          mandatory->second.find(transferSyntax) == mandatory->second.end())
      {
        // Should never fail because of (*)
        throw OrthancException(ErrorCode_InternalError);
      }

      if (!ProposeStorageClass(sopClassUid, mandatory->second, hasPreferred, preferred))
      {
        // Should never happen in real life: There are no more than
        // 128 transfer syntaxes in DICOM!
        throw OrthancException(ErrorCode_InternalError,
                               "Too many transfer syntaxes for SOP class UID: " + sopClassUid);
      }
    }

      
    /**
     * Step 3: Propose all the previously spotted SOP classes, as
     * registered through the "RegisterStorageClass()" method.
     **/
      
    for (RegisteredClasses::const_iterator it = registeredClasses_.begin();
         it != registeredClasses_.end(); ++it)
    {
      if (it->first != sopClassUid)
      {
        ProposeStorageClass(it->first, it->second, hasPreferred, preferred);
      }
    }
      

    /**
     * Step 4: As long as there is room left in the proposed
     * presentation contexts, propose the uncompressed transfer syntaxes
     * for the most common SOP classes, as can be found in the
     * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The
     * preferred transfer syntax is "LittleEndianImplicit".
     **/

    if (proposeCommonClasses_)
    {
      // The method "ProposeStorageClass()" will automatically add
      // "LittleEndianImplicit"
      std::set<DicomTransferSyntax> ts;
        
      for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++)
      {
        std::string c(dcmShortSCUStorageSOPClassUIDs[i]);
          
        if (c != sopClassUid &&
            registeredClasses_.find(c) == registeredClasses_.end())
        {
          ProposeStorageClass(c, ts, hasPreferred, preferred);
        }
      }
    }


    /**
     * Step 5: Open the association, and check whether the pair (SOP
     * class UID, transfer syntax) was accepted by the remote host.
     **/

    association_->Open(parameters_);
    return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax);
  }


  void DicomStoreUserConnection::Store(std::string& sopClassUid,
                                       std::string& sopInstanceUid,
                                       DcmFileFormat& dicom,
                                       bool hasMoveOriginator,
                                       const std::string& moveOriginatorAET,
                                       uint16_t moveOriginatorID,
                                       bool ignoreErrors)
  {
    DicomTransferSyntax transferSyntax;
    LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom);

    LOG(INFO) << "Performing C-Store on instance of SOPClassUID '" << sopClassUid << "'";

    uint8_t presID;
    if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax, proposeUncompressedSyntaxes_,
                                      DicomTransferSyntax_LittleEndianExplicit))
    {
      if (ignoreErrors)
      {
        LOG(INFO) << "No valid presentation context was negotiated for SOP class UID [" << sopClassUid << "] and transfer "
                      "syntax [" << GetTransferSyntaxUid(transferSyntax) << "] "
                      "while sending to modality [" << parameters_.GetRemoteModality().GetApplicationEntityTitle() << "]";
      }
      else
      {
        throw OrthancException(ErrorCode_NetworkProtocol,
                              "No valid presentation context was negotiated for "
                              "SOP class UID [" + sopClassUid + "] and transfer "
                              "syntax [" + GetTransferSyntaxUid(transferSyntax) + "] "
                              "while sending to modality [" +
                              parameters_.GetRemoteModality().GetApplicationEntityTitle() + "]");
      }
    }
    
    // Prepare the transmission of data
    T_DIMSE_C_StoreRQ request;
    memset(&request, 0, sizeof(request));
    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
    strncpy(request.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
    request.Priority = DIMSE_PRIORITY_MEDIUM;
    request.DataSetType = DIMSE_DATASET_PRESENT;
    strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);

    if (hasMoveOriginator)
    {    
      strncpy(request.MoveOriginatorApplicationEntityTitle, 
              moveOriginatorAET.c_str(), DIC_AE_LEN);
      request.opts = O_STORE_MOVEORIGINATORAETITLE;

      request.MoveOriginatorID = moveOriginatorID;  // The type DIC_US is an alias for uint16_t
      request.opts |= O_STORE_MOVEORIGINATORID;
    }

    if (dicom.getDataset() == NULL)
    {
      throw OrthancException(ErrorCode_InternalError);
    }

    // Finally conduct transmission of data
    T_DIMSE_C_StoreRSP response;
    DcmDataset* statusDetail = NULL;
    DicomAssociation::CheckCondition(
      DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request,
                      NULL, dicom.getDataset(), ProgressCallback, NULL,
                      /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
                      /*opt_dimse_timeout*/ GetParameters().GetTimeout(),
                      &response, &statusDetail, NULL),
      GetParameters(), "C-STORE");

    if (statusDetail != NULL) 
    {
      delete statusDetail;
    }

    {
      OFString str;
      CLOG(TRACE, DICOM) << "Received Store Response:" << std::endl
                         << DIMSE_dumpMessage(str, response, DIMSE_INCOMING, NULL, presID);
    }
    
    /**
     * New in Orthanc 1.6.0: Deal with failures during C-STORE.
     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1
     **/
    
    if (response.DimseStatus != 0x0000 &&  // Success
        response.DimseStatus != 0xB000 &&  // Warning - Coercion of Data Elements
        response.DimseStatus != 0xB007 &&  // Warning - Data Set does not match SOP Class
        response.DimseStatus != 0xB006 &&  // Warning - Elements Discarded
        response.DimseStatus != 0x0111)    // Warning - Duplicate SOPInstanceUID (https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3)
    {
      char buf[16];
      sprintf(buf, "%04X", response.DimseStatus);
      throw OrthancException(ErrorCode_NetworkProtocol,
                             "C-STORE SCU to AET \"" +
                             GetParameters().GetRemoteModality().GetApplicationEntityTitle() +
                             "\" has failed with DIMSE status 0x" + buf);
    }
  }


  void DicomStoreUserConnection::Store(std::string& sopClassUid,
                                       std::string& sopInstanceUid,
                                       const void* buffer,
                                       size_t size,
                                       bool hasMoveOriginator,
                                       const std::string& moveOriginatorAET,
                                       uint16_t moveOriginatorID,
                                       bool ignoreErrors)
  {
    std::unique_ptr<DcmFileFormat> dicom(
      FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));

    if (dicom.get() == NULL)
    {
      throw OrthancException(ErrorCode_InternalError);
    }
    
    Store(sopClassUid, sopInstanceUid, *dicom, hasMoveOriginator, moveOriginatorAET, moveOriginatorID, ignoreErrors);
  }


#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
  void DicomStoreUserConnection::LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes,
                                                   const std::string& sopClassUid,
                                                   DicomTransferSyntax sourceSyntax,
                                                   bool hasPreferred,
                                                   DicomTransferSyntax preferred)
  {
    acceptedSyntaxes.clear();
    std::map<DicomTransferSyntax, uint8_t> contexts;

    // Make sure a negotiation has already occurred for this transfer
    // syntax if we have not negotiated yet. 
    // We don't use the return code: Transcoding is possible even if the "sourceSyntax" is not supported.
    if (!association_->IsOpen() || !association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
    {
      uint8_t presID;
      NegotiatePresentationContext(presID, sopClassUid, sourceSyntax, hasPreferred, preferred);
    }

    if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
    {
      for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
             it = contexts.begin(); it != contexts.end(); ++it)
      {
        acceptedSyntaxes.insert(it->first);
      }
    }
  }
#endif
  

#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
  void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
                                           std::string& sopInstanceUid /* out */,
                                           IDicomTranscoder& transcoder,
                                           const void* buffer,
                                           size_t size,
                                           DicomTransferSyntax preferredTransferSyntax,
                                           bool hasMoveOriginator,
                                           const std::string& moveOriginatorAET,
                                           uint16_t moveOriginatorID,
                                           bool ignoreErrors)
  {
    std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
    if (dicom.get() == NULL ||
        dicom->getDataset() == NULL)
    {
      throw OrthancException(ErrorCode_NullPointer);
    }

    DicomTransferSyntax sourceSyntax;
    LookupParameters(sopClassUid, sopInstanceUid, sourceSyntax, *dicom);

    std::set<DicomTransferSyntax> accepted;
    LookupTranscoding(accepted, sopClassUid, sourceSyntax, true, preferredTransferSyntax);

    if (accepted.size() == 0)
    {
      if (ignoreErrors)
      {
        LOG(WARNING) << "Cannot C-Store an instance of SOPClassUID " << 
                      sopClassUid + ", the destination has not accepted any TransferSyntax for this SOPClassUID.";  // TODO: add an option to enable/disable this warning
        return;
      }
      else
      {
        throw OrthancException(ErrorCode_NoPresentationContext, "Cannot C-Store an instance of SOPClassUID " + 
                               sopClassUid + ", the destination has not accepted any TransferSyntax for this SOPClassUID.");
      }
    }

    if (accepted.find(sourceSyntax) != accepted.end())
    {
      // No need for transcoding
      Store(sopClassUid, sopInstanceUid, *dicom,
            hasMoveOriginator, moveOriginatorAET, moveOriginatorID, ignoreErrors);
    }
    else
    {
      // Transcoding is needed
      IDicomTranscoder::DicomImage source;
      source.AcquireParsed(dicom.release());
      source.SetExternalBuffer(buffer, size);

      const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed());
        
      IDicomTranscoder::DicomImage transcoded;
      bool success = false;
      bool isDestructiveCompressionAllowed = false;
      std::set<DicomTransferSyntax> attemptedSyntaxes;

      LOG(INFO) << "Transcoding is required to C-Store an instance of SOPClassUID '" << sopClassUid << "', preferredTransferSyntax is " << GetTransferSyntaxUid(preferredTransferSyntax);

      if (accepted.find(preferredTransferSyntax) != accepted.end())
      {
        // New in Orthanc 1.9.0: The preferred transfer syntax is
        // accepted by the remote modality => transcode to this syntax
        std::set<DicomTransferSyntax> targetSyntaxes;
        targetSyntaxes.insert(preferredTransferSyntax);
        attemptedSyntaxes.insert(preferredTransferSyntax);

        success = transcoder.Transcode(transcoded, source, targetSyntaxes, true);
        isDestructiveCompressionAllowed = true;
      }

      if (!success)
      {
        // Transcode to either one of the uncompressed transfer
        // syntaxes that are accepted by the remote modality

        std::set<DicomTransferSyntax> targetSyntaxes;

        if (accepted.find(DicomTransferSyntax_LittleEndianImplicit) != accepted.end())
        {
          targetSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
          attemptedSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
        }

        if (accepted.find(DicomTransferSyntax_LittleEndianExplicit) != accepted.end())
        {
          targetSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
          attemptedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
        }

        if (accepted.find(DicomTransferSyntax_BigEndianExplicit) != accepted.end())
        {
          targetSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
          attemptedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
        }

        if (!targetSyntaxes.empty())
        {
          success = transcoder.Transcode(transcoded, source, targetSyntaxes, false);
          isDestructiveCompressionAllowed = false;
        }
      }

      if (success)
      {
        std::string targetUid = IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed());
        if (sourceUid != targetUid)
        {
          if (isDestructiveCompressionAllowed)
          {
            LOG(WARNING) << "Because of the use of a preferred transfer syntax that corresponds to "
                         << "a destructive compression, C-STORE SCU has hanged the SOP Instance UID "
                         << "of a DICOM instance from \"" << sourceUid << "\" to \"" << targetUid << "\"";
          }
          else
          {
            throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP "
                                   "Instance UID while transcoding to an uncompressed transfer syntax");
          }
        }

        DicomTransferSyntax transcodedSyntax;
          
        // Sanity check
        if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) ||
            accepted.find(transcodedSyntax) == accepted.end())
        {
          throw OrthancException(ErrorCode_InternalError);
        }
        else
        {
          Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(),
                hasMoveOriginator, moveOriginatorAET, moveOriginatorID, ignoreErrors);
        }
      }
      else
      {
        std::string s;
        for (std::set<DicomTransferSyntax>::const_iterator
               it = attemptedSyntaxes.begin(); it != attemptedSyntaxes.end(); ++it)
        {
          s += " " + std::string(GetTransferSyntaxUid(*it));
        }
        
        throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode instance of SOPClassUID " + 
                               sopClassUid + " from " +
                               std::string(GetTransferSyntaxUid(sourceSyntax)) +
                               " to one of [" + s + " ]");
      }
    }
  }
#endif
  
  
#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
  void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
                                           std::string& sopInstanceUid /* out */,
                                           IDicomTranscoder& transcoder,
                                           const void* buffer,
                                           size_t size,
                                           bool hasMoveOriginator,
                                           const std::string& moveOriginatorAET,
                                           uint16_t moveOriginatorID,
                                           bool ignoreErrors)
  {
    Transcode(sopClassUid, sopInstanceUid, transcoder, buffer, size, DicomTransferSyntax_LittleEndianExplicit,
              hasMoveOriginator, moveOriginatorAET, moveOriginatorID, ignoreErrors);
  }
#endif
}